aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
committerGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
commit98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch)
tree131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e
parent32461366c9e204a527ca05e6e9b9404a2454ac51 (diff)
Initial
-rw-r--r--.gitignore50
-rw-r--r--.travis.yml17
-rw-r--r--AuthSamples/ApiTests/FirebearApiTests.m542
-rw-r--r--AuthSamples/ApiTests/Info.plist22
-rw-r--r--AuthSamples/EarlGreyTests/FirebearEarlGreyTests.m193
-rw-r--r--AuthSamples/EarlGreyTests/Info.plist22
-rw-r--r--AuthSamples/Podfile40
-rw-r--r--AuthSamples/README.md65
-rw-r--r--AuthSamples/Sample/ApplicationDelegate.h48
-rw-r--r--AuthSamples/Sample/ApplicationDelegate.m80
-rw-r--r--AuthSamples/Sample/ApplicationTemplate.plist88
-rw-r--r--AuthSamples/Sample/AuthCredentialsTemplate.h53
-rw-r--r--AuthSamples/Sample/AuthProviders.h74
-rw-r--r--AuthSamples/Sample/AuthProviders.m40
-rw-r--r--AuthSamples/Sample/CustomTokenDataEntryViewController.h55
-rw-r--r--AuthSamples/Sample/CustomTokenDataEntryViewController.m148
-rw-r--r--AuthSamples/Sample/FacebookAuthProvider.h29
-rw-r--r--AuthSamples/Sample/FacebookAuthProvider.m78
-rw-r--r--AuthSamples/Sample/GoogleAuthProvider.h29
-rw-r--r--AuthSamples/Sample/GoogleAuthProvider.m132
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/Contents.json177
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp-1.pngbin0 -> 169 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp.pngbin0 -> 169 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_29in40dp-1.pngbin0 -> 194 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_38in50dp.pngbin0 -> 222 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_42in57dp.pngbin0 -> 251 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_53in72dp.pngbin0 -> 303 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_56in76dp.pngbin0 -> 315 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp-1.pngbin0 -> 248 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp.pngbin0 -> 248 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-1.pngbin0 -> 317 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-2.pngbin0 -> 317 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_38in50dp.pngbin0 -> 390 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_42in57dp.pngbin0 -> 424 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_44in60dp.pngbin0 -> 438 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_53in72dp.pngbin0 -> 540 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_56in76dp.pngbin0 -> 573 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_21in29dp.pngbin0 -> 330 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_29in40dp-1.pngbin0 -> 433 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_44in60dp.pngbin0 -> 661 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/Contents.json6
-rw-r--r--AuthSamples/Sample/Images.xcassets/close.imageset/Contents.json23
-rw-r--r--AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_1x_ios_24dp.pngbin0 -> 164 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_2x_ios_24dp.pngbin0 -> 235 bytes
-rw-r--r--AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_3x_ios_24dp.pngbin0 -> 309 bytes
-rw-r--r--AuthSamples/Sample/MainViewController.h85
-rw-r--r--AuthSamples/Sample/MainViewController.m2496
-rw-r--r--AuthSamples/Sample/MainViewController.xib330
-rw-r--r--AuthSamples/Sample/Sample.entitlements.dev10
-rw-r--r--AuthSamples/Sample/Sample.entitlements.enterprise10
-rw-r--r--AuthSamples/Sample/SettingsViewController.h37
-rw-r--r--AuthSamples/Sample/SettingsViewController.m381
-rw-r--r--AuthSamples/Sample/SettingsViewController.xib55
-rw-r--r--AuthSamples/Sample/StaticContentTableViewManager.h257
-rw-r--r--AuthSamples/Sample/StaticContentTableViewManager.m231
-rw-r--r--AuthSamples/Sample/Strings/ar.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/ca.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/cs.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/da.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/de.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/el.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/en.lproj/FirebaseAuthUI.strings2
-rw-r--r--AuthSamples/Sample/Strings/en_GB.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/es.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/es_MX.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/fi.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/fr.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/he.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/hr.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/hu.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/id.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/it.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/ja.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/ko.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/ms.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/nb.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/nl.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/pl.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/pt.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/pt_BR.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/pt_PT.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/ro.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/ru.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/sk.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/sv.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/th.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/tr.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/uk.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/vi.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/zh_CN.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/Strings/zh_TW.lproj/FirebearSample.strings1
-rw-r--r--AuthSamples/Sample/UIViewController+Alerts.h91
-rw-r--r--AuthSamples/Sample/UIViewController+Alerts.m346
-rw-r--r--AuthSamples/Sample/UserInfoViewController.h55
-rw-r--r--AuthSamples/Sample/UserInfoViewController.m79
-rw-r--r--AuthSamples/Sample/UserInfoViewController.xib55
-rw-r--r--AuthSamples/Sample/UserTableViewCell.h58
-rw-r--r--AuthSamples/Sample/UserTableViewCell.m55
-rw-r--r--AuthSamples/Sample/main.m23
-rw-r--r--AuthSamples/Samples.xcodeproj/project.pbxproj1806
-rw-r--r--AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme119
-rw-r--r--AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/ApiTests.xcscheme56
-rw-r--r--AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/EarlGreyTests.xcscheme56
-rw-r--r--AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/FirebaseAuthUnitTests.xcscheme56
-rw-r--r--AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme111
-rw-r--r--AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/SwiftBear.xcscheme91
-rw-r--r--AuthSamples/SwiftSample/AppDelegate.swift62
-rw-r--r--AuthSamples/SwiftSample/AuthCredentialsTemplate.swift42
-rw-r--r--AuthSamples/SwiftSample/InfoTemplate.plist79
-rw-r--r--AuthSamples/SwiftSample/LaunchScreen.storyboard27
-rw-r--r--AuthSamples/SwiftSample/Main.storyboard186
-rw-r--r--AuthSamples/SwiftSample/ViewController.swift521
-rw-r--r--BuildFrameworks/FrameworkMaker.xcodeproj/project.pbxproj320
-rw-r--r--BuildFrameworks/Podfile12
-rw-r--r--BuildFrameworks/README.md32
-rwxr-xr-xBuildFrameworks/build.swift179
-rw-r--r--CONTRIBUTING.md24
-rw-r--r--Example/Auth/App/Auth-Info.plist49
-rw-r--r--Example/Auth/App/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Example/Auth/App/Base.lproj/Main.storyboard27
-rw-r--r--Example/Auth/App/FIRAppDelegate.h23
-rw-r--r--Example/Auth/App/FIRAppDelegate.m52
-rw-r--r--Example/Auth/App/FIRViewController.h21
-rw-r--r--Example/Auth/App/FIRViewController.m35
-rw-r--r--Example/Auth/App/GoogleService-Info.plist30
-rw-r--r--Example/Auth/App/main.m23
-rw-r--r--Example/Auth/Tests/FIRAdditionalUserInfoTests.m124
-rw-r--r--Example/Auth/Tests/FIRApp+FIRAuthUnitTests.h36
-rw-r--r--Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m45
-rw-r--r--Example/Auth/Tests/FIRAuthAPNSTokenManagerTests.m225
-rw-r--r--Example/Auth/Tests/FIRAuthAPNSTokenTests.m43
-rw-r--r--Example/Auth/Tests/FIRAuthAppCredentialManagerTests.m307
-rw-r--r--Example/Auth/Tests/FIRAuthAppCredentialTests.m67
-rw-r--r--Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m450
-rw-r--r--Example/Auth/Tests/FIRAuthBackendCreateAuthURITests.m104
-rw-r--r--Example/Auth/Tests/FIRAuthBackendRPCImplementationTests.m989
-rw-r--r--Example/Auth/Tests/FIRAuthDispatcherTests.m105
-rw-r--r--Example/Auth/Tests/FIRAuthGlobalWorkQueueTests.m35
-rw-r--r--Example/Auth/Tests/FIRAuthKeychainTests.m314
-rw-r--r--Example/Auth/Tests/FIRAuthNotificationManagerTests.m291
-rw-r--r--Example/Auth/Tests/FIRAuthSerialTaskQueueTests.m113
-rw-r--r--Example/Auth/Tests/FIRAuthTests.m1743
-rw-r--r--Example/Auth/Tests/FIRAuthUserDefaultsStorageTests.m155
-rw-r--r--Example/Auth/Tests/FIRCreateAuthURIRequestTests.m103
-rw-r--r--Example/Auth/Tests/FIRCreateAuthURIResponseTests.m205
-rw-r--r--Example/Auth/Tests/FIRDeleteAccountRequestTests.m98
-rw-r--r--Example/Auth/Tests/FIRDeleteAccountResponseTests.m172
-rw-r--r--Example/Auth/Tests/FIRFakeBackendRPCIssuer.h100
-rw-r--r--Example/Auth/Tests/FIRFakeBackendRPCIssuer.m86
-rw-r--r--Example/Auth/Tests/FIRGetAccountInfoRequestTests.m87
-rw-r--r--Example/Auth/Tests/FIRGetAccountInfoResponseTests.m248
-rw-r--r--Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m149
-rw-r--r--Example/Auth/Tests/FIRGetOOBConfirmationCodeResponseTests.m320
-rw-r--r--Example/Auth/Tests/FIRGitHubAuthProviderTests.m52
-rw-r--r--Example/Auth/Tests/FIRPhoneAuthProviderTests.m550
-rw-r--r--Example/Auth/Tests/FIRResetPasswordRequestTests.m101
-rw-r--r--Example/Auth/Tests/FIRResetPasswordResponseTests.m257
-rw-r--r--Example/Auth/Tests/FIRSendVerificationCodeRequestTests.m119
-rw-r--r--Example/Auth/Tests/FIRSendVerificationCodeResponseTests.m221
-rw-r--r--Example/Auth/Tests/FIRSetAccountInfoRequestTests.m285
-rw-r--r--Example/Auth/Tests/FIRSetAccountInfoResponseTests.m530
-rw-r--r--Example/Auth/Tests/FIRSignUpNewUserRequestTests.m140
-rw-r--r--Example/Auth/Tests/FIRSignUpNewUserResponseTests.m291
-rw-r--r--Example/Auth/Tests/FIRTwitterAuthProviderTests.m60
-rw-r--r--Example/Auth/Tests/FIRUserTests.m1801
-rw-r--r--Example/Auth/Tests/FIRVerifyAssertionRequestTests.m232
-rw-r--r--Example/Auth/Tests/FIRVerifyAssertionResponseTests.m426
-rw-r--r--Example/Auth/Tests/FIRVerifyClientRequestTest.m94
-rw-r--r--Example/Auth/Tests/FIRVerifyClientResponseTests.m178
-rw-r--r--Example/Auth/Tests/FIRVerifyCustomTokenRequestTests.m110
-rw-r--r--Example/Auth/Tests/FIRVerifyCustomTokenResponseTests.m274
-rw-r--r--Example/Auth/Tests/FIRVerifyPasswordRequestTest.m163
-rw-r--r--Example/Auth/Tests/FIRVerifyPasswordResponseTests.m454
-rw-r--r--Example/Auth/Tests/FIRVerifyPhoneNumberRequestTests.m154
-rw-r--r--Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m271
-rw-r--r--Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.h112
-rw-r--r--Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.m103
-rw-r--r--Example/Auth/Tests/Tests-Info.plist22
-rw-r--r--Example/Core/App/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Example/Core/App/Base.lproj/Main.storyboard27
-rw-r--r--Example/Core/App/Core-Info.plist49
-rw-r--r--Example/Core/App/FIRAppDelegate.h23
-rw-r--r--Example/Core/App/FIRAppDelegate.m52
-rw-r--r--Example/Core/App/FIRViewController.h21
-rw-r--r--Example/Core/App/FIRViewController.m35
-rw-r--r--Example/Core/App/GoogleService-Info.plist30
-rw-r--r--Example/Core/App/main.m23
-rw-r--r--Example/Core/Tests/FIRAppAssociationRegistrationUnitTests.m193
-rw-r--r--Example/Core/Tests/FIRAppTest.m582
-rw-r--r--Example/Core/Tests/FIRBundleUtilTest.m86
-rw-r--r--Example/Core/Tests/FIRConfigurationTest.m31
-rw-r--r--Example/Core/Tests/FIRLoggerTest.m265
-rw-r--r--Example/Core/Tests/FIROptionsTest.m468
-rw-r--r--Example/Core/Tests/FIRTestCase.h45
-rw-r--r--Example/Core/Tests/FIRTestCase.m47
-rw-r--r--Example/Core/Tests/Tests-Info.plist22
-rw-r--r--Example/Database/App/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Example/Database/App/Base.lproj/Main.storyboard27
-rw-r--r--Example/Database/App/Database-Info.plist49
-rw-r--r--Example/Database/App/FIRAppDelegate.h23
-rw-r--r--Example/Database/App/FIRAppDelegate.m52
-rw-r--r--Example/Database/App/FIRViewController.h21
-rw-r--r--Example/Database/App/FIRViewController.m35
-rw-r--r--Example/Database/App/main.m23
-rw-r--r--Example/Database/Tests/FirebaseTests-Info.plist22
-rw-r--r--Example/Database/Tests/Helpers/FDevice.h36
-rw-r--r--Example/Database/Tests/Helpers/FDevice.m133
-rw-r--r--Example/Database/Tests/Helpers/FEventTester.h37
-rw-r--r--Example/Database/Tests/Helpers/FEventTester.m172
-rw-r--r--Example/Database/Tests/Helpers/FIRFakeApp.h27
-rw-r--r--Example/Database/Tests/Helpers/FIRFakeApp.m48
-rw-r--r--Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.h28
-rw-r--r--Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m61
-rw-r--r--Example/Database/Tests/Helpers/FMockStorageEngine.h23
-rw-r--r--Example/Database/Tests/Helpers/FMockStorageEngine.m168
-rw-r--r--Example/Database/Tests/Helpers/FTestAuthTokenGenerator.h23
-rw-r--r--Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m90
-rw-r--r--Example/Database/Tests/Helpers/FTestBase.h38
-rw-r--r--Example/Database/Tests/Helpers/FTestBase.m170
-rw-r--r--Example/Database/Tests/Helpers/FTestCachePolicy.h27
-rw-r--r--Example/Database/Tests/Helpers/FTestCachePolicy.m65
-rw-r--r--Example/Database/Tests/Helpers/FTestClock.h28
-rw-r--r--Example/Database/Tests/Helpers/FTestClock.m33
-rw-r--r--Example/Database/Tests/Helpers/FTestContants.h23
-rw-r--r--Example/Database/Tests/Helpers/FTestExpectations.h32
-rw-r--r--Example/Database/Tests/Helpers/FTestExpectations.m88
-rw-r--r--Example/Database/Tests/Helpers/FTestHelpers.h38
-rw-r--r--Example/Database/Tests/Helpers/FTestHelpers.m132
-rw-r--r--Example/Database/Tests/Helpers/FTupleEventTypeString.h33
-rw-r--r--Example/Database/Tests/Helpers/FTupleEventTypeString.m53
-rw-r--r--Example/Database/Tests/Helpers/SenTest+FWaiter.h26
-rw-r--r--Example/Database/Tests/Helpers/SenTest+FWaiter.m57
-rw-r--r--Example/Database/Tests/Integration/FConnectionTest.m77
-rw-r--r--Example/Database/Tests/Integration/FData.h22
-rw-r--r--Example/Database/Tests/Integration/FData.m2687
-rw-r--r--Example/Database/Tests/Integration/FDotInfo.h21
-rw-r--r--Example/Database/Tests/Integration/FDotInfo.m173
-rw-r--r--Example/Database/Tests/Integration/FEventTests.h24
-rw-r--r--Example/Database/Tests/Integration/FEventTests.m506
-rw-r--r--Example/Database/Tests/Integration/FIRAuthTests.m67
-rw-r--r--Example/Database/Tests/Integration/FIRDatabaseQueryTests.h22
-rw-r--r--Example/Database/Tests/Integration/FIRDatabaseQueryTests.m2780
-rw-r--r--Example/Database/Tests/Integration/FIRDatabaseTests.m375
-rw-r--r--Example/Database/Tests/Integration/FKeepSyncedTest.m230
-rw-r--r--Example/Database/Tests/Integration/FOrder.h22
-rw-r--r--Example/Database/Tests/Integration/FOrder.m646
-rw-r--r--Example/Database/Tests/Integration/FOrderByTests.h22
-rw-r--r--Example/Database/Tests/Integration/FOrderByTests.m671
-rw-r--r--Example/Database/Tests/Integration/FPersist.h22
-rw-r--r--Example/Database/Tests/Integration/FPersist.m489
-rw-r--r--Example/Database/Tests/Integration/FRealtime.h22
-rw-r--r--Example/Database/Tests/Integration/FRealtime.m605
-rw-r--r--Example/Database/Tests/Integration/FTransactionTest.h21
-rw-r--r--Example/Database/Tests/Integration/FTransactionTest.m1382
-rw-r--r--Example/Database/Tests/Unit/FArraySortedDictionaryTest.m485
-rw-r--r--Example/Database/Tests/Unit/FCompoundHashTest.m141
-rw-r--r--Example/Database/Tests/Unit/FCompoundWriteTest.m526
-rw-r--r--Example/Database/Tests/Unit/FIRDataSnapshotTests.h21
-rw-r--r--Example/Database/Tests/Unit/FIRDataSnapshotTests.m449
-rw-r--r--Example/Database/Tests/Unit/FIRMutableDataTests.h21
-rw-r--r--Example/Database/Tests/Unit/FIRMutableDataTests.m113
-rw-r--r--Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m583
-rw-r--r--Example/Database/Tests/Unit/FNodeTests.m174
-rw-r--r--Example/Database/Tests/Unit/FPathTests.h21
-rw-r--r--Example/Database/Tests/Unit/FPathTests.m84
-rw-r--r--Example/Database/Tests/Unit/FPersistenceManagerTest.m106
-rw-r--r--Example/Database/Tests/Unit/FPruneForestTest.m98
-rw-r--r--Example/Database/Tests/Unit/FPruningTest.m293
-rw-r--r--Example/Database/Tests/Unit/FQueryParamsTest.m162
-rw-r--r--Example/Database/Tests/Unit/FRangeMergeTest.m271
-rw-r--r--Example/Database/Tests/Unit/FRepoInfoTest.m44
-rw-r--r--Example/Database/Tests/Unit/FSparseSnapshotTests.h21
-rw-r--r--Example/Database/Tests/Unit/FSparseSnapshotTests.m207
-rw-r--r--Example/Database/Tests/Unit/FSyncPointTests.h21
-rw-r--r--Example/Database/Tests/Unit/FSyncPointTests.m905
-rw-r--r--Example/Database/Tests/Unit/FTrackedQueryManagerTest.m338
-rw-r--r--Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m574
-rw-r--r--Example/Database/Tests/Unit/FUtilitiesTest.m116
-rw-r--r--Example/Database/Tests/en.lproj/InfoPlist.strings2
-rw-r--r--Example/Database/Tests/syncPointSpec.json8203
-rw-r--r--Example/Database/Tests/third_party/Base64.h53
-rw-r--r--Example/Database/Tests/third_party/Base64.m202
-rw-r--r--Example/Firebase.xcodeproj/project.pbxproj3524
-rw-r--r--Example/Firebase.xcodeproj/xcshareddata/xcschemes/AllUnitTests.xcscheme201
-rw-r--r--Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_Tests.xcscheme56
-rw-r--r--Example/Firebase.xcodeproj/xcshareddata/xcschemes/Core_Tests.xcscheme56
-rw-r--r--Example/Firebase.xcodeproj/xcshareddata/xcschemes/Database_IntegrationTests.xcscheme56
-rw-r--r--Example/Firebase.xcodeproj/xcshareddata/xcschemes/Database_Tests.xcscheme56
-rw-r--r--Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Example.xcscheme101
-rw-r--r--Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Tests.xcscheme56
-rw-r--r--Example/Firebase.xcodeproj/xcshareddata/xcschemes/Storage_IntegrationTests.xcscheme56
-rw-r--r--Example/Firebase.xcodeproj/xcshareddata/xcschemes/Storage_Tests.xcscheme56
-rw-r--r--Example/Messaging/App/AppDelegate.swift114
-rw-r--r--Example/Messaging/App/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Example/Messaging/App/Base.lproj/Main.storyboard48
-rw-r--r--Example/Messaging/App/Data+MessagingExtensions.swift25
-rw-r--r--Example/Messaging/App/Environment.swift28
-rw-r--r--Example/Messaging/App/GoogleService-Info.plist30
-rw-r--r--Example/Messaging/App/Messaging-Info.plist53
-rw-r--r--Example/Messaging/App/MessagingViewController.swift332
-rw-r--r--Example/Messaging/App/Messaging_Example.entitlements8
-rw-r--r--Example/Messaging/App/NotificationsController.swift132
-rw-r--r--Example/Messaging/Messaging_Example-Bridging-Header.h17
-rw-r--r--Example/Messaging/Tests/FIRMessagingClientTest.m308
-rw-r--r--Example/Messaging/Tests/FIRMessagingCodedInputStreamTest.m116
-rw-r--r--Example/Messaging/Tests/FIRMessagingConnectionTest.m480
-rw-r--r--Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m183
-rw-r--r--Example/Messaging/Tests/FIRMessagingDataMessageManagerTest.m662
-rw-r--r--Example/Messaging/Tests/FIRMessagingFakeConnection.h63
-rw-r--r--Example/Messaging/Tests/FIRMessagingFakeConnection.m150
-rw-r--r--Example/Messaging/Tests/FIRMessagingFakeSocket.h36
-rw-r--r--Example/Messaging/Tests/FIRMessagingFakeSocket.m89
-rw-r--r--Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m94
-rw-r--r--Example/Messaging/Tests/FIRMessagingPendingTopicsListTest.m263
-rw-r--r--Example/Messaging/Tests/FIRMessagingPubSubTest.m81
-rw-r--r--Example/Messaging/Tests/FIRMessagingRegistrarTest.m134
-rw-r--r--Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m279
-rw-r--r--Example/Messaging/Tests/FIRMessagingRmqManagerTest.m332
-rw-r--r--Example/Messaging/Tests/FIRMessagingSecureSocketTest.m323
-rw-r--r--Example/Messaging/Tests/FIRMessagingServiceTest.m288
-rw-r--r--Example/Messaging/Tests/FIRMessagingSyncMessageManagerTest.m256
-rw-r--r--Example/Messaging/Tests/FIRMessagingTest.m214
-rw-r--r--Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.h23
-rw-r--r--Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.m31
-rw-r--r--Example/Messaging/Tests/Info.plist24
-rw-r--r--Example/Podfile61
-rw-r--r--Example/Shared/FIRSampleAppUtilities.h26
-rw-r--r--Example/Shared/FIRSampleAppUtilities.m113
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Contents.json113
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-76.pngbin0 -> 4830 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-76@2x.pngbin0 -> 9882 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-83.5@2x.pngbin0 -> 11532 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Notification.pngbin0 -> 1806 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Notification@2x.pngbin0 -> 2811 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Settings.pngbin0 -> 2233 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Settings@2x.pngbin0 -> 3777 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Spotlight.pngbin0 -> 2811 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Spotlight@2x.pngbin0 -> 4995 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-60@2x.pngbin0 -> 7563 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-60@3x.pngbin0 -> 12489 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Notification@2x.pngbin0 -> 2811 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Notification@3x.pngbin0 -> 3942 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Settings@2x.pngbin0 -> 3777 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Settings@3x.pngbin0 -> 5588 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Spotlight@2x.pngbin0 -> 4995 bytes
-rw-r--r--Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Spotlight@3x.pngbin0 -> 7563 bytes
-rw-r--r--Example/Shared/Shared.xcassets/Contents.json6
-rw-r--r--Example/Storage/App/1mb.datbin0 -> 1048576 bytes
-rw-r--r--Example/Storage/App/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Example/Storage/App/Base.lproj/Main.storyboard27
-rw-r--r--Example/Storage/App/FIRAppDelegate.h23
-rw-r--r--Example/Storage/App/FIRAppDelegate.m52
-rw-r--r--Example/Storage/App/FIRViewController.h21
-rw-r--r--Example/Storage/App/FIRViewController.m35
-rw-r--r--Example/Storage/App/Storage-Info.plist49
-rw-r--r--Example/Storage/App/main.m23
-rw-r--r--Example/Storage/Tests/Integration/FIRStorageIntegrationTests.m520
-rw-r--r--Example/Storage/Tests/Tests-Info.plist22
-rw-r--r--Example/Storage/Tests/Unit/FIRStorageDeleteTests.m164
-rw-r--r--Example/Storage/Tests/Unit/FIRStorageGetMetadataTests.m188
-rw-r--r--Example/Storage/Tests/Unit/FIRStorageMetadataTests.m282
-rw-r--r--Example/Storage/Tests/Unit/FIRStoragePathTests.m234
-rw-r--r--Example/Storage/Tests/Unit/FIRStorageReferenceTests.m163
-rw-r--r--Example/Storage/Tests/Unit/FIRStorageTestHelpers.h127
-rw-r--r--Example/Storage/Tests/Unit/FIRStorageTestHelpers.m128
-rw-r--r--Example/Storage/Tests/Unit/FIRStorageTests.m214
-rw-r--r--Example/Storage/Tests/Unit/FIRStorageTokenAuthorizerTests.m226
-rw-r--r--Example/Storage/Tests/Unit/FIRStorageUpdateMetadataTests.m199
-rw-r--r--Example/Storage/Tests/Unit/FIRStorageUtilsTests.m125
-rw-r--r--Firebase/Auth/CHANGELOG.md62
-rw-r--r--Firebase/Auth/Docs/threading.md119
-rw-r--r--Firebase/Auth/FirebaseAuth.podspec56
-rw-r--r--Firebase/Auth/README.md8
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.h63
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m35
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h48
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m51
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.h49
-rw-r--r--Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.m35
-rw-r--r--Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.h36
-rw-r--r--Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m51
-rw-r--r--Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h51
-rw-r--r--Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m36
-rw-r--r--Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.h41
-rw-r--r--Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m49
-rw-r--r--Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h51
-rw-r--r--Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m36
-rw-r--r--Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.h38
-rw-r--r--Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m54
-rw-r--r--Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.h53
-rw-r--r--Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m37
-rw-r--r--Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.h55
-rw-r--r--Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m50
-rw-r--r--Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.h64
-rw-r--r--Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m42
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.h37
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.m65
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h70
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.h90
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m213
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.h36
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.m36
-rw-r--r--Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.h48
-rw-r--r--Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m51
-rw-r--r--Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.h52
-rw-r--r--Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m36
-rw-r--r--Firebase/Auth/Source/FIRActionCodeSettings.m39
-rw-r--r--Firebase/Auth/Source/FIRAdditionalUserInfo.h59
-rw-r--r--Firebase/Auth/Source/FIRAdditionalUserInfo.m98
-rw-r--r--Firebase/Auth/Source/FIRAuth.h612
-rw-r--r--Firebase/Auth/Source/FIRAuth.m1252
-rw-r--r--Firebase/Auth/Source/FIRAuthAPNSToken.m34
-rw-r--r--Firebase/Auth/Source/FIRAuthAPNSTokenManager.m255
-rw-r--r--Firebase/Auth/Source/FIRAuthAPNSTokenType.h42
-rw-r--r--Firebase/Auth/Source/FIRAuthAppCredential.m64
-rw-r--r--Firebase/Auth/Source/FIRAuthAppCredentialManager.m164
-rw-r--r--Firebase/Auth/Source/FIRAuthAppDelegateProxy.m245
-rw-r--r--Firebase/Auth/Source/FIRAuthCredential.h43
-rw-r--r--Firebase/Auth/Source/FIRAuthCredential.m42
-rw-r--r--Firebase/Auth/Source/FIRAuthDataResult.h51
-rw-r--r--Firebase/Auth/Source/FIRAuthDataResult.m69
-rw-r--r--Firebase/Auth/Source/FIRAuthDispatcher.m46
-rw-r--r--Firebase/Auth/Source/FIRAuthErrorUtils.m794
-rw-r--r--Firebase/Auth/Source/FIRAuthErrors.h258
-rw-r--r--Firebase/Auth/Source/FIRAuthExceptionUtils.h41
-rw-r--r--Firebase/Auth/Source/FIRAuthExceptionUtils.m36
-rw-r--r--Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m26
-rw-r--r--Firebase/Auth/Source/FIRAuthKeychain.m256
-rw-r--r--Firebase/Auth/Source/FIRAuthNotificationManager.m175
-rw-r--r--Firebase/Auth/Source/FIRAuthProvider.m38
-rw-r--r--Firebase/Auth/Source/FIRAuthSerialTaskQueue.m52
-rw-r--r--Firebase/Auth/Source/FIRAuthSwiftNameSupport.h29
-rw-r--r--Firebase/Auth/Source/FIRAuthUserDefaultsStorage.m78
-rw-r--r--Firebase/Auth/Source/FIRSecureTokenService.h96
-rw-r--r--Firebase/Auth/Source/FIRSecureTokenService.m214
-rw-r--r--Firebase/Auth/Source/FIRUser.h463
-rw-r--r--Firebase/Auth/Source/FIRUser.m1170
-rw-r--r--Firebase/Auth/Source/FIRUserInfo.h62
-rw-r--r--Firebase/Auth/Source/FIRUserInfoImpl.h61
-rw-r--r--Firebase/Auth/Source/FIRUserInfoImpl.m127
-rw-r--r--Firebase/Auth/Source/FirebaseAuth.h27
-rwxr-xr-xFirebase/Auth/Source/FirebaseAuthVersion.h27
-rw-r--r--Firebase/Auth/Source/FirebaseAuthVersion.m26
-rw-r--r--Firebase/Auth/Source/Private/FIRActionCodeSettings.h91
-rw-r--r--Firebase/Auth/Source/Private/FIRAdditionalUserInfo_Internal.h46
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthAPNSToken.h54
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthAPNSTokenManager.h69
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthAppCredential.h53
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthAppCredentialManager.h85
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthAppDelegateProxy.h74
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthCredential_Internal.h41
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthDataResult_Internal.h34
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthDispatcher.h63
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthErrorUtils.h418
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthGlobalWorkQueue.h31
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthInternalErrors.h365
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthKeychain.h70
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthNotificationManager.h71
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthSerialTaskQueue.h50
-rw-r--r--Firebase/Auth/Source/Private/FIRAuthUserDefaultsStorage.h47
-rw-r--r--Firebase/Auth/Source/Private/FIRAuth_Internal.h123
-rw-r--r--Firebase/Auth/Source/Private/FIRUser_Internal.h80
-rw-r--r--Firebase/Auth/Source/RPCs/FIRAuthBackend.h496
-rw-r--r--Firebase/Auth/Source/RPCs/FIRAuthBackend.m943
-rw-r--r--Firebase/Auth/Source/RPCs/FIRAuthRPCRequest.h40
-rw-r--r--Firebase/Auth/Source/RPCs/FIRAuthRPCResponse.h49
-rw-r--r--Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.h86
-rw-r--r--Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m95
-rw-r--r--Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h56
-rw-r--r--Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m33
-rw-r--r--Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.h48
-rw-r--r--Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m65
-rw-r--r--Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h26
-rw-r--r--Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m28
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.h51
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m47
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.h146
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m94
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h87
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m135
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.h35
-rw-r--r--Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.m38
-rw-r--r--Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.h57
-rw-r--r--Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.m57
-rw-r--r--Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.h53
-rw-r--r--Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m56
-rw-r--r--Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.h52
-rw-r--r--Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m31
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.h109
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m141
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.h50
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m70
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.h56
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.m73
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.h32
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.m36
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.h149
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m174
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.h98
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m61
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.h67
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m82
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.h44
-rw-r--r--Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m32
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h100
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m142
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h186
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m78
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.h53
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.m63
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.h38
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.m33
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.h56
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m56
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.h47
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m32
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.h79
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m91
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.h72
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m36
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.h78
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.m97
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.h64
-rw-r--r--Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.m42
-rw-r--r--Firebase/Core/FIRAnalyticsConfiguration.h54
-rw-r--r--Firebase/Core/FIRAnalyticsConfiguration.m61
-rw-r--r--Firebase/Core/FIRApp.h126
-rw-r--r--Firebase/Core/FIRApp.m596
-rw-r--r--Firebase/Core/FIRAppAssociationRegistration.m47
-rw-r--r--Firebase/Core/FIRAppEnvironmentUtil.m207
-rw-r--r--Firebase/Core/FIRBundleUtil.m65
-rw-r--r--Firebase/Core/FIRConfiguration.h80
-rw-r--r--Firebase/Core/FIRConfiguration.m51
-rw-r--r--Firebase/Core/FIRCoreSwiftNameSupport.h29
-rw-r--r--Firebase/Core/FIRErrors.m33
-rw-r--r--Firebase/Core/FIRLogger.m228
-rw-r--r--Firebase/Core/FIRLoggerLevel.h30
-rw-r--r--Firebase/Core/FIRMutableDictionary.m97
-rw-r--r--Firebase/Core/FIRNetwork.m390
-rw-r--r--Firebase/Core/FIRNetworkConstants.m39
-rw-r--r--Firebase/Core/FIRNetworkURLSession.m669
-rw-r--r--Firebase/Core/FIROptions.h131
-rw-r--r--Firebase/Core/FIROptions.m427
-rw-r--r--Firebase/Core/FIRReachabilityChecker.m245
-rw-r--r--Firebase/Core/FIRURLSchemeUtil.m43
-rw-r--r--Firebase/Core/FirebaseCore.h21
-rw-r--r--Firebase/Core/FirebaseCore.podspec35
-rw-r--r--Firebase/Core/Private/FIRAnalyticsConfiguration+Internal.h39
-rw-r--r--Firebase/Core/Private/FIRAppAssociationRegistration.h48
-rw-r--r--Firebase/Core/Private/FIRAppEnvironmentUtil.h48
-rw-r--r--Firebase/Core/Private/FIRAppInternal.h140
-rw-r--r--Firebase/Core/Private/FIRBundleUtil.h57
-rw-r--r--Firebase/Core/Private/FIRErrorCode.h55
-rw-r--r--Firebase/Core/Private/FIRErrors.h43
-rw-r--r--Firebase/Core/Private/FIRLogger.h115
-rw-r--r--Firebase/Core/Private/FIRMutableDictionary.h46
-rw-r--r--Firebase/Core/Private/FIRNetwork.h87
-rw-r--r--Firebase/Core/Private/FIRNetworkConstants.h75
-rw-r--r--Firebase/Core/Private/FIRNetworkLoggerProtocol.h50
-rw-r--r--Firebase/Core/Private/FIRNetworkMessageCode.h52
-rw-r--r--Firebase/Core/Private/FIRNetworkURLSession.h57
-rw-r--r--Firebase/Core/Private/FIROptionsInternal.h108
-rw-r--r--Firebase/Core/Private/FIRReachabilityChecker+Internal.h47
-rw-r--r--Firebase/Core/Private/FIRReachabilityChecker.h83
-rw-r--r--Firebase/Core/Private/FIRURLSchemeUtil.h25
-rw-r--r--Firebase/Database/Api/FIRDataEventType.h39
-rw-r--r--Firebase/Database/Api/FIRDataSnapshot.h148
-rw-r--r--Firebase/Database/Api/FIRDataSnapshot.m101
-rw-r--r--Firebase/Database/Api/FIRDatabase.h140
-rw-r--r--Firebase/Database/Api/FIRDatabase.m268
-rw-r--r--Firebase/Database/Api/FIRDatabaseConfig.h63
-rw-r--r--Firebase/Database/Api/FIRDatabaseConfig.m117
-rw-r--r--Firebase/Database/Api/FIRDatabaseQuery.h315
-rw-r--r--Firebase/Database/Api/FIRDatabaseQuery.m525
-rw-r--r--Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h29
-rw-r--r--Firebase/Database/Api/FIRMutableData.h130
-rw-r--r--Firebase/Database/Api/FIRMutableData.m134
-rw-r--r--Firebase/Database/Api/FIRServerValue.h35
-rw-r--r--Firebase/Database/Api/FIRServerValue.m30
-rw-r--r--Firebase/Database/Api/FIRTransactionResult.h47
-rw-r--r--Firebase/Database/Api/FIRTransactionResult.m39
-rw-r--r--Firebase/Database/Api/FirebaseDatabase.h29
-rw-r--r--Firebase/Database/Api/Private/FIRDataSnapshot_Private.h27
-rw-r--r--Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h43
-rw-r--r--Firebase/Database/Api/Private/FIRDatabaseReference_Private.h29
-rw-r--r--Firebase/Database/Api/Private/FIRDatabase_Private.h28
-rw-r--r--Firebase/Database/Api/Private/FIRMutableData_Private.h26
-rw-r--r--Firebase/Database/Api/Private/FIRTransactionResult_Private.h25
-rw-r--r--Firebase/Database/Api/Private/FTypedefs_Private.h56
-rw-r--r--Firebase/Database/Constants/FConstants.h190
-rw-r--r--Firebase/Database/Constants/FConstants.m183
-rw-r--r--Firebase/Database/Core/FCompoundHash.h40
-rw-r--r--Firebase/Database/Core/FCompoundHash.m236
-rw-r--r--Firebase/Database/Core/FListenProvider.h33
-rw-r--r--Firebase/Database/Core/FListenProvider.m26
-rw-r--r--Firebase/Database/Core/FPersistentConnection.h78
-rw-r--r--Firebase/Database/Core/FPersistentConnection.m945
-rw-r--r--Firebase/Database/Core/FQueryParams.h59
-rw-r--r--Firebase/Database/Core/FQueryParams.m372
-rw-r--r--Firebase/Database/Core/FQuerySpec.h36
-rw-r--r--Firebase/Database/Core/FQuerySpec.m85
-rw-r--r--Firebase/Database/Core/FRangeMerge.h35
-rw-r--r--Firebase/Database/Core/FRangeMerge.m107
-rw-r--r--Firebase/Database/Core/FRepo.h76
-rw-r--r--Firebase/Database/Core/FRepo.m1116
-rw-r--r--Firebase/Database/Core/FRepoInfo.h34
-rw-r--r--Firebase/Database/Core/FRepoInfo.m115
-rw-r--r--Firebase/Database/Core/FRepoManager.h32
-rw-r--r--Firebase/Database/Core/FRepoManager.m131
-rw-r--r--Firebase/Database/Core/FRepo_Private.h42
-rw-r--r--Firebase/Database/Core/FServerValues.h30
-rw-r--r--Firebase/Database/Core/FServerValues.m93
-rw-r--r--Firebase/Database/Core/FSnapshotHolder.h27
-rw-r--r--Firebase/Database/Core/FSnapshotHolder.m46
-rw-r--r--Firebase/Database/Core/FSparseSnapshotTree.h34
-rw-r--r--Firebase/Database/Core/FSparseSnapshotTree.m144
-rw-r--r--Firebase/Database/Core/FSyncPoint.h66
-rw-r--r--Firebase/Database/Core/FSyncPoint.m257
-rw-r--r--Firebase/Database/Core/FSyncTree.h61
-rw-r--r--Firebase/Database/Core/FSyncTree.m817
-rw-r--r--Firebase/Database/Core/FWriteRecord.h40
-rw-r--r--Firebase/Database/Core/FWriteRecord.m117
-rw-r--r--Firebase/Database/Core/FWriteTree.h63
-rw-r--r--Firebase/Database/Core/FWriteTree.m458
-rw-r--r--Firebase/Database/Core/FWriteTreeRef.h51
-rw-r--r--Firebase/Database/Core/FWriteTreeRef.m133
-rw-r--r--Firebase/Database/Core/Operation/FAckUserWrite.h35
-rw-r--r--Firebase/Database/Core/Operation/FAckUserWrite.m55
-rw-r--r--Firebase/Database/Core/Operation/FMerge.h30
-rw-r--r--Firebase/Database/Core/Operation/FMerge.m71
-rw-r--r--Firebase/Database/Core/Operation/FOperation.h34
-rw-r--r--Firebase/Database/Core/Operation/FOperationSource.h34
-rw-r--r--Firebase/Database/Core/Operation/FOperationSource.m73
-rw-r--r--Firebase/Database/Core/Operation/FOverwrite.h30
-rw-r--r--Firebase/Database/Core/Operation/FOverwrite.m62
-rw-r--r--Firebase/Database/Core/Utilities/FIRRetryHelper.h33
-rw-r--r--Firebase/Database/Core/Utilities/FIRRetryHelper.m139
-rw-r--r--Firebase/Database/Core/Utilities/FImmutableTree.h51
-rw-r--r--Firebase/Database/Core/Utilities/FImmutableTree.m421
-rw-r--r--Firebase/Database/Core/Utilities/FPath.h45
-rw-r--r--Firebase/Database/Core/Utilities/FPath.m298
-rw-r--r--Firebase/Database/Core/Utilities/FTree.h48
-rw-r--r--Firebase/Database/Core/Utilities/FTree.m183
-rw-r--r--Firebase/Database/Core/Utilities/FTreeNode.h25
-rw-r--r--Firebase/Database/Core/Utilities/FTreeNode.m36
-rw-r--r--Firebase/Database/Core/View/FCacheNode.h44
-rw-r--r--Firebase/Database/Core/View/FCacheNode.m60
-rw-r--r--Firebase/Database/Core/View/FCancelEvent.h30
-rw-r--r--Firebase/Database/Core/View/FCancelEvent.m55
-rw-r--r--Firebase/Database/Core/View/FChange.h38
-rw-r--r--Firebase/Database/Core/View/FChange.m65
-rw-r--r--Firebase/Database/Core/View/FChildEventRegistration.h37
-rw-r--r--Firebase/Database/Core/View/FChildEventRegistration.m92
-rw-r--r--Firebase/Database/Core/View/FDataEvent.h39
-rw-r--r--Firebase/Database/Core/View/FDataEvent.m74
-rw-r--r--Firebase/Database/Core/View/FEvent.h27
-rw-r--r--Firebase/Database/Core/View/FEventRaiser.h35
-rw-r--r--Firebase/Database/Core/View/FEventRaiser.m72
-rw-r--r--Firebase/Database/Core/View/FEventRegistration.h36
-rw-r--r--Firebase/Database/Core/View/FKeepSyncedEventRegistration.h28
-rw-r--r--Firebase/Database/Core/View/FKeepSyncedEventRegistration.m64
-rw-r--r--Firebase/Database/Core/View/FValueEventRegistration.h34
-rw-r--r--Firebase/Database/Core/View/FValueEventRegistration.m89
-rw-r--r--Firebase/Database/Core/View/FView.h53
-rw-r--r--Firebase/Database/Core/View/FView.m223
-rw-r--r--Firebase/Database/Core/View/FViewCache.h35
-rw-r--r--Firebase/Database/Core/View/FViewCache.m61
-rw-r--r--Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h28
-rw-r--r--Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m80
-rw-r--r--Firebase/Database/Core/View/Filter/FCompleteChildSource.h28
-rw-r--r--Firebase/Database/Core/View/Filter/FIndexedFilter.h27
-rw-r--r--Firebase/Database/Core/View/Filter/FIndexedFilter.m147
-rw-r--r--Firebase/Database/Core/View/Filter/FLimitedFilter.h26
-rw-r--r--Firebase/Database/Core/View/Filter/FLimitedFilter.m243
-rw-r--r--Firebase/Database/Core/View/Filter/FNodeFilter.h71
-rw-r--r--Firebase/Database/FClock.h35
-rw-r--r--Firebase/Database/FClock.m58
-rw-r--r--Firebase/Database/FEventGenerator.h27
-rw-r--r--Firebase/Database/FEventGenerator.m141
-rw-r--r--Firebase/Database/FIRDatabaseConfig_Private.h35
-rw-r--r--Firebase/Database/FIRDatabaseReference.h719
-rw-r--r--Firebase/Database/FIRDatabaseReference.m404
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj438
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.h37
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m282
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch7
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h71
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m158
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h38
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m131
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h43
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m87
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h45
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h45
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m245
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.h25
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m99
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist22
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings2
-rw-r--r--Firebase/Database/FIndex.h50
-rw-r--r--Firebase/Database/FIndex.m38
-rw-r--r--Firebase/Database/FKeyIndex.h23
-rw-r--r--Firebase/Database/FKeyIndex.m115
-rw-r--r--Firebase/Database/FListenComplete.h29
-rw-r--r--Firebase/Database/FListenComplete.m51
-rw-r--r--Firebase/Database/FMaxNode.h23
-rw-r--r--Firebase/Database/FMaxNode.m61
-rw-r--r--Firebase/Database/FNamedNode.h32
-rw-r--r--Firebase/Database/FNamedNode.m94
-rw-r--r--Firebase/Database/FPathIndex.h23
-rw-r--r--Firebase/Database/FPathIndex.m125
-rw-r--r--Firebase/Database/FPriorityIndex.h23
-rw-r--r--Firebase/Database/FPriorityIndex.m118
-rw-r--r--Firebase/Database/FRangedFilter.h32
-rw-r--r--Firebase/Database/FRangedFilter.m118
-rw-r--r--Firebase/Database/FTransformedEnumerator.h24
-rw-r--r--Firebase/Database/FTransformedEnumerator.m43
-rw-r--r--Firebase/Database/FTreeSortedDictionary.h46
-rw-r--r--Firebase/Database/FTreeSortedDictionary.m342
-rw-r--r--Firebase/Database/FValueIndex.h23
-rw-r--r--Firebase/Database/FValueIndex.m106
-rw-r--r--Firebase/Database/FViewProcessor.h41
-rw-r--r--Firebase/Database/FViewProcessor.m654
-rw-r--r--Firebase/Database/FViewProcessorResult.h30
-rw-r--r--Firebase/Database/FViewProcessorResult.m35
-rw-r--r--Firebase/Database/Firebase-Prefix.pch7
-rw-r--r--Firebase/Database/FirebaseDatabase.podspec48
-rw-r--r--Firebase/Database/Info.plist26
-rw-r--r--Firebase/Database/Login/FAuthTokenProvider.h36
-rw-r--r--Firebase/Database/Login/FAuthTokenProvider.m162
-rw-r--r--Firebase/Database/Login/FIRNoopAuthTokenProvider.h22
-rw-r--r--Firebase/Database/Login/FIRNoopAuthTokenProvider.m33
-rw-r--r--Firebase/Database/Persistence/FCachePolicy.h41
-rw-r--r--Firebase/Database/Persistence/FCachePolicy.m79
-rw-r--r--Firebase/Database/Persistence/FLevelDBStorageEngine.h37
-rw-r--r--Firebase/Database/Persistence/FLevelDBStorageEngine.m717
-rw-r--r--Firebase/Database/Persistence/FPendingPut.h55
-rw-r--r--Firebase/Database/Persistence/FPendingPut.m112
-rw-r--r--Firebase/Database/Persistence/FPersistenceManager.h52
-rw-r--r--Firebase/Database/Persistence/FPersistenceManager.m190
-rw-r--r--Firebase/Database/Persistence/FPruneForest.h38
-rw-r--r--Firebase/Database/Persistence/FPruneForest.m177
-rw-r--r--Firebase/Database/Persistence/FStorageEngine.h53
-rw-r--r--Firebase/Database/Persistence/FTrackedQuery.h40
-rw-r--r--Firebase/Database/Persistence/FTrackedQuery.m102
-rw-r--r--Firebase/Database/Persistence/FTrackedQueryManager.h51
-rw-r--r--Firebase/Database/Persistence/FTrackedQueryManager.m321
-rw-r--r--Firebase/Database/Realtime/FConnection.h52
-rw-r--r--Firebase/Database/Realtime/FConnection.m211
-rw-r--r--Firebase/Database/Realtime/FWebSocketConnection.h46
-rw-r--r--Firebase/Database/Realtime/FWebSocketConnection.m305
-rw-r--r--Firebase/Database/Snapshot/FChildrenNode.h40
-rw-r--r--Firebase/Database/Snapshot/FChildrenNode.m385
-rw-r--r--Firebase/Database/Snapshot/FCompoundWrite.h61
-rw-r--r--Firebase/Database/Snapshot/FCompoundWrite.m257
-rw-r--r--Firebase/Database/Snapshot/FEmptyNode.h24
-rw-r--r--Firebase/Database/Snapshot/FEmptyNode.m29
-rw-r--r--Firebase/Database/Snapshot/FIndexedNode.h49
-rw-r--r--Firebase/Database/Snapshot/FIndexedNode.m202
-rw-r--r--Firebase/Database/Snapshot/FLeafNode.h28
-rw-r--r--Firebase/Database/Snapshot/FLeafNode.m250
-rw-r--r--Firebase/Database/Snapshot/FNode.h46
-rw-r--r--Firebase/Database/Snapshot/FSnapshotUtilities.h45
-rw-r--r--Firebase/Database/Snapshot/FSnapshotUtilities.m301
-rw-r--r--Firebase/Database/Utilities/FAtomicNumber.h23
-rw-r--r--Firebase/Database/Utilities/FAtomicNumber.m54
-rw-r--r--Firebase/Database/Utilities/FEventEmitter.h33
-rw-r--r--Firebase/Database/Utilities/FEventEmitter.m145
-rw-r--r--Firebase/Database/Utilities/FNextPushId.h23
-rw-r--r--Firebase/Database/Utilities/FNextPushId.m63
-rw-r--r--Firebase/Database/Utilities/FParsedUrl.h25
-rw-r--r--Firebase/Database/Utilities/FParsedUrl.m24
-rw-r--r--Firebase/Database/Utilities/FStringUtilities.h26
-rw-r--r--Firebase/Database/Utilities/FStringUtilities.m61
-rw-r--r--Firebase/Database/Utilities/FTypedefs.h45
-rw-r--r--Firebase/Database/Utilities/FUtilities.h76
-rw-r--r--Firebase/Database/Utilities/FUtilities.m389
-rw-r--r--Firebase/Database/Utilities/FValidation.h45
-rw-r--r--Firebase/Database/Utilities/FValidation.m312
-rw-r--r--Firebase/Database/Utilities/NSString+FURLUtils.h24
-rw-r--r--Firebase/Database/Utilities/NSString+FURLUtils.m38
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleBoolBlock.h25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m22
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleFirebase.h26
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleFirebase.m25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleNodePath.h28
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleNodePath.m33
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjectNode.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjectNode.m32
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjects.h24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjects.m24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m26
-rw-r--r--Firebase/Database/Utilities/Tuples/FTuplePathValue.h25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTuplePathValue.m38
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.h30
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m37
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m33
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleStringNode.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleStringNode.m34
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTSN.h25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTSN.m24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTransaction.h74
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTransaction.m38
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleUserCallback.h31
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleUserCallback.m35
-rw-r--r--Firebase/Database/module.modulemap13
-rw-r--r--Firebase/Database/third_party/SocketRocket/FSRWebSocket.h107
-rw-r--r--Firebase/Database/third_party/SocketRocket/FSRWebSocket.m1848
-rw-r--r--Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h23
-rw-r--r--Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m37
-rw-r--r--Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb33
-rw-r--r--Firebase/Database/third_party/SocketRocket/fbase64.c318
-rw-r--r--Firebase/Database/third_party/SocketRocket/fbase64.h33
-rw-r--r--Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h105
-rw-r--r--Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm500
-rw-r--r--Firebase/Firebase/Firebase.h73
-rwxr-xr-xFirebase/Firebase/module.modulemap4
-rw-r--r--Firebase/Messaging/FIRMMessageCode.h169
-rw-r--r--Firebase/Messaging/FIRMessaging+FIRApp.h24
-rw-r--r--Firebase/Messaging/FIRMessaging+FIRApp.m111
-rw-r--r--Firebase/Messaging/FIRMessaging.m1071
-rw-r--r--Firebase/Messaging/FIRMessagingCheckinService.h53
-rw-r--r--Firebase/Messaging/FIRMessagingCheckinService.m132
-rw-r--r--Firebase/Messaging/FIRMessagingClient.h156
-rw-r--r--Firebase/Messaging/FIRMessagingClient.m490
-rw-r--r--Firebase/Messaging/FIRMessagingCodedInputStream.h28
-rw-r--r--Firebase/Messaging/FIRMessagingCodedInputStream.m142
-rw-r--r--Firebase/Messaging/FIRMessagingConfig.h46
-rw-r--r--Firebase/Messaging/FIRMessagingConfig.m38
-rw-r--r--Firebase/Messaging/FIRMessagingConnection.h107
-rw-r--r--Firebase/Messaging/FIRMessagingConnection.m711
-rw-r--r--Firebase/Messaging/FIRMessagingConstants.h58
-rw-r--r--Firebase/Messaging/FIRMessagingConstants.m51
-rw-r--r--Firebase/Messaging/FIRMessagingContextManagerService.h44
-rw-r--r--Firebase/Messaging/FIRMessagingContextManagerService.m189
-rw-r--r--Firebase/Messaging/FIRMessagingDataMessageManager.h101
-rw-r--r--Firebase/Messaging/FIRMessagingDataMessageManager.m545
-rw-r--r--Firebase/Messaging/FIRMessagingDefines.h96
-rw-r--r--Firebase/Messaging/FIRMessagingDelayedMessageQueue.h35
-rw-r--r--Firebase/Messaging/FIRMessagingDelayedMessageQueue.m146
-rw-r--r--Firebase/Messaging/FIRMessagingFileLogger.h31
-rw-r--r--Firebase/Messaging/FIRMessagingFileLogger.m108
-rw-r--r--Firebase/Messaging/FIRMessagingInstanceIDProxy.h56
-rw-r--r--Firebase/Messaging/FIRMessagingInstanceIDProxy.m123
-rw-r--r--Firebase/Messaging/FIRMessagingLogger.h97
-rw-r--r--Firebase/Messaging/FIRMessagingLogger.m305
-rw-r--r--Firebase/Messaging/FIRMessagingPacketQueue.h43
-rw-r--r--Firebase/Messaging/FIRMessagingPacketQueue.m103
-rw-r--r--Firebase/Messaging/FIRMessagingPendingTopicsList.h118
-rw-r--r--Firebase/Messaging/FIRMessagingPendingTopicsList.m261
-rw-r--r--Firebase/Messaging/FIRMessagingPersistentSyncMessage.h28
-rw-r--r--Firebase/Messaging/FIRMessagingPersistentSyncMessage.m54
-rw-r--r--Firebase/Messaging/FIRMessagingPubSub.h148
-rw-r--r--Firebase/Messaging/FIRMessagingPubSub.m278
-rw-r--r--Firebase/Messaging/FIRMessagingPubSubRegistrar.h56
-rw-r--r--Firebase/Messaging/FIRMessagingPubSubRegistrar.m78
-rw-r--r--Firebase/Messaging/FIRMessagingReceiver.h31
-rw-r--r--Firebase/Messaging/FIRMessagingReceiver.m141
-rw-r--r--Firebase/Messaging/FIRMessagingRegistrar.h87
-rw-r--r--Firebase/Messaging/FIRMessagingRegistrar.m112
-rw-r--r--Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.h40
-rw-r--r--Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m613
-rw-r--r--Firebase/Messaging/FIRMessagingRmq2PersistentStore.h201
-rw-r--r--Firebase/Messaging/FIRMessagingRmq2PersistentStore.m770
-rw-r--r--Firebase/Messaging/FIRMessagingRmqManager.h190
-rw-r--r--Firebase/Messaging/FIRMessagingRmqManager.m264
-rw-r--r--Firebase/Messaging/FIRMessagingSecureSocket.h56
-rw-r--r--Firebase/Messaging/FIRMessagingSecureSocket.m448
-rw-r--r--Firebase/Messaging/FIRMessagingSyncMessageManager.h59
-rw-r--r--Firebase/Messaging/FIRMessagingSyncMessageManager.m147
-rw-r--r--Firebase/Messaging/FIRMessagingTopicOperation.h45
-rw-r--r--Firebase/Messaging/FIRMessagingTopicOperation.m246
-rw-r--r--Firebase/Messaging/FIRMessagingTopicsCommon.h52
-rw-r--r--Firebase/Messaging/FIRMessagingUtilities.h54
-rw-r--r--Firebase/Messaging/FIRMessagingUtilities.m173
-rw-r--r--Firebase/Messaging/FIRMessagingVersionUtilities.h35
-rw-r--r--Firebase/Messaging/FIRMessagingVersionUtilities.m87
-rw-r--r--Firebase/Messaging/FIRMessaging_Private.h56
-rw-r--r--Firebase/Messaging/FirebaseMessaging.h17
-rw-r--r--Firebase/Messaging/FirebaseMessaging.podspec41
-rw-r--r--Firebase/Messaging/InternalHeaders/FIRMessagingInternalUtilities.h30
-rw-r--r--Firebase/Messaging/NSDictionary+FIRMessaging.h45
-rw-r--r--Firebase/Messaging/NSDictionary+FIRMessaging.m59
-rw-r--r--Firebase/Messaging/NSError+FIRMessaging.h68
-rw-r--r--Firebase/Messaging/NSError+FIRMessaging.m35
-rw-r--r--Firebase/Messaging/Protos/GtalkCore.pbobjc.h1344
-rw-r--r--Firebase/Messaging/Protos/GtalkCore.pbobjc.m2947
-rw-r--r--Firebase/Messaging/Protos/GtalkExtensions.pbobjc.h617
-rw-r--r--Firebase/Messaging/Protos/GtalkExtensions.pbobjc.m1407
-rw-r--r--Firebase/Messaging/Public/FIRMessaging.h486
-rwxr-xr-xFirebase/Messaging/Public/FirebaseMessaging.h17
-rw-r--r--Firebase/Storage/FIRStorage.h130
-rw-r--r--Firebase/Storage/FIRStorage.m233
-rw-r--r--Firebase/Storage/FIRStorageConstants.h173
-rw-r--r--Firebase/Storage/FIRStorageConstants.m83
-rw-r--r--Firebase/Storage/FIRStorageDeleteTask.m54
-rw-r--r--Firebase/Storage/FIRStorageDownloadTask.h39
-rw-r--r--Firebase/Storage/FIRStorageDownloadTask.m162
-rw-r--r--Firebase/Storage/FIRStorageErrors.m172
-rw-r--r--Firebase/Storage/FIRStorageGetMetadataTask.m84
-rw-r--r--Firebase/Storage/FIRStorageMetadata.h149
-rw-r--r--Firebase/Storage/FIRStorageMetadata.m227
-rw-r--r--Firebase/Storage/FIRStorageObservableTask.h63
-rw-r--r--Firebase/Storage/FIRStorageObservableTask.m216
-rw-r--r--Firebase/Storage/FIRStoragePath.m199
-rw-r--r--Firebase/Storage/FIRStorageReference.h244
-rw-r--r--Firebase/Storage/FIRStorageReference.m364
-rw-r--r--Firebase/Storage/FIRStorageSwiftNameSupport.h29
-rw-r--r--Firebase/Storage/FIRStorageTask.h76
-rw-r--r--Firebase/Storage/FIRStorageTask.m65
-rw-r--r--Firebase/Storage/FIRStorageTaskSnapshot.h68
-rw-r--r--Firebase/Storage/FIRStorageTaskSnapshot.m88
-rw-r--r--Firebase/Storage/FIRStorageTokenAuthorizer.m131
-rw-r--r--Firebase/Storage/FIRStorageUpdateMetadataTask.m91
-rw-r--r--Firebase/Storage/FIRStorageUploadTask.h39
-rw-r--r--Firebase/Storage/FIRStorageUploadTask.m199
-rw-r--r--Firebase/Storage/FIRStorageUtils.m121
-rw-r--r--Firebase/Storage/FirebaseStorage.h25
-rw-r--r--Firebase/Storage/FirebaseStorage.podspec44
-rw-r--r--Firebase/Storage/Private/FIRStorageConstants_Private.h145
-rw-r--r--Firebase/Storage/Private/FIRStorageDeleteTask.h34
-rw-r--r--Firebase/Storage/Private/FIRStorageDownloadTask_Private.h52
-rw-r--r--Firebase/Storage/Private/FIRStorageErrors.h54
-rw-r--r--Firebase/Storage/Private/FIRStorageGetMetadataTask.h34
-rw-r--r--Firebase/Storage/Private/FIRStorageMetadata_Private.h52
-rw-r--r--Firebase/Storage/Private/FIRStorageObservableTask_Private.h45
-rw-r--r--Firebase/Storage/Private/FIRStoragePath.h106
-rw-r--r--Firebase/Storage/Private/FIRStorageReference_Private.h37
-rw-r--r--Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.h56
-rw-r--r--Firebase/Storage/Private/FIRStorageTask_Private.h77
-rw-r--r--Firebase/Storage/Private/FIRStorageTokenAuthorizer.h44
-rw-r--r--Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h35
-rw-r--r--Firebase/Storage/Private/FIRStorageUploadTask_Private.h69
-rw-r--r--Firebase/Storage/Private/FIRStorageUtils.h93
-rw-r--r--Firebase/Storage/Private/FIRStorage_Private.h38
-rw-r--r--FirebaseDev.podspec140
-rw-r--r--ISSUE_TEMPLATE.md32
-rw-r--r--LICENSE202
-rw-r--r--PULL_REQUEST_TEMPLATE.md19
-rw-r--r--README.md66
-rwxr-xr-xtest.sh29
945 files changed, 138254 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c354c86
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,50 @@
+AuthSamples/Sample/GoogleService-Info_multi.plist
+AuthSamples/Sample/AuthCredentials.h
+AuthSamples/Sample/GoogleService-Info.plist
+AuthSamples/Sample/Application.plist
+AuthSamples/SwiftSample/GoogleService-Info.plist
+AuthSamples/SwiftSample/Info.plist
+AuthSamples/SwiftSample/AuthCredentials.swift
+
+Example/Database/App/GoogleService-Info.plist
+
+Example/Storage/App/GoogleService-Info.plist
+
+# OS X
+.DS_Store
+
+# Xcode
+build/
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+*.xccheckout
+profile
+*.moved-aside
+DerivedData
+*.hmap
+*.ipa
+
+# Bundler
+.bundle
+
+Carthage
+# Cocoapods recommends against adding the Pods directory to your .gitignore. See
+# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
+
+# Since Firebase is building libraries, not apps, we should not check in Pods.
+# Pods are only used in the Examples and tests and doing a 'pod install' better
+# matches our customers' environments.
+#
+# Note: if you ignore the Pods directory, make sure to uncomment
+# `pod install` in .travis.yml
+#
+Pods/
+Podfile.lock
+*.xcworkspace
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..7e42cc2
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,17 @@
+osx_image: xcode8.1
+language: objective-c
+# cache: cocoapods
+podfile: Example/Podfile
+xcode_workspace: Example/Firebase.xcworkspace
+xcode_scheme: AllTests
+
+rvm: 2.3.1
+before_install:
+ - gem uninstall cocoapods -a
+ - gem install cocoapods -v 1.2.0 # Since Travis is not always on latest version
+ - gem install xcpretty
+ - pod install --project-directory=Example --repo-update
+
+script:
+ - ./test.sh
+ - pod lib lint FirebaseDev.podspec
diff --git a/AuthSamples/ApiTests/FirebearApiTests.m b/AuthSamples/ApiTests/FirebearApiTests.m
new file mode 100644
index 0000000..98c4769
--- /dev/null
+++ b/AuthSamples/ApiTests/FirebearApiTests.m
@@ -0,0 +1,542 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+#import "FIRApp.h"
+#import "FirebaseAuth.h"
+#import "../Sample/AuthCredentials.h"
+
+#ifdef NO_NETWORK
+#import "ioReplayer/IORManager.h"
+#import "ioReplayer/IORTestCase.h"
+#endif
+
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import <GTMSessionFetcher/GTMSessionFetcherService.h>
+
+/** Facebook app access token that will be used for Facebook Graph API, which is different from
+ * account access token.
+ */
+static NSString *const kAppAccessToken = KAPP_ACCESS_TOKEN;
+
+/** The user name string for BYOAuth testing account. */
+static NSString *const kBYOAuthTestingAccountUserName = @"John GoogleSpeed";
+
+/** The url for obtaining a valid custom token string used to test BYOAuth. */
+static NSString *const kCustomTokenUrl = @"https://fb-sa-1211.appspot.com/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 = @"MichaelTest";
+
+static NSString *const kGoogleTestAccountName = @"John Test";
+
+/** The invalid custom token string for testing BYOAuth. */
+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";
+
+/** Error message for invalid custom token sign in. */
+NSString *kInvalidTokenErrorMessage =
+ @"The custom token format is incorrect. Please check the documentation.";
+
+/** Cliend Id for Google sign in project "fb-sa-upgraded". Please be sync with the CLIENT_ID in
+ * Firebear/Sample/GoogleService-Info.plist. */
+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. More details at
+ * http://g3doc/company/teams/user-testing/identity/firebear/faq. */
+NSString *kGoogleTestAccountRefreshToken = KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN;
+
+static NSTimeInterval const kExpectationsTimeout = 30;
+
+#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
+
+#ifdef NO_NETWORK
+@interface ApiTests : IORTestCase
+#else
+@interface ApiTests : XCTestCase
+#endif
+@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:^(FIRUser *user, 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)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<id<FIRUserInfo>> *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:^(FIRUser *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)testSignInWithValidBYOAuthToken {
+ 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:@"BYOAuthToken sign-in finished."];
+
+ [auth signInWithCustomToken:customToken
+ completion:^(FIRUser *_Nullable user, 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 BYOAuthToken sign in. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+
+ XCTAssertEqualObjects(auth.currentUser.displayName, kBYOAuthTestingAccountUserName);
+}
+
+- (void)testSignInWithInvalidBYOAuthToken {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ XCTFail(@"Could not obtain auth object.");
+ }
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Invalid BYOAuthToken sign-in finished."];
+
+ [auth signInWithCustomToken:kInvalidCustomToken
+ completion:^(FIRUser *_Nullable user, 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 BYOAuthToken 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
+
+/** 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. More
+ * details at http://g3doc/company/teams/user-testing/identity/firebear/faq.
+ */
+- (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, kAppAccessToken];
+ 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:^(FIRUser *user, 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=%@", kAppAccessToken];
+ 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/AuthSamples/ApiTests/Info.plist b/AuthSamples/ApiTests/Info.plist
new file mode 100644
index 0000000..6c6c23c
--- /dev/null
+++ b/AuthSamples/ApiTests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/AuthSamples/EarlGreyTests/FirebearEarlGreyTests.m b/AuthSamples/EarlGreyTests/FirebearEarlGreyTests.m
new file mode 100644
index 0000000..7f07e2a
--- /dev/null
+++ b/AuthSamples/EarlGreyTests/FirebearEarlGreyTests.m
@@ -0,0 +1,193 @@
+/*
+ * 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 <EarlGrey/EarlGrey.h>
+#import <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+#import "FIRApp.h"
+#import "FirebaseAuth.h"
+
+#ifdef NO_NETWORK
+#import "ioReplayer/IORTestCase.h"
+#endif
+
+/** 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;
+
+#ifdef NO_NETWORK
+@interface BasicUITest : IORTestCase
+#else
+@interface BasicUITest :XCTestCase
+#endif
+@end
+
+/** Convenience function for EarlGrey tests. */
+id<GREYMatcher> 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<GREYMatcher> 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 =
+ @"The custom token format is incorrect. Please check the documentation.";
+
+ [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/AuthSamples/EarlGreyTests/Info.plist b/AuthSamples/EarlGreyTests/Info.plist
new file mode 100644
index 0000000..6c6c23c
--- /dev/null
+++ b/AuthSamples/EarlGreyTests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/AuthSamples/Podfile b/AuthSamples/Podfile
new file mode 100644
index 0000000..473fefc
--- /dev/null
+++ b/AuthSamples/Podfile
@@ -0,0 +1,40 @@
+use_frameworks!
+platform :ios, '8.0'
+
+target 'Sample' do
+ pod 'FirebaseDev/Auth', :path => '../'
+ pod 'FBSDKLoginKit'
+ pod 'GoogleSignIn'
+ # Lock to the 1.0.9 version of InstanceID since 1.0.10 added a dependency
+ # to FirebaseCore
+ pod 'FirebaseInstanceID', '1.0.9'
+end
+
+target 'SwiftBear' do
+ pod 'FirebaseDev/Auth', :path => '../'
+ pod 'GoogleSignIn'
+ pod 'FirebaseInstanceID', '1.0.9'
+end
+
+target 'ApiTests' do
+ pod 'FirebaseDev/Auth', :path => '../'
+ pod 'GoogleSignIn'
+ pod 'FirebaseInstanceID', '1.0.9'
+ pod 'GTMSessionFetcher'
+end
+
+target 'EarlGreyTests' do
+ pod 'FirebaseDev/Auth', :path => '../'
+ pod 'GoogleSignIn'
+ pod 'FirebaseInstanceID', '1.0.9'
+ pod 'EarlGrey'
+end
+
+target 'TestApp' do
+ pod 'FirebaseDev/Auth', :path => '../'
+end
+
+target 'FirebaseAuthUnitTests' do
+ pod 'FirebaseDev/Auth', :path => '../'
+ pod 'OCMock'
+end
diff --git a/AuthSamples/README.md b/AuthSamples/README.md
new file mode 100644
index 0000000..6117293
--- /dev/null
+++ b/AuthSamples/README.md
@@ -0,0 +1,65 @@
+# Firebase Auth Development
+
+This directory contains a set of samples and tests that integrate with
+FirebaseAuth.
+
+The Podfile specifies the dependencies and is used to construct an Xcode
+workspace consisting of the samples, modifiable FirebaseAuth library, and its
+dependencies.
+
+
+### Running Sample Application or Firebear Api Tests
+
+In order to run this application, you'll need to follow the following steps!
+
+#### GoogleService-Info.plist files
+
+You'll need valid `GoogleService-Info.plist` files for those samples. To get your own `GoogleService-Info.plist` files:
+1. Go to the [Firebase Console](https://console.firebase.google.com/)
+2. Create a new Firebase project, if you don't already have one
+3. For each sample app you want to test, create a new Firebase app with the sample app's bundle identifier (e.g. `com.google.FirebaseExperimental1.dev`)
+4. Download the resulting `GoogleService-Info.plist` and place it in [Sample/GoogleService-Info.plist](Sample/GoogleService-Info.plist)
+
+#### GoogleService-Info_multi.plist files
+
+This feature is for advanced testing.
+1. The developer would need to get a GoogleService-Info.plist from a different iOS client (which can be in a different Firebase project)
+2. Save this plist file as GoogleService-Info_multi.plist in [Sample/GoogleService-Info_multi.plist](Sample/GoogleService-Info_multi.plist). This enables testing that FirebaseAuth continues to work after switching the Firebase App in the runtime.
+
+#### Application.plist file
+
+Please follow the instructions in [Sample/ApplicationTemplate.plist](Sample/ApplicationTemplate.plist) to generate the right Application.plist file
+
+#### Getting your own Credential files
+
+Please follow the instructions in [Sample/AuthCredentialsTemplate.h](Sample/AuthCredentialsTemplate.h) to generate the AuthCredentials.h file.
+
+
+### Running SwiftSample Application
+
+In order to run this application, you'll need to follow the following steps!
+
+#### GoogleService-Info.plist files
+
+You'll need valid `GoogleService-Info.plist` files for those samples. To get your own `GoogleService-Info.plist` files:
+1. Go to the [Firebase Console](https://console.firebase.google.com/)
+2. Create a new Firebase project, if you don't already have one
+3. For each sample app you want to test, create a new Firebase app with the sample app's bundle identifier (e.g. `com.google.SwiftBear`)
+4. Download the resulting `GoogleService-Info.plist` and place it in [SwiftSample/GoogleService-Info.plist](SwiftSample/GoogleService-Info.plist)
+
+#### Info.plist file
+
+Please follow the instructions in [SwiftSample/InfoTemplate.plist](SwiftSample/InfoTemplate.plist) to generate the right Info.plist file
+
+#### Getting your own Credential files
+
+Please follow the instructions in [SwiftSample/AuthCredentialsTemplate.swift](SwiftSample/AuthCredentialsTemplate.swift) to generate the AuthCredentials.swift file.
+
+
+## Usage
+
+```
+$ pod update
+$ open Samples.xcworkspace
+```
+Then select a scheme and run.
diff --git a/AuthSamples/Sample/ApplicationDelegate.h b/AuthSamples/Sample/ApplicationDelegate.h
new file mode 100644
index 0000000..110348d
--- /dev/null
+++ b/AuthSamples/Sample/ApplicationDelegate.h
@@ -0,0 +1,48 @@
+/*
+ * 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/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol OpenURLDelegate <NSObject>
+
+/** @fn handleOpenURL:sourceApplication:
+ @brief Handles application:openURL:... methods for @c UIApplicationDelegate .
+ */
+- (BOOL)handleOpenURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication;
+
+@end
+
+/** @class ApplicationDelegate
+ @brief The sample application's delegate.
+ */
+@interface ApplicationDelegate : UIResponder <UIApplicationDelegate>
+
+/** @property window
+ @brief The sample application's @c UIWindow.
+ */
+@property(strong, nonatomic) UIWindow *window;
+
+/** @fn setOpenURLDelegate:
+ @brief Sets the delegate to handle application:openURL:... methods.
+ @param openURLDelegate The delegate which is not retained by this method.
+ */
++ (void)setOpenURLDelegate:(nullable id<OpenURLDelegate>)openURLDelegate;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/AuthSamples/Sample/ApplicationDelegate.m b/AuthSamples/Sample/ApplicationDelegate.m
new file mode 100644
index 0000000..48577ff
--- /dev/null
+++ b/AuthSamples/Sample/ApplicationDelegate.m
@@ -0,0 +1,80 @@
+/*
+ * 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 "ApplicationDelegate.h"
+
+#import "FIRApp.h"
+#import "FirebaseAuth.h"
+#import "AuthProviders.h"
+#import "MainViewController.h"
+
+#if INTERNAL_GOOGLE3_BUILD
+#import "googlemac/iPhone/Identity/Firebear/InternalUtils/FIRSessionFetcherLogging.h"
+#import "third_party/firebase/ios/Source/FirebaseCore/Library/Private/FIRLogger.h"
+#endif
+
+/** @var gOpenURLDelegate
+ @brief The delegate to for application:openURL:... method.
+ */
+static __weak id<OpenURLDelegate> gOpenURLDelegate;
+
+@implementation ApplicationDelegate
+
++ (void)setOpenURLDelegate:(nullable id<OpenURLDelegate>)openURLDelegate {
+ gOpenURLDelegate = openURLDelegate;
+}
+
+- (BOOL)application:(UIApplication *)application
+ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+#if INTERNAL_GOOGLE3_BUILD
+ [FIRSessionFetcherLogging setEnabled:YES];
+ FIRSetLoggerLevel(FIRLoggerLevelInfo);
+#endif
+
+ // Configure the default Firebase application:
+ [FIRApp configure];
+
+ // Load and present the UI:
+ UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
+ window.rootViewController =
+ [[MainViewController alloc] initWithNibName:NSStringFromClass([MainViewController class])
+ bundle:nil];
+ self.window = window;
+ [self.window makeKeyAndVisible];
+
+ return YES;
+}
+
+- (BOOL)application:(nonnull UIApplication *)application
+ openURL:(nonnull NSURL *)url
+ options:(nonnull NSDictionary<NSString *, id> *)options {
+ return [self application:application
+ openURL:url
+ sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]
+ annotation:options[UIApplicationOpenURLOptionsAnnotationKey]];
+}
+
+- (BOOL)application:(UIApplication *)application
+ openURL:(NSURL *)url
+ sourceApplication:(NSString *)sourceApplication
+ annotation:(id)annotation {
+ if ([gOpenURLDelegate handleOpenURL:url sourceApplication:sourceApplication]) {
+ return YES;
+ }
+ return NO;
+}
+
+@end
diff --git a/AuthSamples/Sample/ApplicationTemplate.plist b/AuthSamples/Sample/ApplicationTemplate.plist
new file mode 100644
index 0000000..de0bba1
--- /dev/null
+++ b/AuthSamples/Sample/ApplicationTemplate.plist
@@ -0,0 +1,88 @@
+<!--
+ For this to be a valid plist file replace the following:
+ $REVERSE_CLIENT_ID:
+ Value of REVERSED_CLIENT_ID key in the GoogleService-Info.plist file.
+ $REVERSE_CLIENT_MULTI_ID:
+ Value of REVERSED_CLIENT_ID key in the GoogleService-Info_multi.plist file.
+ This step is optional. If you don't want to use advanced testing just remove
+ the entire dictionary.
+-->
+
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>Firebear SDK Sample</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>CFBundleURLName</key>
+ <string>$REVERSE_CLIENT_ID</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>$REVERSE_CLIENT_ID</string>
+ </array>
+ </dict>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>CFBundleURLName</key>
+ <string>$REVERSE_CLIENT_MULTI_ID</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>$REVERSE_CLIENT_MULTI_ID</string>
+ </array>
+ </dict>
+ </array>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UIBackgroundModes</key>
+ <array>
+ <string>remote-notification</string>
+ </array>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>LSApplicationQueriesSchemes</key>
+ <array>
+ <string>fbauth2</string>
+ </array>
+</dict>
+</plist>
diff --git a/AuthSamples/Sample/AuthCredentialsTemplate.h b/AuthSamples/Sample/AuthCredentialsTemplate.h
new file mode 100644
index 0000000..17146d9
--- /dev/null
+++ b/AuthSamples/Sample/AuthCredentialsTemplate.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.
+ */
+
+/*
+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.h 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 such an id
+
+$KAPP_ACCESS_TOKEN
+Once you have an Facebook App Id, click on dashboard from your app you can see
+both your App ID and the App Secret. Once you have both of these generate the
+access token using the step 13 of https://smashballoon.com/custom-facebook-feed/access-token/
+Follow the same link for comprehensive information on how to get the access token.
+
+$KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN
+GOOGLE_TEST_ACCOUNT_REFRESH_TOKEN is the Google SignIn refresh token obtained for the Google client ID,
+saved for continuous tests.
+
+The users that are behind these tokens must have user names as declared in the code, i.e.,
+"John Test" for Google and "MichaelTest" for Facebook, or the FirebearApiTests will fail.
+This can be found in ApiTests/FirebearApiTests.m with variable names kFacebookTestAccountName and
+kGoogleTestAccountName
+
+*/
+
+#define KAPP_ACCESS_TOKEN $KAPP_ACCESS_TOKEN
+#define KFACEBOOK_APP_ID $KFACEBOOK_APP_ID
+#define KGOOGLE_CLIENT_ID $KGOOGLE_CLIENT_ID
+#define KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN $KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN
diff --git a/AuthSamples/Sample/AuthProviders.h b/AuthSamples/Sample/AuthProviders.h
new file mode 100644
index 0000000..eccbad9
--- /dev/null
+++ b/AuthSamples/Sample/AuthProviders.h
@@ -0,0 +1,74 @@
+/*
+ * 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/UIKit.h>
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef AuthCredentialCallback
+ @brief The type of block invoked when a @c FIRAuthCredential object is ready or an error has
+ occurred.
+ @param credential The auth credential if any.
+ @param error The error which occurred, if any.
+ */
+typedef void (^AuthCredentialCallback)(FIRAuthCredential *_Nullable credential,
+ NSError *_Nullable error);
+/** @protocol AuthProvider
+ @brief A common interface for auth providers to be used by the sample app.
+ */
+@protocol AuthProvider <NSObject>
+
+/** @fn getAuthCredentialWithPresentingViewController:callback:
+ @brief Gets a @c FIRAuthCredential instance for use with Firebase headless API by signing in.
+ @param viewController The view controller to present the UI.
+ @param callback A block which is invoked when the sign-in flow finishes. Invoked asynchronously
+ on an unspecified thread in the future.
+ */
+- (void)getAuthCredentialWithPresentingViewController:(UIViewController *)viewController
+ callback:(AuthCredentialCallback)callback;
+
+/** @fn signOut
+ @brief Logs out the current provider session, which invalidates any cached crendential.
+ */
+- (void)signOut;
+
+@end
+
+/** @class AuthProviders
+ @brief Namespace for @c AuthProvider instances.
+ */
+@interface AuthProviders : NSObject
+
+/** @fn google
+ @brief Returns a Google auth provider.
+ */
++ (id<AuthProvider>)google;
+
+/** @fn facebook
+ @brief Returns a Facebook auth provider.
+ */
++ (id<AuthProvider>)facebook;
+
+/** @fn init
+ @brief This class is not supposed to be instantiated.
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/AuthSamples/Sample/AuthProviders.m b/AuthSamples/Sample/AuthProviders.m
new file mode 100644
index 0000000..825935f
--- /dev/null
+++ b/AuthSamples/Sample/AuthProviders.m
@@ -0,0 +1,40 @@
+/*
+ * 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 "AuthProviders.h"
+
+#import "FacebookAuthProvider.h"
+#import "GoogleAuthProvider.h"
+
+@implementation AuthProviders
+
++ (id<AuthProvider>)google {
+ static id<AuthProvider> googleAuthProvider;
+ if (!googleAuthProvider) {
+ googleAuthProvider = [[GoogleAuthProvider alloc] init];
+ }
+ return googleAuthProvider;
+}
+
++ (id<AuthProvider>)facebook {
+ static id<AuthProvider> facebookAuthProvider;
+ if (!facebookAuthProvider) {
+ facebookAuthProvider = [[FacebookAuthProvider alloc] init];
+ }
+ return facebookAuthProvider;
+}
+
+@end
diff --git a/AuthSamples/Sample/CustomTokenDataEntryViewController.h b/AuthSamples/Sample/CustomTokenDataEntryViewController.h
new file mode 100644
index 0000000..e783bc7
--- /dev/null
+++ b/AuthSamples/Sample/CustomTokenDataEntryViewController.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 <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef CustomTokenDataEntryViewControllerCompletion
+ @brief The type of callback block invoked when a @c CustomTokenDataEntryViewController is
+ dismissed (by either being cancelled or completed by the user.)
+ @param cancelled Indicates the user cancelled the flow and didn't want to enter a token.
+ @param userEnteredTokenText The token text the user entered.
+ */
+typedef void (^CustomTokenDataEntryViewControllerCompletion)
+ (BOOL cancelled, NSString *_Nullable userEnteredTokenText);
+
+/** @class CustomTokenDataEntryViewController
+ @brief Simple view controller to allow data entry of custom BYOAuth tokens.
+ */
+@interface CustomTokenDataEntryViewController : UIViewController
+
+/** @fn initWithNibName:bundle:
+ @brief Please use initWithCompletion:
+ */
+- (instancetype)initWithNibName:(NSString *_Nullable)nibNameOrNil
+ bundle:(NSBundle *_Nullable)nibBundleOrNil NS_UNAVAILABLE;
+
+/** @fn initWithCoder:
+ @brief Please use initWithCompletion:
+ */
+- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
+
+/** @fn initWithCompletion:
+ @brief Designated initializer.
+ @param completion A block which will be invoked when the user either chooses "cancel" or "done".
+ */
+- (nullable instancetype)initWithCompletion:
+ (CustomTokenDataEntryViewControllerCompletion)completion NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/AuthSamples/Sample/CustomTokenDataEntryViewController.m b/AuthSamples/Sample/CustomTokenDataEntryViewController.m
new file mode 100644
index 0000000..b65c244
--- /dev/null
+++ b/AuthSamples/Sample/CustomTokenDataEntryViewController.m
@@ -0,0 +1,148 @@
+/*
+ * 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 "CustomTokenDataEntryViewController.h"
+
+/** @var navigationBarDefaultHeight
+ @brief The default height to use for new navigation bars' frames.
+ */
+static const NSUInteger navigationBarDefaultHeight = 55;
+
+/** @var navigationBarSystemHeight
+ @brief Set after a navigation bar has been created to obtain the system-specified height of the
+ navigation bar.
+ */
+static NSUInteger navigationBarSystemHeight = navigationBarDefaultHeight;
+
+/** @var kTitle
+ @brief The title of the view controller as it appears at the top of the screen in the navigation
+ bar.
+ */
+static NSString *const kTitle = @"Enter Token";
+
+/** @var kCancel
+ @brief The text for the "Cancel" button.
+ */
+static NSString *const kCancel = @"Cancel";
+
+/** @var kDone
+ @brief The text for the "Done" button.
+ */
+static NSString *const kDone = @"Done";
+
+@implementation CustomTokenDataEntryViewController {
+ /** @var _completion
+ @brief The block we will call when the user presses the "cancel" or "done" buttons.
+ @remarks Passed into the initializer.
+ */
+ CustomTokenDataEntryViewControllerCompletion _completion;
+
+ /** @var _tokenTextView
+ @brief The text view allowing the user to enter their custom token text.
+ @remarks Constructed and set in the method: @c loadTextView.
+ */
+ __weak UITextView *_Nullable _tokenTextView;
+}
+
+- (nullable instancetype)initWithCompletion:
+ (CustomTokenDataEntryViewControllerCompletion)completion {
+ self = [super initWithNibName:nil bundle:nil];
+ if (self) {
+ _completion = completion;
+ }
+ return self;
+}
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ [self loadHeader];
+ [self loadTextView];
+}
+
+#pragma mark - View
+
+/** @fn loadHeader
+ @brief Loads the header bar along the top of the view with "Cancel" and "Done" buttons, as well
+ as a brief title asking the user to enter the custom token text.
+ @remarks Updates navigationBarSystemHeight, so should be called before any method which depends
+ on that variable being updated (like the @c loadTextView method, which uses the value to
+ determine how much room is left on the screen.)
+ */
+- (void)loadHeader {
+ CGRect navBarFrame = CGRectMake(0, 0, self.view.bounds.size.width, navigationBarDefaultHeight);
+ UINavigationBar *navBar = [[UINavigationBar alloc] initWithFrame:navBarFrame];
+ navBar.autoresizingMask = UIViewAutoresizingFlexibleWidth
+ | UIViewAutoresizingFlexibleBottomMargin;
+
+ UINavigationItem *navItem = [[UINavigationItem alloc] initWithTitle:kTitle];
+ UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithTitle:kCancel
+ style:UIBarButtonItemStylePlain
+ target:self
+ action:@selector(cancelPressed:)];
+ UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithTitle:kDone
+ style:UIBarButtonItemStylePlain
+ target:self
+ action:@selector(donePressed:)];
+ navItem.leftBarButtonItem = cancelButton;
+ navItem.rightBarButtonItem = doneButton;
+
+ [navBar setItems:@[ navItem ] animated:NO];
+
+ [self.view addSubview:navBar];
+
+ // Obtain the system-specified height of the navigation bar.
+ navigationBarSystemHeight = navBar.frame.size.height;
+}
+
+/** @fn loadTextView
+ @brief Loads the text field for the user to enter their custom token text.
+ @remarks Relies on the navigationBarSystemHeight variable being correct.
+ */
+- (void)loadTextView {
+ CGRect tokenTextViewFrame =
+ CGRectMake(0,
+ navigationBarSystemHeight,
+ self.view.bounds.size.width,
+ self.view.bounds.size.height - navigationBarSystemHeight);
+ UITextView *tokenTextView = [[UITextView alloc] initWithFrame:tokenTextViewFrame];
+ tokenTextView.backgroundColor = [UIColor whiteColor];
+ tokenTextView.textAlignment = NSTextAlignmentLeft;
+
+ [self.view addSubview:tokenTextView];
+ _tokenTextView = tokenTextView;
+}
+
+#pragma mark - Actions
+
+- (void)cancelPressed:(id)sender {
+ [self finishByCancelling:YES withUserEnteredTokenText:nil];
+}
+
+- (void)donePressed:(id)sender {
+ [self finishByCancelling:NO withUserEnteredTokenText:_tokenTextView.text];
+}
+
+#pragma mark - Workflow
+
+- (void)finishByCancelling:(BOOL)cancelled
+ withUserEnteredTokenText:(nullable NSString *)userEnteredTokenText {
+ [self dismissViewControllerAnimated:YES completion:^{
+ _completion(cancelled,
+ cancelled ? nil : userEnteredTokenText);
+ }];
+}
+
+@end
diff --git a/AuthSamples/Sample/FacebookAuthProvider.h b/AuthSamples/Sample/FacebookAuthProvider.h
new file mode 100644
index 0000000..6c2edaf
--- /dev/null
+++ b/AuthSamples/Sample/FacebookAuthProvider.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 <UIKit/UIKit.h>
+
+#import "AuthProviders.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FacebookAuthProvider
+ @brief The implementation for Facebook auth provider related methods.
+ */
+@interface FacebookAuthProvider : NSObject <AuthProvider>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/AuthSamples/Sample/FacebookAuthProvider.m b/AuthSamples/Sample/FacebookAuthProvider.m
new file mode 100644
index 0000000..19ac4c8
--- /dev/null
+++ b/AuthSamples/Sample/FacebookAuthProvider.m
@@ -0,0 +1,78 @@
+/*
+ * 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 "FacebookAuthProvider.h"
+
+#import "FIRFacebookAuthProvider.h"
+#import "ApplicationDelegate.h"
+#import "AuthCredentials.h"
+#import "FBSDKCoreKit.h"
+#import "FBSDKLoginKit.h"
+
+/** @var kFacebookAppId
+ @brief The App ID for the Facebook SDK.
+ */
+static NSString *const kFacebookAppID = KFACEBOOK_APP_ID;
+
+@interface FacebookAuthProvider () <OpenURLDelegate>
+@end
+
+@implementation FacebookAuthProvider {
+ FBSDKLoginManager *_loginManager;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _loginManager = [[FBSDKLoginManager alloc] init];
+ }
+ return self;
+}
+
+- (void)getAuthCredentialWithPresentingViewController:(UIViewController *)viewController
+ callback:(AuthCredentialCallback)callback {
+ [self signOut];
+
+ [ApplicationDelegate setOpenURLDelegate:self];
+ [FBSDKSettings setAppID:kFacebookAppID];
+ [_loginManager logInWithReadPermissions:@[ @"email" ]
+ fromViewController:viewController
+ handler:^(FBSDKLoginManagerLoginResult *result, NSError *error) {
+ [ApplicationDelegate setOpenURLDelegate:nil];
+ if (!error && result.isCancelled) {
+ error = [NSError errorWithDomain:@"com.google.FirebearSample" code:-1 userInfo:nil];
+ }
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ NSString *accessToken = [FBSDKAccessToken currentAccessToken].tokenString;
+ callback([FIRFacebookAuthProvider credentialWithAccessToken:accessToken], nil);
+ }];
+}
+
+- (void)signOut {
+ [_loginManager logOut];
+}
+
+- (BOOL)handleOpenURL:(NSURL *)URL sourceApplication:(NSString *)sourceApplication {
+ return [[FBSDKApplicationDelegate sharedInstance] application:[UIApplication sharedApplication]
+ openURL:URL
+ sourceApplication:sourceApplication
+ annotation:nil];
+}
+
+@end
diff --git a/AuthSamples/Sample/GoogleAuthProvider.h b/AuthSamples/Sample/GoogleAuthProvider.h
new file mode 100644
index 0000000..50679a6
--- /dev/null
+++ b/AuthSamples/Sample/GoogleAuthProvider.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 <UIKit/UIKit.h>
+
+#import "AuthProviders.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class GoogleAuthProvider
+ @brief The implementation for Google auth provider related methods.
+ */
+@interface GoogleAuthProvider : NSObject <AuthProvider>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/AuthSamples/Sample/GoogleAuthProvider.m b/AuthSamples/Sample/GoogleAuthProvider.m
new file mode 100644
index 0000000..a1db437
--- /dev/null
+++ b/AuthSamples/Sample/GoogleAuthProvider.m
@@ -0,0 +1,132 @@
+/*
+ * 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 "GoogleAuthProvider.h"
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+#import "FIRGoogleAuthProvider.h"
+#import "ApplicationDelegate.h"
+
+@import GoogleSignIn;
+
+
+/** @typedef GoogleSignInCallback
+ @brief The type of block invoked when a @c GIDGoogleUser object is ready or an error has
+ occurred.
+ @param user The Google user if any.
+ @param error The error which occurred, if any.
+ */
+typedef void (^GoogleSignInCallback)(GIDGoogleUser *user, NSError *error);
+
+/** @class GoogleAuthDelegate
+ @brief The designated delegate class for Google Sign-In.
+ @param callback A block which is invoked when the sign-in flow finishes. Invoked asynchronously
+ on an unspecified thread in the future.
+ */
+@interface GoogleAuthDelegate : NSObject <GIDSignInDelegate, GIDSignInUIDelegate, OpenURLDelegate>
+
+/** @fn initWithPresentingViewController:callback:
+ @brief Initializes the new instance with the callback.
+ @param presentingViewController The view controller to present the UI.
+ @param callback A block which is invoked when the sign-in flow finishes. Invoked asynchronously
+ on an unspecified thread in the future.
+ */
+- (instancetype)initWithPresentingViewController:(UIViewController *)presentingViewController
+ callback:(nullable GoogleSignInCallback)callback;
+
+@end
+
+@implementation GoogleAuthDelegate {
+ UIViewController *_presentingViewController;
+ GoogleSignInCallback _callback;
+}
+
+- (instancetype)initWithPresentingViewController:(UIViewController *)presentingViewController
+ callback:(nullable GoogleSignInCallback)callback {
+ self = [super init];
+ if (self) {
+ _presentingViewController = presentingViewController;
+ _callback = callback;
+ }
+ return self;
+}
+
+- (void)signIn:(GIDSignIn *)signIn
+ didSignInForUser:(GIDGoogleUser *)user
+ withError:(NSError *)error {
+ GoogleSignInCallback callback = _callback;
+ _callback = nil;
+ if (callback) {
+ callback(user, error);
+ }
+}
+
+- (void)signIn:(GIDSignIn *)signIn presentViewController:(UIViewController *)viewController {
+ [_presentingViewController presentViewController:viewController animated:YES completion:nil];
+}
+
+- (void)signIn:(GIDSignIn *)signIn dismissViewController:(UIViewController *)viewController {
+ [_presentingViewController dismissViewControllerAnimated:YES completion:nil];
+}
+
+- (BOOL)handleOpenURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication {
+ return [[GIDSignIn sharedInstance] handleURL:url
+ sourceApplication:sourceApplication
+ annotation:nil];
+}
+
+@end
+
+@implementation GoogleAuthProvider
+
+- (void)getAuthCredentialWithPresentingViewController:(UIViewController *)viewController
+ callback:(AuthCredentialCallback)callback {
+ [self signOut];
+
+ // The delegate needs to be retained.
+ __block GoogleAuthDelegate *delegate = [[GoogleAuthDelegate alloc]
+ initWithPresentingViewController:viewController
+ callback:^(GIDGoogleUser *user, NSError *error) {
+ [ApplicationDelegate setOpenURLDelegate:nil];
+ delegate = nil;
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ GIDAuthentication *auth = user.authentication;
+ FIRAuthCredential *credential = [FIRGoogleAuthProvider credentialWithIDToken:auth.idToken
+ accessToken:auth.accessToken];
+ callback(credential, error);
+ }];
+ GIDSignIn *signIn = [GIDSignIn sharedInstance];
+ signIn.clientID = [self googleClientID];
+ signIn.shouldFetchBasicProfile = YES;
+ signIn.delegate = delegate;
+ signIn.uiDelegate = delegate;
+ [ApplicationDelegate setOpenURLDelegate:delegate];
+ [signIn signIn];
+}
+
+- (void)signOut {
+ [[GIDSignIn sharedInstance] signOut];
+}
+
+- (NSString *)googleClientID {
+ return [FIRApp defaultApp].options.clientID;
+}
+
+@end
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/Contents.json b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..4f6afa6
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,177 @@
+{
+ "images" : [
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_21in29dp-1.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_21in29dp-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_3x_ios_21in29dp.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_29in40dp-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_3x_ios_29in40dp-1.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "57x57",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_42in57dp.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "57x57",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_42in57dp.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_44in60dp.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_3x_ios_44in60dp.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_21in29dp.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_21in29dp.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_29in40dp-1.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_29in40dp-2.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "50x50",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_38in50dp.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "50x50",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_38in50dp.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "72x72",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_53in72dp.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "72x72",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_53in72dp.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_56in76dp.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_56in76dp.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "83.5x83.5",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "16x16",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "16x16",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "32x32",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "32x32",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "128x128",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "128x128",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "256x256",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "256x256",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "512x512",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "512x512",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp-1.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp-1.png
new file mode 100644
index 0000000..2976035
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp-1.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp.png
new file mode 100644
index 0000000..2976035
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_29in40dp-1.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_29in40dp-1.png
new file mode 100644
index 0000000..32684ce
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_29in40dp-1.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_38in50dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_38in50dp.png
new file mode 100644
index 0000000..0c98554
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_38in50dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_42in57dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_42in57dp.png
new file mode 100644
index 0000000..3ef403a
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_42in57dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_53in72dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_53in72dp.png
new file mode 100644
index 0000000..ff6c804
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_53in72dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_56in76dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_56in76dp.png
new file mode 100644
index 0000000..df8a953
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_56in76dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp-1.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp-1.png
new file mode 100644
index 0000000..4067017
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp-1.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp.png
new file mode 100644
index 0000000..4067017
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-1.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-1.png
new file mode 100644
index 0000000..9452a26
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-1.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-2.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-2.png
new file mode 100644
index 0000000..9452a26
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-2.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_38in50dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_38in50dp.png
new file mode 100644
index 0000000..436be10
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_38in50dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_42in57dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_42in57dp.png
new file mode 100644
index 0000000..e9c869e
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_42in57dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_44in60dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_44in60dp.png
new file mode 100644
index 0000000..8c5ce9d
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_44in60dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_53in72dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_53in72dp.png
new file mode 100644
index 0000000..0ddd720
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_53in72dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_56in76dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_56in76dp.png
new file mode 100644
index 0000000..2f028cb
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_56in76dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_21in29dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_21in29dp.png
new file mode 100644
index 0000000..69bb8d3
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_21in29dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_29in40dp-1.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_29in40dp-1.png
new file mode 100644
index 0000000..7045675
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_29in40dp-1.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_44in60dp.png b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_44in60dp.png
new file mode 100644
index 0000000..211ef93
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_44in60dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/Contents.json b/AuthSamples/Sample/Images.xcassets/Contents.json
new file mode 100644
index 0000000..da4a164
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/AuthSamples/Sample/Images.xcassets/close.imageset/Contents.json b/AuthSamples/Sample/Images.xcassets/close.imageset/Contents.json
new file mode 100644
index 0000000..b38b207
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/close.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "ic_clear_black_1x_ios_24dp.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_clear_black_2x_ios_24dp.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_clear_black_3x_ios_24dp.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_1x_ios_24dp.png b/AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_1x_ios_24dp.png
new file mode 100644
index 0000000..40a1a84
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_1x_ios_24dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_2x_ios_24dp.png b/AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_2x_ios_24dp.png
new file mode 100644
index 0000000..6bc4372
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_2x_ios_24dp.png
Binary files differ
diff --git a/AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_3x_ios_24dp.png b/AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_3x_ios_24dp.png
new file mode 100644
index 0000000..51b4401
--- /dev/null
+++ b/AuthSamples/Sample/Images.xcassets/close.imageset/ic_clear_black_3x_ios_24dp.png
Binary files differ
diff --git a/AuthSamples/Sample/MainViewController.h b/AuthSamples/Sample/MainViewController.h
new file mode 100644
index 0000000..d2390b1
--- /dev/null
+++ b/AuthSamples/Sample/MainViewController.h
@@ -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 <UIKit/UIKit.h>
+
+@class StaticContentTableViewManager;
+@class UserTableViewCell;
+
+/** @var kCreateUserAccessibilityID
+ @brief The "Create User" button accessibility ID.
+ */
+extern NSString *const kCreateUserAccessibilityID;
+
+/** @class MainViewController
+ @brief The first view controller presented when the application is started.
+ */
+@interface MainViewController : UIViewController
+
+/** @property tableViewManager
+ @brief A @c StaticContentTableViewManager which is used to manage the contents of the table
+ view.
+ */
+@property(nonatomic, strong) IBOutlet StaticContentTableViewManager *tableViewManager;
+
+/** @property tableView
+ @brief A UITableView which is used to display user info and a list of actions.
+ */
+@property(nonatomic, weak) IBOutlet UITableView *tableView;
+
+/** @property userInfoTableViewCell
+ @brief A custom UITableViewCell for displaying the user info.
+ */
+@property(nonatomic, strong) IBOutlet UserTableViewCell *userInfoTableViewCell;
+
+/** @property userInMemoryInfoTableViewCell
+ @brief A custom UITableViewCell for displaying the user info.
+ */
+@property(nonatomic, strong) IBOutlet UserTableViewCell *userInMemoryInfoTableViewCell;
+
+/** @property userToUseCell
+ @brief A custom UITableViewCell for choosing which user to use for user operations (either the
+ currently signed-in user, or the user in "memory".
+ */
+@property(nonatomic, strong) IBOutlet UITableViewCell *userToUseCell;
+
+/** @property consoleTextView
+ @brief A UITextView with a log of the actions performed in the sample app.
+ */
+@property(nonatomic, weak) IBOutlet UITextView *consoleTextView;
+
+/** @fn userToUseDidChange:
+ @brief Should be invoked when the user wishes to switch which user to use for user-related
+ operations in the sample app.
+ @param sender The UISegmentedControl which prompted the change in value. It is assumed that the
+ segment at index 0 represents the "signed-in user" and the segment at index 1 represents the
+ "user in memeory".
+ */
+- (IBAction)userToUseDidChange:(UISegmentedControl *)sender;
+
+/** @fn memoryPlus
+ @brief Works like the "M+" button on a calculator; stores the currently signed-in user as the
+ "user in memory" for the application.
+ */
+- (IBAction)memoryPlus;
+
+/** @fn memoryClear
+ @brief Works like the "MC" button on a calculator; clears the currently stored "user in memory"
+ for the application.
+ */
+- (IBAction)memoryClear;
+
+@end
diff --git a/AuthSamples/Sample/MainViewController.m b/AuthSamples/Sample/MainViewController.m
new file mode 100644
index 0000000..3013a3b
--- /dev/null
+++ b/AuthSamples/Sample/MainViewController.m
@@ -0,0 +1,2496 @@
+/*
+ * 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 "MainViewController.h"
+
+#import <objc/runtime.h>
+
+#import "FIRAdditionalUserInfo.h"
+#import "FIRApp.h"
+#import "FIROAuthProvider.h"
+#import "FIRPhoneAuthCredential.h"
+#import "FIRPhoneAuthProvider.h"
+#import "FirebaseAuth.h"
+#import "CustomTokenDataEntryViewController.h"
+#import "FacebookAuthProvider.h"
+#import "GoogleAuthProvider.h"
+#import "SettingsViewController.h"
+#import "StaticContentTableViewManager.h"
+#import "UIViewController+Alerts.h"
+#import "UserInfoViewController.h"
+#import "UserTableViewCell.h"
+
+#if INTERNAL_GOOGLE3_BUILD
+#import "third_party/objective_c/FirebaseDatabase/FirebaseDatabase.framework/Headers/FirebaseDatabase.h"
+#endif
+
+/** @var kTokenGetButtonText
+ @brief The text of the "Get Token" button.
+ */
+static NSString *const kTokenGetButtonText = @"Get Token";
+
+/** @var kTokenRefreshButtonText
+ @brief The text of the "Refresh Token" button.
+ */
+static NSString *const kTokenRefreshButtonText = @"Force Refresh Token";
+
+/** @var kTokenRefreshedAlertTitle
+ @brief The title of the "Token Refreshed" alert.
+ */
+static NSString *const kTokenRefreshedAlertTitle = @"Token";
+
+/** @var kTokenRefreshErrorAlertTitle
+ @brief The title of the "Token Refresh error" alert.
+ */
+static NSString *const kTokenRefreshErrorAlertTitle = @"Get Token Error";
+
+/** @var kSettingsButtonText
+ @brief The text of the "Settings" button.
+ */
+static NSString *const kSettingsButtonText = @"[Sample App Settings]";
+
+/** @var kUserInfoButtonText
+ @brief The text of the "Show User Info" button.
+ */
+static NSString *const kUserInfoButtonText = @"[Show User Info]";
+
+/** @var kSetImageURLText
+ @brief The text of the "Set Photo url" button.
+ */
+static NSString *const kSetPhotoURLText = @"Set Photo url";
+
+/** @var kSignInButtonText
+ @brief The text of the "Sign In" button.
+ */
+static NSString *const kSignInButtonText = @"Sign In (HEADFUL)";
+
+/** @var kSignInGoogleButtonText
+ @brief The text of the "Google SignIn" button.
+ */
+static NSString *const kSignInGoogleButtonText = @"Sign in with Google";
+
+/** @var kSignInAndRetrieveGoogleButtonText
+ @brief The text of the "Sign in with Google and retrieve data" button.
+ */
+static NSString *const kSignInGoogleAndRetrieveDataButtonText =
+ @"Sign in with Google and retrieve data";
+
+/** @var kSignInFacebookButtonText
+ @brief The text of the "Facebook SignIn" button.
+ */
+static NSString *const kSignInFacebookButtonText = @"Sign in with Facebook";
+
+/** @var kSignInFacebookAndRetrieveDataButtonText
+ @brief The text of the "Facebook SignIn and retrieve data" button.
+ */
+static NSString *const kSignInFacebookAndRetrieveDataButtonText =
+ @"Sign in with Facebook and retrieve data";
+
+/** @var kSignInEmailPasswordButtonText
+ @brief The text of the "Email/Password SignIn" button.
+ */
+static NSString *const kSignInEmailPasswordButtonText = @"Sign in with Email/Password";
+
+/** @var kSignInWithCustomTokenButtonText
+ @brief The text of the "Sign In (BYOAuth)" button.
+ */
+static NSString *const kSignInWithCustomTokenButtonText = @"Sign In (BYOAuth)";
+
+/** @var kSignInAnonymouslyButtonText
+ @brief The text of the "Sign In Anonymously" button.
+ */
+static NSString *const kSignInAnonymouslyButtonText = @"Sign In Anonymously";
+
+/** @var kSignedInAlertTitle
+ @brief The text of the "Sign In Succeeded" alert.
+ */
+static NSString *const kSignedInAlertTitle = @"Signed In";
+
+/** @var kSignInErrorAlertTitle
+ @brief The text of the "Sign In Encountered an Error" alert.
+ */
+static NSString *const kSignInErrorAlertTitle = @"Sign-In Error";
+
+/** @var kSignOutButtonText
+ @brief The text of the "Sign Out" button.
+ */
+static NSString *const kSignOutButtonText = @"Sign Out";
+
+/** @var kDeleteAccountText
+ @brief The text of the "Delete Account" button.
+ */
+static NSString *const kDeleteUserText = @"Delete Account";
+
+/** @var kReauthenticateGoogleText
+ @brief The text of the "Reathenticate Google" button.
+ */
+static NSString *const kReauthenticateGoogleText = @"Reauthenticate Google";
+
+/** @var kReauthenticateGoogleAndRetrieveDataText
+ @brief The text of the "Reathenticate Google and retrieve data" button.
+ */
+static NSString *const kReauthenticateGoogleAndRetrieveDataText =
+ @"Reauthenticate Google and retrieve data";
+
+/** @var kReauthenticateFBText
+ @brief The text of the "Reathenticate Facebook" button.
+ */
+static NSString *const kReauthenticateFBText = @"Reauthenticate FB";
+
+/** @var kReauthenticateFBAndRetrieveDataText
+ @brief The text of the "Reathenticate Facebook and retrieve data" button.
+ */
+static NSString *const kReauthenticateFBAndRetrieveDataText =
+ @"Reauthenticate FB and retrieve data";
+
+/** @var kReAuthenticateEmail
+ @brief The text of the "Reathenticate Email" button.
+ */
+static NSString *const kReauthenticateEmailText = @"Reauthenticate Email/Password";
+
+/** @var kOKButtonText
+ @brief The text of the "OK" button for the Sign In result dialogs.
+ */
+static NSString *const kOKButtonText = @"OK";
+
+/** @var kSetDisplayNameTitle
+ @brief The title of the "Set Display Name" error dialog.
+ */
+static NSString *const kSetDisplayNameTitle = @"Set Display Name";
+
+/** @var kUpdateEmailText
+ @brief The title of the "Update Email" button.
+ */
+static NSString *const kUpdateEmailText = @"Update Email";
+
+/** @var kUpdatePasswordText
+ @brief The title of the "Update Password" button.
+ */
+static NSString *const kUpdatePasswordText = @"Update Password";
+
+/** @var kUpdatePhoneNumber
+ @brief The title of the "Update Photo" button.
+ */
+static NSString *const kUpdatePhoneNumber = @"Update Phone Number";
+
+/** @var kLinkPhoneNumber
+ @brief The title of the "Link phone" button.
+ */
+static NSString *const kLinkPhoneNumber = @"Link Phone Number";
+
+/** @var kUnlinkPhone
+ @brief The title of the "Unlink Phone" button for unlinking phone auth provider.
+ */
+static NSString *const kUnlinkPhoneNumber = @"Unlink Phone Number";
+
+/** @var kReloadText
+ @brief The title of the "Reload User" button.
+ */
+static NSString *const kReloadText = @"Reload User";
+
+/** @var kLinkWithGoogleText
+ @brief The title of the "Link with Google" button.
+ */
+static NSString *const kLinkWithGoogleText = @"Link with Google";
+
+/** @var kLinkWithGoogleAndRetrieveDataText
+ @brief The title of the "Link with Google and retrieve data" button.
+ */
+static NSString *const kLinkWithGoogleAndRetrieveDataText = @"Link with Google and retrieve data";
+
+/** @var kLinkWithFacebookText
+ @brief The title of the "Link with Facebook Account" button.
+ */
+static NSString *const kLinkWithFacebookText = @"Link with Facebook";
+
+/** @var kLinkWithFacebookAndRetrieveDataText
+ @brief The title of the "Link with Facebook and retrieve data" button.
+ */
+static NSString *const kLinkWithFacebookAndRetrieveDataText =
+ @"Link with Facebook and retrieve data";
+
+/** @var kLinkWithEmailPasswordText
+ @brief The title of the "Link with Email/Password Account" button.
+ */
+static NSString *const kLinkWithEmailPasswordText = @"Link with Email/Password";
+
+/** @var kUnlinkTitle
+ @brief The text of the "Unlink from Provider" error Dialog.
+ */
+static NSString *const kUnlinkTitle = @"Unlink from Provider";
+
+/** @var kUnlinkFromGoogle
+ @brief The text of the "Unlink from Google" button.
+ */
+static NSString *const kUnlinkFromGoogle = @"Unlink from Google";
+
+/** @var kUnlinkFromFacebook
+ @brief The text of the "Unlink from Facebook" button.
+ */
+static NSString *const kUnlinkFromFacebook = @"Unlink from Facebook";
+
+/** @var kUnlinkFromEmailPassword
+ @brief The text of the "Unlink from Google" button.
+ */
+static NSString *const kUnlinkFromEmailPassword = @"Unlink from Email/Password";
+
+/** @var kGetProvidersForEmail
+ @brief The text of the "Get Provider IDs for Email" button.
+ */
+static NSString *const kGetProvidersForEmail = @"Get Provider IDs for Email";
+
+/** @var kRequestVerifyEmail
+ @brief The text of the "Request Verify Email Link" button.
+ */
+static NSString *const kRequestVerifyEmail = @"Request Verify Email Link";
+
+/** @var kRequestPasswordReset
+ @brief The text of the "Email Password Reset" button.
+ */
+static NSString *const kRequestPasswordReset = @"Send Password Reset Email";
+
+/** @var kResetPassword
+ @brief The text of the "Password Reset" button.
+ */
+static NSString *const kResetPassword = @"Reset Password";
+
+/** @var kCheckActionCode
+ @brief The text of the "Check action code" button.
+ */
+static NSString *const kCheckActionCode = @"Check action code";
+
+/** @var kApplyActionCode
+ @brief The text of the "Apply action code" button.
+ */
+static NSString *const kApplyActionCode = @"Apply action code";
+
+/** @var kVerifyPasswordResetCode
+ @brief The text of the "Verify password reset code" button.
+ */
+static NSString *const kVerifyPasswordResetCode = @"Verify password reset code";
+
+/** @var kSectionTitleSettings
+ @brief The text for the title of the "Settings" section.
+ */
+static NSString *const kSectionTitleSettings = @"SETTINGS";
+
+/** @var kSectionTitleSignIn
+ @brief The text for the title of the "Sign-In" section.
+ */
+static NSString *const kSectionTitleSignIn = @"SIGN-IN";
+
+/** @var kSectionTitleReauthenticate
+ @brief The text for the title of the "Reauthenticate" section.
+ */
+static NSString *const kSectionTitleReauthenticate = @"REAUTHENTICATE";
+
+/** @var kSectionTitleTokenActions
+ @brief The text for the title of the "Token Actions" section.
+ */
+static NSString *const kSectionTitleTokenActions = @"TOKEN ACTIONS";
+
+/** @var kSectionTitleEditUser
+ @brief The text for the title of the "Edit User" section.
+ */
+static NSString *const kSectionTitleEditUser = @"EDIT USER";
+
+/** @var kSectionTitleLinkUnlinkAccount
+ @brief The text for the title of the "Link/Unlink account" section.
+ */
+static NSString *const kSectionTitleLinkUnlinkAccounts = @"LINK/UNLINK ACCOUNT";
+
+/** @var kSectionTitleUserActions
+ @brief The text for the title of the "User Actions" section.
+ */
+static NSString *const kSectionTitleUserActions = @"USER ACTIONS";
+
+/** @var kSectionTitleUserDetails
+ @brief The text for the title of the "User Details" section.
+ */
+static NSString *const kSectionTitleUserDetails = @"SIGNED-IN USER DETAILS";
+
+/** @var kSectionTitleListeners
+ @brief The title for the table view section dedicated to auth state did change listeners.
+ */
+static NSString *const kSectionTitleListeners = @"Listeners";
+
+/** @var kAddAuthStateListenerTitle
+ @brief The title for the table view row which adds a block to the auth state did change
+ listeners.
+ */
+static NSString *const kAddAuthStateListenerTitle = @"Add Auth State Change Listener";
+
+/** @var kRemoveAuthStateListenerTitle
+ @brief The title for the table view row which removes a block to the auth state did change
+ listeners.
+ */
+static NSString *const kRemoveAuthStateListenerTitle = @"Remove Last Auth State Change Listener";
+
+/** @var kAddIDTokenListenerTitle
+ @brief The title for the table view row which adds a block to the ID token did change
+ listeners.
+ */
+static NSString *const kAddIDTokenListenerTitle = @"Add ID Token Change Listener";
+
+/** @var kRemoveIDTokenListenerTitle
+ @brief The title for the table view row which removes a block to the ID token did change
+ listeners.
+ */
+static NSString *const kRemoveIDTokenListenerTitle = @"Remove Last ID Token Change Listener";
+
+/** @var kSectionTitleApp
+ @brief The text for the title of the "App" section.
+ */
+static NSString *const kSectionTitleApp = @"APP";
+
+/** @var kCreateUserTitle
+ @brief The text of the "Create User" button.
+ */
+static NSString *const kCreateUserTitle = @"Create User";
+
+/** @var kDeleteAppTitle
+ @brief The text of the "Delete App" button.
+ */
+static NSString *const kDeleteAppTitle = @"Delete App";
+
+/** @var kSectionTitleManualTests
+ @brief The section title for automated manual tests.
+ */
+static NSString *const kSectionTitleManualTests = @"Manual Tests";
+
+/** @var kAutoBYOAuthTitle
+ @brief The button title for automated BYOAuth operation.
+ */
+static NSString *const kAutoBYOAuthTitle = @"BYOAuth";
+
+/** @var kAutoSignInGoogle
+ @brief The button title for automated Google sign-in operation.
+ */
+static NSString *const kAutoSignInGoogle = @"Sign In With Google";
+
+/** @var kAutoSignInFacebook
+ @brief The button title for automated Facebook sign-in operation.
+ */
+static NSString *const kAutoSignInFacebook = @"Sign In With Facebook";
+
+/** @var kAutoSignUpEmailPassword
+ @brief The button title for automated sign-up with email/password.
+ */
+static NSString *const kAutoSignUpEmailPassword = @"Sign Up With Email/Password";
+
+/** @var kAutoSignInAnonymously
+ @brief The button title for automated sign-in anonymously.
+ */
+static NSString *const kAutoSignInAnonymously = @"Sign In Anonymously";
+
+/** @var kAutoAccountLinking
+ @brief The button title for automated account linking.
+ */
+static NSString *const kAutoAccountLinking = @"Link with Google";
+
+/** @var kGitHubSignInButtonText
+ @brief The button title for signing in with github.
+ */
+static NSString *const kGitHubSignInButtonText = @"Sign In with GitHub";
+
+/** @var kExpiredCustomTokenUrl
+ @brief The url for obtaining a valid custom token string used to test BYOAuth.
+ */
+static NSString *const kCustomTokenUrl = @"https://fb-sa-1211.appspot.com/token";
+
+/** @var kCustomTokenUrl
+ @brief The url for obtaining an expired custom token string used to test BYOAuth.
+ */
+static NSString *const kExpiredCustomTokenUrl = @"https://fb-sa-1211.appspot.com/expired_token";
+
+/** @var kFakeDisplayPhotoUrl
+ @brief The url for obtaining a display displayPhoto used for testing.
+ */
+static NSString *const kFakeDisplayPhotoUrl =
+ @"https://www.gstatic.com/images/branding/product/1x/play_apps_48dp.png";
+
+/** @var kFakeDisplayName
+ @brief Fake display name for testing.
+ */
+static NSString *const kFakeDisplayName = @"John GoogleSpeed";
+
+/** @var kFakeEmail
+ @brief Fake email for testing.
+ */
+static NSString *const kFakeEmail =@"firemail@example.com";
+
+/** @var kFakePassword
+ @brief Fake password for testing.
+ */
+static NSString *const kFakePassword =@"fakePassword";
+
+/** @var kInvalidCustomToken
+ @brief The custom token string for testing BYOAuth.
+ */
+static NSString *const kInvalidCustomToken = @"invalid custom token.";
+
+/** @var kSafariGoogleSignOutMessagePrompt
+ @brief The message text informing user to sign-out from Google on safari before continuing.
+ */
+static NSString *const kSafariGoogleSignOutMessagePrompt = @"This automated test assumes that no "
+ "Google account is signed in on Safari, if your are not prompted for a password, sign out on "
+ "Safari and rerun the test.";
+
+/** @var kSafariFacebookSignOutMessagePrompt
+ @brief The message text informing user to sign-out from Facebook on safari before continuing.
+ */
+static NSString *const kSafariFacebookSignOutMessagePrompt = @"This automated test assumes that no "
+ "Facebook account is signed in on Safari, if your are not prompted for a password, sign out on "
+ "Safari and rerun the test.";
+
+/** @var kUnlinkAccountMessagePrompt
+ @brief The message text informing user to use an unlinked account for account linking.
+ */
+static NSString *const kUnlinkAccountMessagePrompt = @"Sign into gmail with an email address "
+ "that has not been linked to this sample application before. Delete account if necessary.";
+
+// Declared extern in .h file.
+NSString *const kCreateUserAccessibilityID = @"CreateUserAccessibilityID";
+
+/** @var kPhoneAuthSectionTitle
+ @brief The title for the phone auth section of the test app.
+ */
+static NSString *const kPhoneAuthSectionTitle = @"Phone Auth";
+
+/** @var kPhoneNumberSignInTitle
+ @brief The title for button to sign in with phone number.
+ */
+static NSString *const kPhoneNumberSignInTitle = @"Sign in With Phone Number";
+
+/** @typedef showEmailPasswordDialogCompletion
+ @brief The type of block which gets called to complete the Email/Password dialog flow.
+ */
+typedef void (^ShowEmailPasswordDialogCompletion)(FIRAuthCredential *credential);
+
+/** @typedef FIRTokenCallback
+ @brief The type of block which gets called when a token is ready.
+ */
+typedef void (^FIRTokenCallback)(NSString *_Nullable token, NSError *_Nullable error);
+
+/** @brief Method declarations copied from FIRAppInternal.h to avoid importing non-public headers.
+ */
+@interface FIRApp (Internal)
+/** @fn getTokenForcingRefresh:withCallback:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it.
+ @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason
+ other than an expiration.
+ @param callback The block to invoke when the token is available.
+ */
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(nonnull FIRTokenCallback)callback;
+@end
+
+@implementation MainViewController {
+ NSMutableString *_consoleString;
+
+ /** @var _authStateDidChangeListeners
+ @brief An array of handles created during calls to @c FIRAuth.addAuthStateDidChangeListener:
+ */
+ NSMutableArray<FIRAuthStateDidChangeListenerHandle> *_authStateDidChangeListeners;
+
+ /** @var _IDTokenDidChangeListeners
+ @brief An array of handles created during calls to @c FIRAuth.addIDTokenDidChangeListener:
+ */
+ NSMutableArray<FIRAuthStateDidChangeListenerHandle> *_IDTokenDidChangeListeners;
+
+ /** @var _userInMemory
+ @brief Acts like the "memory" function of a calculator. An operation allows sample app users
+ to assign this value based on @c FIRAuth.currentUser or clear this value.
+ */
+ FIRUser *_userInMemory;
+
+ /** @var _useUserInMemory
+ @brief Instructs the application to use _userInMemory instead of @c FIRAuth.currentUser for
+ testing operations. This allows us to test if things still work with a user who is not
+ the @c FIRAuth.currentUser, and also allows us to test those things while
+ @c FIRAuth.currentUser remains nil (after a sign-out) and also when @c FIRAuth.currentUser
+ is non-nil (do to a subsequent sign-in.)
+ */
+ BOOL _useUserInMemory;
+}
+
+/** @fn initWithNibName:bundle:
+ @brief Overridden default initializer.
+ */
+- (id)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil {
+ self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
+ if (self) {
+ _authStateDidChangeListeners = [NSMutableArray array];
+ _IDTokenDidChangeListeners = [NSMutableArray array];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(authStateChangedForAuth:)
+ name:FIRAuthStateDidChangeNotification
+ object:nil];
+ self.useStatusBarSpinner = YES;
+#if INTERNAL_GOOGLE3_BUILD
+ // Trigger automatic token refresh.
+ [[FIRDatabase database] reference];
+#endif
+ }
+ return self;
+}
+
+- (void)viewDidLoad {
+ // Give us a circle for the image view:
+ _userInfoTableViewCell.userInfoProfileURLImageView.layer.cornerRadius =
+ _userInfoTableViewCell.userInfoProfileURLImageView.frame.size.width / 2.0f;
+ _userInfoTableViewCell.userInfoProfileURLImageView.layer.masksToBounds = YES;
+ _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.layer.cornerRadius =
+ _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.frame.size.width / 2.0f;
+ _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.layer.masksToBounds = YES;
+ [self updateTable];
+ [self updateUserInfo];
+}
+
+- (void)updateTable {
+ __weak typeof(self) weakSelf = self;
+ _tableViewManager.contents =
+ [StaticContentTableViewContent contentWithSections:@[
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleUserDetails cells:@[
+ [StaticContentTableViewCell cellWithCustomCell:_userInfoTableViewCell action:^{
+ [weakSelf presentUserInfo];
+ }],
+ [StaticContentTableViewCell cellWithCustomCell:_userToUseCell],
+ [StaticContentTableViewCell cellWithCustomCell:_userInMemoryInfoTableViewCell action:^{
+ [weakSelf presentUserInMemoryInfo];
+ }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleSettings cells:@[
+ [StaticContentTableViewCell cellWithTitle:kSettingsButtonText
+ action:^{ [weakSelf presentSettings]; }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kPhoneAuthSectionTitle cells:@[
+ [StaticContentTableViewCell cellWithTitle:kPhoneNumberSignInTitle
+ action:^{ [weakSelf signInWithPhoneNumber]; }],
+ [StaticContentTableViewCell cellWithTitle:kUpdatePhoneNumber
+ action:^{ [weakSelf updatePhoneNumber]; }],
+ [StaticContentTableViewCell cellWithTitle:kLinkPhoneNumber
+ action:^{ [weakSelf linkPhoneNumber]; }],
+ [StaticContentTableViewCell cellWithTitle:kUnlinkPhoneNumber
+ action:^{
+ [weakSelf unlinkFromProvider:FIRPhoneAuthProviderID];
+ }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleSignIn cells:@[
+ [StaticContentTableViewCell cellWithTitle:kCreateUserTitle
+ value:nil
+ action:^{ [weakSelf createUser]; }
+ accessibilityID:kCreateUserAccessibilityID],
+ [StaticContentTableViewCell cellWithTitle:kSignInGoogleButtonText
+ action:^{ [weakSelf signInGoogle]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInGoogleAndRetrieveDataButtonText
+ action:^{ [weakSelf signInGoogleAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInFacebookButtonText
+ action:^{ [weakSelf signInFacebook]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInFacebookAndRetrieveDataButtonText
+ action:^{ [weakSelf signInFacebookAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInEmailPasswordButtonText
+ action:^{ [weakSelf signInEmailPassword]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInWithCustomTokenButtonText
+ action:^{ [weakSelf signInWithCustomToken]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInAnonymouslyButtonText
+ action:^{ [weakSelf signInAnonymously]; }],
+ [StaticContentTableViewCell cellWithTitle:kGitHubSignInButtonText
+ action:^{ [weakSelf signInWithGitHub]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignOutButtonText
+ action:^{ [weakSelf signOut]; }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleUserActions cells:@[
+ [StaticContentTableViewCell cellWithTitle:kSetDisplayNameTitle
+ action:^{ [weakSelf setDisplayName]; }],
+ [StaticContentTableViewCell cellWithTitle:kSetPhotoURLText
+ action:^{ [weakSelf setPhotoURL]; }],
+ [StaticContentTableViewCell cellWithTitle:kReloadText
+ action:^{ [weakSelf reloadUser]; }],
+ [StaticContentTableViewCell cellWithTitle:kGetProvidersForEmail
+ action:^{ [weakSelf getProvidersForEmail]; }],
+ [StaticContentTableViewCell cellWithTitle:kRequestVerifyEmail
+ action:^{ [weakSelf requestVerifyEmail]; }],
+ [StaticContentTableViewCell cellWithTitle:kRequestPasswordReset
+ action:^{ [weakSelf requestPasswordReset]; }],
+ [StaticContentTableViewCell cellWithTitle:kResetPassword
+ action:^{ [weakSelf resetPassword]; }],
+ [StaticContentTableViewCell cellWithTitle:kCheckActionCode
+ action:^{ [weakSelf checkActionCode]; }],
+ [StaticContentTableViewCell cellWithTitle:kApplyActionCode
+ action:^{ [weakSelf applyActionCode]; }],
+ [StaticContentTableViewCell cellWithTitle:kVerifyPasswordResetCode
+ action:^{ [weakSelf verifyPasswordResetCode]; }],
+ [StaticContentTableViewCell cellWithTitle:kUpdateEmailText
+ action:^{ [weakSelf updateEmail]; }],
+ [StaticContentTableViewCell cellWithTitle:kUpdatePasswordText
+ action:^{ [weakSelf updatePassword]; }],
+ [StaticContentTableViewCell cellWithTitle:kDeleteUserText
+ action:^{ [weakSelf deleteAccount]; }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleReauthenticate cells:@[
+ [StaticContentTableViewCell cellWithTitle:kReauthenticateGoogleText
+ action:^{ [weakSelf reauthenticateGoogle]; }],
+ [StaticContentTableViewCell
+ cellWithTitle:kReauthenticateGoogleAndRetrieveDataText
+ action:^{ [weakSelf reauthenticateGoogleAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kReauthenticateFBText
+ action:^{ [weakSelf reauthenticateFB]; }],
+ [StaticContentTableViewCell cellWithTitle:kReauthenticateFBAndRetrieveDataText
+ action:^{ [weakSelf reauthenticateFBAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kReauthenticateEmailText
+ action:^{ [weakSelf reauthenticateEmailPassword]; }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleTokenActions cells:@[
+ [StaticContentTableViewCell cellWithTitle:kTokenGetButtonText
+ action:^{ [weakSelf getUserTokenWithForce:NO]; }],
+ [StaticContentTableViewCell cellWithTitle:kTokenRefreshButtonText
+ action:^{ [weakSelf getUserTokenWithForce:YES]; }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleLinkUnlinkAccounts cells:@[
+ [StaticContentTableViewCell cellWithTitle:kLinkWithGoogleText
+ action:^{ [weakSelf linkWithGoogle]; }],
+ [StaticContentTableViewCell cellWithTitle:kLinkWithGoogleAndRetrieveDataText
+ action:^{ [weakSelf linkWithGoogleAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kLinkWithFacebookText
+ action:^{ [weakSelf linkWithFacebook]; }],
+ [StaticContentTableViewCell cellWithTitle:kLinkWithFacebookAndRetrieveDataText
+ action:^{ [weakSelf linkWithFacebookAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kLinkWithEmailPasswordText
+ action:^{ [weakSelf linkWithEmailPassword]; }],
+ [StaticContentTableViewCell cellWithTitle:kUnlinkFromGoogle
+ action:^{
+ [weakSelf unlinkFromProvider:FIRGoogleAuthProviderID];
+ }],
+ [StaticContentTableViewCell cellWithTitle:kUnlinkFromFacebook
+ action:^{
+ [weakSelf unlinkFromProvider:FIRFacebookAuthProviderID];
+ }],
+ [StaticContentTableViewCell cellWithTitle:kUnlinkFromEmailPassword
+ action:^{
+ [weakSelf unlinkFromProvider:FIREmailAuthProviderID];
+ }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleApp cells:@[
+ [StaticContentTableViewCell cellWithTitle:kDeleteAppTitle
+ action:^{ [weakSelf deleteApp]; }],
+ [StaticContentTableViewCell cellWithTitle:kTokenGetButtonText
+ action:^{ [weakSelf getAppTokenWithForce:NO]; }],
+ [StaticContentTableViewCell cellWithTitle:kTokenRefreshButtonText
+ action:^{ [weakSelf getAppTokenWithForce:YES]; }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleListeners cells:@[
+ [StaticContentTableViewCell cellWithTitle:kAddAuthStateListenerTitle
+ action:^{ [weakSelf addAuthStateListener]; }],
+ [StaticContentTableViewCell cellWithTitle:kRemoveAuthStateListenerTitle
+ action:^{ [weakSelf removeAuthStateListener]; }],
+ [StaticContentTableViewCell cellWithTitle:kAddIDTokenListenerTitle
+ action:^{ [weakSelf addIDTokenListener]; }],
+ [StaticContentTableViewCell cellWithTitle:kRemoveIDTokenListenerTitle
+ action:^{ [weakSelf removeIDTokenListener]; }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleManualTests cells:@[
+ [StaticContentTableViewCell cellWithTitle:kAutoBYOAuthTitle
+ action:^{ [weakSelf automatedBYOAuth]; }],
+ [StaticContentTableViewCell cellWithTitle:kAutoSignInGoogle
+ action:^{ [weakSelf automatedSignInGoogle]; }],
+ [StaticContentTableViewCell cellWithTitle:kAutoSignInFacebook
+ action:^{ [weakSelf automatedSignInFacebook]; }],
+ [StaticContentTableViewCell cellWithTitle:kAutoSignUpEmailPassword
+ action:^{ [weakSelf automatedEmailSignUp]; }],
+ [StaticContentTableViewCell cellWithTitle:kAutoSignInAnonymously
+ action:^{ [weakSelf automatedAnonymousSignIn]; }],
+ [StaticContentTableViewCell cellWithTitle:kAutoAccountLinking
+ action:^{ [weakSelf automatedAccountLinking]; }]
+ ]]
+ ]];
+}
+
+#pragma mark - Interface Builder Actions
+
+- (IBAction)userToUseDidChange:(UISegmentedControl *)sender {
+ _useUserInMemory = (sender.selectedSegmentIndex == 1);
+}
+
+- (IBAction)memoryPlus {
+ _userInMemory = [FIRAuth auth].currentUser;
+ [self updateUserInfo];
+}
+
+- (IBAction)memoryClear {
+ _userInMemory = nil;
+ [self updateUserInfo];
+}
+
+#pragma mark - Actions
+
+/** @fn signInWithProvider:provider:
+ @brief Perform sign in with credential operataion, for given auth provider.
+ @provider The auth provider.
+ @callback The callback to continue the flow which executed this sign-in.
+ */
+- (void)signInWithProvider:(nonnull id<AuthProvider>)provider callback:(void(^)(void))callback {
+ if (!provider) {
+ [self logFailedTest:@"A valid auth provider was not provided to the signInWithProvider."];
+ return;
+ }
+ [provider getAuthCredentialWithPresentingViewController:self
+ callback:^(FIRAuthCredential *credential,
+ NSError *error) {
+ if (!credential) {
+ [self logFailedTest:@"The test needs a valid credential to continue."];
+ return;
+ }
+ [[FIRAuth auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with provider failed" error:error];
+ [self logFailedTest:@"Sign-in should succeed"];
+ return;
+ } else {
+ [self logSuccess:@"sign-in with provider succeeded."];
+ callback();
+ }
+ }];
+ }];
+}
+
+/** @fn automatedSignInGoogle
+ @brief Automatically executes the manual test for sign-in with Google.
+ */
+- (void)automatedSignInGoogle {
+ [self showMessagePromptWithTitle:kAutoSignInGoogle
+ message:kSafariGoogleSignOutMessagePrompt
+ showCancelButton:NO
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [auth signOut:NULL];
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR GOOGLE SIGN IN:"];
+ [self signInWithProvider:[AuthProviders google] callback:^{
+ [self logSuccess:@"sign-in with Google provider succeeded."];
+ [auth signOut:NULL];
+ [self signInWithProvider:[AuthProviders google] callback:^{
+ [self logSuccess:@"sign-in with Google provider succeeded."];
+ [self updateEmailPasswordWithCompletion:^{
+ [self automatedSignInGoogleDisplayNamePhotoURL];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn automatedSignInGoogleDisplayNamePhotoURL
+ @brief Automatically executes the manual test for setting email and password for sign in with
+ Google.
+ */
+- (void)automatedSignInGoogleDisplayNamePhotoURL {
+ [self signInWithProvider:[AuthProviders google] callback:^{
+ [self updateDisplayNameAndPhotoURlWithCompletion:^{
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR SIGN-IN WITH GOOGlE."];
+ [self reloadUser];
+ }];
+ }];
+}
+
+/** @fn automatedSignInFacebook
+ @brief Automatically executes the manual test for sign-in with Facebook.
+ */
+- (void)automatedSignInFacebook {
+ [self showMessagePromptWithTitle:kAutoSignInFacebook
+ message:kSafariFacebookSignOutMessagePrompt
+ showCancelButton:NO
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [auth signOut:NULL];
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR FACEBOOK SIGN IN:"];
+ [self signInWithProvider:[AuthProviders facebook] callback:^{
+ [self logSuccess:@"sign-in with Facebook provider succeeded."];
+ [auth signOut:NULL];
+ [self signInWithProvider:[AuthProviders facebook] callback:^{
+ [self logSuccess:@"sign-in with Facebook provider succeeded."];
+ [self updateEmailPasswordWithCompletion:^{
+ [self automatedSignInFacebookDisplayNamePhotoURL];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn automatedEmailSignUp
+ @brief Automatically executes the manual test for sign-up with email/password.
+ */
+- (void)automatedEmailSignUp {
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR FACEBOOK SIGN IN:"];
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [self signUpNewEmail:kFakeEmail password:kFakePassword callback:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailedTest: @" Email/Password Account account creation failed"];
+ return;
+ }
+ [auth signOut:NULL];
+ FIRAuthCredential *credential =
+ [FIREmailPasswordAuthProvider credentialWithEmail:kFakeEmail
+ password:kFakePassword];
+ [auth signInWithCredential:credential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with Email/Password failed" error:error];
+ [self logFailedTest:@"sign-in with Email/Password should succeed."];
+ return;
+ }
+ [self logSuccess:@"sign-in with Email/Password succeeded."];
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR SIGN-IN WITH EMAIL/PASSWORD."];
+ // Delete the user so that we can reuse the fake email address for subsequent tests.
+ [auth.currentUser deleteWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"Failed to delete user" error:error];
+ [self logFailedTest:@"Deleting a user that was recently signed-in should succeed."];
+ return;
+ }
+ [self logSuccess:@"User deleted."];
+ }];
+ }];
+ }];
+}
+
+/** @fn automatedAnonymousSignIn
+ @brief Automatically executes the manual test for sign-in anonymously.
+ */
+- (void)automatedAnonymousSignIn {
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR ANONYMOUS SIGN IN:"];
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [auth signOut:NULL];
+ [self signInAnonymouslyWithCallback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (user) {
+ NSString *anonymousUID = user.uid;
+ [self signInAnonymouslyWithCallback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (![user.uid isEqual:anonymousUID]) {
+ [self logFailedTest:@"Consecutive anonymous sign-ins should yeild the same User ID"];
+ return;
+ }
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR ANONYMOUS SIGN IN."];
+ }];
+ }
+ }];
+}
+
+/** @fn signInAnonymouslyWithCallback:
+ @brief Performs anonymous sign in and then executes callback.
+ @param callback The callback to be executed.
+ */
+- (void)signInAnonymouslyWithCallback:(nullable FIRAuthResultCallback)callback {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [auth signInAnonymouslyWithCompletion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in anonymously failed" error:error];
+ [self logFailedTest:@"Recently signed out user should be able to sign in anonymously."];
+ return;
+ }
+ [self logSuccess:@"sign-in anonymously succeeded."];
+ if (callback) {
+ callback(user, nil);
+ }
+ }];
+}
+
+/** @fn automatedAccountLinking
+ @brief Automatically executes the manual test for account linking.
+ */
+- (void)automatedAccountLinking {
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR ACCOUNT LINKING:"];
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [auth signOut:NULL];
+ [self signInAnonymouslyWithCallback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (user) {
+ NSString *anonymousUID = user.uid;
+ [self showMessagePromptWithTitle:@"Sign In Instructions"
+ message:kUnlinkAccountMessagePrompt
+ showCancelButton:NO
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ [[AuthProviders google]
+ getAuthCredentialWithPresentingViewController:self
+ callback:^(FIRAuthCredential *credential,
+ NSError *error) {
+ if (credential) {
+ [user linkWithCredential:credential completion:^(FIRUser *user, NSError *error) {
+ if (error) {
+ [self logFailure:@"link auth provider failed" error:error];
+ [self logFailedTest:@"Account needs to be linked to complete the test."];
+ return;
+ }
+ [self logSuccess:@"link auth provider succeeded."];
+ if (user.isAnonymous) {
+ [self logFailure:@"link auth provider failed, user still anonymous" error:error];
+ [self logFailedTest:@"Account needs to be linked to complete the test."];
+ }
+ if (![user.uid isEqual:anonymousUID]){
+ [self logFailedTest:@"link auth provider failed, UID's are different. Make sure "
+ "you link an account that has NOT been Linked nor Signed-In before."];
+ return;
+ }
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR ACCOUNT LINKING."];
+ }];
+ }
+ }];
+ }];
+ }
+ }];
+}
+
+/** @fn automatedSignInFacebookDisplayNamePhotoURL
+ @brief Automatically executes the manual test for setting email and password for sign-in with
+ Facebook.
+ */
+- (void)automatedSignInFacebookDisplayNamePhotoURL {
+ [self signInWithProvider:[AuthProviders facebook] callback:^{
+ [self updateDisplayNameAndPhotoURlWithCompletion:^{
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR SIGN-IN WITH FACEBOOK."];
+ [self reloadUser];
+ }];
+ }];
+}
+
+/** @fn automatedBYOauth
+ @brief Automatically executes the manual test for BYOAuth.
+ */
+- (void)automatedBYOAuth {
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR BYOAUTH:"];
+ [self showSpinner:^{
+ NSError *error;
+ NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ NSString *expiredCustomToken =
+ [NSString stringWithContentsOfURL:[NSURL URLWithString:kExpiredCustomTokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ [self hideSpinner:^{
+ if (error) {
+ [self log:@"There was an error retrieving the custom token."];
+ return;
+ }
+ FIRAuth *auth = [FIRAuth auth];
+ [auth signInWithCustomToken:customToken
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ [self logFailedTest:@"A fresh custom token should succeed in signing-in."];
+ return;
+ }
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [auth.currentUser getIDTokenForcingRefresh:NO
+ completion:^(NSString *_Nullable token,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"refresh token failed" error:error];
+ [self logFailedTest:@"Refresh token should be available."];
+ return;
+ }
+ [self logSuccess:@"refresh token succeeded."];
+ [auth signOut:NULL];
+ [auth signInWithCustomToken:expiredCustomToken
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (!error) {
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [self logFailedTest:@"sign-in with an expired custom token should NOT succeed."];
+ return;
+ }
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ [auth signInWithCustomToken:kInvalidCustomToken
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (!error) {
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [self logFailedTest:@"sign-in with an invalid custom token should NOT succeed."];
+ return;
+ }
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ //next step of automated test.
+ [self automatedBYOAuthEmailPassword];
+ }];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn automatedBYOAuthEmailPassword
+ @brief Automatically executes the manual test for setting email and password in BYOAuth.
+ */
+- (void)automatedBYOAuthEmailPassword {
+ [self showSpinner:^{
+ NSError *error;
+ NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ [self hideSpinner:^{
+ if (error) {
+ [self log:@"There was an error retrieving the custom token."];
+ return;
+ }
+ [[FIRAuth auth] signInWithCustomToken:customToken
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ [self logFailedTest:@"A fresh custom token should succeed in signing-in."];
+ return;
+ }
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [self updateEmailPasswordWithCompletion:^{
+ [self automatedBYOAuthDisplayNameAndPhotoURl];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn automatedBYOAuthDisplayNameAndPhotoURl
+ @brief Automatically executes the manual test for setting display name and photo url in BYOAuth.
+ */
+- (void)automatedBYOAuthDisplayNameAndPhotoURl {
+ [self showSpinner:^{
+ NSError *error;
+ NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ [self hideSpinner:^{
+ if (error) {
+ [self log:@"There was an error retrieving the custom token."];
+ return;
+ }
+ [[FIRAuth auth] signInWithCustomToken:customToken
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ [self logFailedTest:@"A fresh custom token should succeed in signing-in."];
+ return;
+ }
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [self updateDisplayNameAndPhotoURlWithCompletion:^{
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR BYOAUTH."];
+ [self reloadUser];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn updateEmailPasswordWithCompletion:
+ @brief Updates email and password for automatic manual tests, and signs user in with new email
+ and password.
+ @param completion The completion block to continue the automatic test flow.
+ */
+- (void)updateEmailPasswordWithCompletion:(void(^)(void))completion {
+ FIRAuth *auth = [FIRAuth auth];
+ [auth.currentUser updateEmail:kFakeEmail completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"update email failed" error:error];
+ [self logFailedTest:@"Update email should succeed when properly signed-in."];
+ return;
+ }
+ [self logSuccess:@"update email succeeded."];
+ [auth.currentUser updatePassword:kFakePassword completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"update password failed" error:error];
+ [self logFailedTest:@"Update password should succeed when properly signed-in."];
+ return;
+ }
+ [self logSuccess:@"update password succeeded."];
+ [auth signOut:NULL];
+ FIRAuthCredential *credential =
+ [FIREmailAuthProvider credentialWithEmail:kFakeEmail password:kFakePassword];
+ [auth signInWithCredential:credential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with Email/Password failed" error:error];
+ [self logFailedTest:@"sign-in with Email/Password should succeed."];
+ return;
+ }
+ [self logSuccess:@"sign-in with Email/Password succeeded."];
+ // Delete the user so that we can reuse the fake email address for subsequent tests.
+ [auth.currentUser deleteWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"Failed to delete user." error:error];
+ [self logFailedTest:@"Deleting a user that was recently signed-in should succeed"];
+ return;
+ }
+ [self logSuccess:@"User deleted."];
+ completion();
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn updateDisplayNameAndPhotoURlWithCompletion:
+ @brief Automatically executes the manual test for setting displayName and photoUrl.
+ @param completion The completion block to continue the automatic test flow.
+ */
+- (void)updateDisplayNameAndPhotoURlWithCompletion:(void(^)(void))completion {
+ FIRAuth *auth = [FIRAuth auth];
+ FIRUserProfileChangeRequest *changeRequest = [auth.currentUser profileChangeRequest];
+ changeRequest.photoURL = [NSURL URLWithString:kFakeDisplayPhotoUrl];
+ [changeRequest commitChangesWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"set photo URL failed" error:error];
+ [self logFailedTest:@"Change photo Url should succeed when signed-in."];
+ return;
+ }
+ [self logSuccess:@"set PhotoURL succeeded."];
+ FIRUserProfileChangeRequest *changeRequest = [auth.currentUser profileChangeRequest];
+ changeRequest.displayName = kFakeDisplayName;
+ [changeRequest commitChangesWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"set display name failed" error:error];
+ [self logFailedTest:@"Change display name should succeed when signed-in."];
+ return;
+ }
+ [self logSuccess:@"set display name succeeded."];
+ completion();
+ }];
+ }];
+}
+
+/** @fn addAuthStateListener
+ @brief Adds an auth state did change listener (block).
+ */
+- (void)addAuthStateListener {
+ __weak typeof(self) weakSelf = self;
+ NSUInteger index = _authStateDidChangeListeners.count;
+ [self log:[NSString stringWithFormat:@"Auth State Did Change Listener #%lu was added.",
+ (unsigned long)index]];
+ FIRAuthStateDidChangeListenerHandle handle =
+ [[FIRAuth auth] addAuthStateDidChangeListener:^(FIRAuth *_Nonnull auth,
+ FIRUser *_Nullable user) {
+ [weakSelf log:[NSString stringWithFormat:
+ @"Auth State Did Change Listener #%lu was invoked on user '%@'.",
+ (unsigned long)index, user.uid]];
+ }];
+ [_authStateDidChangeListeners addObject:handle];
+}
+
+/** @fn removeAuthStateListener
+ @brief Removes an auth state did change listener (block).
+ */
+- (void)removeAuthStateListener {
+ if (!_authStateDidChangeListeners.count) {
+ [self log:@"No remaining Auth State Did Change Listeners."];
+ return;
+ }
+ NSUInteger index = _authStateDidChangeListeners.count - 1;
+ FIRAuthStateDidChangeListenerHandle handle = _authStateDidChangeListeners.lastObject;
+ [[FIRAuth auth] removeAuthStateDidChangeListener:handle];
+ [_authStateDidChangeListeners removeObject:handle];
+ NSString *logString =
+ [NSString stringWithFormat:@"Auth State Did Change Listener #%lu was removed.",
+ (unsigned long)index];
+ [self log:logString];
+}
+
+/** @fn addIDTokenListener
+ @brief Adds an ID token did change listener (block).
+ */
+- (void)addIDTokenListener {
+ __weak typeof(self) weakSelf = self;
+ NSUInteger index = _IDTokenDidChangeListeners.count;
+ [self log:[NSString stringWithFormat:@"ID Token Did Change Listener #%lu was added.",
+ (unsigned long)index]];
+ FIRIDTokenDidChangeListenerHandle handle =
+ [[FIRAuth auth] addIDTokenDidChangeListener:^(FIRAuth *_Nonnull auth,
+ FIRUser *_Nullable user) {
+ [weakSelf log:[NSString stringWithFormat:
+ @"ID Token Did Change Listener #%lu was invoked on user '%@'.",
+ (unsigned long)index, user.uid]];
+ }];
+ [_IDTokenDidChangeListeners addObject:handle];
+}
+
+/** @fn removeIDTokenListener
+ @brief Removes an ID token did change listener (block).
+ */
+- (void)removeIDTokenListener {
+ if (!_IDTokenDidChangeListeners.count) {
+ [self log:@"No remaining ID Token Did Change Listeners."];
+ return;
+ }
+ NSUInteger index = _IDTokenDidChangeListeners.count - 1;
+ FIRIDTokenDidChangeListenerHandle handle = _IDTokenDidChangeListeners.lastObject;
+ [[FIRAuth auth] removeIDTokenDidChangeListener:handle];
+ [_IDTokenDidChangeListeners removeObject:handle];
+ NSString *logString =
+ [NSString stringWithFormat:@"ID Token Did Change Listener #%lu was removed.",
+ (unsigned long)index];
+ [self log:logString];
+}
+
+/** @fn log:
+ @brief Prints a log message to the sample app console.
+ @param string The string to add to the console.
+ */
+- (void)log:(NSString *)string {
+ NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
+ dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
+ NSString *date = [dateFormatter stringFromDate:[NSDate date]];
+ if (!_consoleString) {
+ _consoleString = [NSMutableString string];
+ }
+ [_consoleString appendString:[NSString stringWithFormat:@"%@ %@\n", date, string]];
+ _consoleTextView.text = _consoleString;
+
+ CGRect targetRect = CGRectMake(0, _consoleTextView.contentSize.height - 1, 1, 1);
+ [_consoleTextView scrollRectToVisible:targetRect animated:YES];
+}
+
+/** @fn logSuccess:
+ @brief Wraps a string into a succeful log message format.
+ @param string Part of the log message.
+ @remarks The @string parameter should be a string ending with a period, as it is the end of the
+ log message.
+ */
+- (void)logSuccess:(NSString *)string {
+ [self log:[NSString stringWithFormat:@"SUCCESS: %@", string]];
+}
+
+/** @fn logFailure:
+ @brief Wraps a string into a failed log message format.
+ @param string Part of the message to wrap.
+ @remarks The @string parameter should be a string that NEVER ends with a period, as it is
+ guaranteed not to be the last part fo the log message.
+ */
+- (void)logFailure:(NSString *)string error:(NSError *) error {
+ NSString *message =
+ [NSString stringWithFormat:@"FAILURE: %@ Error Description: %@.", string, error.description];
+ [self log:message];
+}
+
+/** @fn logTestFailed
+ @brief Logs test failure to the console.
+ @param reason The reason why the test is considered a failure.
+ @remarks The calling method should immediately terminate after invoking this method i.e by
+ return statement or end of fucntions. The @reason parameter should be a string ending with a
+ period, as it is the end of the log message.
+ */
+- (void)logFailedTest:( NSString *_Nonnull )reason {
+ [self log:[NSString stringWithFormat:@"FAILIURE: TEST FAILED - %@", reason]];
+}
+
+/** @fn presentSettings
+ @brief Invoked when the settings row is pressed.
+ */
+- (void)presentSettings {
+ SettingsViewController *settingsViewController = [[SettingsViewController alloc]
+ initWithNibName:NSStringFromClass([SettingsViewController class])
+ bundle:nil];
+ [self presentViewController:settingsViewController animated:YES completion:nil];
+}
+
+/** @fn presentUserInfo
+ @brief Invoked when the user info row is pressed.
+ */
+- (void)presentUserInfo {
+ UserInfoViewController *userInfoViewController =
+ [[UserInfoViewController alloc] initWithUser:[FIRAuth auth].currentUser];
+ [self presentViewController:userInfoViewController animated:YES completion:nil];
+}
+
+/** @fn presentUserInMemoryInfo
+ @brief Invoked when the user in memory info row is pressed.
+ */
+- (void)presentUserInMemoryInfo {
+ UserInfoViewController *userInfoViewController =
+ [[UserInfoViewController alloc] initWithUser:_userInMemory];
+ [self presentViewController:userInfoViewController animated:YES completion:nil];
+}
+
+/** @fn signInGoogle
+ @brief Invoked when "Sign in with Google" row is pressed.
+ */
+- (void)signInGoogle {
+ [self signinWithProvider:[AuthProviders google] retrieveData:NO];
+}
+
+/** @fn signInGoogleAndRetrieveData
+ @brief Invoked when "Sign in with Google and retrieve data" row is pressed.
+ */
+- (void)signInGoogleAndRetrieveData {
+ [self signinWithProvider:[AuthProviders google] retrieveData:YES];
+}
+
+/** @fn signInFacebook
+ @brief Invoked when "Sign in with Facebook" row is pressed.
+ */
+- (void)signInFacebook {
+ [self signinWithProvider:[AuthProviders facebook] retrieveData:NO];
+}
+
+/** @fn signInFacebookAndRetrieveData
+ @brief Invoked when "Sign in with Facebook and retrieve data" row is pressed.
+ */
+- (void)signInFacebookAndRetrieveData {
+ [self signinWithProvider:[AuthProviders facebook] retrieveData:YES];
+}
+
+/** @fn signInEmailPassword
+ @brief Invoked when "sign in with Email/Password" row is pressed.
+ */
+- (void)signInEmailPassword {
+ [self showTextInputPromptWithMessage:@"Email Address:"
+ keyboardType:UIKeyboardTypeEmailAddress
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable email) {
+ if (!userPressedOK || !email.length) {
+ return;
+ }
+ [self showTextInputPromptWithMessage:@"Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable password) {
+ if (!userPressedOK) {
+ return;
+ }
+ FIRAuthCredential *credential =
+ [FIREmailAuthProvider credentialWithEmail:email
+ password:password];
+ [self showSpinner:^{
+ [[FIRAuth auth] signInWithCredential:credential
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"sign-in with Email/Password failed" error:error];
+ } else {
+ [self logSuccess:@"sign-in with Email/Password succeeded."];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In Error" error:error];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn signUpNewEmail
+ @brief Invoked if sign-in is attempted with new email/password.
+ @remarks Should only be called if @c FIRAuthErrorCodeInvalidEmail is encountered on attepmt to
+ sign in with email/password.
+ */
+- (void)signUpNewEmail:(NSString *)email
+ password:(NSString *)password
+ callback:(nullable FIRAuthResultCallback)callback {
+ [[FIRAuth auth] createUserWithEmail:email
+ password:password
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-up with Email/Password failed" error:error];
+ if (callback) {
+ callback(nil, error);
+ }
+ } else {
+ [self logSuccess:@"sign-up with Email/Password succeeded."];
+ if (callback) {
+ callback(user, nil);
+ }
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In" error:error];
+ }];
+}
+
+/** @fn signInWithCustomToken
+ @brief Signs the user in using a manually-entered custom token.
+ */
+- (void)signInWithCustomToken {
+ CustomTokenDataEntryViewControllerCompletion action =
+ ^(BOOL cancelled, NSString *_Nullable userEnteredTokenText) {
+ if (cancelled) {
+ [self log:@"CANCELLED:sign-in with custom token cancelled."];
+ return;
+ }
+
+ [self doSignInWithCustomToken:userEnteredTokenText];
+ };
+ CustomTokenDataEntryViewController *dataEntryViewController =
+ [[CustomTokenDataEntryViewController alloc] initWithCompletion:action];
+ [self presentViewController:dataEntryViewController animated:YES completion:nil];
+}
+
+/** @fn signOut
+ @brief Signs the user out.
+ */
+- (void)signOut {
+ [[AuthProviders google] signOut];
+ [[AuthProviders facebook] signOut];
+ [[FIRAuth auth] signOut:NULL];
+}
+
+/** @fn deleteAccount
+ @brief Deletes the current user account and signs the user out.
+ */
+- (void)deleteAccount {
+ FIRUser *user = [self user];
+ [user deleteWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"delete account failed" error:error];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:kDeleteUserText error:error];
+ }];
+}
+
+/** @fn reauthenticateGoogle
+ @brief Asks the user to reauthenticate with Google.
+ */
+- (void)reauthenticateGoogle {
+ [self reauthenticate:[AuthProviders google] retrieveData:NO];
+}
+
+/** @fn reauthenticateGoogleAndRetrieveData
+ @brief Asks the user to reauthenticate with Google and retrieve additional data.
+ */
+- (void)reauthenticateGoogleAndRetrieveData {
+ [self reauthenticate:[AuthProviders google] retrieveData:YES];
+}
+
+/** @fn reauthenticateFB
+ @brief Asks the user to reauthenticate with Facebook.
+ */
+- (void)reauthenticateFB {
+ [self reauthenticate:[AuthProviders facebook] retrieveData:NO];
+}
+
+/** @fn reauthenticateFBAndRetrieveData
+ @brief Asks the user to reauthenticate with Facebook and retrieve additional data.
+ */
+- (void)reauthenticateFBAndRetrieveData {
+ [self reauthenticate:[AuthProviders facebook] retrieveData:YES];
+}
+
+/** @fn reauthenticateEmailPassword
+ @brief Asks the user to reauthenticate with email/password.
+ */
+- (void)reauthenticateEmailPassword {
+ FIRUser *user = [self user];
+ if (!user) {
+ NSString *title = @"Missing User";
+ NSString *message = @"There is no signed-in email/password user.";
+ [self showMessagePromptWithTitle:title message:message showCancelButton:NO completion:nil];
+ return;
+ }
+ [self showEmailPasswordDialogWithCompletion:^(FIRAuthCredential *credential) {
+ [self showSpinner:^{
+ [[self user] reauthenticateWithCredential:credential
+ completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"reauthicate with email/password failed" error:error];
+ } else {
+ [self logSuccess:@"reauthicate with email/password succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kReauthenticateEmailText error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn reauthenticate:
+ @brief Reauthenticates user.
+ @param authProvider The auth provider to use for reauthentication.
+ @param retrieveData Defines if additional provider data should be read.
+ */
+- (void)reauthenticate:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
+ FIRUser *user = [self user];
+ if (!user) {
+ NSString *provider = @"Firebase";
+ if ([authProvider isKindOfClass:[GoogleAuthProvider class]]) {
+ provider = @"Google";
+ } else if ([authProvider isKindOfClass:[FacebookAuthProvider class]]) {
+ provider = @"Facebook";
+ }
+ NSString *title = @"Missing User";
+ NSString *message =
+ [NSString stringWithFormat:@"There is no signed-in %@ user.", provider];
+ [self showMessagePromptWithTitle:title message:message showCancelButton:NO completion:nil];
+ return;
+ }
+ [authProvider getAuthCredentialWithPresentingViewController:self
+ callback:^(FIRAuthCredential *credential,
+ NSError *error) {
+ if (credential) {
+ FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"reauthenticate operation failed" error:error];
+ } else {
+ [self logSuccess:@"reauthenticate operation succeeded."];
+ }
+ if (authResult.additionalUserInfo) {
+ [self logSuccess:
+ [NSString stringWithFormat:@"%@", authResult.additionalUserInfo.profile]];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Reauthenticate" error:error];
+ };
+ FIRUserProfileChangeCallback callback = ^(NSError *_Nullable error) {
+ completion(nil, error);
+ };
+ if (retrieveData) {
+ [user reauthenticateAndRetrieveDataWithCredential:credential completion:completion];
+ } else {
+ [user reauthenticateWithCredential:credential completion:callback];
+ }
+ }
+ }];
+}
+
+/** @fn signinWithProvider:
+ @brief Signs in the user with provided auth provider.
+ @param authProvider The auth provider to use for sign-in.
+ @param retrieveData Defines if additional provider data should be read.
+ */
+- (void)signinWithProvider:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ return;
+ }
+ [authProvider getAuthCredentialWithPresentingViewController:self
+ callback:^(FIRAuthCredential *credential,
+ NSError *error) {
+ if (credential) {
+ FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with provider failed" error:error];
+ } else {
+ [self logSuccess:@"sign-in with provider succeeded."];
+ }
+ if (authResult.additionalUserInfo) {
+ [self logSuccess:
+ [NSString stringWithFormat:@"%@", authResult.additionalUserInfo.profile]];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In" error:error];
+ };
+ FIRAuthResultCallback callback = ^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ completion(nil, error);
+ };
+ if (retrieveData) {
+ [auth signInAndRetrieveDataWithCredential:credential completion:completion];
+ } else {
+ [auth signInWithCredential:credential completion:callback];
+ }
+ }
+ }];
+}
+
+/** @fn tokenCallback
+ @return A callback block to show the token.
+ */
+- (FIRAuthTokenCallback)tokenCallback {
+ return ^(NSString *_Nullable token, NSError *_Nullable error) {
+ if (error) {
+ [self showMessagePromptWithTitle:kTokenRefreshErrorAlertTitle
+ message:error.localizedDescription
+ showCancelButton:NO
+ completion:nil];
+ [self logFailure:@"refresh token failed" error:error];
+ return;
+ }
+ [self logSuccess:@"refresh token succeeded."];
+ [self showMessagePromptWithTitle:kTokenRefreshedAlertTitle
+ message:token
+ showCancelButton:NO
+ completion:nil];
+ };
+}
+
+/** @fn getUserTokenWithForce:
+ @brief Gets the token from @c FIRUser , optionally a refreshed one.
+ @param force Whether the refresh is forced or not.
+ */
+- (void)getUserTokenWithForce:(BOOL)force {
+ [[self user] getIDTokenForcingRefresh:force completion:[self tokenCallback]];
+}
+
+/** @fn getAppTokenWithForce:
+ @brief Gets the token from @c FIRApp , optionally a refreshed one.
+ @param force Whether the refresh is forced or not.
+ */
+- (void)getAppTokenWithForce:(BOOL)force {
+ [[FIRApp defaultApp] getTokenForcingRefresh:force withCallback:[self tokenCallback]];
+}
+
+/** @fn setDisplayName
+ @brief Changes the display name of the current user.
+ */
+- (void)setDisplayName {
+ [self showTextInputPromptWithMessage:@"Display Name:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ [self showSpinner:^{
+ FIRUserProfileChangeRequest *changeRequest = [[self user] profileChangeRequest];
+ changeRequest.displayName = userInput;
+ [changeRequest commitChangesWithCompletion:^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"set display name failed" error:error];
+ } else {
+ [self logSuccess:@"set display name succeeded."];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:kSetDisplayNameTitle error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn setPhotoURL
+ @brief Changes the photo url of the current user.
+ */
+- (void)setPhotoURL {
+ [self showTextInputPromptWithMessage:@"Photo URL:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+
+ [self showSpinner:^{
+ FIRUserProfileChangeRequest *changeRequest = [[self user] profileChangeRequest];
+ changeRequest.photoURL = [NSURL URLWithString:userInput];
+ [changeRequest commitChangesWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"set photo URL failed" error:error];
+ } else {
+ [self logSuccess:@"set Photo URL succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kSetPhotoURLText error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn reloadUser
+ @brief Reloads the user from server.
+ */
+- (void)reloadUser {
+ [self showSpinner:^() {
+ [[self user] reloadWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"reload user failed" error:error];
+ } else {
+ [self logSuccess:@"reload user succeeded."];
+ }
+ [self hideSpinner:^() {
+ [self showTypicalUIForUserUpdateResultsWithTitle:kReloadText error:error];
+ }];
+ }];
+ }];
+}
+
+/** @fn linkWithAuthProvider:retrieveData:
+ @brief Asks the user to sign in with an auth provider and link the current user with it.
+ @param authProvider The auth provider to sign in and link with.
+ @param retrieveData Defines if additional provider data should be read.
+ */
+- (void)linkWithAuthProvider:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
+ FIRUser *user = [self user];
+ if (!user) {
+ return;
+ }
+ [authProvider getAuthCredentialWithPresentingViewController:self
+ callback:^(FIRAuthCredential *credential,
+ NSError *error) {
+ if (credential) {
+ FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"link auth provider failed" error:error];
+ } else {
+ [self logSuccess:@"link auth provider succeeded."];
+ }
+ if (authResult.additionalUserInfo) {
+ [self logSuccess:
+ [NSString stringWithFormat:@"%@", authResult.additionalUserInfo.profile]];
+ }
+ if (retrieveData) {
+ [self showUIForAuthDataResultWithResult:authResult error:error];
+ } else {
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Link Account" error:error];
+ }
+ };
+ FIRAuthResultCallback callback = ^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ completion(nil, error);
+ };
+ if (retrieveData) {
+ [user linkAndRetrieveDataWithCredential:credential completion:completion];
+ } else {
+ [user linkWithCredential:credential completion:callback];
+ }
+ }
+ }];
+}
+
+/** @fn linkWithGoogle
+ @brief Asks the user to sign in with Google and link the current user with this Google account.
+ */
+- (void)linkWithGoogle {
+ [self linkWithAuthProvider:[AuthProviders google] retrieveData:NO];
+}
+
+/** @fn linkWithGoogleAndRetrieveData
+ @brief Asks the user to sign in with Google and link the current user with this Google account
+ and retrieve additional data.
+ */
+- (void)linkWithGoogleAndRetrieveData {
+ [self linkWithAuthProvider:[AuthProviders google] retrieveData:YES];
+}
+
+/** @fn linkWithFacebook
+ @brief Asks the user to sign in with Facebook and link the current user with this Facebook
+ account.
+ */
+- (void)linkWithFacebook {
+ [self linkWithAuthProvider:[AuthProviders facebook] retrieveData:NO];
+}
+
+/** @fn linkWithFacebookAndRetrieveData
+ @brief Asks the user to sign in with Facebook and link the current user with this Facebook
+ account and retrieve additional data.
+ */
+- (void)linkWithFacebookAndRetrieveData {
+ [self linkWithAuthProvider:[AuthProviders facebook] retrieveData:YES];
+}
+
+/** @fn linkWithEmailPassword
+ @brief Asks the user to sign in with Facebook and link the current user with this Facebook
+ account.
+ */
+- (void)linkWithEmailPassword {
+ [self showEmailPasswordDialogWithCompletion:^(FIRAuthCredential *credential) {
+ [self showSpinner:^{
+ [[self user] linkWithCredential:credential
+ completion:^(FIRUser *user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"link Email/Password failed" error:error];
+ } else {
+ [self logSuccess:@"link Email/Password succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kLinkWithEmailPasswordText error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn showEmailPasswordDialogWithCompletion:
+ @brief shows email/password input dialog.
+ @param completion The completion block that will do some operation on the credential email
+ /passwowrd credential obtained.
+ */
+- (void)showEmailPasswordDialogWithCompletion:(ShowEmailPasswordDialogCompletion)completion {
+ [self showTextInputPromptWithMessage:@"Email Address:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable email) {
+ if (!userPressedOK || !email.length) {
+ return;
+ }
+ [self showTextInputPromptWithMessage:@"Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable password) {
+ if (!userPressedOK || !password.length) {
+ return;
+ }
+
+ FIRAuthCredential *credential = [FIREmailAuthProvider credentialWithEmail:email
+ password:password];
+ completion(credential);
+ }];
+ }];
+}
+
+/** @fn unlinkFromProvider:
+ @brief Unlinks the current user from the provider with the specified provider ID.
+ @param provider The provider ID of the provider to unlink the current user's account from.
+ */
+- (void)unlinkFromProvider:(NSString *)provider {
+ [[self user] unlinkFromProvider:provider
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"unlink auth provider failed" error:error];
+ } else {
+ [self logSuccess:@"unlink auth provider succeeded."];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:kUnlinkTitle error:error];
+ }];
+}
+
+/** @fn getProvidersForEmail
+ @brief Prompts the user for an email address, calls @c FIRAuth.getProvidersForEmail:callback:
+ and displays the result.
+ */
+- (void)getProvidersForEmail {
+ [self showTextInputPromptWithMessage:@"Email:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+
+ [self showSpinner:^{
+ [[FIRAuth auth] fetchProvidersForEmail:userInput
+ completion:^(NSArray<NSString *> *_Nullable providers,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"get providers for email failed" error:error];
+ } else {
+ [self logSuccess:@"get providers for email succeeded."];
+ }
+ [self hideSpinner:^{
+ if (error) {
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+
+ [self showMessagePrompt:[providers componentsJoinedByString:@", "]];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn requestVerifyEmail
+ @brief Requests a "verify email" email be sent.
+ */
+- (void)requestVerifyEmail {
+ [self showSpinner:^{
+ [[self user] sendEmailVerificationWithCompletion:^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"request verify email failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"request verify email succeeded."];
+ [self showMessagePrompt:@"Sent"];
+ }];
+ }];
+ }];
+}
+
+/** @fn requestPasswordReset
+ @brief Requests a "password reset" email be sent.
+ */
+- (void)requestPasswordReset {
+ [self showTextInputPromptWithMessage:@"Email:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ [self showSpinner:^{
+ [[FIRAuth auth] sendPasswordResetWithEmail:userInput completion:^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"request password reset failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"request password reset succeeded."];
+ [self showMessagePrompt:@"Sent"];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn resetPassword
+ @brief Performs a password reset operation.
+ */
+- (void)resetPassword {
+ [self showTextInputPromptWithMessage:@"OOB Code:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ NSString *code = userInput;
+
+ [self showTextInputPromptWithMessage:@"New Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+
+ [self showSpinner:^{
+ [[FIRAuth auth] confirmPasswordResetWithCode:code
+ newPassword:userInput
+ completion:^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"Password reset failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Password reset succeeded."];
+ [self showMessagePrompt:@"Password reset succeeded."];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn checkActionCode
+ @brief Performs a "check action code" operation.
+ */
+- (void)checkActionCode {
+ [self showTextInputPromptWithMessage:@"OOB Code:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ [self showSpinner:^{
+ [[FIRAuth auth] checkActionCode:userInput completion:^(FIRActionCodeInfo *_Nullable info,
+ NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"Check action code failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Check action code succeeded."];
+ NSString *email = [info dataForKey:FIRActionCodeEmailKey];
+ NSString *operation = [self nameForActionCodeOperation:info.operation];
+ NSString *infoMessage =
+ [[NSString alloc] initWithFormat:@"Email: %@\n Operation: %@", email, operation];
+ [self showMessagePrompt:infoMessage];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn applyActionCode
+ @brief Performs a "apply action code" operation.
+ */
+- (void)applyActionCode {
+ [self showTextInputPromptWithMessage:@"OOB Code:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ [self showSpinner:^{
+
+ [[FIRAuth auth] applyActionCode:userInput completion:^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"Apply action code failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Apply action code succeeded."];
+ [self showMessagePrompt:@"Action code was properly applied."];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn verifyPasswordResetCode
+ @brief Performs a "verify password reset code" operation.
+ */
+- (void)verifyPasswordResetCode {
+ [self showTextInputPromptWithMessage:@"OOB Code:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ [self showSpinner:^{
+ [[FIRAuth auth] verifyPasswordResetCode:userInput completion:^(NSString *_Nullable email,
+ NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"Verify password reset code failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Verify password resest code succeeded."];
+ NSString *alertMessage =
+ [[NSString alloc] initWithFormat:@"Code verified for email: %@", email];
+ [self showMessagePrompt:alertMessage];
+ }];
+ }];
+ }];
+ }];
+}
+
+
+/** @fn nameForActionCodeOperation
+ @brief Returns the string value of the provided FIRActionCodeOperation value.
+ @param operation the FIRActionCodeOperation value to convert to string.
+ @return String conversion of FIRActionCodeOperation value.
+ */
+- (NSString *)nameForActionCodeOperation:(FIRActionCodeOperation)operation {
+ switch (operation) {
+ case FIRActionCodeOperationVerifyEmail:
+ return @"Verify Email";
+ case FIRActionCodeOperationPasswordReset:
+ return @"Password Reset";
+ case FIRActionCodeOperationUnknown:
+ return @"Unknown action";
+ }
+}
+
+/** @fn updateEmail
+ @brief Changes the email address of the current user.
+ */
+- (void)updateEmail {
+ [self showTextInputPromptWithMessage:@"Email Address:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+
+ [self showSpinner:^{
+ [[self user] updateEmail:userInput completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"update email failed" error:error];
+ } else {
+ [self logSuccess:@"update email succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kUpdateEmailText error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn updatePassword
+ @brief Updates the password of the current user.
+ */
+- (void)updatePassword {
+ [self showTextInputPromptWithMessage:@"New Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK) {
+ return;
+ }
+
+ [self showSpinner:^{
+ [[self user] updatePassword:userInput completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"update password failed" error:error];
+ } else {
+ [self logSuccess:@"update password succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kUpdatePasswordText error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn createUser
+ @brief Creates a new user.
+ */
+- (void)createUser {
+ [self showTextInputPromptWithMessage:@"Email:"
+ keyboardType:UIKeyboardTypeEmailAddress
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable email) {
+ if (!userPressedOK || !email.length) {
+ return;
+ }
+
+ [self showTextInputPromptWithMessage:@"Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable password) {
+ if (!userPressedOK) {
+ return;
+ }
+
+ [self showSpinner:^{
+ [[FIRAuth auth] createUserWithEmail:email
+ password:password
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"create user failed" error:error];
+ } else {
+ [self logSuccess:@"create user succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kCreateUserTitle error:error];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn signInWithPhoneNumber
+ @brief Allows sign in with phone number.
+ */
+- (void)signInWithPhoneNumber {
+ [self showTextInputPromptWithMessage:@"Phone #:"
+ keyboardType:UIKeyboardTypePhonePad
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable phoneNumber) {
+ if (!userPressedOK || !phoneNumber.length) {
+ return;
+ }
+ [self showSpinner:^{
+ [[FIRPhoneAuthProvider provider] verifyPhoneNumber:phoneNumber
+ completion:^(NSString *_Nullable verificationID,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"failed to send verification code" error:error];
+ return;
+ }
+ [self logSuccess:@"Code sent"];
+
+ [self showTextInputPromptWithMessage:@"Verification code:"
+ keyboardType:UIKeyboardTypeNumberPad
+ completionBlock:^(BOOL userPressedOK,
+ NSString *_Nullable verificationCode) {
+ if (!userPressedOK || !verificationCode.length) {
+ return;
+ }
+ [self showSpinner:^{
+ FIRAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID
+ verificationCode:verificationCode];
+ [[FIRAuth auth] signInWithCredential:credential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"failed to verify phone number" error:error];
+ return;
+ }
+ }];
+ }];
+ }];
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kCreateUserTitle error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn updatePhoneNumber
+ @brief Allows adding a verified phone number to the currently signed user.
+ */
+- (void)updatePhoneNumber {
+ [self showTextInputPromptWithMessage:@"Phone #:"
+ keyboardType:UIKeyboardTypePhonePad
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable phoneNumber) {
+ if (!userPressedOK || !phoneNumber.length) {
+ return;
+ }
+ [self showSpinner:^{
+ [[FIRPhoneAuthProvider provider] verifyPhoneNumber:phoneNumber
+ completion:^(NSString *_Nullable verificationID,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"failed to send verification code" error:error];
+ return;
+ }
+ [self logSuccess:@"Code sent"];
+
+ [self showTextInputPromptWithMessage:@"Verification code:"
+ keyboardType:UIKeyboardTypeNumberPad
+ completionBlock:^(BOOL userPressedOK,
+ NSString *_Nullable verificationCode) {
+ if (!userPressedOK || !verificationCode.length) {
+ return;
+ }
+ [self showSpinner:^{
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID
+ verificationCode:verificationCode];
+ [[self user] updatePhoneNumberCredential:credential
+ completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"update phone number failed" error:error];
+ } else {
+ [self logSuccess:@"update phone number succeeded."];
+ }
+ }];
+ }];
+ }];
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kCreateUserTitle error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn linkPhoneNumber
+ @brief Allows linking a verified phone number to the currently signed user.
+ */
+- (void)linkPhoneNumber {
+ [self showTextInputPromptWithMessage:@"Phone #:"
+ keyboardType:UIKeyboardTypePhonePad
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable phoneNumber) {
+ if (!userPressedOK || !phoneNumber.length) {
+ return;
+ }
+ [self showSpinner:^{
+ [[FIRPhoneAuthProvider provider] verifyPhoneNumber:phoneNumber
+ completion:^(NSString *_Nullable verificationID,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"failed to send verification code" error:error];
+ return;
+ }
+ [self logSuccess:@"Code sent"];
+
+ [self showTextInputPromptWithMessage:@"Verification code:"
+ keyboardType:UIKeyboardTypeNumberPad
+ completionBlock:^(BOOL userPressedOK,
+ NSString *_Nullable verificationCode) {
+ if (!userPressedOK || !verificationCode.length) {
+ return;
+ }
+ [self showSpinner:^{
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID
+ verificationCode:verificationCode];
+ [[self user] linkWithCredential:credential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ if (error.code == FIRAuthErrorCodeCredentialAlreadyInUse) {
+ [self showMessagePromptWithTitle:@"Phone number is already linked to another user"
+ message:@"Tap Ok to sign in with that user now."
+ showCancelButton:YES
+ completion:^(BOOL userPressedOK,
+ NSString *_Nullable userInput) {
+ if (userPressedOK) {
+ // If FIRAuthErrorCodeCredentialAlreadyInUse error, sign in with the provided
+ // credential.
+ FIRPhoneAuthCredential *credential =
+ error.userInfo[FIRAuthUpdatedCredentialKey];
+ [[FIRAuth auth] signInWithCredential:credential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"failed to verify phone number" error:error];
+ return;
+ }
+ }];
+ }
+ }];
+ } else {
+ [self logFailure:@"link phone number failed" error:error];
+ }
+ return;
+ }
+ [self logSuccess:@"link phone number succeeded."];
+ }];
+ }];
+ }];
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kCreateUserTitle error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn signInAnonymously
+ @brief Signs in as an anonymous user.
+ */
+- (void)signInAnonymously {
+ [[FIRAuth auth] signInAnonymouslyWithCompletion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in anonymously failed" error:error];
+ } else {
+ [self logSuccess:@"sign-in anonymously succeeded."];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:kSignInAnonymouslyButtonText error:error];
+ }];
+}
+
+/** @fn signInWithGitHub
+ @brief Signs in as a GitHub user. Prompts the user for an access token and uses this access
+ token to create a GitHub (generic) credential for signing in.
+ */
+- (void)signInWithGitHub {
+ [self showTextInputPromptWithMessage:@"GitHub Access Token:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable accessToken) {
+ if (!userPressedOK || !accessToken.length) {
+ return;
+ }
+ FIRAuthCredential *credential =
+ [FIROAuthProvider credentialWithProviderID:FIRGitHubAuthProviderID accessToken:accessToken];
+ if (credential) {
+ [[FIRAuth auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with provider failed" error:error];
+ } else {
+ [self logSuccess:@"sign-in with provider succeeded."];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In" error:error];
+ }];
+ }
+ }];
+}
+
+/** @fn deleteApp
+ @brief Deletes the @c FIRApp associated with our @c FIRAuth instance.
+ */
+- (void)deleteApp {
+ [[FIRApp defaultApp] deleteApp:^(BOOL success) {
+ [self log:success ? @"App deleted successfully." : @"Failed to delete app."];
+ }];
+}
+
+#pragma mark - Helpers
+
+/** @fn user
+ @brief The user to use for user operations. Takes into account the "use signed-in user vs. use
+ user in memory" setting.
+ */
+- (FIRUser *)user {
+ return _useUserInMemory ? _userInMemory : [FIRAuth auth].currentUser;
+}
+
+/** @fn showTypicalUIForUserUpdateResultsWithTitle:error:
+ @brief Shows a prompt if error is non-nil with the localized description of the error.
+ @param resultsTitle The title of the prompt
+ @param error The error details to display if non-nil.
+ */
+- (void)showTypicalUIForUserUpdateResultsWithTitle:(NSString *)resultsTitle
+ error:(NSError *)error {
+ if (error) {
+ NSString *message = [NSString stringWithFormat:@"%@ (%ld)\n%@",
+ error.domain,
+ (long)error.code,
+ error.localizedDescription];
+ if (error.code == FIRAuthErrorCodeAccountExistsWithDifferentCredential) {
+ NSString *errorEmail = error.userInfo[FIRAuthErrorUserInfoEmailKey];
+ resultsTitle = [NSString stringWithFormat:@"Existing email : %@", errorEmail];
+ }
+ [self showMessagePromptWithTitle:resultsTitle
+ message:message
+ showCancelButton:NO
+ completion:nil];
+ return;
+ }
+ [self updateUserInfo];
+}
+
+/** @fn showUIForAuthDataResultWithResult:error:
+ @brief Shows a prompt if error is non-nil with the localized description of the error.
+ @param result The auth data result if non-nil.
+ @param error The error details to display if non-nil.
+ */
+- (void)showUIForAuthDataResultWithResult:(FIRAuthDataResult *)result
+ error:(NSError *)error {
+ NSString *errorMessage = [NSString stringWithFormat:@"%@ (%ld)\n%@",
+ error.domain ?: @"",
+ (long)error.code,
+ error.localizedDescription ?: @""];
+ [self showMessagePromptWithTitle:@"Error"
+ message:errorMessage
+ showCancelButton:NO
+ completion:^(BOOL userPressedOK,
+ NSString *_Nullable userInput) {
+ NSString *profileMessaage =
+ [NSString stringWithFormat:@"%@", result.additionalUserInfo.profile ?: @""];
+ [self showMessagePromptWithTitle:@"Profile Info"
+ message:profileMessaage
+ showCancelButton:NO
+ completion:nil];
+ [self updateUserInfo];
+ }];
+}
+
+- (void)doSignInWithCustomToken:(NSString *_Nullable)userEnteredTokenText {
+ [[FIRAuth auth] signInWithCustomToken:userEnteredTokenText
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ [self showMessagePromptWithTitle:kSignInErrorAlertTitle
+ message:error.localizedDescription
+ showCancelButton:NO
+ completion:nil];
+ return;
+ }
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [self showMessagePromptWithTitle:kSignedInAlertTitle
+ message:user.displayName
+ showCancelButton:NO
+ completion:nil];
+ }];
+}
+
+- (void)updateUserInfo {
+ [_userInfoTableViewCell updateContentsWithUser:[FIRAuth auth].currentUser];
+ [_userInMemoryInfoTableViewCell updateContentsWithUser:_userInMemory];
+}
+
+- (void)authStateChangedForAuth:(NSNotification *)notification {
+ [self updateUserInfo];
+ if (notification) {
+ [self log:[NSString stringWithFormat:
+ @"received FIRAuthStateDidChange notification on user '%@'.",
+ ((FIRAuth *)notification.object).currentUser.uid]];
+ }
+}
+
+/** @fn clearConsole
+ @brief Clears the console text.
+ */
+- (IBAction)clearConsole:(id)sender {
+ [_consoleString appendString:@"\n\n"];
+ _consoleTextView.text = @"";
+}
+
+/** @fn copyConsole
+ @brief Copies the current console string to the clipboard.
+ */
+- (IBAction)copyConsole:(id)sender {
+ UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
+ pasteboard.string = _consoleString ?: @"";
+}
+
+@end
diff --git a/AuthSamples/Sample/MainViewController.xib b/AuthSamples/Sample/MainViewController.xib
new file mode 100644
index 0000000..ec4b75a
--- /dev/null
+++ b/AuthSamples/Sample/MainViewController.xib
@@ -0,0 +1,330 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="10116" systemVersion="15E65" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ <capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
+ <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
+ </dependencies>
+ <objects>
+ <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MainViewController">
+ <connections>
+ <outlet property="consoleTextView" destination="n2N-pY-4Y5" id="8Rx-K5-QmU"/>
+ <outlet property="tableView" destination="Hll-rc-gfA" id="0mG-y8-irt"/>
+ <outlet property="tableViewManager" destination="A6y-Y4-frp" id="2mZ-dg-wS4"/>
+ <outlet property="userInMemoryInfoTableViewCell" destination="zh0-45-TuQ" id="Bi6-s7-kLG"/>
+ <outlet property="userInfoTableViewCell" destination="bYf-Pj-jVY" id="p0a-Bx-hvv"/>
+ <outlet property="userToUseCell" destination="LsZ-4s-P51" id="X9v-it-wLD"/>
+ <outlet property="view" destination="iN0-l3-epB" id="sGX-Mq-1Uy"/>
+ </connections>
+ </placeholder>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+ <view contentMode="scaleToFill" id="iN0-l3-epB">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="grouped" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" translatesAutoresizingMaskIntoConstraints="NO" id="Hll-rc-gfA">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="417"/>
+ <color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="calibratedRGB"/>
+ <connections>
+ <outlet property="dataSource" destination="A6y-Y4-frp" id="R4X-fh-aFx"/>
+ <outlet property="delegate" destination="A6y-Y4-frp" id="jHu-Pv-9wv"/>
+ </connections>
+ </tableView>
+ <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="n2N-pY-4Y5">
+ <rect key="frame" x="0.0" y="425" width="600" height="175"/>
+ <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="175" id="s61-xh-P20"/>
+ </constraints>
+ <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
+ </textView>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hEI-H1-vZs">
+ <rect key="frame" x="570" y="425" width="30" height="30"/>
+ <state key="normal" title="CL"/>
+ <connections>
+ <action selector="clearConsole:" destination="-1" eventType="touchUpInside" id="Hn2-Sp-p8a"/>
+ </connections>
+ </button>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ONk-oy-W6d">
+ <rect key="frame" x="570" y="449" width="30" height="30"/>
+ <state key="normal" title="CP"/>
+ <connections>
+ <action selector="copyConsole:" destination="-1" eventType="touchUpInside" id="r9j-zp-kN7"/>
+ </connections>
+ </button>
+ </subviews>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <constraints>
+ <constraint firstAttribute="trailing" secondItem="ONk-oy-W6d" secondAttribute="trailing" id="22R-Wv-nx7"/>
+ <constraint firstAttribute="trailing" secondItem="Hll-rc-gfA" secondAttribute="trailing" id="GWn-K4-V3D"/>
+ <constraint firstAttribute="bottom" secondItem="Hll-rc-gfA" secondAttribute="bottom" constant="183" id="K99-aB-Iqr"/>
+ <constraint firstItem="Hll-rc-gfA" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="UAz-Ca-RCH"/>
+ <constraint firstAttribute="bottom" secondItem="n2N-pY-4Y5" secondAttribute="bottom" id="ZVy-cZ-eIb"/>
+ <constraint firstItem="n2N-pY-4Y5" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="aaF-Ps-Qpu"/>
+ <constraint firstAttribute="bottom" secondItem="ONk-oy-W6d" secondAttribute="bottom" constant="121" id="bcX-TK-lMF"/>
+ <constraint firstAttribute="trailing" secondItem="n2N-pY-4Y5" secondAttribute="trailing" id="jsn-wx-OIr"/>
+ <constraint firstAttribute="trailing" secondItem="hEI-H1-vZs" secondAttribute="trailing" id="mpz-9q-ALh"/>
+ <constraint firstAttribute="bottom" secondItem="hEI-H1-vZs" secondAttribute="bottom" constant="145" id="otK-c8-X66"/>
+ <constraint firstItem="Hll-rc-gfA" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="yT4-GR-MIH"/>
+ </constraints>
+ <point key="canvasLocation" x="448" y="402"/>
+ </view>
+ <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="107" id="bYf-Pj-jVY" customClass="UserTableViewCell">
+ <rect key="frame" x="0.0" y="0.0" width="500" height="107"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="bYf-Pj-jVY" id="Ulk-yV-azP">
+ <rect key="frame" x="0.0" y="0.0" width="500" height="106.5"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="hPP-eY-9fW">
+ <rect key="frame" x="8" y="8" width="90" height="90"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="90" id="5y3-rD-gXk"/>
+ <constraint firstAttribute="width" secondItem="hPP-eY-9fW" secondAttribute="height" multiplier="1:1" id="AqJ-2w-dcT"/>
+ <constraint firstAttribute="width" secondItem="hPP-eY-9fW" secondAttribute="height" multiplier="1:1" id="z7U-Am-95c"/>
+ </constraints>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="z7U-Am-95c"/>
+ </mask>
+ </variation>
+ </imageView>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p6v-63-AuV">
+ <rect key="frame" x="106" y="8" width="314" height="21"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Email" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EfM-PI-Xnj">
+ <rect key="frame" x="106" y="32" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="314" id="KEA-3Z-pMi"/>
+ <constraint firstAttribute="height" constant="21" id="jOF-DJ-Sml"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
+ <nil key="highlightedColor"/>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="KEA-3Z-pMi"/>
+ </mask>
+ </variation>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Provider List" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="my0-p6-pH6">
+ <rect key="frame" x="106" y="78" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="21" id="62Z-fc-RHm"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iaQ-zf-G2U">
+ <rect key="frame" x="428" y="8" width="64" height="90"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="64" id="R5X-0j-Lao"/>
+ </constraints>
+ <state key="normal" title="M+"/>
+ <connections>
+ <action selector="memoryPlus" destination="-1" eventType="touchUpInside" id="gvb-Km-J8l"/>
+ </connections>
+ </button>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="User ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h1y-Rb-peJ">
+ <rect key="frame" x="106" y="55" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="21" id="780-NW-d3a"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
+ <nil key="highlightedColor"/>
+ </label>
+ </subviews>
+ <constraints>
+ <constraint firstItem="hPP-eY-9fW" firstAttribute="top" secondItem="Ulk-yV-azP" secondAttribute="topMargin" id="0ry-HJ-3vx"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="leading" secondItem="hPP-eY-9fW" secondAttribute="trailing" constant="8" symbolic="YES" id="564-bQ-COy"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="bottom" secondItem="hPP-eY-9fW" secondAttribute="bottom" id="9hR-kP-6Tf"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="leading" secondItem="h1y-Rb-peJ" secondAttribute="trailing" constant="8" symbolic="YES" id="BmY-H5-2NO"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="trailing" secondItem="Ulk-yV-azP" secondAttribute="trailingMargin" id="CeI-mb-oeM"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="leading" secondItem="my0-p6-pH6" secondAttribute="leading" id="D91-aG-k83"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="top" secondItem="Ulk-yV-azP" secondAttribute="top" constant="32" id="DLn-9X-y8c"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="bottom" secondItem="hPP-eY-9fW" secondAttribute="bottom" id="EAV-7c-fG4"/>
+ <constraint firstAttribute="bottomMargin" secondItem="my0-p6-pH6" secondAttribute="bottom" constant="-0.5" id="LTJ-U8-4RR"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="leading" secondItem="EfM-PI-Xnj" secondAttribute="trailing" constant="8" symbolic="YES" id="NnB-3j-eyJ"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="leading" secondItem="my0-p6-pH6" secondAttribute="trailing" constant="8" symbolic="YES" id="OVL-5A-xCf"/>
+ <constraint firstItem="hPP-eY-9fW" firstAttribute="leading" secondItem="Ulk-yV-azP" secondAttribute="leadingMargin" id="P1h-Uy-1Ac"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="leading" secondItem="p6v-63-AuV" secondAttribute="trailing" constant="8" symbolic="YES" id="WaP-nw-Pa4"/>
+ <constraint firstItem="my0-p6-pH6" firstAttribute="top" secondItem="h1y-Rb-peJ" secondAttribute="bottom" constant="2" id="YWo-9n-6NP"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="leading" secondItem="p6v-63-AuV" secondAttribute="leading" id="adW-uj-LKW"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="top" secondItem="p6v-63-AuV" secondAttribute="top" id="c9T-vM-kCC"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="trailing" secondItem="Ulk-yV-azP" secondAttribute="trailingMargin" id="cru-24-h18"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="top" secondItem="p6v-63-AuV" secondAttribute="top" id="eEr-FK-szm"/>
+ <constraint firstItem="h1y-Rb-peJ" firstAttribute="top" secondItem="EfM-PI-Xnj" secondAttribute="bottom" constant="2" id="iYn-Tj-cB5"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="top" secondItem="p6v-63-AuV" secondAttribute="bottom" constant="3" id="qhp-Cy-lfn"/>
+ <constraint firstItem="hPP-eY-9fW" firstAttribute="bottom" secondItem="Ulk-yV-azP" secondAttribute="bottomMargin" id="stR-OV-FUd"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="leading" secondItem="h1y-Rb-peJ" secondAttribute="leading" id="tQp-sn-zMN"/>
+ </constraints>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="DLn-9X-y8c"/>
+ <exclude reference="EAV-7c-fG4"/>
+ <exclude reference="cru-24-h18"/>
+ <exclude reference="eEr-FK-szm"/>
+ </mask>
+ </variation>
+ </tableViewCellContentView>
+ <connections>
+ <outlet property="userInfoDisplayNameLabel" destination="p6v-63-AuV" id="jrN-fo-dpo"/>
+ <outlet property="userInfoEmailLabel" destination="EfM-PI-Xnj" id="lup-fo-bWg"/>
+ <outlet property="userInfoProfileURLImageView" destination="hPP-eY-9fW" id="YAH-VZ-TUG"/>
+ <outlet property="userInfoProviderListLabel" destination="my0-p6-pH6" id="MSK-wX-Nst"/>
+ <outlet property="userInfoUserIDLabel" destination="h1y-Rb-peJ" id="3bJ-bB-ofq"/>
+ </connections>
+ <point key="canvasLocation" x="-191" y="325.5"/>
+ </tableViewCell>
+ <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="107" id="zh0-45-TuQ" customClass="UserTableViewCell">
+ <rect key="frame" x="0.0" y="0.0" width="500" height="107"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="zh0-45-TuQ" id="XNx-rO-smo">
+ <rect key="frame" x="0.0" y="0.0" width="500" height="106.5"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="hLa-iS-QtA">
+ <rect key="frame" x="8" y="8" width="90" height="90"/>
+ <constraints>
+ <constraint firstAttribute="width" secondItem="hLa-iS-QtA" secondAttribute="height" multiplier="1:1" id="3WU-f2-f8i"/>
+ <constraint firstAttribute="width" constant="90" id="dGC-3Y-Lqd"/>
+ <constraint firstAttribute="width" secondItem="hLa-iS-QtA" secondAttribute="height" multiplier="1:1" id="kAm-w5-Ron"/>
+ </constraints>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="3WU-f2-f8i"/>
+ </mask>
+ </variation>
+ </imageView>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ycp-Vc-BY7">
+ <rect key="frame" x="106" y="8" width="314" height="21"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Email" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5WQ-KD-Jcl">
+ <rect key="frame" x="106" y="32" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="21" id="umR-qC-7qF"/>
+ <constraint firstAttribute="width" constant="314" id="vXg-uI-D8c"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
+ <nil key="highlightedColor"/>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="vXg-uI-D8c"/>
+ </mask>
+ </variation>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Provider List" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6FV-lo-RuN">
+ <rect key="frame" x="106" y="78" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="21" id="RVn-Wa-Xbp"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bEJ-Be-y7a">
+ <rect key="frame" x="428" y="8" width="64" height="90"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="64" id="yRu-DH-Oet"/>
+ </constraints>
+ <state key="normal" title="MC"/>
+ <connections>
+ <action selector="memoryClear" destination="-1" eventType="touchUpInside" id="joZ-N8-BDY"/>
+ </connections>
+ </button>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="User ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FQp-Lq-EBp">
+ <rect key="frame" x="106" y="55" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="21" id="tsz-9R-eJf"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
+ <nil key="highlightedColor"/>
+ </label>
+ </subviews>
+ <constraints>
+ <constraint firstAttribute="bottomMargin" secondItem="6FV-lo-RuN" secondAttribute="bottom" constant="-0.5" id="1Hv-LI-PbT"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="leading" secondItem="5WQ-KD-Jcl" secondAttribute="trailing" constant="8" symbolic="YES" id="7j5-BA-KFD"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="trailing" secondItem="XNx-rO-smo" secondAttribute="trailingMargin" id="G45-sQ-DYR"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="leading" secondItem="ycp-Vc-BY7" secondAttribute="trailing" constant="8" symbolic="YES" id="OL1-KX-AJU"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="leading" secondItem="6FV-lo-RuN" secondAttribute="leading" id="Q3L-KT-htb"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="leading" secondItem="6FV-lo-RuN" secondAttribute="trailing" constant="8" symbolic="YES" id="RD5-7i-oWq"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="leading" secondItem="ycp-Vc-BY7" secondAttribute="leading" id="RQv-20-b4H"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="top" secondItem="ycp-Vc-BY7" secondAttribute="top" id="U5d-lG-KWY"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="leading" secondItem="hLa-iS-QtA" secondAttribute="trailing" constant="8" symbolic="YES" id="Xme-JH-VMQ"/>
+ <constraint firstItem="hLa-iS-QtA" firstAttribute="bottom" secondItem="XNx-rO-smo" secondAttribute="bottomMargin" id="Zlj-2B-bQb"/>
+ <constraint firstItem="hLa-iS-QtA" firstAttribute="leading" secondItem="XNx-rO-smo" secondAttribute="leadingMargin" id="a7r-k3-LXl"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="bottom" secondItem="hLa-iS-QtA" secondAttribute="bottom" id="dlc-Um-0f3"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="leading" secondItem="FQp-Lq-EBp" secondAttribute="leading" id="ewD-ct-OhC"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="top" secondItem="ycp-Vc-BY7" secondAttribute="top" id="f9v-kk-otf"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="leading" secondItem="FQp-Lq-EBp" secondAttribute="trailing" constant="8" symbolic="YES" id="fNb-Xp-gKX"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="top" secondItem="ycp-Vc-BY7" secondAttribute="bottom" constant="3" id="gkP-oU-TGY"/>
+ <constraint firstItem="hLa-iS-QtA" firstAttribute="top" secondItem="XNx-rO-smo" secondAttribute="topMargin" id="p02-MJ-1ff"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="bottom" secondItem="hLa-iS-QtA" secondAttribute="bottom" id="qFD-Ju-CRU"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="top" secondItem="XNx-rO-smo" secondAttribute="top" constant="32" id="qmQ-z2-aTk"/>
+ <constraint firstItem="6FV-lo-RuN" firstAttribute="top" secondItem="FQp-Lq-EBp" secondAttribute="bottom" constant="2" id="vEE-lZ-IV7"/>
+ <constraint firstItem="FQp-Lq-EBp" firstAttribute="top" secondItem="5WQ-KD-Jcl" secondAttribute="bottom" constant="2" id="wG2-Da-pns"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="trailing" secondItem="XNx-rO-smo" secondAttribute="trailingMargin" id="yt3-Zp-StK"/>
+ </constraints>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="qmQ-z2-aTk"/>
+ <exclude reference="dlc-Um-0f3"/>
+ <exclude reference="yt3-Zp-StK"/>
+ <exclude reference="f9v-kk-otf"/>
+ </mask>
+ </variation>
+ </tableViewCellContentView>
+ <connections>
+ <outlet property="userInfoDisplayNameLabel" destination="ycp-Vc-BY7" id="8Og-Xh-auj"/>
+ <outlet property="userInfoEmailLabel" destination="5WQ-KD-Jcl" id="wdv-q0-g1A"/>
+ <outlet property="userInfoProfileURLImageView" destination="hLa-iS-QtA" id="3X8-jL-d24"/>
+ <outlet property="userInfoProviderListLabel" destination="6FV-lo-RuN" id="FbB-Op-xQz"/>
+ <outlet property="userInfoUserIDLabel" destination="FQp-Lq-EBp" id="SlI-ZP-yuo"/>
+ </connections>
+ <point key="canvasLocation" x="-191" y="574.5"/>
+ </tableViewCell>
+ <customObject id="A6y-Y4-frp" customClass="StaticContentTableViewManager">
+ <connections>
+ <outlet property="tableView" destination="Hll-rc-gfA" id="Lk4-jl-dP0"/>
+ </connections>
+ </customObject>
+ <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="LsZ-4s-P51">
+ <rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="LsZ-4s-P51" id="G9s-Xs-aPm">
+ <rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="Vev-VJ-WgW">
+ <rect key="frame" x="50" y="8" width="221" height="29"/>
+ <segments>
+ <segment title="Signed In User"/>
+ <segment title="User in Memory"/>
+ </segments>
+ <connections>
+ <action selector="userToUseDidChange:" destination="-1" eventType="valueChanged" id="gPh-ZP-M2b"/>
+ </connections>
+ </segmentedControl>
+ </subviews>
+ <constraints>
+ <constraint firstItem="Vev-VJ-WgW" firstAttribute="centerX" secondItem="G9s-Xs-aPm" secondAttribute="centerX" id="E6z-Xc-Mjr"/>
+ <constraint firstItem="Vev-VJ-WgW" firstAttribute="centerY" secondItem="G9s-Xs-aPm" secondAttribute="centerY" id="ijR-6Y-K29"/>
+ </constraints>
+ </tableViewCellContentView>
+ <point key="canvasLocation" x="-191" y="449"/>
+ </tableViewCell>
+ </objects>
+</document>
diff --git a/AuthSamples/Sample/Sample.entitlements.dev b/AuthSamples/Sample/Sample.entitlements.dev
new file mode 100644
index 0000000..9199dae
--- /dev/null
+++ b/AuthSamples/Sample/Sample.entitlements.dev
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>application-identifier</key>
+ <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
+ <key>aps-environment</key>
+ <string>development</string>
+</dict>
+</plist>
diff --git a/AuthSamples/Sample/Sample.entitlements.enterprise b/AuthSamples/Sample/Sample.entitlements.enterprise
new file mode 100644
index 0000000..edc26b2
--- /dev/null
+++ b/AuthSamples/Sample/Sample.entitlements.enterprise
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>application-identifier</key>
+ <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
+ <key>aps-environment</key>
+ <string>production</string>
+</dict>
+</plist>
diff --git a/AuthSamples/Sample/SettingsViewController.h b/AuthSamples/Sample/SettingsViewController.h
new file mode 100644
index 0000000..be7b752
--- /dev/null
+++ b/AuthSamples/Sample/SettingsViewController.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 <UIKit/UIKit.h>
+
+@class StaticContentTableViewManager;
+
+/** @class SettingsViewController
+ @brief A view controller for sample app info and settings.
+ */
+@interface SettingsViewController : UIViewController
+
+/** @property tableViewManager
+ @brief A @c StaticContentTableViewManager which is used to manage the contents of the table
+ view.
+ */
+@property(nonatomic, strong) IBOutlet StaticContentTableViewManager *tableViewManager;
+
+/** @fn done
+ @brief Called when user taps the "Done" button.
+ */
+- (IBAction)done:(id)sender;
+
+@end
diff --git a/AuthSamples/Sample/SettingsViewController.m b/AuthSamples/Sample/SettingsViewController.m
new file mode 100644
index 0000000..e7be9a6
--- /dev/null
+++ b/AuthSamples/Sample/SettingsViewController.m
@@ -0,0 +1,381 @@
+/*
+ * 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 "SettingsViewController.h"
+
+#import <objc/runtime.h>
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+#import "FirebaseAuth.h"
+#import "StaticContentTableViewManager.h"
+#import "UIViewController+Alerts.h"
+
+#if INTERNAL_GOOGLE3_BUILD
+#import "googlemac/iPhone/Identity/Firebear/Auth/Source/FIRAuth_Internal.h"
+#import "googlemac/iPhone/Identity/Firebear/Auth/Source/FIRAuthAPNSToken.h"
+#import "googlemac/iPhone/Identity/Firebear/Auth/Source/FIRAuthAPNSTokenManager.h"
+#import "googlemac/iPhone/Identity/Firebear/Auth/Source/FIRAuthAppCredential.h"
+#import "googlemac/iPhone/Identity/Firebear/Auth/Source/FIRAuthAppCredentialManager.h"
+#import "googlemac/iPhone/InstanceID/Firebase/Lib/Source/FIRInstanceID+Internal.h"
+#else
+@interface FIRInstanceID : NSObject
++ (void)notifyTokenRefresh;
+@end
+#endif
+
+/** @var kIdentityToolkitRequestClassName
+ @brief The class name of Identity Toolkit requests.
+ */
+static NSString *const kIdentityToolkitRequestClassName = @"FIRIdentityToolkitRequest";
+
+/** @var kSecureTokenRequestClassName
+ @brief The class name of Secure Token Service requests.
+ */
+static NSString *const kSecureTokenRequestClassName = @"FIRSecureTokenRequest";
+
+/** @var kIdentityToolkitSandboxHost
+ @brief The host of Identity Toolkit sandbox server.
+ */
+static NSString *const kIdentityToolkitSandboxHost = @"www-googleapis-staging.sandbox.google.com";
+
+/** @var kSecureTokenSandboxHost
+ @brief The host of Secure Token Service sandbox server.
+ */
+static NSString *const kSecureTokenSandboxHost = @"staging-securetoken.sandbox.googleapis.com";
+
+/** @var kGoogleServiceInfoPlists
+ @brief a C-array of plist file base names of Google service info to initialize FIRApp.
+ */
+static NSString *const kGoogleServiceInfoPlists[] = {
+ @"GoogleService-Info",
+ @"GoogleService-Info_multi"
+};
+
+/** @var gAPIEndpoints
+ @brief List of API Hosts by request class name.
+ */
+static NSDictionary<NSString *, NSArray<NSString *> *> *gAPIHosts;
+
+/** @var gFirebaseAppOptions
+ @brief List of FIROptions.
+ */
+static NSArray<FIROptions *> *gFirebaseAppOptions;
+
+/** @protocol RequestClass
+ @brief A de-facto protocol followed by request class objects to access its API host.
+ */
+@protocol RequestClass <NSObject>
+- (NSString *)host;
+- (void)setHost:(NSString *)host;
+@end
+
+/** @category FIROptions(ProjectID)
+ @brief A category to FIROption to add the project ID property.
+ */
+@interface FIROptions (ProjectID)
+
+/** @property projectID
+ @brief The Firebase project ID.
+ */
+@property(nonatomic, copy) NSString *projectID;
+
+@end
+
+@implementation FIROptions (ProjectID)
+
+- (NSString *)projectID {
+ return objc_getAssociatedObject(self, @selector(projectID));
+}
+
+- (void)setProjectID:(NSString *)projectID {
+ objc_setAssociatedObject(self, @selector(projectID), projectID, OBJC_ASSOCIATION_COPY_NONATOMIC);
+}
+
+@end
+
+/** @fn versionString
+ @brief Constructs a version string to display.
+ @param string The version in string form.
+ @param number The version in number form.
+ */
+static NSString *versionString(const unsigned char *string, const double number) {
+ return [NSString stringWithFormat:@"\"%s\" (%g)", string, number];
+}
+
+/** @fn requestHost
+ @brief Retrieves the API host for the request class.
+ @param requestClassName The name of the request class.
+ */
+static NSString *APIHost(NSString *requestClassName) {
+ return [(id<RequestClass>)NSClassFromString(requestClassName) host];
+}
+
+/** @fn truncatedString
+ @brief Truncates a string under a maximum length.
+ @param string The original string to be truncated.
+ @param length The maximum length of the truncated string.
+ @return The truncated string, which is not longer than @c length.
+ */
+static NSString *truncatedString(NSString *string, NSUInteger length) {
+ if (string.length <= length) {
+ return string;
+ }
+ NSUInteger half = (length - 3) / 2;
+ return [NSString stringWithFormat:@"%@...%@",
+ [string substringToIndex:half],
+ [string substringFromIndex:string.length - half]];
+}
+
+/** @fn hexString
+ @brief Converts a piece of data into a hexadecimal string.
+ @param data The raw data.
+ @return The hexadecimal string representation of the data.
+ */
+static NSString *hexString(NSData *data) {
+ NSMutableString *string = [NSMutableString stringWithCapacity:data.length * 2];
+ const unsigned char *bytes = data.bytes;
+ for (int idx = 0; idx < data.length; ++idx) {
+ [string appendFormat:@"%02X", (int)bytes[idx]];
+ }
+ return string;
+}
+
+@implementation SettingsViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ [self setUpAPIHosts];
+ [self setUpFirebaseAppOptions];
+ [self loadTableView];
+}
+
+- (IBAction)done:(id)sender {
+ [self dismissViewControllerAnimated:YES completion:nil];
+}
+
+- (void)setUpAPIHosts {
+ if (gAPIHosts) {
+ return;
+ }
+ gAPIHosts = @{
+ kIdentityToolkitRequestClassName : @[
+ APIHost(kIdentityToolkitRequestClassName),
+ kIdentityToolkitSandboxHost,
+ ],
+ kSecureTokenRequestClassName : @[
+ APIHost(kSecureTokenRequestClassName),
+ kSecureTokenSandboxHost,
+ ],
+ };
+}
+
+- (void)setUpFirebaseAppOptions {
+ if (gFirebaseAppOptions) {
+ return;
+ }
+ int numberOfOptions = sizeof(kGoogleServiceInfoPlists) / sizeof(*kGoogleServiceInfoPlists);
+ NSMutableArray *appOptions = [[NSMutableArray alloc] initWithCapacity:numberOfOptions];
+ for (int i = 0; i < numberOfOptions; i++) {
+ NSString *plistFileName = kGoogleServiceInfoPlists[i];
+ NSString *plistFilePath = [[NSBundle mainBundle] pathForResource:plistFileName
+ ofType:@"plist"];
+ NSDictionary *optionsDictionary = [NSDictionary dictionaryWithContentsOfFile:plistFilePath];
+ FIROptions *options = [[FIROptions alloc]
+ initWithGoogleAppID:optionsDictionary[@"GOOGLE_APP_ID"]
+ bundleID:optionsDictionary[@"BUNDLE_ID"]
+ GCMSenderID:optionsDictionary[@"GCM_SENDER_ID"]
+ APIKey:optionsDictionary[@"API_KEY"]
+ clientID:optionsDictionary[@"CLIENT_ID"]
+ trackingID:optionsDictionary[@"TRACKING_ID"]
+ androidClientID:optionsDictionary[@"ANDROID_CLIENT_ID"]
+ databaseURL:optionsDictionary[@"DATABASE_URL"]
+ storageBucket:optionsDictionary[@"STORAGE_BUCKET"]
+ deepLinkURLScheme:nil];
+ options.projectID = optionsDictionary[@"PROJECT_ID"];
+ [appOptions addObject:options];
+ }
+ gFirebaseAppOptions = [appOptions copy];
+}
+
+- (void)loadTableView {
+ __weak typeof(self) weakSelf = self;
+ _tableViewManager.contents = [StaticContentTableViewContent contentWithSections:@[
+ [StaticContentTableViewSection sectionWithTitle:@"Versions" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"FirebaseAuth"
+ value:versionString(
+ FirebaseAuthVersionString, FirebaseAuthVersionNumber)],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:@"Client Identity" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"Project"
+ value:[self currentProjectID]
+ action:^{
+ [weakSelf toggleClientProject];
+ }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:@"API Hosts" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"Identity Toolkit"
+ value:APIHost(kIdentityToolkitRequestClassName)
+ action:^{
+ [weakSelf toggleAPIHostWithRequestClassName:kIdentityToolkitRequestClassName];
+ }],
+ [StaticContentTableViewCell cellWithTitle:@"Secure Token"
+ value:APIHost(kSecureTokenRequestClassName)
+ action:^{
+ [weakSelf toggleAPIHostWithRequestClassName:kSecureTokenRequestClassName];
+ }],
+ ]],
+#if INTERNAL_GOOGLE3_BUILD
+ [StaticContentTableViewSection sectionWithTitle:@"Phone Auth" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"APNs Token"
+ value:[self APNSTokenString]
+ action:^{
+ [weakSelf clearAPNSToken];
+ }],
+ [StaticContentTableViewCell cellWithTitle:@"App Credential"
+ value:[self appCredentialString]
+ action:^{
+ [weakSelf clearAppCredential];
+ }],
+ ]],
+#endif // INTERNAL_GOOGLE3_BUILD
+ ]];
+}
+
+/** @fn toggleAPIHostWithRequestClassName:
+ @brief Toggles the host name of the server that handles RPCs.
+ @param requestClassName The name of the RPC request class.
+ */
+- (void)toggleAPIHostWithRequestClassName:(NSString *)requestClassName {
+ NSString *currentHost = APIHost(requestClassName);
+ NSArray<NSString *> *allHosts = gAPIHosts[requestClassName];
+ NSString *newHost = allHosts[([allHosts indexOfObject:currentHost] + 1) % allHosts.count];
+ [(id<RequestClass>)NSClassFromString(requestClassName) setHost:newHost];
+ [self loadTableView];
+}
+
+/** @fn currentProjectID
+ @brief Returns the the current Firebase project ID.
+ */
+- (NSString *)currentProjectID {
+ NSString *APIKey = [FIRApp defaultApp].options.APIKey;
+ for (FIROptions *options in gFirebaseAppOptions) {
+ if ([options.APIKey isEqualToString:APIKey]) {
+ return options.projectID;
+ }
+ }
+ return nil;
+}
+
+/** @fn toggleClientProject
+ @brief Toggles the Firebase/Google project this client presents by recreating the default
+ FIRApp instance with different options.
+ */
+- (void)toggleClientProject {
+ NSString *APIKey = [FIRApp defaultApp].options.APIKey;
+ for (NSUInteger i = 0 ; i < gFirebaseAppOptions.count; i++) {
+ FIROptions *options = gFirebaseAppOptions[i];
+ if ([options.APIKey isEqualToString:APIKey]) {
+ __weak typeof(self) weakSelf = self;
+ [[FIRApp defaultApp] deleteApp:^(BOOL success) {
+ if (success) {
+ [FIRInstanceID notifyTokenRefresh]; // b/28967043
+ dispatch_async(dispatch_get_main_queue(), ^() {
+ FIROptions *options = gFirebaseAppOptions[(i + 1) % gFirebaseAppOptions.count];
+ [FIRApp configureWithOptions:options];
+ [weakSelf loadTableView];
+ });
+ }
+ }];
+ return;
+ }
+ }
+}
+
+#if INTERNAL_GOOGLE3_BUILD
+
+/** @fn APNSTokenString
+ @brief Returns a string representing APNS token.
+ */
+- (NSString *)APNSTokenString {
+ FIRAuthAPNSToken *token = [FIRAuth auth].tokenManager.token;
+ if (!token) {
+ return @"";
+ }
+ return [NSString stringWithFormat:@"%@(%@)",
+ truncatedString(hexString(token.data), 19),
+ token.type == FIRAuthAPNSTokenTypeProd ? @"P" : @"S"];
+}
+
+/** @fn clearAPNSToken
+ @brief Clears the saved app credential.
+ */
+- (void)clearAPNSToken {
+ FIRAuthAPNSToken *token = [FIRAuth auth].tokenManager.token;
+ if (!token) {
+ return;
+ }
+ NSString *tokenType = token.type == FIRAuthAPNSTokenTypeProd ? @"Production" : @"Sandbox";
+ NSString *message = [NSString stringWithFormat:@"token: %@\ntype: %@",
+ hexString(token.data), tokenType];
+ [self showMessagePromptWithTitle:@"Clear APNs Token?"
+ message:message
+ showCancelButton:YES
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (userPressedOK) {
+ [FIRAuth auth].tokenManager.token = nil;
+ [self loadTableView];
+ }
+ }];
+}
+
+/** @fn appCredentialString
+ @brief Returns a string representing app credential.
+ */
+- (NSString *)appCredentialString {
+ FIRAuthAppCredential *credential = [FIRAuth auth].appCredentialManager.credential;
+ if (!credential) {
+ return @"";
+ }
+ return [NSString stringWithFormat:@"%@/%@",
+ truncatedString(credential.receipt, 13),
+ truncatedString(credential.secret, 13)];
+}
+
+/** @fn clearAppCredential
+ @brief Clears the saved app credential.
+ */
+- (void)clearAppCredential {
+ FIRAuthAppCredential *credential = [FIRAuth auth].appCredentialManager.credential;
+ if (!credential) {
+ return;
+ }
+ NSString *message = [NSString stringWithFormat:@"receipt: %@\nsecret: %@",
+ credential.receipt, credential.secret];
+ [self showMessagePromptWithTitle:@"Clear App Credential?"
+ message:message
+ showCancelButton:YES
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (userPressedOK) {
+ [[FIRAuth auth].appCredentialManager clearCredential];
+ [self loadTableView];
+ }
+ }];
+}
+
+#endif // INTERNAL_GOOGLE3_BUILD
+
+@end
diff --git a/AuthSamples/Sample/SettingsViewController.xib b/AuthSamples/Sample/SettingsViewController.xib
new file mode 100644
index 0000000..4540047
--- /dev/null
+++ b/AuthSamples/Sample/SettingsViewController.xib
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
+ </dependencies>
+ <objects>
+ <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SettingsViewController">
+ <connections>
+ <outlet property="tableViewManager" destination="U72-zk-rRG" id="KxN-ZQ-cAa"/>
+ <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
+ </connections>
+ </placeholder>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+ <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rRb-jS-QTO">
+ <rect key="frame" x="8" y="16" width="46" height="30"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="30" id="6TV-T6-63H"/>
+ <constraint firstAttribute="width" constant="46" id="kVF-SE-Qzn"/>
+ </constraints>
+ <state key="normal" title="Done"/>
+ <connections>
+ <action selector="done:" destination="-1" eventType="touchUpInside" id="O91-6u-80J"/>
+ </connections>
+ </button>
+ <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" translatesAutoresizingMaskIntoConstraints="NO" id="4BU-Kb-3Zx">
+ <rect key="frame" x="0.0" y="54" width="600" height="546"/>
+ <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
+ <connections>
+ <outlet property="dataSource" destination="U72-zk-rRG" id="GFT-r0-uBK"/>
+ <outlet property="delegate" destination="U72-zk-rRG" id="uzl-Rm-2qd"/>
+ </connections>
+ </tableView>
+ </subviews>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <constraints>
+ <constraint firstItem="rRb-jS-QTO" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" constant="8" id="1PF-3b-y4w"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" id="5Tz-J0-2Dq"/>
+ <constraint firstItem="rRb-jS-QTO" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" constant="16" id="K7t-0g-JBo"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="top" secondItem="rRb-jS-QTO" secondAttribute="bottom" constant="8" id="MyI-Bl-nGS"/>
+ <constraint firstAttribute="bottom" secondItem="4BU-Kb-3Zx" secondAttribute="bottom" id="QMm-Wv-q60"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="centerX" secondItem="i5M-Pr-FkT" secondAttribute="centerX" id="deo-wi-Nmq"/>
+ </constraints>
+ </view>
+ <customObject id="U72-zk-rRG" customClass="StaticContentTableViewManager">
+ <connections>
+ <outlet property="tableView" destination="4BU-Kb-3Zx" id="0OK-KI-QdH"/>
+ </connections>
+ </customObject>
+ </objects>
+</document>
diff --git a/AuthSamples/Sample/StaticContentTableViewManager.h b/AuthSamples/Sample/StaticContentTableViewManager.h
new file mode 100644
index 0000000..cb56391
--- /dev/null
+++ b/AuthSamples/Sample/StaticContentTableViewManager.h
@@ -0,0 +1,257 @@
+/*
+ * 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/UIKit.h>
+
+#pragma mark - Forward Declarations
+
+@class StaticContentTableViewCell;
+@class StaticContentTableViewContent;
+@class StaticContentTableViewSection;
+
+#pragma mark - Block Type Definitions
+
+/** @typedef StaticContentTableViewCellAction
+ @brief The type of block invoked when a cell is tapped.
+ */
+typedef void(^StaticContentTableViewCellAction)(void);
+
+#pragma mark -
+
+/** @class StaticContentTableViewManager
+ @brief Generic class useful for populating a @c UITableView with static content.
+ @remarks Because I keep writing the same UITableView code for every internal testing app, and
+ it's getting too tedious and ugly to keep writing the same thing over and over. It makes
+ our sample apps harder to maintain with all this code sprinkled around everywhere, and
+ we end up cutting corners and making junky testing apps, and spending more time in the
+ process.
+ */
+@interface StaticContentTableViewManager : NSObject<UITableViewDelegate, UITableViewDataSource>
+
+/** @property contents
+ @brief The static contents of the @c UITableView.
+ @remarks Setting this property will reload the @c UITableView.
+ */
+@property(nonatomic, strong, nullable) StaticContentTableViewContent *contents;
+
+/** @property tableView
+ @brief A reference to the managed @c UITableView.
+ @remarks This is needed to automatically reload the table view when the @c contents are changed.
+ */
+@property(nonatomic, weak, nullable) IBOutlet UITableView *tableView;
+
+@end
+
+#pragma mark -
+
+/** @class StaticContentTableViewContent
+ @brief Represents the contents of a @c UITableView.
+ */
+@interface StaticContentTableViewContent : NSObject
+
+/** @property sections
+ @brief The sections for the @c UITableView.
+ */
+@property(nonatomic, copy, readonly, nullable) NSArray<StaticContentTableViewSection *> *sections;
+
+/** @fn contentWithSections:
+ @brief Convenience factory method for creating a new instance of
+ @c StaticContentTableViewContent.
+ @param sections The sections for the @c UITableView.
+ */
++ (nullable instancetype)contentWithSections:
+ (nullable NSArray<StaticContentTableViewSection *> *)sections;
+
+/** @fn init
+ @brief Please use initWithSections:
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithSections:
+ @brief Designated initializer.
+ @param sections The sections in the @c UITableView.
+ */
+- (nullable instancetype)initWithSections:
+ (nullable NSArray<StaticContentTableViewSection *> *)sections;
+
+@end
+
+#pragma mark -
+
+/** @class StaticContentTableViewSection
+ @brief Represents a section in a @c UITableView.
+ @remarks Each section has a title (used for the section title in the @c UITableView) and an
+ array of cells.
+ */
+@interface StaticContentTableViewSection : NSObject
+
+/** @property title
+ @brief The title of the section in the @c UITableView.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *title;
+
+/** @property cells
+ @brief The cells in this section of the @c UITableView.
+ */
+@property(nonatomic, copy, readonly, nullable) NSArray<StaticContentTableViewCell *> *cells;
+
+/** @fn sectionWithTitle:cells:
+ @brief Convenience factory method for creating a new instance of
+ @c StaticContentTableViewSection.
+ @param title The title of the section in the @c UITableView.
+ @param cells The cells in this section of the @c UITableView.
+ */
++ (nullable instancetype)sectionWithTitle:(nullable NSString *)title
+ cells:(nullable NSArray<StaticContentTableViewCell *> *)cells;
+
+/** @fn init
+ @brief Please use initWithTitle:cells:
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithTitle:cells:
+ @brief Designated initializer.
+ @param title The title of the section in the @c UITableView.
+ @param cells The cells in this section of the @c UITableView.
+ */
+- (nullable instancetype)initWithTitle:(nullable NSString *)title
+ cells:(nullable NSArray<StaticContentTableViewCell *> *)cells;
+
+@end
+
+#pragma mark -
+
+/** @class StaticContentTableViewCell
+ @brief Represents a cell in a @c UITableView.
+ @remarks Cells may be custom cells (in which you specify a @c UITableViewCell to use), or
+ simple single-label cells which you supply the title text for. It does not make sense to
+ specify both @c customCell and also @c title, but if a @c customCell is specified, it will
+ be used instead of the @c title.
+ */
+@interface StaticContentTableViewCell : NSObject
+
+/** @property customCell
+ @brief The custom @c UITableViewCell to use for this cell.
+ */
+@property(nonatomic, strong, readonly, nullable) UITableViewCell *customCell;
+
+/** @property title
+ @brief If no custom cell is being used, this is the text of the @c titleLabel of the
+ @c UITableViewCell.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *title;
+
+/** @property value
+ @brief If no custom cell is being used, this is the text of the @c detailTextLabel of the
+ @c UITableViewCell.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *value;
+
+/** @property accessibilityIdentifier
+ @brief The accessibility ID for the corresponding @c UITableViewCell.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *accessibilityIdentifier;
+
+/** @property action
+ @brief A block which is executed when the cell is selected.
+ @remarks Avoid retain cycles. Since these blocked are retained here, and your
+ @c UIViewController's object graph likely retains this object, you don't want these blocks
+ to retain your @c UIViewController. The easiest thing is just to create a weak reference to
+ your @c UIViewController and pass it a message as the only thing the block does.
+ */
+@property(nonatomic, copy, readonly, nullable) StaticContentTableViewCellAction action;
+
+/** @fn cellWithTitle:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param title The text of the @c titleLabel of the @c UITableViewCell.
+ */
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title;
+
+/** @fn cellWithTitle:value:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param title The text of the @c titleLabel of the @c UITableViewCell.
+ @param value The text of the @c detailTextLabel of the @c UITableViewCell.
+ */
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value;
+
+/** @fn cellWithTitle:action:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param title The text of the @c titleLabel of the @c UITableViewCell.
+ @param action A block which is executed when the cell is selected.
+ */
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ action:(nullable StaticContentTableViewCellAction)action;
+
+/** @fn cellWithTitle:value:action:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param title The text of the @c titleLabel of the @c UITableViewCell.
+ @param value The text of the @c detailTextLabel of the @c UITableViewCell.
+ @param action A block which is executed when the cell is selected.
+ */
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action;
+
+/** @fn cellWithTitle:value:action:accessibilityLabel:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param title The text of the @c titleLabel of the @c UITableViewCell.
+ @param value The text of the @c detailTextLabel of the @c UITableViewCell.
+ @param action A block which is executed when the cell is selected.
+ @param accessibilityID The accessibility ID to add to the cell.
+ */
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action
+ accessibilityID:(nullable NSString *)accessibilityID;
+
+/** @fn cellWithCustomCell:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param customCell The custom @c UITableViewCell to use for this cell.
+ */
++ (nullable instancetype)cellWithCustomCell:(nullable UITableViewCell *)customCell;
+
+/** @fn cellWithCustomCell:action:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param customCell The custom @c UITableViewCell to use for this cell.
+ @param action A block which is executed when the cell is selected.
+ */
++ (nullable instancetype)cellWithCustomCell:(nullable UITableViewCell *)customCell
+ action:(nullable StaticContentTableViewCellAction)action;
+
+/** @fn init
+ @brief Please use initWithCustomCell:title:action:
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithCustomCell:title:action:
+ @brief Designated initializer.
+ @param customCell The custom @c UITableViewCell to use for this cell.
+ @param title If no custom cell is being used, this is the text of the @c titleLabel of the
+ @c UITableViewCell.
+ @param title If no custom cell is being used, this is the text of the @c detailTextLabel of the
+ @c UITableViewCell.
+ @param action A block which is executed when the cell is selected.
+ @param accessibilityID The accessibility ID to add to the cell.
+ */
+- (nullable instancetype)initWithCustomCell:(nullable UITableViewCell *)customCell
+ title:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action
+ accessibilityID:(nullable NSString *)accessibilityID
+ NS_DESIGNATED_INITIALIZER;
+
+@end
diff --git a/AuthSamples/Sample/StaticContentTableViewManager.m b/AuthSamples/Sample/StaticContentTableViewManager.m
new file mode 100644
index 0000000..7ac7eb7
--- /dev/null
+++ b/AuthSamples/Sample/StaticContentTableViewManager.m
@@ -0,0 +1,231 @@
+/*
+ * 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 "StaticContentTableViewManager.h"
+
+/** @var kCellReuseIdentitfier
+ @brief The reuse identifier for default style table view cell.
+ */
+static NSString *const kCellReuseIdentitfier = @"reuseIdentifier";
+
+/** @var kCellReuseIdentitfier
+ @brief The reuse identifier for value style table view cell.
+ */
+static NSString *const kValueCellReuseIdentitfier = @"reuseValueIdentifier";
+
+#pragma mark -
+
+@implementation StaticContentTableViewManager
+
+- (void)setContents:(StaticContentTableViewContent *)contents {
+ _contents = contents;
+ [self.tableView reloadData];
+}
+
+- (void)setTableView:(UITableView *)tableView {
+ _tableView = tableView;
+ [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseIdentitfier];
+}
+
+#pragma mark - UITableViewDataSource
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
+ return _contents.sections.count;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+ return _contents.sections[section].cells.count;
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
+ return _contents.sections[section].title;
+}
+
+#pragma mark - UITableViewDelegate
+
+- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
+ StaticContentTableViewCell *cellData = _contents.sections[indexPath.section].cells[indexPath.row];
+ if (cellData.customCell) {
+ return cellData.customCell.frame.size.height;
+ }
+ return 44;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView
+ cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+ StaticContentTableViewCell *cellData = _contents.sections[indexPath.section].cells[indexPath.row];
+ UITableViewCell *cell = cellData.customCell;
+ if (cell) {
+ return cell;
+ }
+ if (cellData.value.length) {
+ cell = [tableView dequeueReusableCellWithIdentifier:kValueCellReuseIdentitfier];
+ if (!cell) {
+ cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
+ reuseIdentifier:kValueCellReuseIdentitfier];
+ cell.detailTextLabel.adjustsFontSizeToFitWidth = YES;
+ cell.detailTextLabel.minimumScaleFactor = 0.5;
+ }
+ cell.detailTextLabel.text = cellData.value;
+ } else {
+ // kCellReuseIdentitfier has already been registered.
+ cell = [tableView dequeueReusableCellWithIdentifier:kCellReuseIdentitfier
+ forIndexPath:indexPath];
+ }
+ cell.textLabel.text = cellData.title;
+ cell.accessibilityIdentifier = cellData.accessibilityIdentifier;
+ return cell;
+}
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
+ StaticContentTableViewCell *cellData = _contents.sections[indexPath.section].cells[indexPath.row];
+ BOOL hasAssociatedAction = cellData.action != nil;
+ if (hasAssociatedAction) {
+ cellData.action();
+ }
+ [tableView deselectRowAtIndexPath:indexPath animated:hasAssociatedAction];
+}
+
+@end
+
+#pragma mark -
+
+@implementation StaticContentTableViewContent
+
++ (nullable instancetype)contentWithSections:
+ (nullable NSArray<StaticContentTableViewSection *> *)sections {
+ return [[self alloc] initWithSections:sections];
+}
+
+- (nullable instancetype)initWithSections:
+ (nullable NSArray<StaticContentTableViewSection *> *)sections {
+ self = [super init];
+ if (self) {
+ _sections = [sections copy];
+ }
+ return self;
+}
+
+@end
+
+#pragma mark -
+
+@implementation StaticContentTableViewSection
+
++ (nullable instancetype)sectionWithTitle:(nullable NSString *)title
+ cells:(nullable NSArray<StaticContentTableViewCell *> *)cells {
+ return [[self alloc] initWithTitle:title cells:cells];
+}
+
+- (nullable instancetype)initWithTitle:(nullable NSString *)title
+ cells:(nullable NSArray<StaticContentTableViewCell *> *)cells {
+ self = [super init];
+ if (self) {
+ _title = [title copy];
+ _cells = [cells copy];
+ }
+ return self;
+}
+
+@end
+
+#pragma mark -
+
+@implementation StaticContentTableViewCell
+
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title {
+ return [[self alloc] initWithCustomCell:nil
+ title:title
+ value:nil
+ action:nil
+ accessibilityID:nil];
+}
+
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value {
+ return [[self alloc] initWithCustomCell:nil
+ title:title
+ value:value
+ action:nil
+ accessibilityID:nil];
+}
+
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ action:(nullable StaticContentTableViewCellAction)action {
+ return [[self alloc] initWithCustomCell:nil
+ title:title
+ value:nil
+ action:action
+ accessibilityID:nil];
+}
+
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action {
+ return [[self alloc] initWithCustomCell:nil
+ title:title
+ value:value
+ action:action
+ accessibilityID:nil];
+}
+
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action
+ accessibilityID:(nullable NSString *)accessibilityID {
+ return [[self alloc] initWithCustomCell:nil
+ title:title
+ value:value
+ action:action
+ accessibilityID:accessibilityID];
+}
+
++ (nullable instancetype)cellWithCustomCell:(nullable UITableViewCell *)customCell {
+ return [[self alloc] initWithCustomCell:customCell
+ title:nil
+ value:nil
+ action:nil
+ accessibilityID:nil];
+}
+
++ (nullable instancetype)cellWithCustomCell:(nullable UITableViewCell *)customCell
+ action:(nullable StaticContentTableViewCellAction)action {
+ return [[self alloc] initWithCustomCell:customCell
+ title:nil
+ value:nil action:action
+ accessibilityID:nil];
+}
+
+- (nullable instancetype)initWithCustomCell:(nullable UITableViewCell *)customCell
+ title:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action
+ accessibilityID:(nullable NSString *)accessibilityID {
+ self = [super init];
+ if (self) {
+ _customCell = customCell;
+ _title = [title copy];
+ _value = [value copy];
+ _action = action;
+ if (accessibilityID) {
+ _accessibilityIdentifier = [accessibilityID copy];
+ self.isAccessibilityElement = YES;
+ }
+ }
+ return self;
+}
+
+@end
diff --git a/AuthSamples/Sample/Strings/ar.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/ar.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/ar.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/ca.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/ca.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/ca.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/cs.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/cs.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/cs.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/da.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/da.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/da.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/de.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/de.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/de.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/el.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/el.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/el.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/en.lproj/FirebaseAuthUI.strings b/AuthSamples/Sample/Strings/en.lproj/FirebaseAuthUI.strings
new file mode 100644
index 0000000..815456e
--- /dev/null
+++ b/AuthSamples/Sample/Strings/en.lproj/FirebaseAuthUI.strings
@@ -0,0 +1,2 @@
+/* Title for auth picker screen. */
+"AuthPickerTitle" = "Welcome, you are using a Custom Bundle";
diff --git a/AuthSamples/Sample/Strings/en_GB.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/en_GB.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/en_GB.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/es.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/es.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/es.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/es_MX.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/es_MX.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/es_MX.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/fi.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/fi.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/fi.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/fr.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/fr.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/fr.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/he.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/he.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/he.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/hr.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/hr.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/hr.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/hu.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/hu.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/hu.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/id.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/id.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/id.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/it.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/it.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/it.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/ja.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/ja.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/ja.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/ko.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/ko.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/ko.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/ms.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/ms.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/ms.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/nb.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/nb.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/nb.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/nl.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/nl.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/nl.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/pl.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/pl.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/pl.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/pt.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/pt.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/pt.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/pt_BR.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/pt_BR.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/pt_BR.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/pt_PT.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/pt_PT.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/pt_PT.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/ro.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/ro.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/ro.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/ru.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/ru.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/ru.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/sk.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/sk.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/sk.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/sv.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/sv.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/sv.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/th.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/th.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/th.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/tr.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/tr.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/tr.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/uk.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/uk.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/uk.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/vi.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/vi.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/vi.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/zh_CN.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/zh_CN.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/zh_CN.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/Strings/zh_TW.lproj/FirebearSample.strings b/AuthSamples/Sample/Strings/zh_TW.lproj/FirebearSample.strings
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/AuthSamples/Sample/Strings/zh_TW.lproj/FirebearSample.strings
@@ -0,0 +1 @@
+
diff --git a/AuthSamples/Sample/UIViewController+Alerts.h b/AuthSamples/Sample/UIViewController+Alerts.h
new file mode 100644
index 0000000..375a0ed
--- /dev/null
+++ b/AuthSamples/Sample/UIViewController+Alerts.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 <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*! @typedef AlertPromptCompletionBlock
+ @brief The type of callback used to report text input prompt results.
+ */
+typedef void (^AlertPromptCompletionBlock)(BOOL userPressedOK, NSString *_Nullable userInput);
+
+/*! @class Alerts
+ @brief Wrapper for @c UIAlertController and @c UIAlertView for backwards compatability with
+ iOS 6+.
+ */
+@interface UIViewController (Alerts)
+
+/*! @property useStatusBarSpinner
+ @brief Uses the status bar to indicate work is occuring instead of a modal "please wait" dialog.
+ This is generally useful for allowing user interaction while things are happening.
+ */
+@property(nonatomic, assign) BOOL useStatusBarSpinner;
+
+/*! @fn showMessagePrompt:
+ @brief Displays an alert with an 'OK' button and a message.
+ @param message The message to display.
+ @remarks The message is also copied to the pasteboard.
+ */
+- (void)showMessagePrompt:(NSString *)message;
+
+/*! @fn showMessagePromptWithTitle:message:
+ @brief Displays a titled alert with an 'OK' button and a message.
+ @param title The title of the alert if it exists.
+ @param message The message to display.
+ @param showCancelButton A flag indicating whether or not a cancel option is available.
+ @param completion The completion block to be executed after the alert is dismissed, if it
+ exists.
+ @remarks The message is also copied to the pasteboard.
+ */
+- (void)showMessagePromptWithTitle:(nullable NSString *)title
+ message:(NSString *)message
+ showCancelButton:(BOOL)showCancelButton
+ completion:(nullable AlertPromptCompletionBlock)completion;
+
+/*! @fn showTextInputPromptWithMessage:keyboardType:completionBlock:
+ @brief Shows a prompt with a text field and 'OK'/'Cancel' buttons.
+ @param message The message to display.
+ @param keyboardType The type of keyboard to display for the UITextView in the prompt.
+ @param completion A block to call when the user taps 'OK' or 'Cancel'.
+ */
+- (void)showTextInputPromptWithMessage:(NSString *)message
+ keyboardType:(UIKeyboardType)keyboardType
+ completionBlock:(AlertPromptCompletionBlock)completion;
+
+/*! @fn showTextInputPromptWithMessage:completionBlock:
+ @brief Shows a prompt with a text field and 'OK'/'Cancel' buttons.
+ @param message The message to display.
+ @param completion A block to call when the user taps 'OK' or 'Cancel'.
+ */
+- (void)showTextInputPromptWithMessage:(NSString *)message
+ completionBlock:(AlertPromptCompletionBlock)completion;
+
+/*! @fn showSpinner
+ @brief Shows the please wait spinner.
+ @param completion Called after the spinner has been hidden.
+ */
+- (void)showSpinner:(nullable void(^)(void))completion;
+
+/*! @fn hideSpinner
+ @brief Hides the please wait spinner.
+ @param completion Called after the spinner has been hidden.
+ */
+- (void)hideSpinner:(nullable void(^)(void))completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/AuthSamples/Sample/UIViewController+Alerts.m b/AuthSamples/Sample/UIViewController+Alerts.m
new file mode 100644
index 0000000..76ef067
--- /dev/null
+++ b/AuthSamples/Sample/UIViewController+Alerts.m
@@ -0,0 +1,346 @@
+/*
+ * 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 "UIViewController+Alerts.h"
+
+#import <objc/runtime.h>
+
+/*! @var kPleaseWaitAssociatedObjectKey
+ @brief Key used to identify the "please wait" spinner associated object.
+ */
+static NSString *const kPleaseWaitAssociatedObjectKey =
+ @"_UIViewControllerAlertCategory_PleaseWaitScreenAssociatedObject";
+
+/*! @var kUseStatusBarSpinnerAssociatedObjectKey
+ @brief The address of this constant is the key used to identify the "use status bar spinner"
+ associated object.
+ */
+static const void *const kUseStatusBarSpinnerAssociatedObjectKey;
+
+/*! @var kOK
+ @brief Text for an 'OK' button.
+ */
+static NSString *const kOK = @"OK";
+
+/*! @var kCancel
+ @brief Text for an 'Cancel' button.
+ */
+static NSString *const kCancel = @"Cancel";
+
+/*! @class SimpleTextPromptDelegate
+ @brief A @c UIAlertViewDelegate which allows @c UIAlertView to be used with blocks more easily.
+ */
+@interface SimpleTextPromptDelegate : NSObject <UIAlertViewDelegate>
+
+/*! @fn init
+ @brief Please use initWithCompletionHandler.
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+/*! @fn initWithCompletionHandler:
+ @brief Designated initializer.
+ @param completionHandler The block to call when the alert view is dismissed.
+ */
+- (nullable instancetype)initWithCompletionHandler:(AlertPromptCompletionBlock)completionHandler
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation UIViewController (Alerts)
+
+- (void)setUseStatusBarSpinner:(BOOL)useStatusBarSpinner {
+ objc_setAssociatedObject(self,
+ &kUseStatusBarSpinnerAssociatedObjectKey,
+ useStatusBarSpinner ? @(YES) : nil,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (BOOL)useStatusBarSpinner {
+ return objc_getAssociatedObject(self, &kUseStatusBarSpinnerAssociatedObjectKey) ? YES : NO;
+}
+
+/*! @fn supportsAlertController
+ @brief Determines if the current platform supports @c UIAlertController.
+ @return YES if the current platform supports @c UIAlertController.
+ */
+- (BOOL)supportsAlertController {
+ return NSClassFromString(@"UIAlertController") != nil;
+}
+
+- (void)showMessagePrompt:(NSString *)message {
+ [self showMessagePromptWithTitle:nil message:message showCancelButton:NO completion:nil];
+}
+
+- (void)showMessagePromptWithTitle:(nullable NSString *)title
+ message:(NSString *)message
+ showCancelButton:(BOOL)showCancelButton
+ completion:(nullable AlertPromptCompletionBlock)completion {
+ if (message) {
+ [UIPasteboard generalPasteboard].string = message;
+ }
+ if ([self supportsAlertController]) {
+ UIAlertController *alert =
+ [UIAlertController alertControllerWithTitle:title
+ message:message
+ preferredStyle:UIAlertControllerStyleAlert];
+ UIAlertAction *okAction =
+ [UIAlertAction actionWithTitle:kOK
+ style:UIAlertActionStyleDefault
+ handler:^(UIAlertAction * _Nonnull action) {
+ if (completion) {
+ completion(YES, nil);
+ }
+ }];
+ [alert addAction:okAction];
+
+ if (showCancelButton) {
+ UIAlertAction *cancelAction =
+ [UIAlertAction actionWithTitle:kCancel
+ style:UIAlertActionStyleCancel
+ handler:^(UIAlertAction * _Nonnull action) {
+ completion(NO, nil);
+ }];
+ [alert addAction:cancelAction];
+ }
+ [self presentViewController:alert animated:YES completion:nil];
+ } else {
+ UIAlertView *alert =
+ [[UIAlertView alloc] initWithTitle:title
+ message:message
+ delegate:nil
+ cancelButtonTitle:nil
+ otherButtonTitles:kOK, nil];
+ [alert show];
+ }
+}
+
+- (void)showTextInputPromptWithMessage:(NSString *)message
+ completionBlock:(AlertPromptCompletionBlock)completion {
+ [self showTextInputPromptWithMessage:message
+ keyboardType:UIKeyboardTypeDefault
+ completionBlock:completion];
+}
+
+- (void)showTextInputPromptWithMessage:(NSString *)message
+ keyboardType:(UIKeyboardType)keyboardType
+ completionBlock:(nonnull AlertPromptCompletionBlock)completion {
+ if ([self supportsAlertController]) {
+ UIAlertController *prompt =
+ [UIAlertController alertControllerWithTitle:nil
+ message:message
+ preferredStyle:UIAlertControllerStyleAlert];
+ __weak UIAlertController *weakPrompt = prompt;
+ UIAlertAction *cancelAction =
+ [UIAlertAction actionWithTitle:kCancel
+ style:UIAlertActionStyleCancel
+ handler:^(UIAlertAction * _Nonnull action) {
+ completion(NO, nil);
+ }];
+ UIAlertAction *okAction = [UIAlertAction actionWithTitle:kOK
+ style:UIAlertActionStyleDefault
+ handler:^(UIAlertAction * _Nonnull action) {
+ UIAlertController *strongPrompt = weakPrompt;
+ completion(YES, strongPrompt.textFields[0].text);
+ }];
+ [prompt addTextFieldWithConfigurationHandler:^(UITextField *_Nonnull textField) {
+ textField.keyboardType = keyboardType;
+ }];
+ [prompt addAction:cancelAction];
+ [prompt addAction:okAction];
+ [self presentViewController:prompt animated:YES completion:nil];
+ } else {
+ SimpleTextPromptDelegate *prompt =
+ [[SimpleTextPromptDelegate alloc] initWithCompletionHandler:completion];
+ UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil
+ message:message
+ delegate:prompt
+ cancelButtonTitle:@"Cancel"
+ otherButtonTitles:@"Ok", nil];
+ alertView.alertViewStyle = UIAlertViewStylePlainTextInput;
+ [alertView show];
+ }
+}
+
+- (void)showSpinner:(nullable void(^)(void))completion {
+ if (self.useStatusBarSpinner) {
+ [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
+ completion();
+ return;
+ }
+ if ([self supportsAlertController]) {
+ [self showModernSpinner:completion];
+ } else {
+ [self showIOS7Spinner:completion];
+ }
+}
+
+- (void)showModernSpinner:(nullable void (^)(void))completion {
+ UIAlertController *pleaseWaitAlert =
+ objc_getAssociatedObject(self,
+ (__bridge const void *)kPleaseWaitAssociatedObjectKey);
+ if (pleaseWaitAlert) {
+ if (completion) {
+ completion();
+ }
+ return;
+ }
+ pleaseWaitAlert = [UIAlertController alertControllerWithTitle:nil
+ message:@"Please Wait...\n\n\n\n"
+ preferredStyle:UIAlertControllerStyleAlert];
+
+ UIActivityIndicatorView *spinner =
+ [[UIActivityIndicatorView alloc]
+ initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
+ spinner.color = [UIColor blackColor];
+ spinner.center = CGPointMake(pleaseWaitAlert.view.bounds.size.width / 2,
+ pleaseWaitAlert.view.bounds.size.height / 2);
+ spinner.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin |
+ UIViewAutoresizingFlexibleTopMargin |
+ UIViewAutoresizingFlexibleLeftMargin |
+ UIViewAutoresizingFlexibleRightMargin;
+ [spinner startAnimating];
+ [pleaseWaitAlert.view addSubview:spinner];
+
+ objc_setAssociatedObject(self,
+ (__bridge const void *)(kPleaseWaitAssociatedObjectKey),
+ pleaseWaitAlert,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ [self presentViewController:pleaseWaitAlert animated:YES completion:completion];
+}
+
+- (void)showIOS7Spinner:(nullable void (^)(void))completion {
+ UIWindow *pleaseWaitWindow =
+ objc_getAssociatedObject(self,
+ (__bridge const void *)kPleaseWaitAssociatedObjectKey);
+
+ if (pleaseWaitWindow) {
+ if (completion) {
+ completion();
+ }
+ return;
+ }
+
+ pleaseWaitWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
+ pleaseWaitWindow.backgroundColor = [UIColor clearColor];
+ pleaseWaitWindow.windowLevel = UIWindowLevelStatusBar - 1;
+
+ UIView *pleaseWaitView = [[UIView alloc] initWithFrame:pleaseWaitWindow.bounds];
+ pleaseWaitView.autoresizingMask = UIViewAutoresizingFlexibleWidth |
+ UIViewAutoresizingFlexibleHeight;
+ pleaseWaitView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
+ UIActivityIndicatorView *spinner =
+ [[UIActivityIndicatorView alloc]
+ initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
+ spinner.center = pleaseWaitView.center;
+ [pleaseWaitView addSubview:spinner];
+ [spinner startAnimating];
+
+ pleaseWaitView.layer.opacity = 0.0;
+ [self.view addSubview:pleaseWaitView];
+
+ [pleaseWaitWindow addSubview:pleaseWaitView];
+
+ [pleaseWaitWindow makeKeyAndVisible];
+
+ objc_setAssociatedObject(self,
+ (__bridge const void *)(kPleaseWaitAssociatedObjectKey),
+ pleaseWaitWindow,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+
+ [UIView animateWithDuration:0.5f animations:^{
+ pleaseWaitView.layer.opacity = 1.0f;
+ } completion:^(BOOL finished) {
+ if (completion) {
+ completion();
+ }
+ }];
+}
+
+- (void)hideSpinner:(nullable void(^)(void))completion {
+ if (self.useStatusBarSpinner) {
+ [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
+ completion();
+ return;
+ }
+ if ([self supportsAlertController]) {
+ [self hideModernSpinner:completion];
+ } else {
+ [self hideIOS7Spinner:completion];
+ }
+}
+
+- (void)hideModernSpinner:(nullable void(^)(void))completion {
+ UIAlertController *pleaseWaitAlert =
+ objc_getAssociatedObject(self,
+ (__bridge const void *)kPleaseWaitAssociatedObjectKey);
+
+ [pleaseWaitAlert dismissViewControllerAnimated:YES completion:completion];
+
+ objc_setAssociatedObject(self,
+ (__bridge const void *)(kPleaseWaitAssociatedObjectKey),
+ nil,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (void)hideIOS7Spinner:(nullable void(^)(void))completion {
+ UIWindow *pleaseWaitWindow =
+ objc_getAssociatedObject(self,
+ (__bridge const void *)kPleaseWaitAssociatedObjectKey);
+
+ UIView *pleaseWaitView;
+ pleaseWaitView = pleaseWaitWindow.subviews.firstObject;
+
+ [UIView animateWithDuration:0.5f animations:^{
+ pleaseWaitView.layer.opacity = 0.0f;
+ } completion:^(BOOL finished) {
+ [pleaseWaitWindow resignKeyWindow];
+ objc_setAssociatedObject(self,
+ (__bridge const void *)(kPleaseWaitAssociatedObjectKey),
+ nil,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (completion) {
+ completion();
+ }
+ }];
+}
+
+@end
+
+@implementation SimpleTextPromptDelegate {
+ AlertPromptCompletionBlock _completionHandler;
+ SimpleTextPromptDelegate *_retainedSelf;
+}
+
+- (instancetype)initWithCompletionHandler:(AlertPromptCompletionBlock)completionHandler {
+ self = [super init];
+ if (self) {
+ _completionHandler = completionHandler;
+ _retainedSelf = self;
+ }
+ return self;
+}
+
+- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
+ if (buttonIndex == alertView.firstOtherButtonIndex) {
+ _completionHandler(YES, [alertView textFieldAtIndex:0].text);
+ } else {
+ _completionHandler(NO, nil);
+ }
+ _completionHandler = nil;
+ _retainedSelf = nil;
+}
+
+@end
diff --git a/AuthSamples/Sample/UserInfoViewController.h b/AuthSamples/Sample/UserInfoViewController.h
new file mode 100644
index 0000000..de19bfc
--- /dev/null
+++ b/AuthSamples/Sample/UserInfoViewController.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 <UIKit/UIKit.h>
+
+@class FIRUser;
+@class StaticContentTableViewManager;
+
+/** @class UserInfoViewController
+ @brief A view controller for displaying @c FIRUser data.
+ */
+@interface UserInfoViewController : UIViewController
+
+/** @property tableViewManager
+ @brief A @c StaticContentTableViewManager which is used to manage the contents of the table
+ view.
+ */
+@property(nonatomic, strong) IBOutlet StaticContentTableViewManager *tableViewManager;
+
+/** @fn initWithUser:
+ @biref Initializes with a @c FIRUser instance.
+ @param user The user to be displayed in the view.
+ */
+- (instancetype)initWithUser:(FIRUser *)user NS_DESIGNATED_INITIALIZER;
+
+/** @fn initWithNibName:bundle:
+ @brief Not available. Call initWithUser: instead.
+ */
+- (instancetype)initWithNibName:(NSString *)nibNameOrNil
+ bundle:(NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
+
+/** @fn initWithCoder:
+ @brief Not available. Call initWithUser: instead.
+ */
+- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
+
+/** @fn done
+ @brief Called when user taps the "Done" button.
+ */
+- (IBAction)done:(id)sender;
+
+@end
diff --git a/AuthSamples/Sample/UserInfoViewController.m b/AuthSamples/Sample/UserInfoViewController.m
new file mode 100644
index 0000000..aa5b7fe
--- /dev/null
+++ b/AuthSamples/Sample/UserInfoViewController.m
@@ -0,0 +1,79 @@
+/*
+ * 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 "UserInfoViewController.h"
+
+#import "FIRUser.h"
+#import "FIRUserInfo.h"
+#import "StaticContentTableViewManager.h"
+
+/** @fn stringWithBool
+ @brief Converts a boolean value to a string for display.
+ @param boolValue the boolean value.
+ @return The string form of the boolean value.
+ */
+static NSString *stringWithBool(BOOL boolValue) {
+ return boolValue ? @"YES" : @"NO";
+}
+
+@implementation UserInfoViewController {
+ FIRUser *_user;
+}
+
+- (instancetype)initWithUser:(FIRUser *)user {
+ self = [super initWithNibName:NSStringFromClass([self class]) bundle:nil];
+ if (self) {
+ _user = user;
+ }
+ return self;
+}
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ [self loadTableView];
+}
+
+- (void)loadTableView {
+ NSMutableArray<StaticContentTableViewSection *> *sections = [@[
+ [StaticContentTableViewSection sectionWithTitle:@"User" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"anonymous" value:stringWithBool(_user.anonymous)],
+ [StaticContentTableViewCell cellWithTitle:@"emailVerified"
+ value:stringWithBool(_user.emailVerified)],
+ [StaticContentTableViewCell cellWithTitle:@"refreshToken" value:_user.refreshToken],
+ ]]
+ ] mutableCopy];
+ [sections addObject:[self sectionWithUserInfo:_user]];
+ for (id<FIRUserInfo> userInfo in _user.providerData) {
+ [sections addObject:[self sectionWithUserInfo:userInfo]];
+ }
+ _tableViewManager.contents = [StaticContentTableViewContent contentWithSections:sections];
+}
+
+- (StaticContentTableViewSection *)sectionWithUserInfo:(id<FIRUserInfo>)userInfo {
+ return [StaticContentTableViewSection sectionWithTitle:userInfo.providerID cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"uid" value:userInfo.uid],
+ [StaticContentTableViewCell cellWithTitle:@"displayName" value:userInfo.displayName],
+ [StaticContentTableViewCell cellWithTitle:@"photoURL" value:[userInfo.photoURL absoluteString]],
+ [StaticContentTableViewCell cellWithTitle:@"email" value:userInfo.email],
+ [StaticContentTableViewCell cellWithTitle:@"phoneNumber" value:userInfo.phoneNumber]
+ ]];
+}
+
+- (IBAction)done:(id)sender {
+ [self dismissViewControllerAnimated:YES completion:nil];
+}
+
+@end
diff --git a/AuthSamples/Sample/UserInfoViewController.xib b/AuthSamples/Sample/UserInfoViewController.xib
new file mode 100644
index 0000000..9f2db98
--- /dev/null
+++ b/AuthSamples/Sample/UserInfoViewController.xib
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
+ </dependencies>
+ <objects>
+ <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="UserInfoViewController">
+ <connections>
+ <outlet property="tableViewManager" destination="U72-zk-rRG" id="KxN-ZQ-cAa"/>
+ <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
+ </connections>
+ </placeholder>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+ <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rRb-jS-QTO">
+ <rect key="frame" x="8" y="16" width="46" height="30"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="30" id="6TV-T6-63H"/>
+ <constraint firstAttribute="width" constant="46" id="kVF-SE-Qzn"/>
+ </constraints>
+ <state key="normal" title="Done"/>
+ <connections>
+ <action selector="done:" destination="-1" eventType="touchUpInside" id="O91-6u-80J"/>
+ </connections>
+ </button>
+ <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" translatesAutoresizingMaskIntoConstraints="NO" id="4BU-Kb-3Zx">
+ <rect key="frame" x="0.0" y="54" width="600" height="546"/>
+ <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
+ <connections>
+ <outlet property="dataSource" destination="U72-zk-rRG" id="GFT-r0-uBK"/>
+ <outlet property="delegate" destination="U72-zk-rRG" id="uzl-Rm-2qd"/>
+ </connections>
+ </tableView>
+ </subviews>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <constraints>
+ <constraint firstItem="rRb-jS-QTO" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" constant="8" id="1PF-3b-y4w"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" id="5Tz-J0-2Dq"/>
+ <constraint firstItem="rRb-jS-QTO" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" constant="16" id="K7t-0g-JBo"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="top" secondItem="rRb-jS-QTO" secondAttribute="bottom" constant="8" id="MyI-Bl-nGS"/>
+ <constraint firstAttribute="bottom" secondItem="4BU-Kb-3Zx" secondAttribute="bottom" id="QMm-Wv-q60"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="centerX" secondItem="i5M-Pr-FkT" secondAttribute="centerX" id="deo-wi-Nmq"/>
+ </constraints>
+ </view>
+ <customObject id="U72-zk-rRG" customClass="StaticContentTableViewManager">
+ <connections>
+ <outlet property="tableView" destination="4BU-Kb-3Zx" id="0OK-KI-QdH"/>
+ </connections>
+ </customObject>
+ </objects>
+</document>
diff --git a/AuthSamples/Sample/UserTableViewCell.h b/AuthSamples/Sample/UserTableViewCell.h
new file mode 100644
index 0000000..072b455
--- /dev/null
+++ b/AuthSamples/Sample/UserTableViewCell.h
@@ -0,0 +1,58 @@
+/*
+ * 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/UIKit.h>
+
+@class FIRUser;
+
+/** @class UserTableViewCell
+ @brief Represents a user in a table view.
+ */
+@interface UserTableViewCell : UITableViewCell
+
+/** @property userInfoProfileURLImageView
+ @brief A UIImageView whose image is set to the user's profile URL.
+ */
+@property(nonatomic, weak) IBOutlet UIImageView *userInfoProfileURLImageView;
+
+/** @property userInfoDisplayNameLabel
+ @brief A UILabel whose text is set to the user's display name.
+ */
+@property(nonatomic, weak) IBOutlet UILabel *userInfoDisplayNameLabel;
+
+/** @property userInfoEmailLabel
+ @brief A UILabel whose text is set to the user's email.
+ */
+@property(nonatomic, weak) IBOutlet UILabel *userInfoEmailLabel;
+
+/** @property userInfoUserIDLabel
+ @brief A UILabel whose text is set to the user's User ID.
+ */
+@property(nonatomic, weak) IBOutlet UILabel *userInfoUserIDLabel;
+
+/** @property userInfoProviderListLabel
+ @brief A UILabel whose text is set to the user's comma-delimited list of federated sign in
+ provider IDs.
+ */
+@property(nonatomic, weak) IBOutlet UILabel *userInfoProviderListLabel;
+
+/** @fn updateContentsWithUser:
+ @brief Updates the values of the controls on this table view cell to represent the user.
+ @param user The user whose values should be used to populate this cell.
+ */
+- (void)updateContentsWithUser:(FIRUser *)user;
+
+@end
diff --git a/AuthSamples/Sample/UserTableViewCell.m b/AuthSamples/Sample/UserTableViewCell.m
new file mode 100644
index 0000000..fef3025
--- /dev/null
+++ b/AuthSamples/Sample/UserTableViewCell.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 "UserTableViewCell.h"
+
+#import "FIRUser.h"
+
+@implementation UserTableViewCell {
+ /** @var _lastPhotoURL
+ @brief Used to make sure only the last requested image is used to update the UIImageView.
+ */
+ NSURL *_lastPhotoURL;
+}
+
+- (void)updateContentsWithUser:(FIRUser *)user {
+ _userInfoDisplayNameLabel.text = user.displayName;
+ _userInfoEmailLabel.text = user.email;
+ _userInfoUserIDLabel.text = user.uid;
+
+ NSMutableArray<NSString *> *providerIDs = [NSMutableArray array];
+ for (id<FIRUserInfo> userInfo in user.providerData) {
+ [providerIDs addObject:userInfo.providerID];
+ }
+ _userInfoProviderListLabel.text = [providerIDs componentsJoinedByString:@", "];
+
+ NSURL *photoURL = user.photoURL;
+ _lastPhotoURL = photoURL; // to prevent eariler image overwrites later one.
+ if (photoURL) {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^() {
+ UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:photoURL]];
+ dispatch_async(dispatch_get_main_queue(), ^() {
+ if (photoURL == _lastPhotoURL) {
+ _userInfoProfileURLImageView.image = image;
+ }
+ });
+ });
+ } else {
+ _userInfoProfileURLImageView.image = nil;
+ }
+}
+
+@end
diff --git a/AuthSamples/Sample/main.m b/AuthSamples/Sample/main.m
new file mode 100644
index 0000000..1eadced
--- /dev/null
+++ b/AuthSamples/Sample/main.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 "ApplicationDelegate.h"
+
+int main(int argc, char *argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([ApplicationDelegate class]));
+ }
+}
diff --git a/AuthSamples/Samples.xcodeproj/project.pbxproj b/AuthSamples/Samples.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..963eabe
--- /dev/null
+++ b/AuthSamples/Samples.xcodeproj/project.pbxproj
@@ -0,0 +1,1806 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ DE5C700E1EA17F6900A965D2 /* AllTests */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = DE5C700F1EA17F6900A965D2 /* Build configuration list for PBXAggregateTarget "AllTests" */;
+ buildPhases = (
+ );
+ dependencies = (
+ DE5C70131EA17F7200A965D2 /* PBXTargetDependency */,
+ DE5C70151EA17F7200A965D2 /* PBXTargetDependency */,
+ DE5C70171EA17F7200A965D2 /* PBXTargetDependency */,
+ );
+ name = AllTests;
+ productName = AllTests;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 569C3F4E18627674CABE02AE /* Pods_EarlGreyTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AEE2E563FADF8C3382956B4F /* Pods_EarlGreyTests.framework */; };
+ 67AFFB52FF0FC4668D92F2E4 /* Pods_Sample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5FE06BD9AA795DFBA9EFAAD /* Pods_Sample.framework */; };
+ A7609DCAD8A247411F27EA14 /* Pods_TestApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDD2401395E91D0923BC5CD8 /* Pods_TestApp.framework */; };
+ AB62D09AF8C1196E07F37D3B /* Pods_SwiftBear.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1F689EE8E0E6F83D82429F0 /* Pods_SwiftBear.framework */; };
+ BD555A1DCF4E889DC3338248 /* Pods_FirebaseAuthUnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FFAD3F37BC4D7CEF0CAD579 /* Pods_FirebaseAuthUnitTests.framework */; };
+ BE7B447C1EC2508300FA4C1B /* AuthCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE7B447A1EC2507800FA4C1B /* AuthCredentials.swift */; };
+ DE5371B31EA7E89D000DA57F /* FIRAdditionalUserInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371831EA7E89D000DA57F /* FIRAdditionalUserInfoTests.m */; };
+ DE5371B41EA7E89D000DA57F /* FIRApp+FIRAuthUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371851EA7E89D000DA57F /* FIRApp+FIRAuthUnitTests.m */; };
+ DE5371B51EA7E89D000DA57F /* FIRAuthAppCredentialTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371861EA7E89D000DA57F /* FIRAuthAppCredentialTests.m */; };
+ DE5371B61EA7E89D000DA57F /* FIRAuthAppDelegateProxyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371871EA7E89D000DA57F /* FIRAuthAppDelegateProxyTests.m */; };
+ DE5371B71EA7E89D000DA57F /* FIRAuthBackendCreateAuthURITests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371881EA7E89D000DA57F /* FIRAuthBackendCreateAuthURITests.m */; };
+ DE5371B81EA7E89D000DA57F /* FIRAuthBackendRPCImplementationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371891EA7E89D000DA57F /* FIRAuthBackendRPCImplementationTests.m */; };
+ DE5371B91EA7E89D000DA57F /* FIRAuthDispatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53718A1EA7E89D000DA57F /* FIRAuthDispatcherTests.m */; };
+ DE5371BA1EA7E89D000DA57F /* FIRAuthGlobalWorkQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53718B1EA7E89D000DA57F /* FIRAuthGlobalWorkQueueTests.m */; };
+ DE5371BB1EA7E89D000DA57F /* FIRAuthKeychainTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53718C1EA7E89D000DA57F /* FIRAuthKeychainTests.m */; };
+ DE5371BC1EA7E89D000DA57F /* FIRAuthSerialTaskQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53718D1EA7E89D000DA57F /* FIRAuthSerialTaskQueueTests.m */; };
+ DE5371BD1EA7E89D000DA57F /* FIRAuthTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53718E1EA7E89D000DA57F /* FIRAuthTests.m */; };
+ DE5371BE1EA7E89D000DA57F /* FIRAuthUserDefaultsStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53718F1EA7E89D000DA57F /* FIRAuthUserDefaultsStorageTests.m */; };
+ DE5371BF1EA7E89D000DA57F /* FIRCreateAuthURIRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371901EA7E89D000DA57F /* FIRCreateAuthURIRequestTests.m */; };
+ DE5371C01EA7E89D000DA57F /* FIRCreateAuthURIResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371911EA7E89D000DA57F /* FIRCreateAuthURIResponseTests.m */; };
+ DE5371C11EA7E89D000DA57F /* FIRDeleteAccountRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371921EA7E89D000DA57F /* FIRDeleteAccountRequestTests.m */; };
+ DE5371C21EA7E89D000DA57F /* FIRDeleteAccountResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371931EA7E89D000DA57F /* FIRDeleteAccountResponseTests.m */; };
+ DE5371C31EA7E89D000DA57F /* FIRFakeBackendRPCIssuer.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371951EA7E89D000DA57F /* FIRFakeBackendRPCIssuer.m */; };
+ DE5371C41EA7E89D000DA57F /* FIRGetAccountInfoRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371961EA7E89D000DA57F /* FIRGetAccountInfoRequestTests.m */; };
+ DE5371C51EA7E89D000DA57F /* FIRGetAccountInfoResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371971EA7E89D000DA57F /* FIRGetAccountInfoResponseTests.m */; };
+ DE5371C61EA7E89D000DA57F /* FIRGetOOBConfirmationCodeRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371981EA7E89D000DA57F /* FIRGetOOBConfirmationCodeRequestTests.m */; };
+ DE5371C71EA7E89D000DA57F /* FIRGetOOBConfirmationCodeResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371991EA7E89D000DA57F /* FIRGetOOBConfirmationCodeResponseTests.m */; };
+ DE5371C81EA7E89D000DA57F /* FIRGitHubAuthProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53719A1EA7E89D000DA57F /* FIRGitHubAuthProviderTests.m */; };
+ DE5371C91EA7E89D000DA57F /* FIRPhoneAuthProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53719B1EA7E89D000DA57F /* FIRPhoneAuthProviderTests.m */; };
+ DE5371CA1EA7E89D000DA57F /* FIRResetPasswordRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53719C1EA7E89D000DA57F /* FIRResetPasswordRequestTests.m */; };
+ DE5371CB1EA7E89D000DA57F /* FIRResetPasswordResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53719D1EA7E89D000DA57F /* FIRResetPasswordResponseTests.m */; };
+ DE5371CC1EA7E89D000DA57F /* FIRSendVerificationCodeRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53719E1EA7E89D000DA57F /* FIRSendVerificationCodeRequestTests.m */; };
+ DE5371CD1EA7E89D000DA57F /* FIRSendVerificationCodeResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE53719F1EA7E89D000DA57F /* FIRSendVerificationCodeResponseTests.m */; };
+ DE5371CE1EA7E89D000DA57F /* FIRSetAccountInfoRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371A01EA7E89D000DA57F /* FIRSetAccountInfoRequestTests.m */; };
+ DE5371CF1EA7E89D000DA57F /* FIRSetAccountInfoResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371A11EA7E89D000DA57F /* FIRSetAccountInfoResponseTests.m */; };
+ DE5371D01EA7E89D000DA57F /* FIRSignUpNewUserRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371A21EA7E89D000DA57F /* FIRSignUpNewUserRequestTests.m */; };
+ DE5371D11EA7E89D000DA57F /* FIRSignUpNewUserResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371A31EA7E89D000DA57F /* FIRSignUpNewUserResponseTests.m */; };
+ DE5371D21EA7E89D000DA57F /* FIRTwitterAuthProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371A41EA7E89D000DA57F /* FIRTwitterAuthProviderTests.m */; };
+ DE5371D31EA7E89D000DA57F /* FIRUserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371A51EA7E89D000DA57F /* FIRUserTests.m */; };
+ DE5371D41EA7E89D000DA57F /* FIRVerifyAssertionRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371A61EA7E89D000DA57F /* FIRVerifyAssertionRequestTests.m */; };
+ DE5371D51EA7E89D000DA57F /* FIRVerifyAssertionResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371A71EA7E89D000DA57F /* FIRVerifyAssertionResponseTests.m */; };
+ DE5371D61EA7E89D000DA57F /* FIRVerifyClientRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371A81EA7E89D000DA57F /* FIRVerifyClientRequestTest.m */; };
+ DE5371D71EA7E89D000DA57F /* FIRVerifyClientResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371A91EA7E89D000DA57F /* FIRVerifyClientResponseTests.m */; };
+ DE5371D81EA7E89D000DA57F /* FIRVerifyCustomTokenRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371AA1EA7E89D000DA57F /* FIRVerifyCustomTokenRequestTests.m */; };
+ DE5371D91EA7E89D000DA57F /* FIRVerifyCustomTokenResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371AB1EA7E89D000DA57F /* FIRVerifyCustomTokenResponseTests.m */; };
+ DE5371DA1EA7E89D000DA57F /* FIRVerifyPasswordRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371AC1EA7E89D000DA57F /* FIRVerifyPasswordRequestTest.m */; };
+ DE5371DB1EA7E89D000DA57F /* FIRVerifyPasswordResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371AD1EA7E89D000DA57F /* FIRVerifyPasswordResponseTests.m */; };
+ DE5371DC1EA7E89D000DA57F /* FIRVerifyPhoneNumberRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371AE1EA7E89D000DA57F /* FIRVerifyPhoneNumberRequestTests.m */; };
+ DE5371DD1EA7E89D000DA57F /* FIRVerifyPhoneNumberResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371AF1EA7E89D000DA57F /* FIRVerifyPhoneNumberResponseTests.m */; };
+ DE5371DE1EA7E89D000DA57F /* OCMStubRecorder+FIRAuthUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE5371B11EA7E89D000DA57F /* OCMStubRecorder+FIRAuthUnitTests.m */; };
+ DE5371DF1EA7E89D000DA57F /* Tests-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DE5371B21EA7E89D000DA57F /* Tests-Info.plist */; };
+ DECE04E21E9FEAE600164CA4 /* Application.plist in Resources */ = {isa = PBXBuildFile; fileRef = DECE049B1E9FEAE600164CA4 /* Application.plist */; };
+ DECE04E31E9FEAE600164CA4 /* ApplicationDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE049D1E9FEAE600164CA4 /* ApplicationDelegate.m */; };
+ DECE04E41E9FEAE600164CA4 /* AuthProviders.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE049F1E9FEAE600164CA4 /* AuthProviders.m */; };
+ DECE04E51E9FEAE600164CA4 /* CustomTokenDataEntryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE04A21E9FEAE600164CA4 /* CustomTokenDataEntryViewController.m */; };
+ DECE04E61E9FEAE600164CA4 /* FacebookAuthProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE04A41E9FEAE600164CA4 /* FacebookAuthProvider.m */; };
+ DECE04E71E9FEAE600164CA4 /* GoogleAuthProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE04A61E9FEAE600164CA4 /* GoogleAuthProvider.m */; };
+ DECE04E91E9FEAE600164CA4 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DECE04A81E9FEAE600164CA4 /* GoogleService-Info.plist */; };
+ DECE04EA1E9FEAE600164CA4 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DECE04A91E9FEAE600164CA4 /* Images.xcassets */; };
+ DECE04EB1E9FEAE600164CA4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE04AA1E9FEAE600164CA4 /* main.m */; };
+ DECE04EC1E9FEAE600164CA4 /* MainViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE04AC1E9FEAE600164CA4 /* MainViewController.m */; };
+ DECE04ED1E9FEAE600164CA4 /* MainViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DECE04AD1E9FEAE600164CA4 /* MainViewController.xib */; };
+ DECE04EF1E9FEAE600164CA4 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE04B01E9FEAE600164CA4 /* SettingsViewController.m */; };
+ DECE04F01E9FEAE600164CA4 /* SettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DECE04B11E9FEAE600164CA4 /* SettingsViewController.xib */; };
+ DECE04F11E9FEAE600164CA4 /* StaticContentTableViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE04B31E9FEAE600164CA4 /* StaticContentTableViewManager.m */; };
+ DECE04F21E9FEAE600164CA4 /* FirebearSample.strings in Resources */ = {isa = PBXBuildFile; fileRef = DECE04B51E9FEAE600164CA4 /* FirebearSample.strings */; };
+ DECE04F31E9FEAE600164CA4 /* FirebaseAuthUI.strings in Resources */ = {isa = PBXBuildFile; fileRef = DECE04BC1E9FEAE600164CA4 /* FirebaseAuthUI.strings */; };
+ DECE04F41E9FEAE600164CA4 /* UIViewController+Alerts.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE04DC1E9FEAE600164CA4 /* UIViewController+Alerts.m */; };
+ DECE04F51E9FEAE600164CA4 /* UserInfoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE04DE1E9FEAE600164CA4 /* UserInfoViewController.m */; };
+ DECE04F61E9FEAE600164CA4 /* UserInfoViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DECE04DF1E9FEAE600164CA4 /* UserInfoViewController.xib */; };
+ DECE04F71E9FEAE600164CA4 /* UserTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE04E11E9FEAE600164CA4 /* UserTableViewCell.m */; };
+ DECEA56C1EBBED1200273585 /* FIRAuthAPNSTokenManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DECEA5681EBBED1200273585 /* FIRAuthAPNSTokenManagerTests.m */; };
+ DECEA56D1EBBED1200273585 /* FIRAuthAPNSTokenTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DECEA5691EBBED1200273585 /* FIRAuthAPNSTokenTests.m */; };
+ DECEA56E1EBBED1200273585 /* FIRAuthAppCredentialManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DECEA56A1EBBED1200273585 /* FIRAuthAppCredentialManagerTests.m */; };
+ DECEA56F1EBBED1200273585 /* FIRAuthNotificationManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DECEA56B1EBBED1200273585 /* FIRAuthNotificationManagerTests.m */; };
+ DEE13A051E9FFC9500D1BABA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE13A041E9FFC9500D1BABA /* AppDelegate.swift */; };
+ DEE13A071E9FFC9500D1BABA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE13A061E9FFC9500D1BABA /* ViewController.swift */; };
+ DEE13A161E9FFD1F00D1BABA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEE13A141E9FFD1F00D1BABA /* LaunchScreen.storyboard */; };
+ DEE13A171E9FFD1F00D1BABA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEE13A151E9FFD1F00D1BABA /* Main.storyboard */; };
+ DEE13A1A1E9FFD2E00D1BABA /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DEE13A191E9FFD2E00D1BABA /* GoogleService-Info.plist */; };
+ DEE13A2B1EA125CD00D1BABA /* FirebearApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE13A2A1EA125CD00D1BABA /* FirebearApiTests.m */; };
+ DEE13A3D1EA164E100D1BABA /* FirebearEarlGreyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE13A3C1EA164E100D1BABA /* FirebearEarlGreyTests.m */; };
+ DEE13AA21EA1724300D1BABA /* FirebaseDev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEE13A2C1EA12B8D00D1BABA /* FirebaseDev.framework */; };
+ DEE13ACB1EA1764B00D1BABA /* Auth-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DEE13ABE1EA1764B00D1BABA /* Auth-Info.plist */; };
+ DEE13ACC1EA1764B00D1BABA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEE13AC01EA1764B00D1BABA /* LaunchScreen.storyboard */; };
+ DEE13ACD1EA1764B00D1BABA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEE13AC21EA1764B00D1BABA /* Main.storyboard */; };
+ DEE13ACE1EA1764B00D1BABA /* FIRAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE13AC51EA1764B00D1BABA /* FIRAppDelegate.m */; };
+ DEE13ACF1EA1764B00D1BABA /* FIRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE13AC71EA1764B00D1BABA /* FIRViewController.m */; };
+ DEE13AD01EA1764B00D1BABA /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DEE13AC81EA1764B00D1BABA /* GoogleService-Info.plist */; };
+ DEE13AD11EA1764B00D1BABA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DEE13AC91EA1764B00D1BABA /* Images.xcassets */; };
+ DEE13AD21EA1764B00D1BABA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE13ACA1EA1764B00D1BABA /* main.m */; };
+ E7989F47679257E9190C787F /* Pods_ApiTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EC09307D636721EAAB89BB2 /* Pods_ApiTests.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ DE5C70121EA17F7200A965D2 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = DECE045E1E9FEA1000164CA4 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DEE13A1F1EA1252D00D1BABA;
+ remoteInfo = ApiTests;
+ };
+ DE5C70141EA17F7200A965D2 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = DECE045E1E9FEA1000164CA4 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DEE13A311EA1642A00D1BABA;
+ remoteInfo = EarlGreyTests;
+ };
+ DE5C70161EA17F7200A965D2 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = DECE045E1E9FEA1000164CA4 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DEE13A411EA16EE400D1BABA;
+ remoteInfo = FirebaseAuthUnitTests;
+ };
+ DEA41FF51EA17A030072DA74 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = DECE045E1E9FEA1000164CA4 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DEE13AA61EA1761B00D1BABA;
+ remoteInfo = TestApp;
+ };
+ DEE13A251EA1252D00D1BABA /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = DECE045E1E9FEA1000164CA4 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DECE04831E9FEA7500164CA4;
+ remoteInfo = Sample;
+ };
+ DEE13A371EA1642A00D1BABA /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = DECE045E1E9FEA1000164CA4 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DECE04831E9FEA7500164CA4;
+ remoteInfo = Sample;
+ };
+ DEE13A471EA16EE400D1BABA /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = DECE045E1E9FEA1000164CA4 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DECE04831E9FEA7500164CA4;
+ remoteInfo = Sample;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ 113C36392871524143A53B07 /* Pods-ApiTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApiTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ApiTests/Pods-ApiTests.debug.xcconfig"; sourceTree = "<group>"; };
+ 151B5A61F0B0D152CD55DF81 /* Pods-SwiftBear.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBear.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBear/Pods-SwiftBear.release.xcconfig"; sourceTree = "<group>"; };
+ 178AA1A3F0690CDEC44E2BF1 /* Pods-FirebaseAuthUnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseAuthUnitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-FirebaseAuthUnitTests/Pods-FirebaseAuthUnitTests.release.xcconfig"; sourceTree = "<group>"; };
+ 1B16EC50B5315C8E9E2D21CE /* Pods-EarlGreyTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EarlGreyTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-EarlGreyTests/Pods-EarlGreyTests.debug.xcconfig"; sourceTree = "<group>"; };
+ 36794820176319C88B4F11E5 /* Pods-SwiftBear.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBear.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBear/Pods-SwiftBear.debug.xcconfig"; sourceTree = "<group>"; };
+ 495F22BEDDFD4DFE3FB2D522 /* Pods-SwiftSample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftSample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftSample/Pods-SwiftSample.debug.xcconfig"; sourceTree = "<group>"; };
+ 4FFAD3F37BC4D7CEF0CAD579 /* Pods_FirebaseAuthUnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FirebaseAuthUnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 57150555A6B03949ECB58AD9 /* Pods-TestApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestApp.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TestApp/Pods-TestApp.debug.xcconfig"; sourceTree = "<group>"; };
+ 6EC09307D636721EAAB89BB2 /* Pods_ApiTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ApiTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 81ED9C5F2E61472DE3FA17CC /* Pods-ApiTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApiTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ApiTests/Pods-ApiTests.release.xcconfig"; sourceTree = "<group>"; };
+ 920E926BD468CBC593349A36 /* Pods-FirebaseAuthUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseAuthUnitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FirebaseAuthUnitTests/Pods-FirebaseAuthUnitTests.debug.xcconfig"; sourceTree = "<group>"; };
+ 94E3B3EB70D34E55CFF2E45D /* Pods-EarlGreyTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EarlGreyTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-EarlGreyTests/Pods-EarlGreyTests.release.xcconfig"; sourceTree = "<group>"; };
+ A1F689EE8E0E6F83D82429F0 /* Pods_SwiftBear.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftBear.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A75CAF1D0796E27A3E899DE4 /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = "<group>"; };
+ AEE2E563FADF8C3382956B4F /* Pods_EarlGreyTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_EarlGreyTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ B78FD2B21A8D72D5E38E3E79 /* Pods-TestApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestApp.release.xcconfig"; path = "Pods/Target Support Files/Pods-TestApp/Pods-TestApp.release.xcconfig"; sourceTree = "<group>"; };
+ BC8C39EF1F42A0C750FF5186 /* Pods_SwiftSample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftSample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ BE7B447A1EC2507800FA4C1B /* AuthCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthCredentials.swift; sourceTree = "<group>"; };
+ BED403DD1EBC057E00885C2C /* AuthCredentialsTemplate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AuthCredentialsTemplate.h; sourceTree = "<group>"; };
+ CDD2401395E91D0923BC5CD8 /* Pods_TestApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TestApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ D1EE09E5B9A6C092236222A9 /* Pods-Sample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"; sourceTree = "<group>"; };
+ D5FE06BD9AA795DFBA9EFAAD /* Pods_Sample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ D9D6F0DE0BB3F49EF1B7CBB3 /* Pods-SwiftSample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftSample.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftSample/Pods-SwiftSample.release.xcconfig"; sourceTree = "<group>"; };
+ DE5371831EA7E89D000DA57F /* FIRAdditionalUserInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAdditionalUserInfoTests.m; path = ../../Example/Auth/Tests/FIRAdditionalUserInfoTests.m; sourceTree = "<group>"; };
+ DE5371841EA7E89D000DA57F /* FIRApp+FIRAuthUnitTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FIRApp+FIRAuthUnitTests.h"; path = "../../Example/Auth/Tests/FIRApp+FIRAuthUnitTests.h"; sourceTree = "<group>"; };
+ DE5371851EA7E89D000DA57F /* FIRApp+FIRAuthUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FIRApp+FIRAuthUnitTests.m"; path = "../../Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m"; sourceTree = "<group>"; };
+ DE5371861EA7E89D000DA57F /* FIRAuthAppCredentialTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthAppCredentialTests.m; path = ../../Example/Auth/Tests/FIRAuthAppCredentialTests.m; sourceTree = "<group>"; };
+ DE5371871EA7E89D000DA57F /* FIRAuthAppDelegateProxyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthAppDelegateProxyTests.m; path = ../../Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m; sourceTree = "<group>"; };
+ DE5371881EA7E89D000DA57F /* FIRAuthBackendCreateAuthURITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthBackendCreateAuthURITests.m; path = ../../Example/Auth/Tests/FIRAuthBackendCreateAuthURITests.m; sourceTree = "<group>"; };
+ DE5371891EA7E89D000DA57F /* FIRAuthBackendRPCImplementationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthBackendRPCImplementationTests.m; path = ../../Example/Auth/Tests/FIRAuthBackendRPCImplementationTests.m; sourceTree = "<group>"; };
+ DE53718A1EA7E89D000DA57F /* FIRAuthDispatcherTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthDispatcherTests.m; path = ../../Example/Auth/Tests/FIRAuthDispatcherTests.m; sourceTree = "<group>"; };
+ DE53718B1EA7E89D000DA57F /* FIRAuthGlobalWorkQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthGlobalWorkQueueTests.m; path = ../../Example/Auth/Tests/FIRAuthGlobalWorkQueueTests.m; sourceTree = "<group>"; };
+ DE53718C1EA7E89D000DA57F /* FIRAuthKeychainTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthKeychainTests.m; path = ../../Example/Auth/Tests/FIRAuthKeychainTests.m; sourceTree = "<group>"; };
+ DE53718D1EA7E89D000DA57F /* FIRAuthSerialTaskQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthSerialTaskQueueTests.m; path = ../../Example/Auth/Tests/FIRAuthSerialTaskQueueTests.m; sourceTree = "<group>"; };
+ DE53718E1EA7E89D000DA57F /* FIRAuthTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthTests.m; path = ../../Example/Auth/Tests/FIRAuthTests.m; sourceTree = "<group>"; };
+ DE53718F1EA7E89D000DA57F /* FIRAuthUserDefaultsStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthUserDefaultsStorageTests.m; path = ../../Example/Auth/Tests/FIRAuthUserDefaultsStorageTests.m; sourceTree = "<group>"; };
+ DE5371901EA7E89D000DA57F /* FIRCreateAuthURIRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRCreateAuthURIRequestTests.m; path = ../../Example/Auth/Tests/FIRCreateAuthURIRequestTests.m; sourceTree = "<group>"; };
+ DE5371911EA7E89D000DA57F /* FIRCreateAuthURIResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRCreateAuthURIResponseTests.m; path = ../../Example/Auth/Tests/FIRCreateAuthURIResponseTests.m; sourceTree = "<group>"; };
+ DE5371921EA7E89D000DA57F /* FIRDeleteAccountRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRDeleteAccountRequestTests.m; path = ../../Example/Auth/Tests/FIRDeleteAccountRequestTests.m; sourceTree = "<group>"; };
+ DE5371931EA7E89D000DA57F /* FIRDeleteAccountResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRDeleteAccountResponseTests.m; path = ../../Example/Auth/Tests/FIRDeleteAccountResponseTests.m; sourceTree = "<group>"; };
+ DE5371941EA7E89D000DA57F /* FIRFakeBackendRPCIssuer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FIRFakeBackendRPCIssuer.h; path = ../../Example/Auth/Tests/FIRFakeBackendRPCIssuer.h; sourceTree = "<group>"; };
+ DE5371951EA7E89D000DA57F /* FIRFakeBackendRPCIssuer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRFakeBackendRPCIssuer.m; path = ../../Example/Auth/Tests/FIRFakeBackendRPCIssuer.m; sourceTree = "<group>"; };
+ DE5371961EA7E89D000DA57F /* FIRGetAccountInfoRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRGetAccountInfoRequestTests.m; path = ../../Example/Auth/Tests/FIRGetAccountInfoRequestTests.m; sourceTree = "<group>"; };
+ DE5371971EA7E89D000DA57F /* FIRGetAccountInfoResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRGetAccountInfoResponseTests.m; path = ../../Example/Auth/Tests/FIRGetAccountInfoResponseTests.m; sourceTree = "<group>"; };
+ DE5371981EA7E89D000DA57F /* FIRGetOOBConfirmationCodeRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRGetOOBConfirmationCodeRequestTests.m; path = ../../Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m; sourceTree = "<group>"; };
+ DE5371991EA7E89D000DA57F /* FIRGetOOBConfirmationCodeResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRGetOOBConfirmationCodeResponseTests.m; path = ../../Example/Auth/Tests/FIRGetOOBConfirmationCodeResponseTests.m; sourceTree = "<group>"; };
+ DE53719A1EA7E89D000DA57F /* FIRGitHubAuthProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRGitHubAuthProviderTests.m; path = ../../Example/Auth/Tests/FIRGitHubAuthProviderTests.m; sourceTree = "<group>"; };
+ DE53719B1EA7E89D000DA57F /* FIRPhoneAuthProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRPhoneAuthProviderTests.m; path = ../../Example/Auth/Tests/FIRPhoneAuthProviderTests.m; sourceTree = "<group>"; };
+ DE53719C1EA7E89D000DA57F /* FIRResetPasswordRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRResetPasswordRequestTests.m; path = ../../Example/Auth/Tests/FIRResetPasswordRequestTests.m; sourceTree = "<group>"; };
+ DE53719D1EA7E89D000DA57F /* FIRResetPasswordResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRResetPasswordResponseTests.m; path = ../../Example/Auth/Tests/FIRResetPasswordResponseTests.m; sourceTree = "<group>"; };
+ DE53719E1EA7E89D000DA57F /* FIRSendVerificationCodeRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRSendVerificationCodeRequestTests.m; path = ../../Example/Auth/Tests/FIRSendVerificationCodeRequestTests.m; sourceTree = "<group>"; };
+ DE53719F1EA7E89D000DA57F /* FIRSendVerificationCodeResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRSendVerificationCodeResponseTests.m; path = ../../Example/Auth/Tests/FIRSendVerificationCodeResponseTests.m; sourceTree = "<group>"; };
+ DE5371A01EA7E89D000DA57F /* FIRSetAccountInfoRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRSetAccountInfoRequestTests.m; path = ../../Example/Auth/Tests/FIRSetAccountInfoRequestTests.m; sourceTree = "<group>"; };
+ DE5371A11EA7E89D000DA57F /* FIRSetAccountInfoResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRSetAccountInfoResponseTests.m; path = ../../Example/Auth/Tests/FIRSetAccountInfoResponseTests.m; sourceTree = "<group>"; };
+ DE5371A21EA7E89D000DA57F /* FIRSignUpNewUserRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRSignUpNewUserRequestTests.m; path = ../../Example/Auth/Tests/FIRSignUpNewUserRequestTests.m; sourceTree = "<group>"; };
+ DE5371A31EA7E89D000DA57F /* FIRSignUpNewUserResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRSignUpNewUserResponseTests.m; path = ../../Example/Auth/Tests/FIRSignUpNewUserResponseTests.m; sourceTree = "<group>"; };
+ DE5371A41EA7E89D000DA57F /* FIRTwitterAuthProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRTwitterAuthProviderTests.m; path = ../../Example/Auth/Tests/FIRTwitterAuthProviderTests.m; sourceTree = "<group>"; };
+ DE5371A51EA7E89D000DA57F /* FIRUserTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRUserTests.m; path = ../../Example/Auth/Tests/FIRUserTests.m; sourceTree = "<group>"; };
+ DE5371A61EA7E89D000DA57F /* FIRVerifyAssertionRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRVerifyAssertionRequestTests.m; path = ../../Example/Auth/Tests/FIRVerifyAssertionRequestTests.m; sourceTree = "<group>"; };
+ DE5371A71EA7E89D000DA57F /* FIRVerifyAssertionResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRVerifyAssertionResponseTests.m; path = ../../Example/Auth/Tests/FIRVerifyAssertionResponseTests.m; sourceTree = "<group>"; };
+ DE5371A81EA7E89D000DA57F /* FIRVerifyClientRequestTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRVerifyClientRequestTest.m; path = ../../Example/Auth/Tests/FIRVerifyClientRequestTest.m; sourceTree = "<group>"; };
+ DE5371A91EA7E89D000DA57F /* FIRVerifyClientResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRVerifyClientResponseTests.m; path = ../../Example/Auth/Tests/FIRVerifyClientResponseTests.m; sourceTree = "<group>"; };
+ DE5371AA1EA7E89D000DA57F /* FIRVerifyCustomTokenRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRVerifyCustomTokenRequestTests.m; path = ../../Example/Auth/Tests/FIRVerifyCustomTokenRequestTests.m; sourceTree = "<group>"; };
+ DE5371AB1EA7E89D000DA57F /* FIRVerifyCustomTokenResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRVerifyCustomTokenResponseTests.m; path = ../../Example/Auth/Tests/FIRVerifyCustomTokenResponseTests.m; sourceTree = "<group>"; };
+ DE5371AC1EA7E89D000DA57F /* FIRVerifyPasswordRequestTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRVerifyPasswordRequestTest.m; path = ../../Example/Auth/Tests/FIRVerifyPasswordRequestTest.m; sourceTree = "<group>"; };
+ DE5371AD1EA7E89D000DA57F /* FIRVerifyPasswordResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRVerifyPasswordResponseTests.m; path = ../../Example/Auth/Tests/FIRVerifyPasswordResponseTests.m; sourceTree = "<group>"; };
+ DE5371AE1EA7E89D000DA57F /* FIRVerifyPhoneNumberRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRVerifyPhoneNumberRequestTests.m; path = ../../Example/Auth/Tests/FIRVerifyPhoneNumberRequestTests.m; sourceTree = "<group>"; };
+ DE5371AF1EA7E89D000DA57F /* FIRVerifyPhoneNumberResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRVerifyPhoneNumberResponseTests.m; path = ../../Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m; sourceTree = "<group>"; };
+ DE5371B01EA7E89D000DA57F /* OCMStubRecorder+FIRAuthUnitTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "OCMStubRecorder+FIRAuthUnitTests.h"; path = "../../Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.h"; sourceTree = "<group>"; };
+ DE5371B11EA7E89D000DA57F /* OCMStubRecorder+FIRAuthUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "OCMStubRecorder+FIRAuthUnitTests.m"; path = "../../Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.m"; sourceTree = "<group>"; };
+ DE5371B21EA7E89D000DA57F /* Tests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Tests-Info.plist"; path = "../../Example/Auth/Tests/Tests-Info.plist"; sourceTree = "<group>"; };
+ DECE04841E9FEA7500164CA4 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ DECE049B1E9FEAE600164CA4 /* Application.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Application.plist; sourceTree = "<group>"; };
+ DECE049C1E9FEAE600164CA4 /* ApplicationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApplicationDelegate.h; sourceTree = "<group>"; };
+ DECE049D1E9FEAE600164CA4 /* ApplicationDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApplicationDelegate.m; sourceTree = "<group>"; };
+ DECE049E1E9FEAE600164CA4 /* AuthProviders.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuthProviders.h; sourceTree = "<group>"; };
+ DECE049F1E9FEAE600164CA4 /* AuthProviders.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AuthProviders.m; sourceTree = "<group>"; };
+ DECE04A11E9FEAE600164CA4 /* CustomTokenDataEntryViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomTokenDataEntryViewController.h; sourceTree = "<group>"; };
+ DECE04A21E9FEAE600164CA4 /* CustomTokenDataEntryViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomTokenDataEntryViewController.m; sourceTree = "<group>"; };
+ DECE04A31E9FEAE600164CA4 /* FacebookAuthProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FacebookAuthProvider.h; sourceTree = "<group>"; };
+ DECE04A41E9FEAE600164CA4 /* FacebookAuthProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FacebookAuthProvider.m; sourceTree = "<group>"; };
+ DECE04A51E9FEAE600164CA4 /* GoogleAuthProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GoogleAuthProvider.h; sourceTree = "<group>"; };
+ DECE04A61E9FEAE600164CA4 /* GoogleAuthProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GoogleAuthProvider.m; sourceTree = "<group>"; };
+ DECE04A81E9FEAE600164CA4 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
+ DECE04A91E9FEAE600164CA4 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
+ DECE04AA1E9FEAE600164CA4 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ DECE04AB1E9FEAE600164CA4 /* MainViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainViewController.h; sourceTree = "<group>"; };
+ DECE04AC1E9FEAE600164CA4 /* MainViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MainViewController.m; sourceTree = "<group>"; };
+ DECE04AD1E9FEAE600164CA4 /* MainViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainViewController.xib; sourceTree = "<group>"; };
+ DECE04AF1E9FEAE600164CA4 /* SettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsViewController.h; sourceTree = "<group>"; };
+ DECE04B01E9FEAE600164CA4 /* SettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsViewController.m; sourceTree = "<group>"; };
+ DECE04B11E9FEAE600164CA4 /* SettingsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsViewController.xib; sourceTree = "<group>"; };
+ DECE04B21E9FEAE600164CA4 /* StaticContentTableViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StaticContentTableViewManager.h; sourceTree = "<group>"; };
+ DECE04B31E9FEAE600164CA4 /* StaticContentTableViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StaticContentTableViewManager.m; sourceTree = "<group>"; };
+ DECE04B61E9FEAE600164CA4 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04B71E9FEAE600164CA4 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04B81E9FEAE600164CA4 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04B91E9FEAE600164CA4 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04BA1E9FEAE600164CA4 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04BB1E9FEAE600164CA4 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04BD1E9FEAE600164CA4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/FirebaseAuthUI.strings; sourceTree = "<group>"; };
+ DECE04BE1E9FEAE600164CA4 /* en_GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en_GB; path = en_GB.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04BF1E9FEAE600164CA4 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04C01E9FEAE600164CA4 /* es_MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es_MX; path = es_MX.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04C11E9FEAE600164CA4 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04C21E9FEAE600164CA4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04C31E9FEAE600164CA4 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04C41E9FEAE600164CA4 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04C51E9FEAE600164CA4 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04C61E9FEAE600164CA4 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04C71E9FEAE600164CA4 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04C81E9FEAE600164CA4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04C91E9FEAE600164CA4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04CA1E9FEAE600164CA4 /* ms */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ms; path = ms.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04CB1E9FEAE600164CA4 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04CC1E9FEAE600164CA4 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04CD1E9FEAE600164CA4 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04CE1E9FEAE600164CA4 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04CF1E9FEAE600164CA4 /* pt_BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt_BR; path = pt_BR.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04D01E9FEAE600164CA4 /* pt_PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt_PT; path = pt_PT.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04D11E9FEAE600164CA4 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04D21E9FEAE600164CA4 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04D31E9FEAE600164CA4 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04D41E9FEAE600164CA4 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04D51E9FEAE600164CA4 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04D61E9FEAE600164CA4 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04D71E9FEAE600164CA4 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04D81E9FEAE600164CA4 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04D91E9FEAE600164CA4 /* zh_CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_CN; path = zh_CN.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04DA1E9FEAE600164CA4 /* zh_TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_TW; path = zh_TW.lproj/FirebearSample.strings; sourceTree = "<group>"; };
+ DECE04DB1E9FEAE600164CA4 /* UIViewController+Alerts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Alerts.h"; sourceTree = "<group>"; };
+ DECE04DC1E9FEAE600164CA4 /* UIViewController+Alerts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+Alerts.m"; sourceTree = "<group>"; };
+ DECE04DD1E9FEAE600164CA4 /* UserInfoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserInfoViewController.h; sourceTree = "<group>"; };
+ DECE04DE1E9FEAE600164CA4 /* UserInfoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserInfoViewController.m; sourceTree = "<group>"; };
+ DECE04DF1E9FEAE600164CA4 /* UserInfoViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = UserInfoViewController.xib; sourceTree = "<group>"; };
+ DECE04E01E9FEAE600164CA4 /* UserTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserTableViewCell.h; sourceTree = "<group>"; };
+ DECE04E11E9FEAE600164CA4 /* UserTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserTableViewCell.m; sourceTree = "<group>"; };
+ DECEA5661EBBDFB400273585 /* AuthCredentials.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuthCredentials.h; sourceTree = "<group>"; };
+ DECEA5681EBBED1200273585 /* FIRAuthAPNSTokenManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthAPNSTokenManagerTests.m; path = ../../Example/Auth/Tests/FIRAuthAPNSTokenManagerTests.m; sourceTree = "<group>"; };
+ DECEA5691EBBED1200273585 /* FIRAuthAPNSTokenTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthAPNSTokenTests.m; path = ../../Example/Auth/Tests/FIRAuthAPNSTokenTests.m; sourceTree = "<group>"; };
+ DECEA56A1EBBED1200273585 /* FIRAuthAppCredentialManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthAppCredentialManagerTests.m; path = ../../Example/Auth/Tests/FIRAuthAppCredentialManagerTests.m; sourceTree = "<group>"; };
+ DECEA56B1EBBED1200273585 /* FIRAuthNotificationManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthNotificationManagerTests.m; path = ../../Example/Auth/Tests/FIRAuthNotificationManagerTests.m; sourceTree = "<group>"; };
+ DEE13A021E9FFC9500D1BABA /* SwiftBear.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftBear.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ DEE13A041E9FFC9500D1BABA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+ DEE13A061E9FFC9500D1BABA /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
+ DEE13A101E9FFC9500D1BABA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ DEE13A141E9FFD1F00D1BABA /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
+ DEE13A151E9FFD1F00D1BABA /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; };
+ DEE13A191E9FFD2E00D1BABA /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
+ DEE13A201EA1252D00D1BABA /* ApiTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ApiTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ DEE13A241EA1252D00D1BABA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ DEE13A2A1EA125CD00D1BABA /* FirebearApiTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FirebearApiTests.m; sourceTree = "<group>"; };
+ DEE13A2C1EA12B8D00D1BABA /* FirebaseDev.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FirebaseDev.framework; path = "../../../Library/Developer/Xcode/DerivedData/Samples-dzyyktlzbrpvmobgpcqwaobjmibu/Build/Products/Debug-iphonesimulator/FirebaseDev/FirebaseDev.framework"; sourceTree = "<group>"; };
+ DEE13A321EA1642A00D1BABA /* EarlGreyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EarlGreyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ DEE13A361EA1642A00D1BABA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ DEE13A3C1EA164E100D1BABA /* FirebearEarlGreyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FirebearEarlGreyTests.m; sourceTree = "<group>"; };
+ DEE13A421EA16EE400D1BABA /* FirebaseAuthUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FirebaseAuthUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ DEE13AA71EA1761B00D1BABA /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ DEE13ABE1EA1764B00D1BABA /* Auth-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Auth-Info.plist"; path = "../../Example/Auth/App/Auth-Info.plist"; sourceTree = "<group>"; };
+ DEE13AC11EA1764B00D1BABA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
+ DEE13AC31EA1764B00D1BABA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Main.storyboard; sourceTree = "<group>"; };
+ DEE13AC41EA1764B00D1BABA /* FIRAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FIRAppDelegate.h; path = ../../Example/Auth/App/FIRAppDelegate.h; sourceTree = "<group>"; };
+ DEE13AC51EA1764B00D1BABA /* FIRAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAppDelegate.m; path = ../../Example/Auth/App/FIRAppDelegate.m; sourceTree = "<group>"; };
+ DEE13AC61EA1764B00D1BABA /* FIRViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FIRViewController.h; path = ../../Example/Auth/App/FIRViewController.h; sourceTree = "<group>"; };
+ DEE13AC71EA1764B00D1BABA /* FIRViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRViewController.m; path = ../../Example/Auth/App/FIRViewController.m; sourceTree = "<group>"; };
+ DEE13AC81EA1764B00D1BABA /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../Example/Auth/App/GoogleService-Info.plist"; sourceTree = "<group>"; };
+ DEE13AC91EA1764B00D1BABA /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ../../Example/Auth/App/Images.xcassets; sourceTree = "<group>"; };
+ DEE13ACA1EA1764B00D1BABA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ../../Example/Auth/App/main.m; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ DECE04811E9FEA7500164CA4 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 67AFFB52FF0FC4668D92F2E4 /* Pods_Sample.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE139FF1E9FFC9500D1BABA /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ AB62D09AF8C1196E07F37D3B /* Pods_SwiftBear.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13A1D1EA1252D00D1BABA /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E7989F47679257E9190C787F /* Pods_ApiTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13A2F1EA1642A00D1BABA /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 569C3F4E18627674CABE02AE /* Pods_EarlGreyTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13A3F1EA16EE400D1BABA /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEE13AA21EA1724300D1BABA /* FirebaseDev.framework in Frameworks */,
+ BD555A1DCF4E889DC3338248 /* Pods_FirebaseAuthUnitTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13AA41EA1761B00D1BABA /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A7609DCAD8A247411F27EA14 /* Pods_TestApp.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 2BC00404B97D81ACA2DF9DE8 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ D1EE09E5B9A6C092236222A9 /* Pods-Sample.debug.xcconfig */,
+ A75CAF1D0796E27A3E899DE4 /* Pods-Sample.release.xcconfig */,
+ 495F22BEDDFD4DFE3FB2D522 /* Pods-SwiftSample.debug.xcconfig */,
+ D9D6F0DE0BB3F49EF1B7CBB3 /* Pods-SwiftSample.release.xcconfig */,
+ 113C36392871524143A53B07 /* Pods-ApiTests.debug.xcconfig */,
+ 81ED9C5F2E61472DE3FA17CC /* Pods-ApiTests.release.xcconfig */,
+ 36794820176319C88B4F11E5 /* Pods-SwiftBear.debug.xcconfig */,
+ 151B5A61F0B0D152CD55DF81 /* Pods-SwiftBear.release.xcconfig */,
+ 1B16EC50B5315C8E9E2D21CE /* Pods-EarlGreyTests.debug.xcconfig */,
+ 94E3B3EB70D34E55CFF2E45D /* Pods-EarlGreyTests.release.xcconfig */,
+ 920E926BD468CBC593349A36 /* Pods-FirebaseAuthUnitTests.debug.xcconfig */,
+ 178AA1A3F0690CDEC44E2BF1 /* Pods-FirebaseAuthUnitTests.release.xcconfig */,
+ 57150555A6B03949ECB58AD9 /* Pods-TestApp.debug.xcconfig */,
+ B78FD2B21A8D72D5E38E3E79 /* Pods-TestApp.release.xcconfig */,
+ );
+ name = Pods;
+ sourceTree = "<group>";
+ };
+ DECE045D1E9FEA1000164CA4 = {
+ isa = PBXGroup;
+ children = (
+ DECE04851E9FEA7500164CA4 /* Sample */,
+ DEE13A031E9FFC9500D1BABA /* SwiftSample */,
+ DEE13A211EA1252D00D1BABA /* ApiTests */,
+ DEE13A331EA1642A00D1BABA /* EarlGreyTests */,
+ DEE13A431EA16EE400D1BABA /* FirebaseAuthUnitTests */,
+ DEE13AA81EA1761B00D1BABA /* TestApp */,
+ DECE04671E9FEA1000164CA4 /* Products */,
+ 2BC00404B97D81ACA2DF9DE8 /* Pods */,
+ FC34A5565C2ACFA8C5AA3B1E /* Frameworks */,
+ );
+ sourceTree = "<group>";
+ };
+ DECE04671E9FEA1000164CA4 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ DECE04841E9FEA7500164CA4 /* Sample.app */,
+ DEE13A021E9FFC9500D1BABA /* SwiftBear.app */,
+ DEE13A201EA1252D00D1BABA /* ApiTests.xctest */,
+ DEE13A321EA1642A00D1BABA /* EarlGreyTests.xctest */,
+ DEE13A421EA16EE400D1BABA /* FirebaseAuthUnitTests.xctest */,
+ DEE13AA71EA1761B00D1BABA /* TestApp.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ DECE04851E9FEA7500164CA4 /* Sample */ = {
+ isa = PBXGroup;
+ children = (
+ DECEA5661EBBDFB400273585 /* AuthCredentials.h */,
+ DECE049B1E9FEAE600164CA4 /* Application.plist */,
+ DECE049C1E9FEAE600164CA4 /* ApplicationDelegate.h */,
+ BED403DD1EBC057E00885C2C /* AuthCredentialsTemplate.h */,
+ DECE049D1E9FEAE600164CA4 /* ApplicationDelegate.m */,
+ DECE049E1E9FEAE600164CA4 /* AuthProviders.h */,
+ DECE049F1E9FEAE600164CA4 /* AuthProviders.m */,
+ DECE04A01E9FEAE600164CA4 /* Base.lproj */,
+ DECE04A11E9FEAE600164CA4 /* CustomTokenDataEntryViewController.h */,
+ DECE04A21E9FEAE600164CA4 /* CustomTokenDataEntryViewController.m */,
+ DECE04A31E9FEAE600164CA4 /* FacebookAuthProvider.h */,
+ DECE04A41E9FEAE600164CA4 /* FacebookAuthProvider.m */,
+ DECE04A51E9FEAE600164CA4 /* GoogleAuthProvider.h */,
+ DECE04A61E9FEAE600164CA4 /* GoogleAuthProvider.m */,
+ DECE04A81E9FEAE600164CA4 /* GoogleService-Info.plist */,
+ DECE04A91E9FEAE600164CA4 /* Images.xcassets */,
+ DECE04AA1E9FEAE600164CA4 /* main.m */,
+ DECE04AB1E9FEAE600164CA4 /* MainViewController.h */,
+ DECE04AC1E9FEAE600164CA4 /* MainViewController.m */,
+ DECE04AD1E9FEAE600164CA4 /* MainViewController.xib */,
+ DECE04AF1E9FEAE600164CA4 /* SettingsViewController.h */,
+ DECE04B01E9FEAE600164CA4 /* SettingsViewController.m */,
+ DECE04B11E9FEAE600164CA4 /* SettingsViewController.xib */,
+ DECE04B21E9FEAE600164CA4 /* StaticContentTableViewManager.h */,
+ DECE04B31E9FEAE600164CA4 /* StaticContentTableViewManager.m */,
+ DECE04B41E9FEAE600164CA4 /* Strings */,
+ DECE04DB1E9FEAE600164CA4 /* UIViewController+Alerts.h */,
+ DECE04DC1E9FEAE600164CA4 /* UIViewController+Alerts.m */,
+ DECE04DD1E9FEAE600164CA4 /* UserInfoViewController.h */,
+ DECE04DE1E9FEAE600164CA4 /* UserInfoViewController.m */,
+ DECE04DF1E9FEAE600164CA4 /* UserInfoViewController.xib */,
+ DECE04E01E9FEAE600164CA4 /* UserTableViewCell.h */,
+ DECE04E11E9FEAE600164CA4 /* UserTableViewCell.m */,
+ );
+ path = Sample;
+ sourceTree = "<group>";
+ };
+ DECE04A01E9FEAE600164CA4 /* Base.lproj */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ path = Base.lproj;
+ sourceTree = "<group>";
+ };
+ DECE04B41E9FEAE600164CA4 /* Strings */ = {
+ isa = PBXGroup;
+ children = (
+ DECE04B51E9FEAE600164CA4 /* FirebearSample.strings */,
+ DECE04BC1E9FEAE600164CA4 /* FirebaseAuthUI.strings */,
+ );
+ path = Strings;
+ sourceTree = "<group>";
+ };
+ DEE13A031E9FFC9500D1BABA /* SwiftSample */ = {
+ isa = PBXGroup;
+ children = (
+ BE7B447A1EC2507800FA4C1B /* AuthCredentials.swift */,
+ DEE13A181E9FFD2E00D1BABA /* Base.lproj */,
+ DEE13A191E9FFD2E00D1BABA /* GoogleService-Info.plist */,
+ DEE13A141E9FFD1F00D1BABA /* LaunchScreen.storyboard */,
+ DEE13A151E9FFD1F00D1BABA /* Main.storyboard */,
+ DEE13A041E9FFC9500D1BABA /* AppDelegate.swift */,
+ DEE13A061E9FFC9500D1BABA /* ViewController.swift */,
+ DEE13A101E9FFC9500D1BABA /* Info.plist */,
+ );
+ path = SwiftSample;
+ sourceTree = "<group>";
+ };
+ DEE13A181E9FFD2E00D1BABA /* Base.lproj */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ path = Base.lproj;
+ sourceTree = "<group>";
+ };
+ DEE13A211EA1252D00D1BABA /* ApiTests */ = {
+ isa = PBXGroup;
+ children = (
+ DEE13A2A1EA125CD00D1BABA /* FirebearApiTests.m */,
+ DEE13A241EA1252D00D1BABA /* Info.plist */,
+ );
+ path = ApiTests;
+ sourceTree = "<group>";
+ };
+ DEE13A331EA1642A00D1BABA /* EarlGreyTests */ = {
+ isa = PBXGroup;
+ children = (
+ DEE13A3C1EA164E100D1BABA /* FirebearEarlGreyTests.m */,
+ DEE13A361EA1642A00D1BABA /* Info.plist */,
+ );
+ path = EarlGreyTests;
+ sourceTree = "<group>";
+ };
+ DEE13A431EA16EE400D1BABA /* FirebaseAuthUnitTests */ = {
+ isa = PBXGroup;
+ children = (
+ DECEA5681EBBED1200273585 /* FIRAuthAPNSTokenManagerTests.m */,
+ DECEA5691EBBED1200273585 /* FIRAuthAPNSTokenTests.m */,
+ DECEA56A1EBBED1200273585 /* FIRAuthAppCredentialManagerTests.m */,
+ DECEA56B1EBBED1200273585 /* FIRAuthNotificationManagerTests.m */,
+ DE5371831EA7E89D000DA57F /* FIRAdditionalUserInfoTests.m */,
+ DE5371841EA7E89D000DA57F /* FIRApp+FIRAuthUnitTests.h */,
+ DE5371851EA7E89D000DA57F /* FIRApp+FIRAuthUnitTests.m */,
+ DE5371861EA7E89D000DA57F /* FIRAuthAppCredentialTests.m */,
+ DE5371871EA7E89D000DA57F /* FIRAuthAppDelegateProxyTests.m */,
+ DE5371881EA7E89D000DA57F /* FIRAuthBackendCreateAuthURITests.m */,
+ DE5371891EA7E89D000DA57F /* FIRAuthBackendRPCImplementationTests.m */,
+ DE53718A1EA7E89D000DA57F /* FIRAuthDispatcherTests.m */,
+ DE53718B1EA7E89D000DA57F /* FIRAuthGlobalWorkQueueTests.m */,
+ DE53718C1EA7E89D000DA57F /* FIRAuthKeychainTests.m */,
+ DE53718D1EA7E89D000DA57F /* FIRAuthSerialTaskQueueTests.m */,
+ DE53718E1EA7E89D000DA57F /* FIRAuthTests.m */,
+ DE53718F1EA7E89D000DA57F /* FIRAuthUserDefaultsStorageTests.m */,
+ DE5371901EA7E89D000DA57F /* FIRCreateAuthURIRequestTests.m */,
+ DE5371911EA7E89D000DA57F /* FIRCreateAuthURIResponseTests.m */,
+ DE5371921EA7E89D000DA57F /* FIRDeleteAccountRequestTests.m */,
+ DE5371931EA7E89D000DA57F /* FIRDeleteAccountResponseTests.m */,
+ DE5371941EA7E89D000DA57F /* FIRFakeBackendRPCIssuer.h */,
+ DE5371951EA7E89D000DA57F /* FIRFakeBackendRPCIssuer.m */,
+ DE5371961EA7E89D000DA57F /* FIRGetAccountInfoRequestTests.m */,
+ DE5371971EA7E89D000DA57F /* FIRGetAccountInfoResponseTests.m */,
+ DE5371981EA7E89D000DA57F /* FIRGetOOBConfirmationCodeRequestTests.m */,
+ DE5371991EA7E89D000DA57F /* FIRGetOOBConfirmationCodeResponseTests.m */,
+ DE53719A1EA7E89D000DA57F /* FIRGitHubAuthProviderTests.m */,
+ DE53719B1EA7E89D000DA57F /* FIRPhoneAuthProviderTests.m */,
+ DE53719C1EA7E89D000DA57F /* FIRResetPasswordRequestTests.m */,
+ DE53719D1EA7E89D000DA57F /* FIRResetPasswordResponseTests.m */,
+ DE53719E1EA7E89D000DA57F /* FIRSendVerificationCodeRequestTests.m */,
+ DE53719F1EA7E89D000DA57F /* FIRSendVerificationCodeResponseTests.m */,
+ DE5371A01EA7E89D000DA57F /* FIRSetAccountInfoRequestTests.m */,
+ DE5371A11EA7E89D000DA57F /* FIRSetAccountInfoResponseTests.m */,
+ DE5371A21EA7E89D000DA57F /* FIRSignUpNewUserRequestTests.m */,
+ DE5371A31EA7E89D000DA57F /* FIRSignUpNewUserResponseTests.m */,
+ DE5371A41EA7E89D000DA57F /* FIRTwitterAuthProviderTests.m */,
+ DE5371A51EA7E89D000DA57F /* FIRUserTests.m */,
+ DE5371A61EA7E89D000DA57F /* FIRVerifyAssertionRequestTests.m */,
+ DE5371A71EA7E89D000DA57F /* FIRVerifyAssertionResponseTests.m */,
+ DE5371A81EA7E89D000DA57F /* FIRVerifyClientRequestTest.m */,
+ DE5371A91EA7E89D000DA57F /* FIRVerifyClientResponseTests.m */,
+ DE5371AA1EA7E89D000DA57F /* FIRVerifyCustomTokenRequestTests.m */,
+ DE5371AB1EA7E89D000DA57F /* FIRVerifyCustomTokenResponseTests.m */,
+ DE5371AC1EA7E89D000DA57F /* FIRVerifyPasswordRequestTest.m */,
+ DE5371AD1EA7E89D000DA57F /* FIRVerifyPasswordResponseTests.m */,
+ DE5371AE1EA7E89D000DA57F /* FIRVerifyPhoneNumberRequestTests.m */,
+ DE5371AF1EA7E89D000DA57F /* FIRVerifyPhoneNumberResponseTests.m */,
+ DE5371B01EA7E89D000DA57F /* OCMStubRecorder+FIRAuthUnitTests.h */,
+ DE5371B11EA7E89D000DA57F /* OCMStubRecorder+FIRAuthUnitTests.m */,
+ DE5371B21EA7E89D000DA57F /* Tests-Info.plist */,
+ );
+ path = FirebaseAuthUnitTests;
+ sourceTree = "<group>";
+ };
+ DEE13AA81EA1761B00D1BABA /* TestApp */ = {
+ isa = PBXGroup;
+ children = (
+ DEE13ABE1EA1764B00D1BABA /* Auth-Info.plist */,
+ DEE13ABF1EA1764B00D1BABA /* Base.lproj */,
+ DEE13AC41EA1764B00D1BABA /* FIRAppDelegate.h */,
+ DEE13AC51EA1764B00D1BABA /* FIRAppDelegate.m */,
+ DEE13AC61EA1764B00D1BABA /* FIRViewController.h */,
+ DEE13AC71EA1764B00D1BABA /* FIRViewController.m */,
+ DEE13AC81EA1764B00D1BABA /* GoogleService-Info.plist */,
+ DEE13AC91EA1764B00D1BABA /* Images.xcassets */,
+ DEE13ACA1EA1764B00D1BABA /* main.m */,
+ );
+ path = TestApp;
+ sourceTree = "<group>";
+ };
+ DEE13ABF1EA1764B00D1BABA /* Base.lproj */ = {
+ isa = PBXGroup;
+ children = (
+ DEE13AC01EA1764B00D1BABA /* LaunchScreen.storyboard */,
+ DEE13AC21EA1764B00D1BABA /* Main.storyboard */,
+ );
+ name = Base.lproj;
+ path = ../../Example/Auth/App/Base.lproj;
+ sourceTree = "<group>";
+ };
+ FC34A5565C2ACFA8C5AA3B1E /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ DEE13A2C1EA12B8D00D1BABA /* FirebaseDev.framework */,
+ D5FE06BD9AA795DFBA9EFAAD /* Pods_Sample.framework */,
+ BC8C39EF1F42A0C750FF5186 /* Pods_SwiftSample.framework */,
+ 6EC09307D636721EAAB89BB2 /* Pods_ApiTests.framework */,
+ A1F689EE8E0E6F83D82429F0 /* Pods_SwiftBear.framework */,
+ AEE2E563FADF8C3382956B4F /* Pods_EarlGreyTests.framework */,
+ 4FFAD3F37BC4D7CEF0CAD579 /* Pods_FirebaseAuthUnitTests.framework */,
+ CDD2401395E91D0923BC5CD8 /* Pods_TestApp.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ DECE04831E9FEA7500164CA4 /* Sample */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DECE04981E9FEA7500164CA4 /* Build configuration list for PBXNativeTarget "Sample" */;
+ buildPhases = (
+ 8666ED2AA464450C3FE8DEE2 /* [CP] Check Pods Manifest.lock */,
+ DECE04801E9FEA7500164CA4 /* Sources */,
+ DECE04811E9FEA7500164CA4 /* Frameworks */,
+ DECE04821E9FEA7500164CA4 /* Resources */,
+ 14D1078EE81275B0906224AE /* [CP] Embed Pods Frameworks */,
+ 785E0AFEF965753D65D704A3 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Sample;
+ productName = Sample;
+ productReference = DECE04841E9FEA7500164CA4 /* Sample.app */;
+ productType = "com.apple.product-type.application";
+ };
+ DEE13A011E9FFC9500D1BABA /* SwiftBear */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DEE13A111E9FFC9500D1BABA /* Build configuration list for PBXNativeTarget "SwiftBear" */;
+ buildPhases = (
+ DD7BB43E0B7B486BDBF34616 /* [CP] Check Pods Manifest.lock */,
+ DEE139FE1E9FFC9500D1BABA /* Sources */,
+ DEE139FF1E9FFC9500D1BABA /* Frameworks */,
+ DEE13A001E9FFC9500D1BABA /* Resources */,
+ B241AC8AA926740AE9FBD0DD /* [CP] Embed Pods Frameworks */,
+ C341173EBB67728BFF59F1A6 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = SwiftBear;
+ productName = SwiftSample;
+ productReference = DEE13A021E9FFC9500D1BABA /* SwiftBear.app */;
+ productType = "com.apple.product-type.application";
+ };
+ DEE13A1F1EA1252D00D1BABA /* ApiTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DEE13A291EA1252D00D1BABA /* Build configuration list for PBXNativeTarget "ApiTests" */;
+ buildPhases = (
+ DECFE0EC98C60CEE719E03CA /* [CP] Check Pods Manifest.lock */,
+ DEE13A1C1EA1252D00D1BABA /* Sources */,
+ DEE13A1D1EA1252D00D1BABA /* Frameworks */,
+ DEE13A1E1EA1252D00D1BABA /* Resources */,
+ C3FCDC32190B715603C4E5EA /* [CP] Embed Pods Frameworks */,
+ D625EF1F1CC955CEFC768099 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ DEE13A261EA1252D00D1BABA /* PBXTargetDependency */,
+ );
+ name = ApiTests;
+ productName = ApiTests;
+ productReference = DEE13A201EA1252D00D1BABA /* ApiTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DEE13A311EA1642A00D1BABA /* EarlGreyTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DEE13A391EA1642B00D1BABA /* Build configuration list for PBXNativeTarget "EarlGreyTests" */;
+ buildPhases = (
+ 31C9CD5738CD8A86F3E29FD4 /* [CP] Check Pods Manifest.lock */,
+ DEE13A2E1EA1642A00D1BABA /* Sources */,
+ DEE13A2F1EA1642A00D1BABA /* Frameworks */,
+ DEE13A301EA1642A00D1BABA /* Resources */,
+ 45E0A7D32BD529EA299F169F /* [CP] Embed Pods Frameworks */,
+ C5EB2DFC39A4D32D63305071 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ DEE13A381EA1642A00D1BABA /* PBXTargetDependency */,
+ );
+ name = EarlGreyTests;
+ productName = EarlGreyTests;
+ productReference = DEE13A321EA1642A00D1BABA /* EarlGreyTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DEE13A411EA16EE400D1BABA /* FirebaseAuthUnitTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DEE13A4B1EA16EE400D1BABA /* Build configuration list for PBXNativeTarget "FirebaseAuthUnitTests" */;
+ buildPhases = (
+ 19197348303E74218FE9F86A /* [CP] Check Pods Manifest.lock */,
+ DEE13A3E1EA16EE400D1BABA /* Sources */,
+ DEE13A3F1EA16EE400D1BABA /* Frameworks */,
+ DEE13A401EA16EE400D1BABA /* Resources */,
+ F2982E8E37D71A5B31C19526 /* [CP] Embed Pods Frameworks */,
+ A7955AE6B74F92A6A1A61539 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ DEE13A481EA16EE400D1BABA /* PBXTargetDependency */,
+ DEA41FF61EA17A030072DA74 /* PBXTargetDependency */,
+ );
+ name = FirebaseAuthUnitTests;
+ productName = FirebaseAuthUnitTests;
+ productReference = DEE13A421EA16EE400D1BABA /* FirebaseAuthUnitTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DEE13AA61EA1761B00D1BABA /* TestApp */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DEE13ABD1EA1761C00D1BABA /* Build configuration list for PBXNativeTarget "TestApp" */;
+ buildPhases = (
+ 0C85A097DBA90CB3BA988388 /* [CP] Check Pods Manifest.lock */,
+ DEE13AA31EA1761B00D1BABA /* Sources */,
+ DEE13AA41EA1761B00D1BABA /* Frameworks */,
+ DEE13AA51EA1761B00D1BABA /* Resources */,
+ 90FD4EF0F0A764C0793CAA62 /* [CP] Embed Pods Frameworks */,
+ 91A5B60E1FF1F6B7A1979A7E /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = TestApp;
+ productName = TestApp;
+ productReference = DEE13AA71EA1761B00D1BABA /* TestApp.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ DECE045E1E9FEA1000164CA4 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0830;
+ LastUpgradeCheck = 0830;
+ ORGANIZATIONNAME = paulbeusterien;
+ TargetAttributes = {
+ DE5C700E1EA17F6900A965D2 = {
+ CreatedOnToolsVersion = 8.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ };
+ DECE04831E9FEA7500164CA4 = {
+ CreatedOnToolsVersion = 8.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ LastSwiftMigration = 0830;
+ ProvisioningStyle = Automatic;
+ };
+ DEE13A011E9FFC9500D1BABA = {
+ CreatedOnToolsVersion = 8.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ };
+ DEE13A1F1EA1252D00D1BABA = {
+ CreatedOnToolsVersion = 8.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ TestTargetID = DECE04831E9FEA7500164CA4;
+ };
+ DEE13A311EA1642A00D1BABA = {
+ CreatedOnToolsVersion = 8.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ TestTargetID = DECE04831E9FEA7500164CA4;
+ };
+ DEE13A411EA16EE400D1BABA = {
+ CreatedOnToolsVersion = 8.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ TestTargetID = DEE13AA61EA1761B00D1BABA;
+ };
+ DEE13AA61EA1761B00D1BABA = {
+ CreatedOnToolsVersion = 8.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ };
+ };
+ };
+ buildConfigurationList = DECE04611E9FEA1000164CA4 /* Build configuration list for PBXProject "Samples" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ ar,
+ ca,
+ cs,
+ da,
+ de,
+ el,
+ en_GB,
+ es,
+ es_MX,
+ fi,
+ fr,
+ he,
+ hr,
+ hu,
+ id,
+ it,
+ ja,
+ ko,
+ ms,
+ nb,
+ nl,
+ pl,
+ pt,
+ pt_BR,
+ pt_PT,
+ ro,
+ ru,
+ sk,
+ sv,
+ th,
+ tr,
+ uk,
+ vi,
+ zh_CN,
+ zh_TW,
+ );
+ mainGroup = DECE045D1E9FEA1000164CA4;
+ productRefGroup = DECE04671E9FEA1000164CA4 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ DECE04831E9FEA7500164CA4 /* Sample */,
+ DEE13A011E9FFC9500D1BABA /* SwiftBear */,
+ DEE13A1F1EA1252D00D1BABA /* ApiTests */,
+ DEE13A311EA1642A00D1BABA /* EarlGreyTests */,
+ DEE13A411EA16EE400D1BABA /* FirebaseAuthUnitTests */,
+ DEE13AA61EA1761B00D1BABA /* TestApp */,
+ DE5C700E1EA17F6900A965D2 /* AllTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ DECE04821E9FEA7500164CA4 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DECE04F21E9FEAE600164CA4 /* FirebearSample.strings in Resources */,
+ DECE04E21E9FEAE600164CA4 /* Application.plist in Resources */,
+ DECE04EA1E9FEAE600164CA4 /* Images.xcassets in Resources */,
+ DECE04ED1E9FEAE600164CA4 /* MainViewController.xib in Resources */,
+ DECE04F31E9FEAE600164CA4 /* FirebaseAuthUI.strings in Resources */,
+ DECE04F61E9FEAE600164CA4 /* UserInfoViewController.xib in Resources */,
+ DECE04F01E9FEAE600164CA4 /* SettingsViewController.xib in Resources */,
+ DECE04E91E9FEAE600164CA4 /* GoogleService-Info.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13A001E9FFC9500D1BABA /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEE13A161E9FFD1F00D1BABA /* LaunchScreen.storyboard in Resources */,
+ DEE13A171E9FFD1F00D1BABA /* Main.storyboard in Resources */,
+ DEE13A1A1E9FFD2E00D1BABA /* GoogleService-Info.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13A1E1EA1252D00D1BABA /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13A301EA1642A00D1BABA /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13A401EA16EE400D1BABA /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE5371DF1EA7E89D000DA57F /* Tests-Info.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13AA51EA1761B00D1BABA /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEE13ACD1EA1764B00D1BABA /* Main.storyboard in Resources */,
+ DEE13AD11EA1764B00D1BABA /* Images.xcassets in Resources */,
+ DEE13AD01EA1764B00D1BABA /* GoogleService-Info.plist in Resources */,
+ DEE13ACB1EA1764B00D1BABA /* Auth-Info.plist in Resources */,
+ DEE13ACC1EA1764B00D1BABA /* LaunchScreen.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 0C85A097DBA90CB3BA988388 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ 14D1078EE81275B0906224AE /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 19197348303E74218FE9F86A /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ 31C9CD5738CD8A86F3E29FD4 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ 45E0A7D32BD529EA299F169F /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-EarlGreyTests/Pods-EarlGreyTests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 785E0AFEF965753D65D704A3 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 8666ED2AA464450C3FE8DEE2 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ 90FD4EF0F0A764C0793CAA62 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-TestApp/Pods-TestApp-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 91A5B60E1FF1F6B7A1979A7E /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-TestApp/Pods-TestApp-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ A7955AE6B74F92A6A1A61539 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FirebaseAuthUnitTests/Pods-FirebaseAuthUnitTests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ B241AC8AA926740AE9FBD0DD /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBear/Pods-SwiftBear-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ C341173EBB67728BFF59F1A6 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBear/Pods-SwiftBear-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ C3FCDC32190B715603C4E5EA /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ApiTests/Pods-ApiTests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ C5EB2DFC39A4D32D63305071 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-EarlGreyTests/Pods-EarlGreyTests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ D625EF1F1CC955CEFC768099 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ApiTests/Pods-ApiTests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ DD7BB43E0B7B486BDBF34616 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ DECFE0EC98C60CEE719E03CA /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ F2982E8E37D71A5B31C19526 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FirebaseAuthUnitTests/Pods-FirebaseAuthUnitTests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ DECE04801E9FEA7500164CA4 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DECE04E61E9FEAE600164CA4 /* FacebookAuthProvider.m in Sources */,
+ DECE04F41E9FEAE600164CA4 /* UIViewController+Alerts.m in Sources */,
+ DECE04E51E9FEAE600164CA4 /* CustomTokenDataEntryViewController.m in Sources */,
+ DECE04E41E9FEAE600164CA4 /* AuthProviders.m in Sources */,
+ DECE04EF1E9FEAE600164CA4 /* SettingsViewController.m in Sources */,
+ DECE04E71E9FEAE600164CA4 /* GoogleAuthProvider.m in Sources */,
+ DECE04F51E9FEAE600164CA4 /* UserInfoViewController.m in Sources */,
+ DECE04EC1E9FEAE600164CA4 /* MainViewController.m in Sources */,
+ DECE04F71E9FEAE600164CA4 /* UserTableViewCell.m in Sources */,
+ DECE04F11E9FEAE600164CA4 /* StaticContentTableViewManager.m in Sources */,
+ DECE04EB1E9FEAE600164CA4 /* main.m in Sources */,
+ DECE04E31E9FEAE600164CA4 /* ApplicationDelegate.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE139FE1E9FFC9500D1BABA /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEE13A071E9FFC9500D1BABA /* ViewController.swift in Sources */,
+ BE7B447C1EC2508300FA4C1B /* AuthCredentials.swift in Sources */,
+ DEE13A051E9FFC9500D1BABA /* AppDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13A1C1EA1252D00D1BABA /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEE13A2B1EA125CD00D1BABA /* FirebearApiTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13A2E1EA1642A00D1BABA /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEE13A3D1EA164E100D1BABA /* FirebearEarlGreyTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13A3E1EA16EE400D1BABA /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE5371B31EA7E89D000DA57F /* FIRAdditionalUserInfoTests.m in Sources */,
+ DE5371B61EA7E89D000DA57F /* FIRAuthAppDelegateProxyTests.m in Sources */,
+ DE5371DD1EA7E89D000DA57F /* FIRVerifyPhoneNumberResponseTests.m in Sources */,
+ DE5371D31EA7E89D000DA57F /* FIRUserTests.m in Sources */,
+ DE5371BC1EA7E89D000DA57F /* FIRAuthSerialTaskQueueTests.m in Sources */,
+ DECEA56E1EBBED1200273585 /* FIRAuthAppCredentialManagerTests.m in Sources */,
+ DE5371DE1EA7E89D000DA57F /* OCMStubRecorder+FIRAuthUnitTests.m in Sources */,
+ DE5371DC1EA7E89D000DA57F /* FIRVerifyPhoneNumberRequestTests.m in Sources */,
+ DE5371B51EA7E89D000DA57F /* FIRAuthAppCredentialTests.m in Sources */,
+ DE5371CE1EA7E89D000DA57F /* FIRSetAccountInfoRequestTests.m in Sources */,
+ DE5371D41EA7E89D000DA57F /* FIRVerifyAssertionRequestTests.m in Sources */,
+ DE5371D91EA7E89D000DA57F /* FIRVerifyCustomTokenResponseTests.m in Sources */,
+ DECEA56F1EBBED1200273585 /* FIRAuthNotificationManagerTests.m in Sources */,
+ DECEA56D1EBBED1200273585 /* FIRAuthAPNSTokenTests.m in Sources */,
+ DE5371D81EA7E89D000DA57F /* FIRVerifyCustomTokenRequestTests.m in Sources */,
+ DE5371BD1EA7E89D000DA57F /* FIRAuthTests.m in Sources */,
+ DE5371C01EA7E89D000DA57F /* FIRCreateAuthURIResponseTests.m in Sources */,
+ DE5371B91EA7E89D000DA57F /* FIRAuthDispatcherTests.m in Sources */,
+ DE5371CF1EA7E89D000DA57F /* FIRSetAccountInfoResponseTests.m in Sources */,
+ DE5371C81EA7E89D000DA57F /* FIRGitHubAuthProviderTests.m in Sources */,
+ DE5371BB1EA7E89D000DA57F /* FIRAuthKeychainTests.m in Sources */,
+ DE5371B71EA7E89D000DA57F /* FIRAuthBackendCreateAuthURITests.m in Sources */,
+ DE5371C51EA7E89D000DA57F /* FIRGetAccountInfoResponseTests.m in Sources */,
+ DE5371B81EA7E89D000DA57F /* FIRAuthBackendRPCImplementationTests.m in Sources */,
+ DE5371DB1EA7E89D000DA57F /* FIRVerifyPasswordResponseTests.m in Sources */,
+ DE5371D71EA7E89D000DA57F /* FIRVerifyClientResponseTests.m in Sources */,
+ DE5371DA1EA7E89D000DA57F /* FIRVerifyPasswordRequestTest.m in Sources */,
+ DE5371CA1EA7E89D000DA57F /* FIRResetPasswordRequestTests.m in Sources */,
+ DE5371D51EA7E89D000DA57F /* FIRVerifyAssertionResponseTests.m in Sources */,
+ DE5371D01EA7E89D000DA57F /* FIRSignUpNewUserRequestTests.m in Sources */,
+ DE5371C41EA7E89D000DA57F /* FIRGetAccountInfoRequestTests.m in Sources */,
+ DE5371B41EA7E89D000DA57F /* FIRApp+FIRAuthUnitTests.m in Sources */,
+ DE5371D61EA7E89D000DA57F /* FIRVerifyClientRequestTest.m in Sources */,
+ DE5371BE1EA7E89D000DA57F /* FIRAuthUserDefaultsStorageTests.m in Sources */,
+ DE5371C91EA7E89D000DA57F /* FIRPhoneAuthProviderTests.m in Sources */,
+ DE5371D11EA7E89D000DA57F /* FIRSignUpNewUserResponseTests.m in Sources */,
+ DE5371C31EA7E89D000DA57F /* FIRFakeBackendRPCIssuer.m in Sources */,
+ DE5371BF1EA7E89D000DA57F /* FIRCreateAuthURIRequestTests.m in Sources */,
+ DE5371CC1EA7E89D000DA57F /* FIRSendVerificationCodeRequestTests.m in Sources */,
+ DECEA56C1EBBED1200273585 /* FIRAuthAPNSTokenManagerTests.m in Sources */,
+ DE5371CD1EA7E89D000DA57F /* FIRSendVerificationCodeResponseTests.m in Sources */,
+ DE5371C71EA7E89D000DA57F /* FIRGetOOBConfirmationCodeResponseTests.m in Sources */,
+ DE5371C21EA7E89D000DA57F /* FIRDeleteAccountResponseTests.m in Sources */,
+ DE5371CB1EA7E89D000DA57F /* FIRResetPasswordResponseTests.m in Sources */,
+ DE5371D21EA7E89D000DA57F /* FIRTwitterAuthProviderTests.m in Sources */,
+ DE5371C11EA7E89D000DA57F /* FIRDeleteAccountRequestTests.m in Sources */,
+ DE5371BA1EA7E89D000DA57F /* FIRAuthGlobalWorkQueueTests.m in Sources */,
+ DE5371C61EA7E89D000DA57F /* FIRGetOOBConfirmationCodeRequestTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE13AA31EA1761B00D1BABA /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEE13ACF1EA1764B00D1BABA /* FIRViewController.m in Sources */,
+ DEE13AD21EA1764B00D1BABA /* main.m in Sources */,
+ DEE13ACE1EA1764B00D1BABA /* FIRAppDelegate.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ DE5C70131EA17F7200A965D2 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DEE13A1F1EA1252D00D1BABA /* ApiTests */;
+ targetProxy = DE5C70121EA17F7200A965D2 /* PBXContainerItemProxy */;
+ };
+ DE5C70151EA17F7200A965D2 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DEE13A311EA1642A00D1BABA /* EarlGreyTests */;
+ targetProxy = DE5C70141EA17F7200A965D2 /* PBXContainerItemProxy */;
+ };
+ DE5C70171EA17F7200A965D2 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DEE13A411EA16EE400D1BABA /* FirebaseAuthUnitTests */;
+ targetProxy = DE5C70161EA17F7200A965D2 /* PBXContainerItemProxy */;
+ };
+ DEA41FF61EA17A030072DA74 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DEE13AA61EA1761B00D1BABA /* TestApp */;
+ targetProxy = DEA41FF51EA17A030072DA74 /* PBXContainerItemProxy */;
+ };
+ DEE13A261EA1252D00D1BABA /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DECE04831E9FEA7500164CA4 /* Sample */;
+ targetProxy = DEE13A251EA1252D00D1BABA /* PBXContainerItemProxy */;
+ };
+ DEE13A381EA1642A00D1BABA /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DECE04831E9FEA7500164CA4 /* Sample */;
+ targetProxy = DEE13A371EA1642A00D1BABA /* PBXContainerItemProxy */;
+ };
+ DEE13A481EA16EE400D1BABA /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DECE04831E9FEA7500164CA4 /* Sample */;
+ targetProxy = DEE13A471EA16EE400D1BABA /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ DECE04B51E9FEAE600164CA4 /* FirebearSample.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DECE04B61E9FEAE600164CA4 /* ar */,
+ DECE04B71E9FEAE600164CA4 /* ca */,
+ DECE04B81E9FEAE600164CA4 /* cs */,
+ DECE04B91E9FEAE600164CA4 /* da */,
+ DECE04BA1E9FEAE600164CA4 /* de */,
+ DECE04BB1E9FEAE600164CA4 /* el */,
+ DECE04BE1E9FEAE600164CA4 /* en_GB */,
+ DECE04BF1E9FEAE600164CA4 /* es */,
+ DECE04C01E9FEAE600164CA4 /* es_MX */,
+ DECE04C11E9FEAE600164CA4 /* fi */,
+ DECE04C21E9FEAE600164CA4 /* fr */,
+ DECE04C31E9FEAE600164CA4 /* he */,
+ DECE04C41E9FEAE600164CA4 /* hr */,
+ DECE04C51E9FEAE600164CA4 /* hu */,
+ DECE04C61E9FEAE600164CA4 /* id */,
+ DECE04C71E9FEAE600164CA4 /* it */,
+ DECE04C81E9FEAE600164CA4 /* ja */,
+ DECE04C91E9FEAE600164CA4 /* ko */,
+ DECE04CA1E9FEAE600164CA4 /* ms */,
+ DECE04CB1E9FEAE600164CA4 /* nb */,
+ DECE04CC1E9FEAE600164CA4 /* nl */,
+ DECE04CD1E9FEAE600164CA4 /* pl */,
+ DECE04CE1E9FEAE600164CA4 /* pt */,
+ DECE04CF1E9FEAE600164CA4 /* pt_BR */,
+ DECE04D01E9FEAE600164CA4 /* pt_PT */,
+ DECE04D11E9FEAE600164CA4 /* ro */,
+ DECE04D21E9FEAE600164CA4 /* ru */,
+ DECE04D31E9FEAE600164CA4 /* sk */,
+ DECE04D41E9FEAE600164CA4 /* sv */,
+ DECE04D51E9FEAE600164CA4 /* th */,
+ DECE04D61E9FEAE600164CA4 /* tr */,
+ DECE04D71E9FEAE600164CA4 /* uk */,
+ DECE04D81E9FEAE600164CA4 /* vi */,
+ DECE04D91E9FEAE600164CA4 /* zh_CN */,
+ DECE04DA1E9FEAE600164CA4 /* zh_TW */,
+ );
+ name = FirebearSample.strings;
+ sourceTree = "<group>";
+ };
+ DECE04BC1E9FEAE600164CA4 /* FirebaseAuthUI.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DECE04BD1E9FEAE600164CA4 /* en */,
+ );
+ name = FirebaseAuthUI.strings;
+ sourceTree = "<group>";
+ };
+ DEE13AC01EA1764B00D1BABA /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DEE13AC11EA1764B00D1BABA /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+ DEE13AC21EA1764B00D1BABA /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DEE13AC31EA1764B00D1BABA /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ DE5C70101EA17F6900A965D2 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ DE5C70111EA17F6900A965D2 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+ DECE047B1E9FEA1000164CA4 /* 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++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_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_OBJC_ROOT_CLASS = YES_ERROR;
+ 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;
+ };
+ DECE047C1E9FEA1000164CA4 /* 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++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_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_OBJC_ROOT_CLASS = YES_ERROR;
+ 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;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ DECE04991E9FEA7500164CA4 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = D1EE09E5B9A6C092236222A9 /* Pods-Sample.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "$(SRCROOT)/Sample/Application.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseExperimental1.dev;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Debug;
+ };
+ DECE049A1E9FEA7500164CA4 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = A75CAF1D0796E27A3E899DE4 /* Pods-Sample.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "$(SRCROOT)/Sample/Application.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseExperimental1.dev;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Release;
+ };
+ DEE13A121E9FFC9500D1BABA /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 36794820176319C88B4F11E5 /* Pods-SwiftBear.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = SwiftSample/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.SwiftBear;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Debug;
+ };
+ DEE13A131E9FFC9500D1BABA /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 151B5A61F0B0D152CD55DF81 /* Pods-SwiftBear.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = SwiftSample/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.SwiftBear;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Release;
+ };
+ DEE13A271EA1252D00D1BABA /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 113C36392871524143A53B07 /* Pods-ApiTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = ApiTests/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.ApiTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/Sample";
+ };
+ name = Debug;
+ };
+ DEE13A281EA1252D00D1BABA /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 81ED9C5F2E61472DE3FA17CC /* Pods-ApiTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = ApiTests/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.ApiTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/Sample";
+ };
+ name = Release;
+ };
+ DEE13A3A1EA1642B00D1BABA /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 1B16EC50B5315C8E9E2D21CE /* Pods-EarlGreyTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = EarlGreyTests/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.EarlGreyTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/Sample";
+ };
+ name = Debug;
+ };
+ DEE13A3B1EA1642B00D1BABA /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 94E3B3EB70D34E55CFF2E45D /* Pods-EarlGreyTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = EarlGreyTests/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.EarlGreyTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/Sample";
+ };
+ name = Release;
+ };
+ DEE13A491EA16EE400D1BABA /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 920E926BD468CBC593349A36 /* Pods-FirebaseAuthUnitTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"$(SRCROOT)/../Firebase/Auth/Source/RPCs\"",
+ "\"$(SRCROOT)/../Firebase/Auth/Source/Private\"",
+ "\"$(SRCROOT)/../Firebase/Auth/Source/AuthProviders\"",
+ "\"$(SRCROOT)/../Firebase/Core/Private\"",
+ );
+ INFOPLIST_FILE = "$(SRCROOT)/../Example/Auth/Tests/Tests-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseAuthUnitTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestApp.app/TestApp";
+ };
+ name = Debug;
+ };
+ DEE13A4A1EA16EE400D1BABA /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 178AA1A3F0690CDEC44E2BF1 /* Pods-FirebaseAuthUnitTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"$(SRCROOT)/../Firebase/Auth/Source/RPCs\"",
+ "\"$(SRCROOT)/../Firebase/Auth/Source/Private\"",
+ "\"$(SRCROOT)/../Firebase/Auth/Source/AuthProviders\"",
+ "\"$(SRCROOT)/../Firebase/Core/Private\"",
+ );
+ INFOPLIST_FILE = "$(SRCROOT)/../Example/Auth/Tests/Tests-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseAuthUnitTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestApp.app/TestApp";
+ };
+ name = Release;
+ };
+ DEE13ABB1EA1761C00D1BABA /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 57150555A6B03949ECB58AD9 /* Pods-TestApp.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = "";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "$(SRCROOT)/../Example/Auth/App/Auth-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.TestApp;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ DEE13ABC1EA1761C00D1BABA /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = B78FD2B21A8D72D5E38E3E79 /* Pods-TestApp.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = "";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "$(SRCROOT)/../Example/Auth/App/Auth-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.TestApp;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ DE5C700F1EA17F6900A965D2 /* Build configuration list for PBXAggregateTarget "AllTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE5C70101EA17F6900A965D2 /* Debug */,
+ DE5C70111EA17F6900A965D2 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DECE04611E9FEA1000164CA4 /* Build configuration list for PBXProject "Samples" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DECE047B1E9FEA1000164CA4 /* Debug */,
+ DECE047C1E9FEA1000164CA4 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DECE04981E9FEA7500164CA4 /* Build configuration list for PBXNativeTarget "Sample" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DECE04991E9FEA7500164CA4 /* Debug */,
+ DECE049A1E9FEA7500164CA4 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DEE13A111E9FFC9500D1BABA /* Build configuration list for PBXNativeTarget "SwiftBear" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DEE13A121E9FFC9500D1BABA /* Debug */,
+ DEE13A131E9FFC9500D1BABA /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DEE13A291EA1252D00D1BABA /* Build configuration list for PBXNativeTarget "ApiTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DEE13A271EA1252D00D1BABA /* Debug */,
+ DEE13A281EA1252D00D1BABA /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DEE13A391EA1642B00D1BABA /* Build configuration list for PBXNativeTarget "EarlGreyTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DEE13A3A1EA1642B00D1BABA /* Debug */,
+ DEE13A3B1EA1642B00D1BABA /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DEE13A4B1EA16EE400D1BABA /* Build configuration list for PBXNativeTarget "FirebaseAuthUnitTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DEE13A491EA16EE400D1BABA /* Debug */,
+ DEE13A4A1EA16EE400D1BABA /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DEE13ABD1EA1761C00D1BABA /* Build configuration list for PBXNativeTarget "TestApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DEE13ABB1EA1761C00D1BABA /* Debug */,
+ DEE13ABC1EA1761C00D1BABA /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = DECE045E1E9FEA1000164CA4 /* Project object */;
+}
diff --git a/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme
new file mode 100644
index 0000000..c00e18c
--- /dev/null
+++ b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE5C700E1EA17F6900A965D2"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A1F1EA1252D00D1BABA"
+ BuildableName = "ApiTests.xctest"
+ BlueprintName = "ApiTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A311EA1642A00D1BABA"
+ BuildableName = "EarlGreyTests.xctest"
+ BlueprintName = "EarlGreyTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A411EA16EE400D1BABA"
+ BuildableName = "FirebaseAuthUnitTests.xctest"
+ BlueprintName = "FirebaseAuthUnitTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE5C700E1EA17F6900A965D2"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE5C700E1EA17F6900A965D2"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE5C700E1EA17F6900A965D2"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/ApiTests.xcscheme b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/ApiTests.xcscheme
new file mode 100644
index 0000000..5973e7d
--- /dev/null
+++ b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/ApiTests.xcscheme
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A1F1EA1252D00D1BABA"
+ BuildableName = "ApiTests.xctest"
+ BlueprintName = "ApiTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/EarlGreyTests.xcscheme b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/EarlGreyTests.xcscheme
new file mode 100644
index 0000000..bc91613
--- /dev/null
+++ b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/EarlGreyTests.xcscheme
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A311EA1642A00D1BABA"
+ BuildableName = "EarlGreyTests.xctest"
+ BlueprintName = "EarlGreyTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/FirebaseAuthUnitTests.xcscheme b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/FirebaseAuthUnitTests.xcscheme
new file mode 100644
index 0000000..8282ca0
--- /dev/null
+++ b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/FirebaseAuthUnitTests.xcscheme
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A411EA16EE400D1BABA"
+ BuildableName = "FirebaseAuthUnitTests.xctest"
+ BlueprintName = "FirebaseAuthUnitTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme
new file mode 100644
index 0000000..36aebc2
--- /dev/null
+++ b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DECE04831E9FEA7500164CA4"
+ BuildableName = "Sample.app"
+ BlueprintName = "Sample"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A1F1EA1252D00D1BABA"
+ BuildableName = "ApiTests.xctest"
+ BlueprintName = "ApiTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A311EA1642A00D1BABA"
+ BuildableName = "EarlGreyTests.xctest"
+ BlueprintName = "EarlGreyTests"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DECE04831E9FEA7500164CA4"
+ BuildableName = "Sample.app"
+ BlueprintName = "Sample"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DECE04831E9FEA7500164CA4"
+ BuildableName = "Sample.app"
+ BlueprintName = "Sample"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DECE04831E9FEA7500164CA4"
+ BuildableName = "Sample.app"
+ BlueprintName = "Sample"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/SwiftBear.xcscheme b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/SwiftBear.xcscheme
new file mode 100644
index 0000000..1869be5
--- /dev/null
+++ b/AuthSamples/Samples.xcodeproj/xcshareddata/xcschemes/SwiftBear.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A011E9FFC9500D1BABA"
+ BuildableName = "SwiftBear.app"
+ BlueprintName = "SwiftBear"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A011E9FFC9500D1BABA"
+ BuildableName = "SwiftBear.app"
+ BlueprintName = "SwiftBear"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A011E9FFC9500D1BABA"
+ BuildableName = "SwiftBear.app"
+ BlueprintName = "SwiftBear"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE13A011E9FFC9500D1BABA"
+ BuildableName = "SwiftBear.app"
+ BlueprintName = "SwiftBear"
+ ReferencedContainer = "container:Samples.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/AuthSamples/SwiftSample/AppDelegate.swift b/AuthSamples/SwiftSample/AppDelegate.swift
new file mode 100644
index 0000000..abe6b2a
--- /dev/null
+++ b/AuthSamples/SwiftSample/AppDelegate.swift
@@ -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 UIKit
+
+import FirebaseDev // FirebaseCore
+import GoogleSignIn // 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/AuthSamples/SwiftSample/AuthCredentialsTemplate.swift b/AuthSamples/SwiftSample/AuthCredentialsTemplate.swift
new file mode 100644
index 0000000..f4a3780
--- /dev/null
+++ b/AuthSamples/SwiftSample/AuthCredentialsTemplate.swift
@@ -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.
+ */
+
+
+/*
+ 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/AuthSamples/SwiftSample/InfoTemplate.plist b/AuthSamples/SwiftSample/InfoTemplate.plist
new file mode 100644
index 0000000..e3c9457
--- /dev/null
+++ b/AuthSamples/SwiftSample/InfoTemplate.plist
@@ -0,0 +1,79 @@
+<!--
+ For this to be a valid plist file replace the following
+ $REVERSE_CLIENT_ID:
+ Value of REVERSED_CLIENT_ID key in the GoogleService-Info.plist file.
+-->
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>CFBundleURLName</key>
+ <string>com.google.swiftbear</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>com.google.swiftbear</string>
+ </array>
+ </dict>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>CFBundleURLName</key>
+ <string>$REVERSE_CLIENT_ID</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>$REVERSE_CLIENT_ID</string>
+ </array>
+ </dict>
+ </array>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>LSApplicationQueriesSchemes</key>
+ <array>
+ <string>fbauth2</string>
+ </array>
+</dict>
+</plist>
diff --git a/AuthSamples/SwiftSample/LaunchScreen.storyboard b/AuthSamples/SwiftSample/LaunchScreen.storyboard
new file mode 100644
index 0000000..8326657
--- /dev/null
+++ b/AuthSamples/SwiftSample/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/AuthSamples/SwiftSample/Main.storyboard b/AuthSamples/SwiftSample/Main.storyboard
new file mode 100644
index 0000000..6c60d61
--- /dev/null
+++ b/AuthSamples/SwiftSample/Main.storyboard
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+ <device id="retina4_7" orientation="portrait">
+ <adaptation id="fullscreen"/>
+ </device>
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
+ <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
+ <capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="SwiftBear" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="f0Y-rP-2xL">
+ <rect key="frame" x="16" y="20" width="100" height="100"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="100" id="1z8-5e-1FT"/>
+ <constraint firstAttribute="width" constant="100" id="dtU-ZR-yuk"/>
+ </constraints>
+ </imageView>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lWp-nO-ZaW">
+ <rect key="frame" x="116" y="20" width="263" height="25"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Dql-K9-Pb8">
+ <rect key="frame" x="116" y="45" width="263" height="25"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="8" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="x6V-xc-ti7">
+ <rect key="frame" x="116" y="70" width="263" height="25"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="26Z-WM-zPg">
+ <rect key="frame" x="116" y="95" width="263" height="25"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pbc-lK-fyt">
+ <rect key="frame" x="110" y="128" width="249" height="167"/>
+ <connections>
+ <outlet property="dataSource" destination="BYZ-38-t0r" id="UQZ-I6-nrk"/>
+ <outlet property="delegate" destination="BYZ-38-t0r" id="TUP-ja-Ewo"/>
+ </connections>
+ </pickerView>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UCG-40-lyD">
+ <rect key="frame" x="121" y="478" width="132" height="53"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="132" id="6tv-Vh-n1Q"/>
+ <constraint firstAttribute="height" constant="53" id="OTC-oP-oh6"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="24"/>
+ <state key="normal" title="Execute"/>
+ <connections>
+ <action selector="execute:" destination="BYZ-38-t0r" eventType="touchUpInside" id="Y7t-ah-5za"/>
+ </connections>
+ </button>
+ <pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eNd-jh-yrb">
+ <rect key="frame" x="16" y="303" width="163.5" height="167"/>
+ <connections>
+ <outlet property="dataSource" destination="BYZ-38-t0r" id="XZ2-uf-4BE"/>
+ <outlet property="delegate" destination="BYZ-38-t0r" id="brP-1Q-rxy"/>
+ </connections>
+ </pickerView>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Email" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MZv-Jr-rbJ">
+ <rect key="frame" x="195.5" y="303" width="163.5" height="42"/>
+ <fontDescription key="fontDescription" type="system" pointSize="20"/>
+ <nil key="textColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Password" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cJs-WA-Zoh">
+ <rect key="frame" x="195.5" y="387" width="163.5" height="41.5"/>
+ <fontDescription key="fontDescription" type="system" pointSize="20"/>
+ <nil key="textColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" clearButtonMode="always" translatesAutoresizingMaskIntoConstraints="NO" id="wEw-yN-DT4">
+ <rect key="frame" x="195.5" y="345" width="163.5" height="42"/>
+ <nil key="textColor"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <textInputTraits key="textInputTraits"/>
+ </textField>
+ <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" clearButtonMode="always" translatesAutoresizingMaskIntoConstraints="NO" id="kph-aS-EnS">
+ <rect key="frame" x="195.5" y="428.5" width="163.5" height="41.5"/>
+ <nil key="textColor"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <textInputTraits key="textInputTraits"/>
+ </textField>
+ <pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="p1i-PA-aCy">
+ <rect key="frame" x="16" y="128" width="86" height="167"/>
+ <connections>
+ <outlet property="dataSource" destination="BYZ-38-t0r" id="DRb-wg-dEY"/>
+ <outlet property="delegate" destination="BYZ-38-t0r" id="mZd-1x-Tz0"/>
+ </connections>
+ </pickerView>
+ </subviews>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <constraints>
+ <constraint firstItem="pbc-lK-fyt" firstAttribute="width" secondItem="8bC-Xf-vdC" secondAttribute="width" multiplier="3/4" constant="-32" id="0zH-Sz-NAW"/>
+ <constraint firstItem="lWp-nO-ZaW" firstAttribute="leading" secondItem="f0Y-rP-2xL" secondAttribute="trailing" id="8gd-NX-K1r"/>
+ <constraint firstItem="cJs-WA-Zoh" firstAttribute="top" secondItem="wEw-yN-DT4" secondAttribute="bottom" id="CFl-wo-Ta4"/>
+ <constraint firstItem="kph-aS-EnS" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailingMargin" id="DqA-5n-kgu"/>
+ <constraint firstItem="pbc-lK-fyt" firstAttribute="top" secondItem="f0Y-rP-2xL" secondAttribute="bottom" constant="8" id="EfK-6i-xV4"/>
+ <constraint firstItem="wEw-yN-DT4" firstAttribute="height" secondItem="cJs-WA-Zoh" secondAttribute="height" id="FgK-9k-ah5"/>
+ <constraint firstItem="wEw-yN-DT4" firstAttribute="width" secondItem="cJs-WA-Zoh" secondAttribute="width" id="G1D-hB-bvJ"/>
+ <constraint firstItem="MZv-Jr-rbJ" firstAttribute="width" secondItem="8bC-Xf-vdC" secondAttribute="width" multiplier="1/2" constant="-24" id="GaI-EN-daq"/>
+ <constraint firstItem="26Z-WM-zPg" firstAttribute="centerX" secondItem="x6V-xc-ti7" secondAttribute="centerX" id="HuK-J8-Fpz"/>
+ <constraint firstItem="eNd-jh-yrb" firstAttribute="top" secondItem="pbc-lK-fyt" secondAttribute="bottom" constant="8" id="LVJ-D1-3Vd"/>
+ <constraint firstItem="Dql-K9-Pb8" firstAttribute="centerX" secondItem="lWp-nO-ZaW" secondAttribute="centerX" id="Ne8-Rv-TLV"/>
+ <constraint firstItem="f0Y-rP-2xL" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="PEc-mF-DfU"/>
+ <constraint firstItem="26Z-WM-zPg" firstAttribute="top" secondItem="x6V-xc-ti7" secondAttribute="bottom" id="PvE-LR-R3h"/>
+ <constraint firstItem="cJs-WA-Zoh" firstAttribute="height" secondItem="kph-aS-EnS" secondAttribute="height" id="SW6-bi-RqF"/>
+ <constraint firstItem="cJs-WA-Zoh" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailingMargin" id="SiE-1X-xmu"/>
+ <constraint firstItem="p1i-PA-aCy" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" id="UFI-LW-2pG"/>
+ <constraint firstItem="Dql-K9-Pb8" firstAttribute="height" secondItem="lWp-nO-ZaW" secondAttribute="height" id="UFa-1x-biN"/>
+ <constraint firstItem="UCG-40-lyD" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="UJ5-Yk-u6Z"/>
+ <constraint firstAttribute="trailingMargin" secondItem="lWp-nO-ZaW" secondAttribute="trailing" constant="-20" id="V6f-WE-gEK"/>
+ <constraint firstItem="x6V-xc-ti7" firstAttribute="height" secondItem="Dql-K9-Pb8" secondAttribute="height" id="VrF-29-yZ6"/>
+ <constraint firstItem="pbc-lK-fyt" firstAttribute="height" secondItem="8bC-Xf-vdC" secondAttribute="height" multiplier="0.25" id="a0i-ba-sYb"/>
+ <constraint firstItem="MZv-Jr-rbJ" firstAttribute="top" secondItem="eNd-jh-yrb" secondAttribute="top" id="aC6-BD-zI0"/>
+ <constraint firstItem="x6V-xc-ti7" firstAttribute="centerX" secondItem="Dql-K9-Pb8" secondAttribute="centerX" id="cIk-ax-zhs"/>
+ <constraint firstItem="MZv-Jr-rbJ" firstAttribute="width" secondItem="wEw-yN-DT4" secondAttribute="width" id="crF-HH-d4y"/>
+ <constraint firstItem="26Z-WM-zPg" firstAttribute="width" secondItem="x6V-xc-ti7" secondAttribute="width" id="evU-m7-CI6"/>
+ <constraint firstItem="cJs-WA-Zoh" firstAttribute="width" secondItem="kph-aS-EnS" secondAttribute="width" id="fDS-yd-RnX"/>
+ <constraint firstItem="wEw-yN-DT4" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailingMargin" id="fLC-np-N5n"/>
+ <constraint firstItem="kph-aS-EnS" firstAttribute="top" secondItem="cJs-WA-Zoh" secondAttribute="bottom" id="fQZ-1s-seC"/>
+ <constraint firstItem="MZv-Jr-rbJ" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailingMargin" id="fuk-vI-lWD"/>
+ <constraint firstItem="eNd-jh-yrb" firstAttribute="height" secondItem="8bC-Xf-vdC" secondAttribute="height" multiplier="1/4" id="gGV-2A-Djo"/>
+ <constraint firstItem="x6V-xc-ti7" firstAttribute="width" secondItem="Dql-K9-Pb8" secondAttribute="width" id="gnn-hK-Tev"/>
+ <constraint firstItem="MZv-Jr-rbJ" firstAttribute="height" secondItem="wEw-yN-DT4" secondAttribute="height" id="iy8-Im-XOx"/>
+ <constraint firstItem="p1i-PA-aCy" firstAttribute="height" secondItem="pbc-lK-fyt" secondAttribute="height" id="kBZ-fH-wyv"/>
+ <constraint firstItem="pbc-lK-fyt" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailingMargin" id="kPG-I5-PXU"/>
+ <constraint firstItem="p1i-PA-aCy" firstAttribute="centerY" secondItem="pbc-lK-fyt" secondAttribute="centerY" id="mYj-of-8sB"/>
+ <constraint firstItem="eNd-jh-yrb" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" id="mvG-uN-2NO"/>
+ <constraint firstItem="26Z-WM-zPg" firstAttribute="height" secondItem="x6V-xc-ti7" secondAttribute="height" id="o9Q-eA-Mgg"/>
+ <constraint firstItem="pbc-lK-fyt" firstAttribute="leading" secondItem="p1i-PA-aCy" secondAttribute="trailing" constant="8" id="ocO-I3-JB4"/>
+ <constraint firstItem="x6V-xc-ti7" firstAttribute="top" secondItem="Dql-K9-Pb8" secondAttribute="bottom" id="p2W-xj-wNc"/>
+ <constraint firstItem="wEw-yN-DT4" firstAttribute="top" secondItem="MZv-Jr-rbJ" secondAttribute="bottom" id="p57-vW-AI1"/>
+ <constraint firstItem="kph-aS-EnS" firstAttribute="bottom" secondItem="eNd-jh-yrb" secondAttribute="bottom" id="pMo-uD-Ium"/>
+ <constraint firstItem="Dql-K9-Pb8" firstAttribute="width" secondItem="lWp-nO-ZaW" secondAttribute="width" id="qIA-Vy-0Ts"/>
+ <constraint firstItem="UCG-40-lyD" firstAttribute="top" secondItem="eNd-jh-yrb" secondAttribute="bottom" constant="8" id="qLA-FE-fz7"/>
+ <constraint firstItem="lWp-nO-ZaW" firstAttribute="height" secondItem="f0Y-rP-2xL" secondAttribute="height" multiplier="0.25" id="s9E-w5-zQ2"/>
+ <constraint firstItem="f0Y-rP-2xL" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" id="sFw-Yh-RRZ"/>
+ <constraint firstItem="eNd-jh-yrb" firstAttribute="width" secondItem="8bC-Xf-vdC" secondAttribute="width" multiplier="1/2" constant="-24" id="sdf-aZ-Z6h"/>
+ <constraint firstItem="Dql-K9-Pb8" firstAttribute="top" secondItem="lWp-nO-ZaW" secondAttribute="bottom" id="zo6-AD-yBu"/>
+ <constraint firstItem="lWp-nO-ZaW" firstAttribute="top" secondItem="f0Y-rP-2xL" secondAttribute="top" id="ztC-x1-RUf"/>
+ </constraints>
+ </view>
+ <connections>
+ <outlet property="actionPicker" destination="pbc-lK-fyt" id="C5u-X6-0sb"/>
+ <outlet property="actionTypePicker" destination="p1i-PA-aCy" id="EXj-7M-WaM"/>
+ <outlet property="credentialTypePicker" destination="eNd-jh-yrb" id="J4Z-9q-X2Q"/>
+ <outlet property="displayNameLabel" destination="lWp-nO-ZaW" id="4Kn-aZ-lS4"/>
+ <outlet property="emailField" destination="wEw-yN-DT4" id="3dQ-Dt-Vev"/>
+ <outlet property="emailInputLabel" destination="MZv-Jr-rbJ" id="jFy-DW-9Af"/>
+ <outlet property="emailLabel" destination="Dql-K9-Pb8" id="Ap8-hR-VyQ"/>
+ <outlet property="passwordField" destination="kph-aS-EnS" id="clH-Ac-p5S"/>
+ <outlet property="passwordInputLabel" destination="cJs-WA-Zoh" id="QbJ-ri-FOY"/>
+ <outlet property="profileImage" destination="f0Y-rP-2xL" id="Y14-pW-FuW"/>
+ <outlet property="providerListLabel" destination="26Z-WM-zPg" id="dJq-hC-Dw8"/>
+ <outlet property="userIDLabel" destination="x6V-xc-ti7" id="Cqb-bH-R8C"/>
+ </connections>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="304.80000000000001" y="428.63568215892059"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/AuthSamples/SwiftSample/ViewController.swift b/AuthSamples/SwiftSample/ViewController.swift
new file mode 100644
index 0000000..0b7481a
--- /dev/null
+++ b/AuthSamples/SwiftSample/ViewController.swift
@@ -0,0 +1,521 @@
+/*
+ * 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 FirebaseDev // FirebaseAuth
+import GoogleSignIn // GoogleSignIn
+
+final class ViewController: UIViewController {
+ /// 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 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))!
+ }
+ }
+ }
+
+ /// 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)
+ }
+ }
+
+ /// 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().signIn(with: credential) { user, error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Signed In With Credential", message: user?.textDescription)
+ }
+ }
+ }
+ case .createUser:
+ Auth.auth().createUser(withEmail: emailField.text!, password: passwordField.text!) {
+ user, error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Signed In With Credential", message: 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 .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!.reauthenticate(with: credential) { error in
+ self.ifNoError(error) {
+ 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!.link(with: credential) { user, error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Linked With Credential", message: 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!))
+ }
+ }
+
+ /// 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
+ DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.background).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
+ }
+
+ fileprivate func showAlert(title: String, message: String? = "") {
+ 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 credential.
+ var requiresCredential : Bool { get }
+
+ /// Whether or not the action requires email.
+ var requiresEmail: Bool { get }
+
+ /// Whether or not the credential requires password.
+ var requiresPassword: 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
+ }
+}
+
+/// The list of all possible actions the operator can take on the User object.
+fileprivate enum UserAction: Int, Action {
+
+ case updateEmail, 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 .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
+ }
+}
+
+/// The list of all possible credential types the operator can use to sign in or link.
+fileprivate enum CredentialType: Int {
+
+ case google, password
+
+ /// Total number of enum values.
+ static var count: Int {
+ return CredentialType.password.rawValue + 1
+ }
+
+ /// The text description for a particular enum value.
+ var text : String {
+ switch self {
+ case .google:
+ return "Google"
+ case .password:
+ return "Password ➡️️"
+ }
+ }
+
+ /// Whether or not the credential requires email.
+ var requiresEmail : Bool {
+ return self == .password
+ }
+
+ /// Whether or not the credential requires password.
+ var requiresPassword : Bool {
+ return self == .password
+ }
+}
+
+fileprivate extension User {
+ var textDescription: String {
+ return self.displayName ?? self.email ?? self.uid
+ }
+}
diff --git a/BuildFrameworks/FrameworkMaker.xcodeproj/project.pbxproj b/BuildFrameworks/FrameworkMaker.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..a66077c
--- /dev/null
+++ b/BuildFrameworks/FrameworkMaker.xcodeproj/project.pbxproj
@@ -0,0 +1,320 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ C8F75C1E8772455450E51C69 /* libPods-FrameworkMaker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D25AC01A0F56F8BC5375DD2 /* libPods-FrameworkMaker.a */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 01F29B956E7F6E45EF34DE72 /* Pods-FrameworkMaker.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameworkMaker.release.xcconfig"; path = "Pods/Target Support Files/Pods-FrameworkMaker/Pods-FrameworkMaker.release.xcconfig"; sourceTree = "<group>"; };
+ 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FrameworkMaker.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 1D25AC01A0F56F8BC5375DD2 /* libPods-FrameworkMaker.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FrameworkMaker.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ C8DA4EE8A169B227B0576C02 /* Pods-FrameworkMaker.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameworkMaker.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FrameworkMaker/Pods-FrameworkMaker.debug.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 05A46BD41CC9B2BE007BDB33 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C8F75C1E8772455450E51C69 /* libPods-FrameworkMaker.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 05A46BCE1CC9B2BE007BDB33 = {
+ isa = PBXGroup;
+ children = (
+ 05A46BD81CC9B2BE007BDB33 /* Products */,
+ AA03828B8B59297B5A3389B0 /* Pods */,
+ D3884AD1918E82D7FD21433D /* Frameworks */,
+ );
+ sourceTree = "<group>";
+ };
+ 05A46BD81CC9B2BE007BDB33 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ AA03828B8B59297B5A3389B0 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ C8DA4EE8A169B227B0576C02 /* Pods-FrameworkMaker.debug.xcconfig */,
+ 01F29B956E7F6E45EF34DE72 /* Pods-FrameworkMaker.release.xcconfig */,
+ );
+ name = Pods;
+ sourceTree = "<group>";
+ };
+ D3884AD1918E82D7FD21433D /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 1D25AC01A0F56F8BC5375DD2 /* libPods-FrameworkMaker.a */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 05A46BD61CC9B2BE007BDB33 /* FrameworkMaker */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 05A46BEE1CC9B2BE007BDB33 /* Build configuration list for PBXNativeTarget "FrameworkMaker" */;
+ buildPhases = (
+ AC1C2B143A86214CE77C9932 /* [CP] Check Pods Manifest.lock */,
+ 05A46BD31CC9B2BE007BDB33 /* Sources */,
+ 05A46BD41CC9B2BE007BDB33 /* Frameworks */,
+ 05A46BD51CC9B2BE007BDB33 /* Resources */,
+ 11182BBE1E5DB1C0F58623BB /* [CP] Embed Pods Frameworks */,
+ 5040608D1004852F08A22A14 /* [CP] Copy Pods 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 PBXShellScriptBuildPhase section */
+ 11182BBE1E5DB1C0F58623BB /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FrameworkMaker/Pods-FrameworkMaker-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 5040608D1004852F08A22A14 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FrameworkMaker/Pods-FrameworkMaker-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ AC1C2B143A86214CE77C9932 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase 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;
+ baseConfigurationReference = C8DA4EE8A169B227B0576C02 /* Pods-FrameworkMaker.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ INFOPLIST_FILE = FrameworkMaker/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;
+ baseConfigurationReference = 01F29B956E7F6E45EF34DE72 /* Pods-FrameworkMaker.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ INFOPLIST_FILE = FrameworkMaker/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/BuildFrameworks/Podfile b/BuildFrameworks/Podfile
new file mode 100644
index 0000000..e7bf344
--- /dev/null
+++ b/BuildFrameworks/Podfile
@@ -0,0 +1,12 @@
+platform :ios, '7.0'
+
+project 'FrameworkMaker.xcodeproj'
+
+target 'FrameworkMaker' do
+ pod 'FirebaseAuth', :path => '../Firebase/Auth'
+ pod 'FirebaseCore', :path => '../Firebase/Core'
+ pod 'FirebaseDatabase', :path => '../Firebase/Database'
+ pod 'FirebaseMessaging', :path => '../Firebase/Messaging'
+ pod 'FirebaseStorage', :path => '../Firebase/Storage'
+end
+
diff --git a/BuildFrameworks/README.md b/BuildFrameworks/README.md
new file mode 100644
index 0000000..3eb3061
--- /dev/null
+++ b/BuildFrameworks/README.md
@@ -0,0 +1,32 @@
+# Build Firebase static frameworks
+
+[build.swift](build.swift) is a script that will build a static framework for
+one or more of FirebaseAuth, FirebaseCore, FirebaseDatabase, FirebaseMessaging,
+and FirebaseStorage.
+
+Frameworks built with this script can be used alongside the official Firebase
+CocoaPods and
+[zip](https://firebase.google.com/docs/ios/setup#frameworks) distributions.
+
+
+## Usage
+
+```
+$ ./build.swift -f FirebaseAuth -f FirebaseMessaging ....
+```
+or
+```
+$ ./build.swift -all
+```
+
+The script will output the location of the new frameworks when it finishes
+the build.
+
+
+## Issues
+
+* Xcode's module cache may not properly update after a framework is replaced.
+The workaround is `rm -rf ~/Library/Developer/Xcode/DerivedData/ModuleCache/`
+
+* To replace the 4.0.0 version of FirebaseDatabase, the leveldb-library pod
+will need to be linked in. Add `pod 'leveldb-library'` to your Podfile.
diff --git a/BuildFrameworks/build.swift b/BuildFrameworks/build.swift
new file mode 100755
index 0000000..7bcda2f
--- /dev/null
+++ b/BuildFrameworks/build.swift
@@ -0,0 +1,179 @@
+#!/usr/bin/env xcrun swift
+
+/*
+ * 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 Foundation
+
+enum Colors: String {
+ case black = "\u{001B}[0;30m"
+ case red = "\u{001B}[0;31m"
+ case green = "\u{001B}[0;32m"
+ case yellow = "\u{001B}[0;33m"
+ case blue = "\u{001B}[0;34m"
+ case magenta = "\u{001B}[0;35m"
+ case cyan = "\u{001B}[0;36m"
+ case white = "\u{001B}[0;37m"
+}
+
+func colorPrint(color: Colors, text: String) {
+ print(color.rawValue + text + "\u{001B}[0;0m")
+}
+
+let allFrameworks = ["FirebaseAuth",
+ "FirebaseCore",
+ "FirebaseDatabase",
+ "FirebaseMessaging",
+ "FirebaseStorage"]
+
+let currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
+let url = URL(fileURLWithPath: CommandLine.arguments[0], relativeTo: currentDirectoryURL)
+let commandPath = url.deletingLastPathComponent().path
+
+func usage() -> Never {
+ print("usage: ./build.swift -f {framework1} -f {framework2} ....")
+ print("usage: ./build.swift -all")
+ print("Valid frameworks are \(allFrameworks)")
+ exit(1)
+}
+
+func processOptions() -> [String] {
+ guard CommandLine.arguments.count > 1 else {
+ usage()
+ }
+ var doFrameworks = [String]()
+ var optIndex = 1
+ whileLoop: while optIndex < CommandLine.arguments.count {
+ switch CommandLine.arguments[optIndex] {
+ case "-all":
+ guard doFrameworks.count == 0, CommandLine.arguments.count == 2 else {
+ colorPrint(color:Colors.red, text:"-all must be a solo option")
+ usage()
+ }
+ doFrameworks = allFrameworks
+ break whileLoop
+ case "-f":
+ optIndex += 1
+ guard optIndex < CommandLine.arguments.count else {
+ colorPrint(color:Colors.red, text:"The -f option must be followed by a framework name")
+ usage()
+ }
+ let framework = CommandLine.arguments[optIndex]
+ guard allFrameworks.contains(framework) else {
+ colorPrint(color:Colors.red, text:"\(framework) is not a valid framework")
+ usage()
+ }
+ doFrameworks += [framework]
+ optIndex += 1
+ default:
+ colorPrint(color:Colors.red, text: "Invalid option: \(CommandLine.arguments[optIndex])")
+ usage()
+ }
+ }
+ return doFrameworks
+}
+
+func tempDir() -> String {
+ let directory = NSTemporaryDirectory()
+ let fileName = NSUUID().uuidString
+ guard let dir = NSURL.fileURL(withPathComponents:[directory, fileName]) else {
+ colorPrint(color:Colors.red, text:"Failed to create temp directory")
+ exit(1)
+ }
+ return dir.path
+}
+
+func syncExec(command: String, args: [String] = []) {
+ let task = Process()
+ task.launchPath = command
+ task.arguments = args
+ task.currentDirectoryPath = commandPath
+ task.launch()
+ task.waitUntilExit()
+ guard (task.terminationStatus == 0) else {
+ colorPrint(color:Colors.red, text:"Command failed:")
+ colorPrint(color:Colors.red, text:command + " " + args.joined(separator:" "))
+ exit(1)
+ }
+}
+
+func buildThin(framework: String, arch: String, sdk: String, parentDir: String) -> [String] {
+ let buildDir = parentDir + "/" + arch
+ let standardOptions = [ "build",
+ "-configuration", "release",
+ "-workspace", "FrameworkMaker.xcworkspace",
+ "-scheme", framework,
+ "GCC_GENERATE_DEBUGGING_SYMBOLS=No"]
+ let bitcode = (sdk == "iphoneos") ? ["OTHER_CFLAGS=\"" + "-fembed-bitcode\""] : []
+ let args = standardOptions + ["ARCHS=" + arch, "BUILD_DIR=" + buildDir, "-sdk", sdk] + bitcode
+ syncExec(command:"/usr/bin/xcodebuild", args:args)
+ return [buildDir + "/Release-" + sdk + "/" + framework + "/lib" + framework + ".a"]
+}
+
+func createFile(file: String, content: String) {
+ let data = content.data(using:String.Encoding.utf8)
+ guard FileManager.default.createFile(atPath:file, contents: data, attributes: nil) else {
+ print("Error creating " + file)
+ exit(1)
+ }
+}
+
+// TODO: Add support for adding library and framework dependencies to makeModuleMap
+func makeModuleMap(framework: String, dir: String) {
+ let moduleDir = dir + "/Modules"
+ syncExec(command:"/bin/mkdir", args:["-p", moduleDir])
+ let moduleFile = moduleDir + "/module.modulemap"
+ let content = "framework module " + framework + " {\n" +
+ " umbrella header \"" + framework + ".h\"\n" +
+ " export *\n" +
+ " module * { export *}\n" +
+ "}\n"
+ createFile(file:moduleFile, content:content)
+}
+
+func buildFramework(withName framework: String, outputDir: String) {
+ let buildDir = tempDir()
+ var thinArchives = [String]()
+ thinArchives += buildThin(framework:framework, arch:"arm64", sdk:"iphoneos", parentDir:buildDir)
+ thinArchives += buildThin(framework:framework, arch:"armv7", sdk:"iphoneos", parentDir:buildDir)
+ thinArchives += buildThin(framework:framework, arch:"i386", sdk:"iphonesimulator", parentDir:buildDir)
+ thinArchives += buildThin(framework:framework, arch:"x86_64", sdk:"iphonesimulator", parentDir:buildDir)
+
+ let frameworkDir = outputDir + "/" + framework + ".framework"
+ syncExec(command:"/bin/mkdir", args:["-p", frameworkDir])
+ let fatArchive = frameworkDir + "/" + framework
+ syncExec(command:"/usr/bin/lipo", args:["-create", "-output", fatArchive] + thinArchives)
+ syncExec(command:"/bin/rm", args:["-rf"] + thinArchives)
+ let headersDir = frameworkDir + "/Headers"
+ syncExec(command:"/bin/mv", args:[NSString(string:thinArchives[0]).deletingLastPathComponent, headersDir])
+ syncExec(command:"/bin/rm", args:["-rf", buildDir])
+ makeModuleMap(framework:framework, dir:frameworkDir)
+}
+
+let frameworks = processOptions()
+colorPrint(color:Colors.green, text:"Building \(frameworks)")
+
+let outputDir = tempDir()
+
+syncExec(command:"/usr/local/bin/pod", args:["update"])
+
+for f in frameworks {
+ buildFramework(withName:f, outputDir:outputDir)
+}
+
+print()
+colorPrint(color:Colors.magenta, text:"The frameworks are available at the locations below:")
+syncExec(command:"/usr/bin/find", args:[outputDir, "-depth", "1"])
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0786fdf
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,24 @@
+# How to contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult [GitHub Help] for more
+information on using pull requests.
+
+[GitHub Help]: https://help.github.com/articles/about-pull-requests/
diff --git a/Example/Auth/App/Auth-Info.plist b/Example/Auth/App/Auth-Info.plist
new file mode 100644
index 0000000..7576a0d
--- /dev/null
+++ b/Example/Auth/App/Auth-Info.plist
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Auth/App/Base.lproj/LaunchScreen.storyboard b/Example/Auth/App/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..66a7681
--- /dev/null
+++ b/Example/Auth/App/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Auth/App/Base.lproj/Main.storyboard b/Example/Auth/App/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..d164a23
--- /dev/null
+++ b/Example/Auth/App/Base.lproj/Main.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="whP-gf-Uak">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="wQg-tq-qST">
+ <objects>
+ <viewController id="whP-gf-Uak" customClass="FIRViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="uEw-UM-LJ8"/>
+ <viewControllerLayoutGuide type="bottom" id="Mvr-aV-6Um"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="TpU-gO-2f1">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="tc2-Qw-aMS" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="305" y="433"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Auth/App/FIRAppDelegate.h b/Example/Auth/App/FIRAppDelegate.h
new file mode 100644
index 0000000..e3fba8f
--- /dev/null
+++ b/Example/Auth/App/FIRAppDelegate.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 UIKit;
+
+@interface FIRAppDelegate : UIResponder <UIApplicationDelegate>
+
+@property (strong, nonatomic) UIWindow *window;
+
+@end
diff --git a/Example/Auth/App/FIRAppDelegate.m b/Example/Auth/App/FIRAppDelegate.m
new file mode 100644
index 0000000..0ecfdea
--- /dev/null
+++ b/Example/Auth/App/FIRAppDelegate.m
@@ -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 "FIRAppDelegate.h"
+
+@implementation FIRAppDelegate
+
+- (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 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/Auth/App/FIRViewController.h b/Example/Auth/App/FIRViewController.h
new file mode 100644
index 0000000..64b4b74
--- /dev/null
+++ b/Example/Auth/App/FIRViewController.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 UIKit;
+
+@interface FIRViewController : UIViewController
+
+@end
diff --git a/Example/Auth/App/FIRViewController.m b/Example/Auth/App/FIRViewController.m
new file mode 100644
index 0000000..901accf
--- /dev/null
+++ b/Example/Auth/App/FIRViewController.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 "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/Auth/App/GoogleService-Info.plist b/Example/Auth/App/GoogleService-Info.plist
new file mode 100644
index 0000000..89afffe
--- /dev/null
+++ b/Example/Auth/App/GoogleService-Info.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>API_KEY</key>
+ <string>correct_api_key</string>
+ <key>TRACKING_ID</key>
+ <string>correct_tracking_id</string>
+ <key>CLIENT_ID</key>
+ <string>correct_client_id</string>
+ <key>REVERSED_CLIENT_ID</key>
+ <string>correct_reversed_client_id</string>
+ <key>ANDROID_CLIENT_ID</key>
+ <string>correct_android_client_id</string>
+ <key>GOOGLE_APP_ID</key>
+ <string>1:123:ios:123abc</string>
+ <key>GCM_SENDER_ID</key>
+ <string>correct_gcm_sender_id</string>
+ <key>PLIST_VERSION</key>
+ <string>1</string>
+ <key>BUNDLE_ID</key>
+ <string>com.google.FirebaseSDKTests</string>
+ <key>PROJECT_ID</key>
+ <string>abc-xyz-123</string>
+ <key>DATABASE_URL</key>
+ <string>https://abc-xyz-123.firebaseio.com</string>
+ <key>STORAGE_BUCKET</key>
+ <string>project-id-123.storage.firebase.com</string>
+</dict>
+</plist>
diff --git a/Example/Auth/App/main.m b/Example/Auth/App/main.m
new file mode 100644
index 0000000..03b5c12
--- /dev/null
+++ b/Example/Auth/App/main.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 UIKit;
+#import "FIRAppDelegate.h"
+
+int main(int argc, char * argv[])
+{
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class]));
+ }
+}
diff --git a/Example/Auth/Tests/FIRAdditionalUserInfoTests.m b/Example/Auth/Tests/FIRAdditionalUserInfoTests.m
new file mode 100644
index 0000000..d50380e
--- /dev/null
+++ b/Example/Auth/Tests/FIRAdditionalUserInfoTests.m
@@ -0,0 +1,124 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAdditionalUserInfo_Internal.h"
+#import "FIRVerifyAssertionResponse.h"
+#import <OCMock/OCMock.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kUserName
+ @brief The fake user name.
+ */
+static NSString *const kUserName = @"User Doe";
+
+/** @var kIsNewUser
+ @brief The fake flag that indicates the user has signed in for the first time.
+ */
+static BOOL kIsNewUser = YES;
+
+/** @var kProviderID
+ @brief The fake Provider ID.
+ */
+static NSString *const kProviderID = @"PROVIDER_ID";
+
+/** @class FIRAdditionalUserInfoTests
+ @brief Tests for @c FIRAdditionalUserInfo .
+ */
+@interface FIRAdditionalUserInfoTests : XCTestCase
+@end
+
+@implementation FIRAdditionalUserInfoTests
+
+/** @fn googleProfile
+ @brief The fake user profile under additional user data in @c FIRVerifyAssertionResponse.
+ */
++ (NSDictionary *)profile {
+ static NSDictionary *kProfile = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ kProfile = @{
+ @"email": @"user@mail.com",
+ @"given_name": @"User",
+ @"family_name": @"Doe"
+ };
+ });
+ return kProfile;
+}
+
+/** @fn testAditionalUserInfoCreation
+ @brief Tests succuessful creation of @c FIRAdditionalUserInfo with
+ @c initWithProviderID:profile:username: call.
+ */
+- (void)testAditionalUserInfoCreation {
+ FIRAdditionalUserInfo *userInfo =
+ [[FIRAdditionalUserInfo alloc] initWithProviderID:kProviderID
+ profile:[[self class] profile]
+ username:kUserName
+ isNewUser:kIsNewUser];
+ XCTAssertEqualObjects(userInfo.providerID, kProviderID);
+ XCTAssertEqualObjects(userInfo.profile, [[self class] profile]);
+ XCTAssertEqualObjects(userInfo.username, kUserName);
+ XCTAssertEqual(userInfo.isNewUser, kIsNewUser);
+}
+
+/** @fn testAditionalUserInfoCreationWithStaticInitializer
+ @brief Tests succuessful creation of @c FIRAdditionalUserInfo with
+ @c userInfoWithVerifyAssertionResponse call.
+ */
+- (void)testAditionalUserInfoCreationWithStaticInitializer {
+ id mockVeriyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]);
+ OCMExpect([mockVeriyAssertionResponse providerID]).andReturn(kProviderID);
+ OCMExpect([mockVeriyAssertionResponse profile]).andReturn([[self class] profile]);
+ OCMExpect([mockVeriyAssertionResponse username]).andReturn(kUserName);
+ OCMExpect([mockVeriyAssertionResponse isNewUser]).andReturn(kIsNewUser);
+
+ FIRAdditionalUserInfo *userInfo =
+ [FIRAdditionalUserInfo userInfoWithVerifyAssertionResponse:mockVeriyAssertionResponse];
+ XCTAssertEqualObjects(userInfo.providerID, kProviderID);
+ XCTAssertEqualObjects(userInfo.profile, [[self class] profile]);
+ XCTAssertEqualObjects(userInfo.username, kUserName);
+ XCTAssertEqual(userInfo.isNewUser, kIsNewUser);
+ OCMVerifyAll(mockVeriyAssertionResponse);
+}
+
+/** @fn testAdditionalUserInfoCoding
+ @brief Tests successful archiving and unarchiving of @c FIRAdditionalUserInfo.
+ */
+- (void)testAdditionalUserInfoCoding {
+ FIRAdditionalUserInfo *userInfo =
+ [[FIRAdditionalUserInfo alloc] initWithProviderID:kProviderID
+ profile:[[self class] profile]
+ username:kUserName
+ isNewUser:kIsNewUser];
+ NSData *data = [NSKeyedArchiver archivedDataWithRootObject:userInfo];
+ XCTAssertNotNil(data, @"Should not be nil if archiving succeeded.");
+ XCTAssertNoThrow([NSKeyedUnarchiver unarchiveObjectWithData:data],
+ @"Unarchiving should not throw and exception.");
+ FIRAdditionalUserInfo *unarchivedUserInfo = [NSKeyedUnarchiver unarchiveObjectWithData:data];
+ XCTAssertTrue([unarchivedUserInfo isKindOfClass:[FIRAdditionalUserInfo class]],
+ @"Unarchived object must be of kind FIRAdditionalUserInfo class.");
+ XCTAssertEqualObjects(unarchivedUserInfo.providerID, userInfo.providerID);
+ XCTAssertEqualObjects(unarchivedUserInfo.profile, userInfo.profile);
+ XCTAssertEqualObjects(unarchivedUserInfo.username, userInfo.username);
+ XCTAssertEqual(unarchivedUserInfo.isNewUser, unarchivedUserInfo.isNewUser);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.h b/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.h
new file mode 100644
index 0000000..c0e6d13
--- /dev/null
+++ b/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.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 "FIRAppInternal.h"
+
+/** @category FIRApp (FIRAuthUnitTests)
+ @brief Tests for @c FIRAuth.
+ */
+@interface FIRApp (FIRAuthUnitTests)
+
+/** @fn resetAppForAuthUnitTests
+ @brief Resets the Firebase app for unit tests.
+ */
++ (void)resetAppForAuthUnitTests;
+
+/** @fn appForAuthUnitTestsWithName:
+ @brief Creates a Firebase app with given name.
+ @param name The name for the app.
+ @return A @c FIRApp with the specified name.
+ */
++ (FIRApp *)appForAuthUnitTestsWithName:(NSString *)name;
+
+@end
diff --git a/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m b/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m
new file mode 100644
index 0000000..aba4136
--- /dev/null
+++ b/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m
@@ -0,0 +1,45 @@
+/*
+ * 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 "FIRApp+FIRAuthUnitTests.h"
+
+#import "FIROptionsInternal.h"
+
+@implementation FIRApp (FIRAuthUnitTests)
+
+/** @fn appOptions
+ @brief Gets Firebase app options to be used for tests.
+ @return A @c FIROptions instance.
+ */
++ (FIROptions *)appOptions {
+ return [[FIROptions alloc] initInternalWithOptionsDictionary:@{
+ @"GOOGLE_APP_ID" : @"1:1085102361755:ios:f790a919483d5bdf",
+ @"API_KEY" : @"FAKE_API_KEY",
+ @"GCM_SENDER_ID": @"217397612173"
+ }];
+}
+
++ (void)resetAppForAuthUnitTests {
+ [FIRApp resetApps];
+ [FIRApp configureWithOptions:[self appOptions]];
+}
+
++ (FIRApp *)appForAuthUnitTestsWithName:(NSString *)name {
+ return [[FIRApp alloc] initInstanceWithName:name options:[self appOptions]];
+}
+
+
+@end
diff --git a/Example/Auth/Tests/FIRAuthAPNSTokenManagerTests.m b/Example/Auth/Tests/FIRAuthAPNSTokenManagerTests.m
new file mode 100644
index 0000000..37d95a6
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthAPNSTokenManagerTests.m
@@ -0,0 +1,225 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthAPNSToken.h"
+#import "FIRAuthAPNSTokenManager.h"
+#import <OCMock/OCMock.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kRegistrationTimeout
+ @brief The registration timeout used for testing.
+ */
+static const NSTimeInterval kRegistrationTimeout = .5;
+
+/** @var kExpectationTimeout
+ @brief The test expectation timeout.
+ @remarks This must be considerably greater than @c kVerificationTimeout .
+ */
+static const NSTimeInterval kExpectationTimeout = 1;
+
+/** @class FIRAuthLegacyUIApplication
+ @brief A fake legacy (< iOS 7) UIApplication class.
+ @remarks A custom class is needed because `respondsToSelector:` itself cannot be mocked.
+ */
+@interface FIRAuthLegacyUIApplication : NSObject
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+- (void)registerForRemoteNotificationTypes:(UIRemoteNotificationType)types;
+#pragma clang diagnostic pop
+
+@end
+@implementation FIRAuthLegacyUIApplication
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+- (void)registerForRemoteNotificationTypes:(UIRemoteNotificationType)types {
+}
+#pragma clang diagnostic pop
+
+@end
+
+/** @class FIRAuthAPNSTokenManagerTests
+ @brief Unit tests for @c FIRAuthAPNSTokenManager .
+ */
+@interface FIRAuthAPNSTokenManagerTests : XCTestCase
+@end
+@implementation FIRAuthAPNSTokenManagerTests {
+ /** @var _mockApplication
+ @brief The mock application for testing.
+ */
+ id _mockApplication;
+
+ /** @var _manager
+ @brief The @c FIRAuthAPNSTokenManager instance under tests.
+ */
+ FIRAuthAPNSTokenManager *_manager;
+
+ /** @var _data
+ @brief One piece of data used for testing.
+ */
+ NSData *_data;
+
+ /** @var _otherData
+ @brief Another piece of data used for testing.
+ */
+ NSData *_otherData;
+}
+
+- (void)setUp {
+ _mockApplication = OCMClassMock([UIApplication class]);
+ _manager = [[FIRAuthAPNSTokenManager alloc] initWithApplication:_mockApplication];
+ _data = [@"qwerty" dataUsingEncoding:NSUTF8StringEncoding];
+ _otherData = [@"!@#$" dataUsingEncoding:NSUTF8StringEncoding];
+}
+
+/** @fn testSetToken
+ @brief Tests setting and getting the `token` property.
+ */
+- (void)testSetToken {
+ XCTAssertNil(_manager.token);
+ _manager.token = [[FIRAuthAPNSToken alloc] initWithData:_data type:FIRAuthAPNSTokenTypeProd];
+ XCTAssertEqualObjects(_manager.token.data, _data);
+ XCTAssertEqual(_manager.token.type, FIRAuthAPNSTokenTypeProd);
+ _manager.token = nil;
+ XCTAssertNil(_manager.token);
+}
+
+/** @fn testDetectTokenType
+ @brief Tests automatic detection of token type.
+ */
+- (void)testDetectTokenType {
+ XCTAssertNil(_manager.token);
+ _manager.token = [[FIRAuthAPNSToken alloc] initWithData:_data type:FIRAuthAPNSTokenTypeUnknown];
+ XCTAssertEqualObjects(_manager.token.data, _data);
+ XCTAssertNotEqual(_manager.token.type, FIRAuthAPNSTokenTypeUnknown);
+}
+
+/** @fn testCallback
+ @brief Tests callbacks are called.
+ */
+- (void)testCallback {
+ // Add first callback, which is yet to be called.
+ OCMExpect([_mockApplication registerForRemoteNotifications]);
+ __block BOOL firstCallbackCalled = NO;
+ [_manager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token) {
+ XCTAssertEqualObjects(token.data, _data);
+ XCTAssertEqual(token.type, FIRAuthAPNSTokenTypeSandbox);
+ firstCallbackCalled = YES;
+ }];
+ XCTAssertFalse(firstCallbackCalled);
+
+ // Add second callback, which is yet to be called either.
+ __block BOOL secondCallbackCalled = NO;
+ [_manager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token) {
+ XCTAssertEqualObjects(token.data, _data);
+ XCTAssertEqual(token.type, FIRAuthAPNSTokenTypeSandbox);
+ secondCallbackCalled = YES;
+ }];
+ XCTAssertFalse(secondCallbackCalled);
+
+ // Setting nil token shouldn't trigger either callbacks.
+ _manager.token = nil;
+ XCTAssertFalse(firstCallbackCalled);
+ XCTAssertFalse(secondCallbackCalled);
+ XCTAssertNil(_manager.token);
+
+ // Setting a real token should trigger both callbacks.
+ _manager.token = [[FIRAuthAPNSToken alloc] initWithData:_data type:FIRAuthAPNSTokenTypeSandbox];
+ XCTAssertTrue(firstCallbackCalled);
+ XCTAssertTrue(secondCallbackCalled);
+ XCTAssertEqualObjects(_manager.token.data, _data);
+ XCTAssertEqual(_manager.token.type, FIRAuthAPNSTokenTypeSandbox);
+
+ // Add third callback, which should be called back immediately.
+ __block BOOL thirdCallbackCalled = NO;
+ [_manager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token) {
+ XCTAssertEqualObjects(token.data, _data);
+ XCTAssertEqual(token.type, FIRAuthAPNSTokenTypeSandbox);
+ thirdCallbackCalled = YES;
+ }];
+ XCTAssertTrue(thirdCallbackCalled);
+
+ // Verify the mock in the main thread.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"verify mock"];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ OCMVerifyAll(_mockApplication);
+ [expectation fulfill];
+ });
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+}
+
+/** @fn testTimeout
+ @brief Tests callbacks can be timed out.
+ */
+- (void)testTimeout {
+ // Set up timeout.
+ XCTAssertGreaterThan(_manager.timeout, 0);
+ _manager.timeout = kRegistrationTimeout;
+
+ // Add callback to time out.
+ OCMExpect([_mockApplication registerForRemoteNotifications]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [_manager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token) {
+ XCTAssertNil(token);
+ [expectation fulfill];
+ }];
+
+ // Time out.
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockApplication);
+}
+
+/** @fn testLegacyRegistration
+ @brief Tests remote notification registration on legacy systems.
+ */
+- (void)testLegacyRegistration {
+ // Use a custom class for `respondsToSelector:` to work.
+ _mockApplication = OCMClassMock([FIRAuthLegacyUIApplication class]);
+ _manager = [[FIRAuthAPNSTokenManager alloc] initWithApplication:_mockApplication];
+
+ // Add callback.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [[[_mockApplication expect] ignoringNonObjectArgs] registerForRemoteNotificationTypes:0];
+#pragma clang diagnostic pop
+ __block BOOL callbackCalled = NO;
+ [_manager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token) {
+ XCTAssertEqualObjects(token.data, _data);
+ XCTAssertNotEqual(token.type, FIRAuthAPNSTokenTypeUnknown);
+ callbackCalled = YES;
+ }];
+ XCTAssertFalse(callbackCalled);
+
+ // Set the token.
+ _manager.token = [[FIRAuthAPNSToken alloc] initWithData:_data type:FIRAuthAPNSTokenTypeUnknown];
+ XCTAssertTrue(callbackCalled);
+
+ // Verify the mock in the main thread.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"verify mock"];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ OCMVerifyAll(_mockApplication);
+ [expectation fulfill];
+ });
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/FIRAuthAPNSTokenTests.m b/Example/Auth/Tests/FIRAuthAPNSTokenTests.m
new file mode 100644
index 0000000..d2cd0b5
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthAPNSTokenTests.m
@@ -0,0 +1,43 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthAPNSToken.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthAPNSTokenTests
+ @brief Unit tests for @c FIRAuthAPNSToken .
+ */
+@interface FIRAuthAPNSTokenTests : XCTestCase
+@end
+@implementation FIRAuthAPNSTokenTests
+
+/** @fn testInitializer
+ @brief Tests the initializer of the class.
+ */
+- (void)testInitializer {
+ NSData *data = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
+ FIRAuthAPNSToken *token = [[FIRAuthAPNSToken alloc] initWithData:data
+ type:FIRAuthAPNSTokenTypeProd];
+ XCTAssertEqualObjects(token.data, data);
+ XCTAssertEqual(token.type, FIRAuthAPNSTokenTypeProd);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/FIRAuthAppCredentialManagerTests.m b/Example/Auth/Tests/FIRAuthAppCredentialManagerTests.m
new file mode 100644
index 0000000..32af8cd
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthAppCredentialManagerTests.m
@@ -0,0 +1,307 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthAppCredential.h"
+#import "FIRAuthAppCredentialManager.h"
+#import "FIRAuthKeychain.h"
+#import <OCMock/OCMock.h>
+
+#define ANY_ERROR_POINTER ((NSError *__autoreleasing *_Nullable)[OCMArg anyPointer])
+#define SAVE_TO(var) [OCMArg checkWithBlock:^BOOL(id arg) { var = arg; return YES; }]
+
+/** @var kReceipt
+ @brief A fake receipt used for testing.
+ */
+static NSString *const kReceipt = @"FAKE_RECEIPT";
+
+/** @var kAnotherReceipt
+ @brief Another fake receipt used for testing.
+ */
+static NSString *const kAnotherReceipt = @"OTHER_RECEIPT";
+
+/** @var kSecret
+ @brief A fake secret used for testing.
+ */
+static NSString *const kSecret = @"FAKE_SECRET";
+
+/** @var kAnotherSecret
+ @brief Another fake secret used for testing.
+ */
+static NSString *const kAnotherSecret = @"OTHER_SECRET";
+
+/** @var kVerificationTimeout
+ @brief The verification timeout used for testing.
+ */
+static const NSTimeInterval kVerificationTimeout = 1;
+
+/** @var kExpectationTimeout
+ @brief The test expectation timeout.
+ @remarks This must be considerably greater than @c kVerificationTimeout .
+ */
+static const NSTimeInterval kExpectationTimeout = 2;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthAppCredentialManagerTests
+ @brief Unit tests for @c FIRAuthAppCredentialManager .
+ */
+@interface FIRAuthAppCredentialManagerTests : XCTestCase
+@end
+@implementation FIRAuthAppCredentialManagerTests {
+ /** @var _mockKeychain
+ @brief The mock keychain for testing.
+ */
+ id _mockKeychain;
+}
+
+- (void)setUp {
+ _mockKeychain = OCMClassMock([FIRAuthKeychain class]);
+}
+
+/** @fn testCompletion
+ @brief Tests a successfully completed verification flow.
+ */
+- (void)testCompletion {
+ // Initial empty state.
+ OCMExpect([_mockKeychain dataForKey:OCMOCK_ANY error:ANY_ERROR_POINTER]).andReturn(nil);
+ FIRAuthAppCredentialManager *manager =
+ [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain];
+ XCTAssertNil(manager.credential);
+ OCMVerifyAll(_mockKeychain);
+
+ // Start verification.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ [manager didStartVerificationWithReceipt:kReceipt
+ timeout:kVerificationTimeout
+ callback:^(FIRAuthAppCredential *credential) {
+ XCTAssertEqualObjects(credential.receipt, kReceipt);
+ XCTAssertEqualObjects(credential.secret, kSecret);
+ [expectation fulfill];
+ }];
+ XCTAssertNil(manager.credential);
+ OCMVerifyAll(_mockKeychain);
+
+ // Mismatched receipt shouldn't finish verification.
+ XCTAssertFalse([manager canFinishVerificationWithReceipt:kAnotherReceipt secret:kAnotherSecret]);
+ XCTAssertNil(manager.credential);
+
+ // Finish verification.
+ OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ XCTAssertTrue([manager canFinishVerificationWithReceipt:kReceipt secret:kSecret]);
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNotNil(manager.credential);
+ XCTAssertEqualObjects(manager.credential.receipt, kReceipt);
+ XCTAssertEqualObjects(manager.credential.secret, kSecret);
+ OCMVerifyAll(_mockKeychain);
+
+ // Repeated receipt should have no effect.
+ XCTAssertFalse([manager canFinishVerificationWithReceipt:kReceipt secret:kAnotherSecret]);
+ XCTAssertEqualObjects(manager.credential.secret, kSecret);
+}
+
+/** @fn testTimeout
+ @brief Tests a verification flow that times out.
+ */
+- (void)testTimeout {
+ // Initial empty state.
+ OCMExpect([_mockKeychain dataForKey:OCMOCK_ANY error:ANY_ERROR_POINTER]).andReturn(nil);
+ FIRAuthAppCredentialManager *manager =
+ [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain];
+ XCTAssertNil(manager.credential);
+ OCMVerifyAll(_mockKeychain);
+
+ // Start verification.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ [manager didStartVerificationWithReceipt:kReceipt
+ timeout:kVerificationTimeout
+ callback:^(FIRAuthAppCredential *credential) {
+ XCTAssertEqualObjects(credential.receipt, kReceipt);
+ XCTAssertNil(credential.secret);
+ [expectation fulfill];
+ }];
+ XCTAssertNil(manager.credential);
+ OCMVerifyAll(_mockKeychain);
+
+ // Time-out.
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNil(manager.credential);
+
+ // Completion after timeout.
+ OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ XCTAssertTrue([manager canFinishVerificationWithReceipt:kReceipt secret:kSecret]);
+ XCTAssertNotNil(manager.credential);
+ XCTAssertEqualObjects(manager.credential.receipt, kReceipt);
+ XCTAssertEqualObjects(manager.credential.secret, kSecret);
+ OCMVerifyAll(_mockKeychain);
+}
+
+/** @fn testMaximumPendingReceipt
+ @brief Tests the maximum allowed number of pending receipt.
+ */
+- (void)testMaximumPendingReceipt {
+ // Initial empty state.
+ OCMExpect([_mockKeychain dataForKey:OCMOCK_ANY error:ANY_ERROR_POINTER]).andReturn(nil);
+ FIRAuthAppCredentialManager *manager =
+ [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain];
+ XCTAssertNil(manager.credential);
+ OCMVerifyAll(_mockKeychain);
+
+ // Start verification of the target receipt.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ [manager didStartVerificationWithReceipt:kReceipt
+ timeout:kVerificationTimeout
+ callback:^(FIRAuthAppCredential *credential) {
+ XCTAssertEqualObjects(credential.receipt, kReceipt);
+ XCTAssertEqualObjects(credential.secret, kSecret);
+ [expectation fulfill];
+ }];
+ XCTAssertNil(manager.credential);
+ OCMVerifyAll(_mockKeychain);
+
+ // Start verification of a number of random receipts without overflowing.
+ for (NSUInteger i = 1; i < manager.maximumNumberOfPendingReceipts; i++) {
+ OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ NSString *randomReceipt = [NSString stringWithFormat:@"RANDOM_%lu", (unsigned long)i];
+ XCTestExpectation *randomExpectation = [self expectationWithDescription:randomReceipt];
+ [manager didStartVerificationWithReceipt:randomReceipt
+ timeout:kVerificationTimeout
+ callback:^(FIRAuthAppCredential *credential) {
+ // They all should get full credential because one is available at this point.
+ XCTAssertEqualObjects(credential.receipt, kReceipt);
+ XCTAssertEqualObjects(credential.secret, kSecret);
+ [randomExpectation fulfill];
+ }];
+ }
+
+ // Finish verification of target receipt.
+ OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ XCTAssertTrue([manager canFinishVerificationWithReceipt:kReceipt secret:kSecret]);
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNotNil(manager.credential);
+ XCTAssertEqualObjects(manager.credential.receipt, kReceipt);
+ XCTAssertEqualObjects(manager.credential.secret, kSecret);
+ OCMVerifyAll(_mockKeychain);
+
+ // Clear credential to prepare for next round.
+ [manager clearCredential];
+ XCTAssertNil(manager.credential);
+
+ // Start verification of another target receipt.
+ expectation = [self expectationWithDescription:@"another callback"];
+ OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ [manager didStartVerificationWithReceipt:kAnotherReceipt
+ timeout:kVerificationTimeout
+ callback:^(FIRAuthAppCredential *credential) {
+ XCTAssertEqualObjects(credential.receipt, kAnotherReceipt);
+ XCTAssertNil(credential.secret);
+ [expectation fulfill];
+ }];
+ XCTAssertNil(manager.credential);
+ OCMVerifyAll(_mockKeychain);
+
+ // Start verification of a number of random receipts to overflow.
+ for (NSUInteger i = 0; i < manager.maximumNumberOfPendingReceipts; i++) {
+ OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ NSString *randomReceipt = [NSString stringWithFormat:@"RANDOM_%lu", (unsigned long)i];
+ XCTestExpectation *randomExpectation = [self expectationWithDescription:randomReceipt];
+ [manager didStartVerificationWithReceipt:randomReceipt
+ timeout:kVerificationTimeout
+ callback:^(FIRAuthAppCredential *credential) {
+ // They all should get partial credential because verification has never completed.
+ XCTAssertEqualObjects(credential.receipt, randomReceipt);
+ XCTAssertNil(credential.secret);
+ [randomExpectation fulfill];
+ }];
+ }
+
+ // Finish verification of the other target receipt.
+ XCTAssertFalse([manager canFinishVerificationWithReceipt:kAnotherReceipt secret:kAnotherSecret]);
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNil(manager.credential);
+}
+
+/** @fn testKeychain
+ @brief Tests state preservation in the keychain.
+ */
+- (void)testKeychain {
+ // Initial empty state.
+ OCMExpect([_mockKeychain dataForKey:OCMOCK_ANY error:ANY_ERROR_POINTER]).andReturn(nil);
+ FIRAuthAppCredentialManager *manager =
+ [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain];
+ XCTAssertNil(manager.credential);
+ OCMVerifyAll(_mockKeychain);
+
+ // Start verification.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ __block NSString *key;
+ __block NSString *data;
+ OCMExpect([_mockKeychain setData:SAVE_TO(data) forKey:SAVE_TO(key) error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ [manager didStartVerificationWithReceipt:kReceipt
+ timeout:kVerificationTimeout
+ callback:^(FIRAuthAppCredential *credential) {
+ XCTAssertEqualObjects(credential.receipt, kReceipt);
+ XCTAssertNil(credential.secret);
+ [expectation fulfill];
+ }];
+ XCTAssertNil(manager.credential);
+ OCMVerifyAll(_mockKeychain);
+
+ // Time-out.
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNil(manager.credential);
+
+ // Start a new manager with saved data in keychain.
+ OCMExpect([_mockKeychain dataForKey:key error:ANY_ERROR_POINTER]).andReturn(data);
+ manager = [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain];
+ XCTAssertNil(manager.credential);
+ OCMVerifyAll(_mockKeychain);
+
+ // Finish verification.
+ OCMExpect([_mockKeychain setData:SAVE_TO(data) forKey:SAVE_TO(key) error:ANY_ERROR_POINTER])
+ .andReturn(YES);
+ XCTAssertTrue([manager canFinishVerificationWithReceipt:kReceipt secret:kSecret]);
+ XCTAssertNotNil(manager.credential);
+ XCTAssertEqualObjects(manager.credential.receipt, kReceipt);
+ XCTAssertEqualObjects(manager.credential.secret, kSecret);
+ OCMVerifyAll(_mockKeychain);
+
+ // Start yet another new manager with saved data in keychain.
+ OCMExpect([_mockKeychain dataForKey:key error:ANY_ERROR_POINTER]).andReturn(data);
+ manager = [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain];
+ XCTAssertNotNil(manager.credential);
+ XCTAssertEqualObjects(manager.credential.receipt, kReceipt);
+ XCTAssertEqualObjects(manager.credential.secret, kSecret);
+ OCMVerifyAll(_mockKeychain);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/FIRAuthAppCredentialTests.m b/Example/Auth/Tests/FIRAuthAppCredentialTests.m
new file mode 100644
index 0000000..45dd6ef
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthAppCredentialTests.m
@@ -0,0 +1,67 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthAppCredential.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kReceipt
+ @brief The fake receipt value for testing.
+ */
+static NSString *const kReceipt = @"RECEIPT";
+
+/** @var kSecret
+ @brief The fake secret value for testing.
+ */
+static NSString *const kSecret = @"SECRET";
+
+/** @class FIRAuthAppCredentialTests
+ @brief Unit tests for @c FIRAuthAppCredential .
+ */
+@interface FIRAuthAppCredentialTests : XCTestCase
+@end
+@implementation FIRAuthAppCredentialTests
+
+/** @fn testInitializer
+ @brief Tests the initializer of the class.
+ */
+- (void)testInitializer {
+ FIRAuthAppCredential *credential = [[FIRAuthAppCredential alloc] initWithReceipt:kReceipt
+ secret:kSecret];
+ XCTAssertEqualObjects(credential.receipt, kReceipt);
+ XCTAssertEqualObjects(credential.secret, kSecret);
+}
+
+/** @fn testSecureCoding
+ @brief Tests the implementation of NSSecureCoding protocol.
+ */
+- (void)testSecureCoding {
+ XCTAssertTrue([FIRAuthAppCredential supportsSecureCoding]);
+
+ FIRAuthAppCredential *credential = [[FIRAuthAppCredential alloc] initWithReceipt:kReceipt
+ secret:kSecret];
+ NSData *data = [NSKeyedArchiver archivedDataWithRootObject:credential];
+ XCTAssertNotNil(data);
+ FIRAuthAppCredential *otherCredential = [NSKeyedUnarchiver unarchiveObjectWithData:data];
+ XCTAssertEqualObjects(otherCredential.receipt, kReceipt);
+ XCTAssertEqualObjects(otherCredential.secret, kSecret);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m b/Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m
new file mode 100644
index 0000000..9ff7473
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m
@@ -0,0 +1,450 @@
+/*
+ * 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/XCTest.h>
+
+#import <objc/runtime.h>
+
+#import "FIRAuthAppDelegateProxy.h"
+#import <OCMock/OCMock.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthEmptyAppDelegate
+ @brief A @c UIApplicationDelegate implementation that does nothing.
+ */
+@interface FIRAuthEmptyAppDelegate : NSObject <UIApplicationDelegate>
+@end
+@implementation FIRAuthEmptyAppDelegate
+@end
+
+/** @class FIRAuthLegacyAppDelegate
+ @brief A @c UIApplicationDelegate implementation that implements
+ `application:didReceiveRemoteNotification:`.
+ */
+@interface FIRAuthLegacyAppDelegate : NSObject <UIApplicationDelegate>
+
+/** @var notificationReceived
+ @brief The last notification received, if any.
+ */
+@property(nonatomic, copy, nullable) NSDictionary *notificationReceived;
+
+@end
+
+@implementation FIRAuthLegacyAppDelegate
+
+- (void)application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)userInfo {
+ self.notificationReceived = userInfo;
+}
+
+@end
+
+/** @class FIRAuthModernAppDelegate
+ @brief A @c UIApplicationDelegate implementation that implements both
+ `application:didRegisterForRemoteNotificationsWithDeviceToken:` and
+ `application:didReceiveRemoteNotification:fetchCompletionHandler:`.
+ */
+@interface FIRAuthModernAppDelegate : NSObject <UIApplicationDelegate>
+
+/** @var deviceTokenReceived
+ @brief The last device token received, if any.
+ */
+@property(nonatomic, copy, nullable) NSData *deviceTokenReceived;
+
+/** @var notificationReceived
+ @brief The last notification received, if any.
+ */
+@property(nonatomic, copy, nullable) NSDictionary *notificationReceived;
+
+@end
+
+@implementation FIRAuthModernAppDelegate
+
+- (void)application:(UIApplication *)application
+ didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
+ self.deviceTokenReceived = deviceToken;
+}
+
+- (void)application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)userInfo
+ fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
+ self.notificationReceived = userInfo;
+ completionHandler(UIBackgroundFetchResultNewData);
+}
+
+@end
+
+/** @class FIRAuthAppDelegateProxyTests
+ @brief Unit tests for @c FIRAuthAppDelegateProxy .
+ */
+@interface FIRAuthAppDelegateProxyTests : XCTestCase
+@end
+@implementation FIRAuthAppDelegateProxyTests {
+ /** @var _mockApplication
+ @brief The mock UIApplication used for testing.
+ */
+ id _mockApplication;
+
+ /** @var _deviceToken
+ @brief The fake APNs device token for testing.
+ */
+ NSData *_deviceToken;
+
+ /** @var _notification
+ @brief The fake notification for testing.
+ */
+ NSDictionary* _notification;
+}
+
+- (void)setUp {
+ [super setUp];
+ _mockApplication = OCMClassMock([UIApplication class]);
+ _deviceToken = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
+ _notification = @{ @"zxcv" : @1234 };
+}
+
+- (void)tearDown {
+ OCMVerifyAll(_mockApplication);
+ [super tearDown];
+}
+
+/** @fn testSharedInstance
+ @brief Tests that the shared instance is the same one.
+ */
+- (void)testSharedInstance {
+ FIRAuthAppDelegateProxy *proxy1 = [FIRAuthAppDelegateProxy sharedInstance];
+ FIRAuthAppDelegateProxy *proxy2 = [FIRAuthAppDelegateProxy sharedInstance];
+ XCTAssertEqual(proxy1, proxy2);
+}
+
+/** @fn testNilApplication
+ @brief Tests that initialization fails if the application is nil.
+ */
+- (void)testNilApplication {
+ XCTAssertNil([[FIRAuthAppDelegateProxy alloc] initWithApplication:nil]);
+}
+
+/** @fn testNilDelegate
+ @brief Tests that initialization fails if the application's delegate is nil.
+ */
+- (void)testNilDelegate {
+ OCMExpect([_mockApplication delegate]).andReturn(nil);
+ XCTAssertNil([[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication]);
+}
+
+/** @fn testNonconformingDelegate
+ @brief Tests that initialization fails if the application's delegate does not conform to
+ @c UIApplicationDelegate protocol.
+ */
+- (void)testNonconformingDelegate {
+ OCMExpect([_mockApplication delegate]).andReturn(@"abc");
+ XCTAssertNil([[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication]);
+}
+
+/** @fn testDisabledByBundleEntry
+ @brief Tests that initialization fails if the proxy is disabled by a bundle entry.
+ */
+- (void)testDisabledByBundleEntry {
+ // Swizzle NSBundle's objectForInfoDictionaryKey to return @NO for the specific key.
+ Method method = class_getInstanceMethod([NSBundle class], @selector(objectForInfoDictionaryKey:));
+ __block IMP originalImplementation;
+ IMP newImplmentation = imp_implementationWithBlock(^id(id object, NSString *key) {
+ if ([key isEqualToString:@"FirebaseAppDelegateProxyEnabled"]) {
+ return @NO;
+ }
+ typedef id (*Implementation)(id object, SEL cmd, NSString *key);
+ return ((Implementation)originalImplementation)(object, @selector(objectForInfoDictionaryKey:),
+ key);
+ });
+ originalImplementation = method_setImplementation(method, newImplmentation);
+
+ // Verify that initialization fails.
+ FIRAuthEmptyAppDelegate *delegate = [[FIRAuthEmptyAppDelegate alloc] init];
+ OCMStub([_mockApplication delegate]).andReturn(delegate);
+ XCTAssertNil([[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication]);
+
+ // Unswizzle.
+ imp_removeBlock(method_setImplementation(method, originalImplementation));
+}
+
+/** @fn testEmptyDelegateOneHandler
+ @brief Tests that the proxy works against an empty @c UIApplicationDelegate for one handler.
+ */
+- (void)testEmptyDelegateOneHandler {
+ FIRAuthEmptyAppDelegate *delegate = [[FIRAuthEmptyAppDelegate alloc] init];
+ OCMExpect([_mockApplication delegate]).andReturn(delegate);
+ __weak id weakProxy;
+ @autoreleasepool {
+ FIRAuthAppDelegateProxy *proxy =
+ [[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication];
+ XCTAssertNotNil(proxy);
+
+ // Verify `application:didReceiveRemoteNotification:` is not swizzled.
+ XCTAssertFalse([delegate respondsToSelector:
+ @selector(application:didReceiveRemoteNotification:)]);
+
+ // Verify the handler is called after being added.
+ __weak id weakHandler;
+ @autoreleasepool {
+ id mockHandler = OCMProtocolMock(@protocol(FIRAuthAppDelegateHandler));
+ [proxy addHandler:mockHandler];
+
+ // Verify handling of `application:didRegisterForRemoteNotificationsWithDeviceToken:`.
+ OCMExpect([mockHandler setAPNSToken:_deviceToken]);
+ [delegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ OCMVerifyAll(mockHandler);
+
+ // Verify handling of `application:didReceiveRemoteNotification:fetchCompletionHandler:`.
+ OCMExpect([mockHandler canHandleNotification:_notification]).andReturn(YES);
+ __block BOOL fetchCompletionHandlerCalled = NO;
+ [delegate application:_mockApplication
+ didReceiveRemoteNotification:_notification
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {
+ XCTAssertEqual(result, UIBackgroundFetchResultNoData);
+ fetchCompletionHandlerCalled = YES;
+ }];
+ OCMVerifyAll(mockHandler);
+ XCTAssertTrue(fetchCompletionHandlerCalled);
+
+ weakHandler = mockHandler;
+ XCTAssertNotNil(weakHandler);
+ }
+ // Verify the handler is not retained by the proxy.
+ XCTAssertNil(weakHandler);
+
+ // Verify nothing bad happens after the handler is released.
+ [delegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ [delegate application:_mockApplication
+ didReceiveRemoteNotification:_notification
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {
+ XCTFail(@"Should not call completion handler.");
+ }];
+
+ weakProxy = proxy;
+ XCTAssertNotNil(weakProxy);
+ }
+ // Verify the proxy does not retain itself.
+ XCTAssertNil(weakProxy);
+ // Verify nothing bad happens after the proxy is released.
+ [delegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ [delegate application:_mockApplication
+ didReceiveRemoteNotification:_notification
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {
+ XCTFail(@"Should not call completion handler.");
+ }];
+}
+
+/** @fn testLegacyDelegateTwoHandlers
+ @brief Tests that the proxy works against a legacy @c UIApplicationDelegate for two handlers.
+ */
+- (void)testLegacyDelegateTwoHandlers {
+ FIRAuthLegacyAppDelegate *delegate = [[FIRAuthLegacyAppDelegate alloc] init];
+ OCMExpect([_mockApplication delegate]).andReturn(delegate);
+ __weak id weakProxy;
+ @autoreleasepool {
+ FIRAuthAppDelegateProxy *proxy =
+ [[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication];
+ XCTAssertNotNil(proxy);
+
+ // Verify `application:didReceiveRemoteNotification:fetchCompletionHandler` is not swizzled.
+ XCTAssertFalse([delegate respondsToSelector:
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]);
+
+ // Verify the handler is called after being added.
+ __weak id weakHandler1;
+ @autoreleasepool {
+ id mockHandler1 = OCMProtocolMock(@protocol(FIRAuthAppDelegateHandler));
+ [proxy addHandler:mockHandler1];
+ __weak id weakHandler2;
+ @autoreleasepool {
+ id mockHandler2 = OCMProtocolMock(@protocol(FIRAuthAppDelegateHandler));
+ [proxy addHandler:mockHandler2];
+
+ // Verify handling of `application:didRegisterForRemoteNotificationsWithDeviceToken:`.
+ OCMExpect([mockHandler1 setAPNSToken:_deviceToken]);
+ OCMExpect([mockHandler2 setAPNSToken:_deviceToken]);
+ [delegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ OCMVerifyAll(mockHandler1);
+ OCMVerifyAll(mockHandler2);
+
+ // Verify handling of `application:didReceiveRemoteNotification:fetchCompletionHandler:`.
+ OCMExpect([mockHandler1 canHandleNotification:_notification]).andReturn(YES);
+ // handler2 shouldn't been invoked because it is already handled by handler1.
+ [delegate application:_mockApplication didReceiveRemoteNotification:_notification];
+ OCMVerifyAll(mockHandler1);
+ OCMVerifyAll(mockHandler2);
+ XCTAssertNil(delegate.notificationReceived);
+
+ weakHandler2 = mockHandler2;
+ XCTAssertNotNil(weakHandler2);
+ }
+ // Verify the handler2 is not retained by the proxy.
+ XCTAssertNil(weakHandler2);
+
+ // Verify handling of `application:didRegisterForRemoteNotificationsWithDeviceToken:`.
+ OCMExpect([mockHandler1 setAPNSToken:_deviceToken]);
+ [delegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ OCMVerifyAll(mockHandler1);
+
+ // Verify NOT handling of `application:didReceiveRemoteNotification:fetchCompletionHandler:`.
+ OCMExpect([mockHandler1 canHandleNotification:_notification]).andReturn(NO);
+ [delegate application:_mockApplication didReceiveRemoteNotification:_notification];
+ OCMVerifyAll(mockHandler1);
+ XCTAssertEqualObjects(delegate.notificationReceived, _notification);
+ delegate.notificationReceived = nil;
+
+ weakHandler1 = mockHandler1;
+ XCTAssertNotNil(weakHandler1);
+ }
+ // Verify the handler1 is not retained by the proxy.
+ XCTAssertNil(weakHandler1);
+
+ // Verify the delegate still works after all handlers are released.
+ [delegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ [delegate application:_mockApplication didReceiveRemoteNotification:_notification];
+ XCTAssertEqualObjects(delegate.notificationReceived, _notification);
+ delegate.notificationReceived = nil;
+
+ weakProxy = proxy;
+ XCTAssertNotNil(weakProxy);
+ }
+ // Verify the proxy does not retain itself.
+ XCTAssertNil(weakProxy);
+
+ // Verify the delegate still works after the proxy is released.
+ [delegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ [delegate application:_mockApplication didReceiveRemoteNotification:_notification];
+ XCTAssertEqualObjects(delegate.notificationReceived, _notification);
+ delegate.notificationReceived = nil;
+}
+
+/** @fn testModernDelegateWithOtherInstance
+ @brief Tests that the proxy works against a modern @c UIApplicationDelegate along with
+ another unaffected instance.
+ */
+- (void)testModernDelegateWithUnaffectedInstance {
+ FIRAuthModernAppDelegate *delegate = [[FIRAuthModernAppDelegate alloc] init];
+ OCMExpect([_mockApplication delegate]).andReturn(delegate);
+ FIRAuthModernAppDelegate *unaffectedDelegate = [[FIRAuthModernAppDelegate alloc] init];
+ __weak id weakProxy;
+ @autoreleasepool {
+ FIRAuthAppDelegateProxy *proxy =
+ [[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication];
+ XCTAssertNotNil(proxy);
+
+ // Verify `application:didReceiveRemoteNotification:` is not swizzled.
+ XCTAssertFalse([delegate respondsToSelector:
+ @selector(application:didReceiveRemoteNotification:)]);
+
+ // Verify the handler is called after being added.
+ __weak id weakHandler;
+ @autoreleasepool {
+ id mockHandler = OCMProtocolMock(@protocol(FIRAuthAppDelegateHandler));
+ [proxy addHandler:mockHandler];
+
+ // Verify handling of `application:didRegisterForRemoteNotificationsWithDeviceToken:`.
+ OCMExpect([mockHandler setAPNSToken:_deviceToken]);
+ [delegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ OCMVerifyAll(mockHandler);
+ XCTAssertEqualObjects(delegate.deviceTokenReceived, _deviceToken);
+ delegate.deviceTokenReceived = nil;
+
+ // Verify handling of `application:didReceiveRemoteNotification:fetchCompletionHandler:`.
+ OCMExpect([mockHandler canHandleNotification:_notification]).andReturn(YES);
+ __block BOOL fetchCompletionHandlerCalled = NO;
+ [delegate application:_mockApplication
+ didReceiveRemoteNotification:_notification
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {
+ XCTAssertEqual(result, UIBackgroundFetchResultNoData);
+ fetchCompletionHandlerCalled = YES;
+ }];
+ OCMVerifyAll(mockHandler);
+ XCTAssertTrue(fetchCompletionHandlerCalled);
+ XCTAssertNil(delegate.notificationReceived);
+
+ // Verify unaffected delegate instance.
+ [unaffectedDelegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ XCTAssertEqualObjects(unaffectedDelegate.deviceTokenReceived, _deviceToken);
+ unaffectedDelegate.deviceTokenReceived = nil;
+ fetchCompletionHandlerCalled = NO;
+ [unaffectedDelegate application:_mockApplication
+ didReceiveRemoteNotification:_notification
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {
+ XCTAssertEqual(result, UIBackgroundFetchResultNewData);
+ fetchCompletionHandlerCalled = YES;
+ }];
+ XCTAssertTrue(fetchCompletionHandlerCalled);
+ XCTAssertEqualObjects(unaffectedDelegate.notificationReceived, _notification);
+ unaffectedDelegate.notificationReceived = nil;
+
+ weakHandler = mockHandler;
+ XCTAssertNotNil(weakHandler);
+ }
+ // Verify the handler is not retained by the proxy.
+ XCTAssertNil(weakHandler);
+
+ // Verify the delegate still works after the handler is released.
+ [delegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ XCTAssertEqualObjects(delegate.deviceTokenReceived, _deviceToken);
+ delegate.deviceTokenReceived = nil;
+ __block BOOL fetchCompletionHandlerCalled = NO;
+ [delegate application:_mockApplication
+ didReceiveRemoteNotification:_notification
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {
+ XCTAssertEqual(result, UIBackgroundFetchResultNewData);
+ fetchCompletionHandlerCalled = YES;
+ }];
+ XCTAssertEqualObjects(delegate.notificationReceived, _notification);
+ delegate.notificationReceived = nil;
+ XCTAssertTrue(fetchCompletionHandlerCalled);
+
+ weakProxy = proxy;
+ XCTAssertNotNil(weakProxy);
+ }
+ // Verify the proxy does not retain itself.
+ XCTAssertNil(weakProxy);
+
+ // Verify the delegate still works after the proxy is released.
+ [delegate application:_mockApplication
+ didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken];
+ XCTAssertEqualObjects(delegate.deviceTokenReceived, _deviceToken);
+ delegate.deviceTokenReceived = nil;
+ __block BOOL fetchCompletionHandlerCalled = NO;
+ [delegate application:_mockApplication
+ didReceiveRemoteNotification:_notification
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {
+ XCTAssertEqual(result, UIBackgroundFetchResultNewData);
+ fetchCompletionHandlerCalled = YES;
+ }];
+ XCTAssertEqualObjects(delegate.notificationReceived, _notification);
+ delegate.notificationReceived = nil;
+ XCTAssertTrue(fetchCompletionHandlerCalled);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/FIRAuthBackendCreateAuthURITests.m b/Example/Auth/Tests/FIRAuthBackendCreateAuthURITests.m
new file mode 100644
index 0000000..5d40343
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthBackendCreateAuthURITests.m
@@ -0,0 +1,104 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrorUtils.h"
+#import "FIRAuthBackend.h"
+#import "FIRCreateAuthURIRequest.h"
+#import "FIRCreateAuthURIResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestIdentifier
+ @brief A test value for @c FIRCreateAuthURIRequest.identifier
+ */
+static NSString *const kTestIdentifier = @"identifier_value";
+
+/** @var kTestContinueURI
+ @brief A test value for @c FIRCreateAuthURIRequest.continueURI
+ */
+static NSString *const kTestContinueURI = @"https://www.example.com/";
+
+/** @var kTestAPIKey
+ @brief A test value for @c FIRCreateAuthURIRequest.APIKey
+ */
+static NSString *const kTestAPIKey = @"apikey_value";
+
+/** @var kTestExpectedRequestURL
+ @brief The URL we are expecting should be requested by valid requests.
+ */
+static NSString *const kTestExpectedRequestURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/createAuthUri?key=apikey_value";
+
+/** @var kTestExpectedKind
+ @brief The expected value for the "kind" parameter of a successful response.
+ */
+static NSString *const kTestExpectedKind = @"identitytoolkit#CreateAuthUriResponse";
+
+/** @var kTestProviderID1
+ @brief A valid value for a provider ID in the @c FIRCreateAuthURIResponse.allProviders array.
+ */
+static NSString *const kTestProviderID1 = @"google.com";
+
+/** @var kTestProviderID2
+ @brief A valid value for a provider ID in the @c FIRCreateAuthURIResponse.allProviders array.
+ */
+static NSString *const kTestProviderID2 = @"facebook.com";
+
+/** @class FIRAuthBackendCreateAuthURITests
+ @brief Unit tests for createAuthURI.
+ */
+@interface FIRAuthBackendCreateAuthURITests : XCTestCase
+@end
+@implementation FIRAuthBackendCreateAuthURITests
+
+- (void)testRequestAndResponseEncoding {
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+
+ FIRCreateAuthURIRequest *request =
+ [[FIRCreateAuthURIRequest alloc] initWithIdentifier:kTestIdentifier
+ continueURI:kTestContinueURI
+ APIKey:kTestAPIKey];
+
+ __block FIRCreateAuthURIResponse *createAuthURIResponse;
+ __block NSError *createAuthURIError;
+ __block BOOL callbackInvoked;
+ [FIRAuthBackend createAuthURI:request
+ callback:^(FIRCreateAuthURIResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ createAuthURIResponse = response;
+ createAuthURIError = error;
+ }];
+
+ XCTAssertEqualObjects(RPCIssuer.requestURL.absoluteString, kTestExpectedRequestURL);
+ XCTAssertEqualObjects(RPCIssuer.decodedRequest[@"identifier"], kTestIdentifier);
+ XCTAssertEqualObjects(RPCIssuer.decodedRequest[@"continueUri"], kTestContinueURI);
+
+ [RPCIssuer respondWithJSON:@{
+ @"kind" : kTestExpectedKind,
+ @"allProviders" : @[ kTestProviderID1, kTestProviderID2 ]
+ }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(createAuthURIError);
+ XCTAssertEqual(createAuthURIResponse.allProviders.count, 2);
+ XCTAssertEqualObjects(createAuthURIResponse.allProviders[0], kTestProviderID1);
+ XCTAssertEqualObjects(createAuthURIResponse.allProviders[1], kTestProviderID2);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRAuthBackendRPCImplementationTests.m b/Example/Auth/Tests/FIRAuthBackendRPCImplementationTests.m
new file mode 100644
index 0000000..5930e13
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthBackendRPCImplementationTests.m
@@ -0,0 +1,989 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrorUtils.h"
+#import "FIRAuthInternalErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRAuthRPCRequest.h"
+#import "FIRAuthRPCResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kFakeRequestURL
+ @brief Used as a fake URL for a fake RPC request. We don't test this here, since it's tested
+ for the specific RPC requests in their various unit tests.
+ */
+static NSString *const kFakeRequestURL = @"https://www.google.com/";
+
+/** @var kFakeErrorDomain
+ @brief A value to use for fake @c NSErrors.
+ */
+static NSString *const kFakeErrorDomain = @"fakeDomain";
+
+/** @var kFakeErrorCode
+ @brief A value to use for fake @c NSErrors.
+ */
+static const NSUInteger kFakeErrorCode = -1;
+
+/** @var kUnknownServerErrorMessage
+ @brief A value to use for fake server errors with an unknown message.
+ */
+static NSString *const kUnknownServerErrorMessage = @"UNKNOWN_MESSAGE";
+
+/** @var kErrorMessageCaptchaRequired
+ @brief The error message in JSON responses from the server for CAPTCHA required.
+ */
+static NSString *const kErrorMessageCaptchaRequired = @"CAPTCHA_REQUIRED";
+
+/** @var kErrorMessageCaptchaRequiredInvalidPassword
+ @brief The error message in JSON responses from the server for CAPTCHA required with invalid
+ password.
+ */
+static NSString *const kErrorMessageCaptchaRequiredInvalidPassword =
+ @"CAPTCHA_REQUIRED_INVALID_PASSWORD";
+
+/** @var kErrorMessageCaptchaCheckFailed
+ @brief The error message in JSON responses from the server for CAPTCHA check failed.
+ */
+static NSString *const kErrorMessageCaptchaCheckFailed = @"CAPTCHA_CHECK_FAILED";
+
+/** @var kErrorMessageEmailExists
+ @brief The error message in JSON responses from the server for user's email already exists.
+ */
+static NSString *const kErrorMessageEmailExists = @"EMAIL_EXISTS";
+
+/** @var kErrorMessageKey
+ @brief The key for the error message in an error response.
+ */
+static NSString *const kErrorMessageKey = @"message";
+
+/** @var kTestKey
+ @brief A key to use for a successful response dictionary.
+ */
+static NSString *const kTestKey = @"TestKey";
+
+/** @var kUserDisabledErrorMessage
+ @brief This is the base error message the server will respond with if the user's account has
+ been disabled.
+ */
+static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED";
+
+/** @var kFakeUserDisabledCustomErrorMessage
+ @brief This is a fake custom error message the server can respond with if the user's account has
+ been disabled.
+ */
+static NSString *const kFakeUserDisabledCustomErrorMessage = @"The user has been disabled.";
+
+/** @var kServerErrorDetailMarker
+ @brief This marker indicates that the server error message contains a detail error message which
+ should be used instead of the hardcoded client error message.
+ */
+static NSString *const kServerErrorDetailMarker = @" : ";
+
+/** @var kTestValue
+ @brief A value to use for a successful response dictionary.
+ */
+static NSString *const kTestValue = @"TestValue";
+
+/** @class FIRAuthBackendRPCImplementation
+ @brief Exposes an otherwise private class to these tests. See the real implementation for
+ documentation.
+ */
+@interface FIRAuthBackendRPCImplementation : NSObject <FIRAuthBackendImplementation>
+
+/** @fn postWithRequest:response:callback:
+ @brief Calls the RPC using HTTP POST.
+ @remarks Possible error responses:
+ @see FIRAuthInternalErrorCodeRPCRequestEncodingError
+ @see FIRAuthInternalErrorCodeJSONSerializationError
+ @see FIRAuthInternalErrorCodeNetworkError
+ @see FIRAuthInternalErrorCodeUnexpectedErrorResponse
+ @see FIRAuthInternalErrorCodeUnexpectedResponse
+ @see FIRAuthInternalErrorCodeRPCResponseDecodingError
+ @param request The request.
+ @param response The empty response to be filled.
+ @param callback The callback for both success and failure.
+ */
+- (void)postWithRequest:(id<FIRAuthRPCRequest>)request
+ response:(id<FIRAuthRPCResponse>)response
+ callback:(void (^)(NSError *error))callback;
+
+@end
+
+/** @extension FIRAuthBackend
+ @brief This class extension exposes the otherwise private @c implementation method. We use this
+ here to directly call the @c postWithRequest:response:callback: method of
+ @c FIRAuthBackendRPCImplementation in some of the tests.
+ */
+@interface FIRAuthBackend ()
+
+/** @fn implementation
+ @brief Exposes the otherwise private @c implementation method. We use this here to directly call
+ the @c postWithRequest:response:callback: method of @c FIRAuthBackendRPCImplementation in
+ some of the tests.
+ */
++ (FIRAuthBackendRPCImplementation *)implementation;
+
+@end
+
+/** @class FIRFakeRequest
+ @brief Allows us to fake a request with deterministic request bodies and encoding errors
+ returned from the @c FIRAuthRPCRequest-specified @c unencodedHTTPRequestBodyWithError:
+ method.
+ */
+@interface FIRFakeRequest : NSObject <FIRAuthRPCRequest>
+
+/** @fn fakeRequest
+ @brief A "normal" request which returns an encodable request object with no error.
+ */
++ (nullable instancetype)fakeRequest;
+
+/** @fn fakeRequestWithEncodingError
+ @brief A request which returns a fake error during the encoding process.
+ */
++ (nullable instancetype)fakeRequestWithEncodingError:(NSError *)error;
+
+/** @fn fakeRequestWithUnserializableRequestBody
+ @brief A request which returns a request object which can not be properly serialized by
+ @c NSJSONSerialization.
+ */
++ (nullable instancetype)fakeRequestWithUnserializableRequestBody;
+
+/** @fn fakeRequestWithNoBody
+ @brief A request which returns a nil request body but no error.
+ */
++ (nullable instancetype)fakeRequestWithNoBody;
+
+/** @fn init
+ @brief Please use initWithRequestBody:encodingError:
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithRequestBody:encodingError:
+ @brief Designated initializer.
+ @param requestBody The fake request body to return when @c unencodedHTTPRequestBodyWithError: is
+ invoked.
+ @param encodingError The fake error to return when @c unencodedHTTPRequestBodyWithError is
+ invoked.
+ */
+- (nullable instancetype)initWithRequestBody:(nullable id)requestBody
+ encodingError:(nullable NSError *)encodingError
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FIRFakeRequest {
+ /** @var _requestBody
+ @brief The fake request body object we will return when @c unencodedHTTPRequestBodyWithError:
+ is invoked.
+ */
+ id _Nullable _requestBody;
+
+ /** @var _requestEncodingError
+ @brief The fake error object we will return when @c unencodedHTTPRequestBodyWithError:
+ is invoked.
+ */
+ NSError *_Nullable _requestEncodingError;
+}
+
++ (nullable instancetype)fakeRequest {
+ return [[self alloc] initWithRequestBody:@{ } encodingError:nil];
+}
+
++ (nullable instancetype)fakeRequestWithEncodingError:(NSError *)error {
+ return [[self alloc] initWithRequestBody:nil encodingError:error];
+}
+
++ (nullable instancetype)fakeRequestWithUnserializableRequestBody {
+ return [[self alloc] initWithRequestBody:@{ @"unencodableValue" : self } encodingError:nil];
+}
+
++ (nullable instancetype)fakeRequestWithNoBody {
+ return [[self alloc] initWithRequestBody:nil encodingError:nil];
+}
+
+- (nullable instancetype)initWithRequestBody:(nullable id)requestBody
+ encodingError:(nullable NSError *)encodingError {
+ self = [super init];
+ if (self) {
+ _requestBody = requestBody;
+ _requestEncodingError = encodingError;
+ }
+ return self;
+}
+
+- (NSURL *)requestURL {
+ return [NSURL URLWithString:kFakeRequestURL];
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ if (error) {
+ *error = _requestEncodingError;
+ }
+ return _requestBody;
+}
+
+@end
+
+/** @class FIRFakeResponse
+ @brief Allows us to inspect the dictionaries received by @c FIRAuthRPCResponse classes, and
+ provide deterministic responses to the @c setWithDictionary:error:
+ methods.
+ */
+@interface FIRFakeResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property receivedDictionary
+ @brief The dictionary passed to the @c setWithDictionary:error: method.
+ */
+@property(nonatomic, strong, readonly, nullable) NSDictionary *receivedDictionary;
+
+/** @fn fakeResponse
+ @brief A "normal" sucessful response (no error, no expected kind.)
+ */
++ (nullable instancetype)fakeResponse;
+
+/** @fn fakeResponseWithDecodingError
+ @brief A response which returns a fake error during the decoding process.
+ */
++ (nullable instancetype)fakeResponseWithDecodingError;
+
+/** @fn init
+ @brief Please use initWithDecodingError:
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+- (nullable instancetype)initWithDecodingError:(nullable NSError *)decodingError
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FIRFakeResponse {
+ /** @var _responseDecodingError
+ @brief The value to return for an error when the @c setWithDictionary:error: method is
+ invoked.
+ */
+ NSError *_Nullable _responseDecodingError;
+}
+
++ (nullable instancetype)fakeResponse {
+ return [[self alloc] initWithDecodingError:nil];
+}
+
++ (nullable instancetype)fakeResponseWithDecodingError {
+ NSError *decodingError = [FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:self];
+ return [[self alloc] initWithDecodingError:decodingError];
+}
+
+- (nullable instancetype)initWithDecodingError:(nullable NSError *)decodingError {
+ self = [super init];
+ if (self) {
+ _responseDecodingError = decodingError;
+ }
+ return self;
+}
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ if (_responseDecodingError) {
+ if (error) {
+ *error = _responseDecodingError;
+ }
+ return NO;
+ }
+ _receivedDictionary = dictionary;
+ return YES;
+}
+
+@end
+
+/** @class FIRAuthBackendRPCImplementationTests
+ @brief This set of unit tests is designed primarily to test the possible outcomes of the
+ @c FIRAuthBackendRPCImplementation.postWithRequest:response:callback: method.
+ */
+@interface FIRAuthBackendRPCImplementationTests : XCTestCase
+@end
+@implementation FIRAuthBackendRPCImplementationTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+
+ /** @var _RPCImplementation
+ @brief This backend RPC implementation is used to make fake network requests for each test in
+ the suite.
+ */
+ FIRAuthBackendRPCImplementation *_RPCImplementation;
+}
+
+- (void)setUp {
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+ _RPCImplementation = [FIRAuthBackend implementation];
+}
+
+- (void)tearDown {
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ _RPCIssuer = nil;
+ _RPCImplementation = nil;
+}
+
+/** @fn testRequestEncodingError
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ request passed returns an error during it's unencodedHTTPRequestBodyWithError: method.
+ The error returned should be delivered to the caller without any change.
+ */
+- (void)testRequestEncodingError {
+ NSError *encodingError =
+ [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }];
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequestWithEncodingError:encodingError];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ // There is no need to call [_RPCIssuer respondWithError:...] in this test because a request
+ // should never have been tried - and we we know that's the case when we test @c callbackInvoked.
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeRPCRequestEncodingError);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingUnderlyingError);
+ XCTAssertEqualObjects(underlyingUnderlyingError.domain, kFakeErrorDomain);
+ XCTAssertEqual(underlyingUnderlyingError.code, kFakeErrorCode);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNil(deserializedResponse);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testBodyDataSerializationError
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ request returns an object which isn't serializable by @c NSJSONSerialization.
+ The error from @c NSJSONSerialization should be returned as the underlyingError for an
+ @c NSError with the code @c FIRAuthErrorCodeJSONSerializationError.
+ */
+- (void)testBodyDataSerializationError {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequestWithUnserializableRequestBody];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ // There is no need to call [_RPCIssuer respondWithError:...] in this test because a request
+ // should never have been tried - and we we know that's the case when we test @c callbackInvoked.
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeJSONSerializationError);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNil(underlyingUnderlyingError);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNil(deserializedResponse);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testNetworkError
+ @brief This test checks to make sure a network error is properly wrapped and forwarded with the
+ correct code (FIRAuthErrorCodeNetworkError).
+ */
+- (void)testNetworkError {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ // It shouldn't matter what the error domain/code/userInfo are, any junk values are suitable. The
+ // implementation should treat any error with no response data as a network error.
+ NSError *responseError = [NSError errorWithDomain:kFakeErrorDomain
+ code:kFakeErrorCode
+ userInfo:nil];
+ [_RPCIssuer respondWithError:responseError];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeNetworkError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, kFakeErrorDomain);
+ XCTAssertEqual(underlyingError.code, kFakeErrorCode);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNil(underlyingUnderlyingError);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNil(deserializedResponse);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testUnparsableErrorResponse
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ response isn't deserializable by @c NSJSONSerialization and an error
+ condition (with an associated error response message) was expected. We are expecting to
+ receive the original network error wrapped in an @c NSError with the code
+ @c FIRAuthErrorCodeUnexpectedHTTPResponse.
+ */
+- (void)testUnparsableErrorResponse {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ NSData *data =
+ [@"<html><body>An error occurred.</body></html>" dataUsingEncoding:NSUTF8StringEncoding];
+ NSError *error =
+ [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }];
+ [_RPCIssuer respondWithData:data error:error];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingUnderlyingError);
+ XCTAssertEqualObjects(underlyingUnderlyingError.domain, kFakeErrorDomain);
+ XCTAssertEqual(underlyingUnderlyingError.code, kFakeErrorCode);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNil(deserializedResponse);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNotNil(dataResponse);
+ XCTAssertEqualObjects(dataResponse, data);
+}
+
+/** @fn testUnparsableSuccessResponse
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ response isn't deserializable by @c NSJSONSerialization and no error
+ condition was indicated. We are expecting to
+ receive the @c NSJSONSerialization error wrapped in an @c NSError with the code
+ @c FIRAuthErrorCodeUnexpectedServerResponse.
+ */
+- (void)testUnparsableSuccessResponse {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ NSData *data =
+ [@"<xml>Some non-JSON value.</xml>" dataUsingEncoding:NSUTF8StringEncoding];
+ [_RPCIssuer respondWithData:data error:nil];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedResponse);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingUnderlyingError);
+ XCTAssertEqualObjects(underlyingUnderlyingError.domain, NSCocoaErrorDomain);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNil(deserializedResponse);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNotNil(dataResponse);
+ XCTAssertEqualObjects(dataResponse, data);
+}
+
+/** @fn testNonDictionaryErrorResponse
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ response deserialized by @c NSJSONSerialization is not a dictionary, and an error was
+ expected. We are expecting to receive an @c NSError with the code
+ @c FIRAuthErrorCodeUnexpectedErrorServerResponse with the decoded response in the
+ @c NSError.userInfo dictionary associated with the key
+ @c FIRAuthErrorUserInfoDecodedResponseKey.
+ */
+- (void)testNonDictionaryErrorResponse {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ // We are responding with a JSON-encoded string value representing an array - which is unexpected.
+ // It should normally be a dictionary, and we need to check for this sort of thing. Because we can
+ // successfully decode this value, however, we do return it in the error results. We check for
+ // this array later in the test.
+ NSData *data = [@"[]" dataUsingEncoding:NSUTF8StringEncoding];
+ NSError *error =
+ [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }];
+ [_RPCIssuer respondWithData:data error:error];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNil(underlyingUnderlyingError);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNotNil(deserializedResponse);
+ XCTAssert([deserializedResponse isKindOfClass:[NSArray class]]);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testNonDictionarySuccessResponse
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ response deserialized by @c NSJSONSerialization is not a dictionary, and no error was
+ expected. We are expecting to receive an @c NSError with the code
+ @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded response in the
+ @c NSError.userInfo dictionary associated with the key
+ @c FIRAuthErrorUserInfoDecodedResponseKey.
+ */
+- (void)testNonDictionarySuccessResponse {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ // We are responding with a JSON-encoded string value representing an array - which is unexpected.
+ // It should normally be a dictionary, and we need to check for this sort of thing. Because we can
+ // successfully decode this value, however, we do return it in the error results. We check for
+ // this array later in the test.
+ NSData *data = [@"[]" dataUsingEncoding:NSUTF8StringEncoding];
+ [_RPCIssuer respondWithData:data error:nil];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedResponse);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNil(underlyingUnderlyingError);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNotNil(deserializedResponse);
+ XCTAssert([deserializedResponse isKindOfClass:[NSArray class]]);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testCaptchaRequiredResponse
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ we get an error message indicating captcha is required. The backend should not be returning
+ this error to mobile clients. If it does, we should wrap it in an @c NSError with the code
+ @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded error message in the
+ @c NSError.userInfo dictionary associated with the key
+ @c FIRAuthErrorUserInfoDecodedErrorResponseKey.
+ */
+- (void)testCaptchaRequiredResponse {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ NSError *error =
+ [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }];
+ [_RPCIssuer respondWithServerErrorMessage:kErrorMessageCaptchaRequired error:error];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNil(underlyingUnderlyingError);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNotNil(deserializedResponse);
+ XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]);
+ XCTAssertNotNil(deserializedResponse[@"message"]);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testCaptchaCheckFailedResponse
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ we get an error message indicating captcha check failed. The backend should not be returning
+ this error to mobile clients. If it does, we should wrap it in an @c NSError with the code
+ @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded error message in the
+ @c NSError.userInfo dictionary associated with the key
+ @c FIRAuthErrorUserInfoDecodedErrorResponseKey.
+ */
+- (void)testCaptchaCheckFailedResponse {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ NSError *error =
+ [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }];
+ [_RPCIssuer respondWithServerErrorMessage:kErrorMessageCaptchaCheckFailed error:error];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNil(underlyingUnderlyingError);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNotNil(deserializedResponse);
+ XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]);
+ XCTAssertNotNil(deserializedResponse[@"message"]);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testCaptchaRequiredInvalidPasswordResponse
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ we get an error message indicating captcha is required and an invalid password was entered.
+ The backend should not be returning this error to mobile clients. If it does, we should wrap
+ it in an @c NSError with the code
+ @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded error message in the
+ @c NSError.userInfo dictionary associated with the key
+ @c FIRAuthErrorUserInfoDecodedErrorResponseKey.
+ */
+- (void)testCaptchaRequiredInvalidPasswordResponse {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ NSError *error =
+ [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }];
+ [_RPCIssuer respondWithServerErrorMessage:kErrorMessageCaptchaRequiredInvalidPassword
+ error:error];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNil(underlyingUnderlyingError);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNotNil(deserializedResponse);
+ XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]);
+ XCTAssertNotNil(deserializedResponse[@"message"]);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testDecodableErrorResponseWithUnknownMessage
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ response deserialized by @c NSJSONSerialization represents a valid error response (and an
+ error was indicated) but we didn't receive an error message we know about. We are expecting
+ an @c NSError with the code @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded
+ error message in the @c NSError.userInfo dictionary associated with the key
+ @c FIRAuthErrorUserInfoDecodedErrorResponseKey.
+ */
+- (void)testDecodableErrorResponseWithUnknownMessage {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ // We need to return a valid "error" response here, but we are going to intentionally use a bogus
+ // error message.
+ NSError *error =
+ [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }];
+ [_RPCIssuer respondWithServerErrorMessage:kUnknownServerErrorMessage error:error];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNil(underlyingUnderlyingError);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNotNil(deserializedResponse);
+ XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]);
+ XCTAssertNotNil(deserializedResponse[@"message"]);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testErrorResponseWithNoErrorMessage
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ response deserialized by @c NSJSONSerialization is a dictionary, and an error was indicated,
+ but no error information was present in the decoded response. We are expecting an @c NSError
+ with the code @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded
+ response message in the @c NSError.userInfo dictionary associated with the key
+ @c FIRAuthErrorUserInfoDecodedResponseKey.
+ */
+- (void)testErrorResponseWithNoErrorMessage {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ NSError *error =
+ [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }];
+ [_RPCIssuer respondWithJSON:@{ } error:error];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse);
+
+ NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNil(underlyingUnderlyingError);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNotNil(deserializedResponse);
+ XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testClientErrorResponse
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ response contains a client error specified by an error messsage sent from the backend.
+ */
+- (void)testClientErrorResponse {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackerror;
+ __block BOOL callBackInvoked;
+ [_RPCImplementation postWithRequest: request response:response callback:^(NSError *error) {
+ callBackInvoked = YES;
+ callbackerror = error;
+ }];
+ NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil];
+ NSString *customErrorMessage =[NSString stringWithFormat:@"%@%@%@",
+ kUserDisabledErrorMessage,
+ kServerErrorDetailMarker,
+ kFakeUserDisabledCustomErrorMessage];
+ [_RPCIssuer respondWithServerErrorMessage:customErrorMessage error:error];
+ XCTAssertNotNil(callbackerror, @"An error should be returned from callback.");
+ XCTAssert(callBackInvoked);
+ XCTAssertEqual(callbackerror.code, FIRAuthErrorCodeUserDisabled);
+ NSString *customMessage = callbackerror.userInfo[NSLocalizedDescriptionKey];
+ XCTAssertEqualObjects(customMessage, kFakeUserDisabledCustomErrorMessage);
+}
+
+/** @fn testUndecodableSuccessResponse
+ @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
+ response isn't decodable by the response class but no error condition was expected. We are
+ expecting to receive an @c NSError with the code
+ @c FIRAuthErrorCodeUnexpectedServerResponse and the error from @c setWithDictionary:error:
+ as the value of the underlyingError.
+ */
+- (void)testUndecodableSuccessResponse {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponseWithDecodingError];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ // It doesn't matter what we respond with here, as long as it's not an error response. The fake
+ // response will deterministicly simulate a decoding error regardless of the response value it was
+ // given.
+ [_RPCIssuer respondWithJSON:@{ }];
+
+ XCTAssert(callbackInvoked);
+
+ XCTAssertNotNil(callbackError);
+ XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError);
+
+ NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain);
+ XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeRPCResponseDecodingError);
+
+ id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey];
+ XCTAssertNotNil(deserializedResponse);
+ XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]);
+
+ id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey];
+ XCTAssertNil(dataResponse);
+}
+
+/** @fn testSuccessfulResponse
+ @brief Tests that a decoded dictionary is handed to the response instance.
+ */
+- (void)testSuccessfulResponse {
+ FIRFakeRequest *request = [FIRFakeRequest fakeRequest];
+ FIRFakeResponse *response = [FIRFakeResponse fakeResponse];
+
+ __block NSError *callbackError;
+ __block BOOL callbackInvoked;
+ [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) {
+ callbackInvoked = YES;
+ callbackError = error;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{ kTestKey : kTestValue }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(callbackError);
+ XCTAssertNotNil(response.receivedDictionary);
+ XCTAssertEqualObjects(response.receivedDictionary[kTestKey], kTestValue);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRAuthDispatcherTests.m b/Example/Auth/Tests/FIRAuthDispatcherTests.m
new file mode 100644
index 0000000..9b0abc4
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthDispatcherTests.m
@@ -0,0 +1,105 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthDispatcher.h"
+
+/** @var kMaxDifferenceBetweenTimeIntervals
+ @brief The maximum difference between time intervals (in seconds), after which they will be
+ considered different.
+ */
+static const NSTimeInterval kMaxDifferenceBetweenTimeIntervals = 0.1;
+
+/** @var kTestDelay
+ @brief Fake time delay before tasks are dispatched.
+ */
+NSTimeInterval kTestDelay = 0.1;
+
+/** @var kExpectationTimeout
+ @brief The maximum time waiting for expectations to fulfill.
+ */
+static const NSTimeInterval kExpectationTimeout = 1;
+
+id<OS_dispatch_queue> testWorkQueue;
+
+/** @class FIRAuthDispatcherTests
+ @brief Tests for @c FIRAuthDispatcher.
+ */
+@interface FIRAuthDispatcherTests : XCTestCase
+@end
+@implementation FIRAuthDispatcherTests
+
+- (void)setUp {
+ [super setUp];
+ testWorkQueue = dispatch_queue_create("test.work.queue", NULL);
+}
+
+/** @fn testSharedInstance
+ @brief Tests @c sharedInstance returns the same object.
+ */
+- (void)testSharedInstance {
+ FIRAuthDispatcher *instance1 = [FIRAuthDispatcher sharedInstance];
+ FIRAuthDispatcher *instance2 = [FIRAuthDispatcher sharedInstance];
+ XCTAssertEqual(instance1, instance2);
+}
+
+/** @fn testDispatchAfterDelay
+ @brief Tests @c dispatchAfterDelay indeed dispatches the specified task after the provided
+ delay.
+ */
+- (void)testDispatchAfterDelay {
+ FIRAuthDispatcher *dispatcher = [FIRAuthDispatcher sharedInstance];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"dispatchAfterCallback"];
+ NSDate *dateBeforeDispatch = [NSDate date];
+ dispatcher.dispatchAfterImplementation = nil;
+ [dispatcher dispatchAfterDelay:kTestDelay
+ queue:testWorkQueue
+ task:^{
+ NSTimeInterval timeSinceDispatch = fabs([dateBeforeDispatch timeIntervalSinceNow]) - kTestDelay;
+ XCTAssert(timeSinceDispatch < kMaxDifferenceBetweenTimeIntervals);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ dispatcher = nil;
+}
+
+/** @fn testSetDispatchAfterImplementation
+ @brief Tests taht @c dispatchAfterImplementation indeed configures a custom implementation for
+ @c dispatchAfterDelay.
+ */
+- (void)testSetDispatchAfterImplementation {
+ FIRAuthDispatcher *dispatcher = [FIRAuthDispatcher sharedInstance];
+ XCTestExpectation *expectation1 = [self expectationWithDescription:@"setDispatchTokenCallback"];
+ [dispatcher setDispatchAfterImplementation:^(NSTimeInterval delay,
+ id<OS_dispatch_queue> _Nonnull queue,
+ void (^task)(void)) {
+ XCTAssertEqual(kTestDelay, delay);
+ XCTAssertEqual(testWorkQueue, queue);
+ [expectation1 fulfill];
+ }];
+ [dispatcher dispatchAfterDelay:kTestDelay
+ queue:testWorkQueue
+ task:^{
+ // Fail to ensure this code is never executed.
+ XCTFail();
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ dispatcher.dispatchAfterImplementation = nil;;
+}
+
+
+@end
diff --git a/Example/Auth/Tests/FIRAuthGlobalWorkQueueTests.m b/Example/Auth/Tests/FIRAuthGlobalWorkQueueTests.m
new file mode 100644
index 0000000..a492c3d
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthGlobalWorkQueueTests.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 <XCTest/XCTest.h>
+
+#import "FIRAuthGlobalWorkQueue.h"
+
+/** @class FIRAuthGlobalWorkQueueTests
+ @brief Tests for @c FIRAuthGlobalWorkQueue .
+ */
+@interface FIRAuthGlobalWorkQueueTests : XCTestCase
+@end
+@implementation FIRAuthGlobalWorkQueueTests
+
+- (void)testSingleton {
+ dispatch_queue_t queue1 = FIRAuthGlobalWorkQueue();
+ XCTAssertNotNil(queue1);
+ dispatch_queue_t queue2 = FIRAuthGlobalWorkQueue();
+ XCTAssertEqual(queue1, queue2);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRAuthKeychainTests.m b/Example/Auth/Tests/FIRAuthKeychainTests.m
new file mode 100644
index 0000000..374e868
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthKeychainTests.m
@@ -0,0 +1,314 @@
+/*
+ * 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 <Security/Security.h>
+#import <XCTest/XCTest.h>
+
+#import "FIRAuthKeychain.h"
+
+/** @var kAccountPrefix
+ @brief The keychain account prefix assumed by the tests.
+ */
+static NSString *const kAccountPrefix = @"firebase_auth_1_";
+
+/** @var kKey
+ @brief The key used in tests.
+ */
+static NSString *const kKey = @"ACCOUNT";
+
+/** @var kService
+ @brief The keychain service used in tests.
+ */
+static NSString *const kService = @"SERVICE";
+
+/** @var kOtherService
+ @brief Another keychain service used in tests.
+ */
+static NSString *const kOtherService = @"OTHER_SERVICE";
+
+/** @var kData
+ @brief A piece of keychain data used in tests.
+ */
+static NSString *const kData = @"DATA";
+
+/** @var kOtherData
+ @brief Another piece of keychain data used in tests.
+ */
+static NSString *const kOtherData = @"OTHER_DATA";
+
+/** @fn accountFromKey
+ @brief Converts a key string to an account string.
+ @param key The key string to be converted from.
+ @return The account string being the conversion result.
+ */
+static NSString *accountFromKey(NSString *key) {
+ return [kAccountPrefix stringByAppendingString:key];
+}
+
+/** @fn dataFromString
+ @brief Converts a NSString to NSData.
+ @param string The NSString to be converted from.
+ @return The NSData being the conversion result.
+ */
+static NSData *dataFromString(NSString *string) {
+ return [string dataUsingEncoding:NSUTF8StringEncoding];
+}
+
+/** @fn stringFromData
+ @brief Converts a NSData to NSString.
+ @param data The NSData to be converted from.
+ @return The NSString being the conversion result.
+ */
+static NSString *stringFromData(NSData *data) {
+ return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+}
+
+/** @fn fakeError
+ @brief Creates a fake error object.
+ @return a non-nil NSError instance.
+ */
+static NSError *fakeError() {
+ return [NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil];
+}
+
+/** @class FIRAuthKeychainTests
+ @brief Tests for @c FIRAuthKeychainTests .
+ */
+@interface FIRAuthKeychainTests : XCTestCase
+@end
+
+@implementation FIRAuthKeychainTests
+
+/** @fn testReadNonexisting
+ @brief Tests reading non-existing keychain item.
+ */
+- (void)testReadNonexisting {
+ [self setPassword:nil account:accountFromKey(kKey) service:kService];
+ [self setPassword:nil account:kKey service:nil]; // legacy form
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ NSError *error = fakeError();
+ XCTAssertNil([keychain dataForKey:kKey error:&error]);
+ XCTAssertNil(error);
+}
+
+/** @fn testReadExisting
+ @brief Tests reading existing keychain item.
+ */
+- (void)testReadExisting {
+ [self setPassword:kData account:accountFromKey(kKey) service:kService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ NSError *error = fakeError();
+ XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
+ XCTAssertNil(error);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
+}
+
+/** @fn testNotReadOtherService
+ @brief Tests not reading keychain item belonging to other service.
+ */
+- (void)testNotReadOtherService {
+ [self setPassword:nil account:accountFromKey(kKey) service:kService];
+ [self setPassword:kData account:accountFromKey(kKey) service:kOtherService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ NSError *error = fakeError();
+ XCTAssertNil([keychain dataForKey:kKey error:&error]);
+ XCTAssertNil(error);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kOtherService];
+}
+
+/** @fn testWriteNonexisting
+ @brief Tests writing new keychain item.
+ */
+- (void)testWriteNonexisting {
+ [self setPassword:nil account:accountFromKey(kKey) service:kService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ XCTAssertTrue([keychain setData:dataFromString(kData) forKey:kKey error:NULL]);
+ XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
+}
+
+/** @fn testWriteExisting
+ @brief Tests overwriting existing keychain item.
+ */
+- (void)testWriteExisting {
+ [self setPassword:kData account:accountFromKey(kKey) service:kService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ XCTAssertTrue([keychain setData:dataFromString(kOtherData) forKey:kKey error:NULL]);
+ XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService],
+ kOtherData);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
+}
+
+/** @fn testDeleteNonexisting
+ @brief Tests deleting non-existing keychain item.
+ */
+- (void)testDeleteNonexisting {
+ [self setPassword:nil account:accountFromKey(kKey) service:kService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
+ XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
+}
+
+/** @fn testDeleteExisting
+ @brief Tests deleting existing keychain item.
+ */
+- (void)testDeleteExisting {
+ [self setPassword:kData account:accountFromKey(kKey) service:kService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
+ XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
+}
+
+/** @fn testReadLegacy
+ @brief Tests reading legacy keychain item.
+ */
+- (void)testReadLegacy {
+ [self setPassword:nil account:accountFromKey(kKey) service:kService];
+ [self setPassword:kData account:kKey service:nil]; // legacy form
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ NSError *error = fakeError();
+ XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
+ XCTAssertNil(error);
+ // Legacy item should have been moved to current form.
+ XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
+ XCTAssertNil([self passwordWithAccount:kKey service:nil]);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
+}
+
+/** @fn testNotReadLegacy
+ @brief Tests not reading legacy keychain item because current keychain item exists.
+ */
+- (void)testNotReadLegacy {
+ [self setPassword:kData account:accountFromKey(kKey) service:kService];
+ [self setPassword:kOtherData account:kKey service:nil]; // legacy form
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ NSError *error = fakeError();
+ XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
+ XCTAssertNil(error);
+ // Legacy item should have leave untouched.
+ XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
+ XCTAssertEqualObjects([self passwordWithAccount:kKey service:nil], kOtherData);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
+ [self deletePasswordWithAccount:kKey service:nil];
+}
+
+/** @fn testRemoveLegacy
+ @brief Tests removing keychain item also removes legacy keychain item.
+ */
+- (void)testRemoveLegacy {
+ [self setPassword:kData account:accountFromKey(kKey) service:kService];
+ [self setPassword:kOtherData account:kKey service:nil]; // legacy form
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
+ XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
+ XCTAssertNil([self passwordWithAccount:kKey service:nil]);
+}
+
+/** @fn testNullErrorParameter
+ @brief Tests that 'NULL' can be safely passed in.
+ */
+- (void)testNullErrorParameter {
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ [keychain dataForKey:kKey error:NULL];
+ [keychain setData:dataFromString(kData) forKey:kKey error:NULL];
+ [keychain removeDataForKey:kKey error:NULL];
+}
+
+#pragma mark - Helpers
+
+/** @fn passwordWithAccount:service:
+ @brief Reads a generic password string from the keychain.
+ @param account The account attribute of the keychain item.
+ @param service The service attribute of the keychain item, if provided.
+ @return The generic password string, if the keychain item exists.
+ */
+- (nullable NSString *)passwordWithAccount:(nonnull NSString *)account
+ service:(nullable NSString *)service {
+ NSMutableDictionary *query = [@{
+ (__bridge id)kSecReturnData : @YES,
+ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+ (__bridge id)kSecAttrAccount : account,
+ } mutableCopy];
+ if (service) {
+ query[(__bridge id)kSecAttrService] = service;
+ }
+ CFDataRef result;
+ OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
+ if (status == errSecItemNotFound) {
+ return nil;
+ }
+ XCTAssertEqual(status, errSecSuccess);
+ return stringFromData((__bridge NSData *)(result));
+}
+
+/** @fn addPassword:account:service:
+ @brief Adds a generic password string to the keychain.
+ @param password The value attribute for the password to write to the keychain item.
+ @param account The account attribute of the keychain item.
+ @param service The service attribute of the keychain item, if provided.
+ */
+- (void)addPassword:(nonnull NSString *)password
+ account:(nonnull NSString *)account
+ service:(nullable NSString *)service {
+ NSMutableDictionary *query = [@{
+ (__bridge id)kSecValueData : dataFromString(password),
+ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+ (__bridge id)kSecAttrAccount : account,
+ } mutableCopy];
+ if (service) {
+ query[(__bridge id)kSecAttrService] = service;
+ }
+ OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
+ XCTAssertEqual(status, errSecSuccess);
+}
+
+/** @fn deletePasswordWithAccount:service:
+ @brief Deletes a generic password string from the keychain.
+ @param account The account attribute of the keychain item.
+ @param service The service attribute of the keychain item, if provided.
+ */
+- (void)deletePasswordWithAccount:(nonnull NSString *)account
+ service:(nullable NSString *)service {
+ NSMutableDictionary *query = [@{
+ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+ (__bridge id)kSecAttrAccount : account,
+ } mutableCopy];
+ if (service) {
+ query[(__bridge id)kSecAttrService] = service;
+ }
+ OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
+ XCTAssertEqual(status, errSecSuccess);
+}
+
+/** @fn setPasswordWithString:account:service:
+ @brief Sets a generic password string to the keychain.
+ @param password The value attribute of the keychain item, if provided, or nil to delete the
+ existing password if any.
+ @param account The account attribute of the keychain item.
+ @param service The service attribute of the keychain item, if provided.
+ */
+- (void)setPassword:(nullable NSString *)password
+ account:(nonnull NSString *)account
+ service:(nullable NSString *)service {
+ if ([self passwordWithAccount:account service:service]) {
+ [self deletePasswordWithAccount:account service:service];
+ }
+ if (password) {
+ [self addPassword:password account:account service:service];
+ }
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRAuthNotificationManagerTests.m b/Example/Auth/Tests/FIRAuthNotificationManagerTests.m
new file mode 100644
index 0000000..c980eac
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthNotificationManagerTests.m
@@ -0,0 +1,291 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthAppCredential.h"
+#import "FIRAuthAppCredentialManager.h"
+#import "FIRAuthNotificationManager.h"
+#import <OCMock/OCMock.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kReceipt
+ @brief A fake receipt used for testing.
+ */
+static NSString *const kReceipt = @"FAKE_RECEIPT";
+
+/** @var kSecret
+ @brief A fake secret used for testing.
+ */
+static NSString *const kSecret = @"FAKE_SECRET";
+
+/** @class FIRAuthFakeForwardingDelegate
+ @brief The base class for a fake UIApplicationDelegate that forwards remote notifications.
+ */
+@interface FIRAuthFakeForwardingDelegate : NSObject<UIApplicationDelegate>
+
+/** @property notificationManager
+ @brief The notification manager to forward.
+ */
+@property(nonatomic, strong) FIRAuthNotificationManager *notificationManager;
+
+/** @property forwardsNotification
+ @brief Whether notifications are being forwarded.
+ */
+@property(nonatomic, assign) BOOL forwardsNotification;
+
+/** @property notificationReceived
+ @brief Whether a notification has been received.
+ */
+@property(nonatomic, assign) BOOL notificationReceived;
+
+/** @property notificationhandled
+ @brief Whether a notification has been handled.
+ */
+@property(nonatomic, assign) BOOL notificationhandled;
+
+@end
+@implementation FIRAuthFakeForwardingDelegate
+@end
+
+/** @class FIRAuthFakeForwardingDelegate
+ @brief A fake UIApplicationDelegate that implements the modern deegate method to receive
+ notification.
+ */
+@interface FIRAuthModernForwardingDelegate : FIRAuthFakeForwardingDelegate
+@end
+@implementation FIRAuthModernForwardingDelegate
+
+- (void)application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)userInfo
+ fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
+ self.notificationReceived = YES;
+ if (self.forwardsNotification) {
+ self.notificationhandled = [self.notificationManager canHandleNotification:userInfo];
+ }
+}
+
+@end
+
+/** @class FIRAuthLegacyForwardingDelegate
+ @brief A fake UIApplicationDelegate that implements the legacy deegate method to receive
+ notification.
+ */
+@interface FIRAuthLegacyForwardingDelegate : FIRAuthFakeForwardingDelegate
+@end
+@implementation FIRAuthLegacyForwardingDelegate
+
+- (void)application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)userInfo {
+ self.notificationReceived = YES;
+ if (self.forwardsNotification) {
+ self.notificationhandled = [self.notificationManager canHandleNotification:userInfo];
+ }
+}
+
+@end
+
+/** @class FIRAuthNotificationManagerTests
+ @brief Unit tests for @c FIRAuthNotificationManager .
+ */
+@interface FIRAuthNotificationManagerTests : XCTestCase
+@end
+@implementation FIRAuthNotificationManagerTests {
+ /** @var _mockApplication
+ @brief The mock UIApplication for testing.
+ */
+ id _mockApplication;
+
+ /** @var _mockAppCredentialManager
+ @brief The mock FIRAuthAppCredentialManager for testing.
+ */
+ id _mockAppCredentialManager;
+
+ /** @var _notificationManager
+ @brief The FIRAuthNotificationManager to be tested.
+ */
+ FIRAuthNotificationManager *_notificationManager;
+
+ /** @var _modernDelegate
+ @brief The modern fake UIApplicationDelegate for testing.
+ */
+ FIRAuthModernForwardingDelegate *_modernDelegate;
+
+ /** @var _legacyDelegate
+ @brief The legacy fake UIApplicationDelegate for testing.
+ */
+ FIRAuthLegacyForwardingDelegate *_legacyDelegate;
+}
+
+- (void)setUp {
+ _mockApplication = OCMClassMock([UIApplication class]);
+ _mockAppCredentialManager = OCMClassMock([FIRAuthAppCredentialManager class]);
+ _notificationManager =
+ [[FIRAuthNotificationManager alloc] initWithApplication:_mockApplication
+ appCredentialManager:_mockAppCredentialManager];
+ _modernDelegate = [[FIRAuthModernForwardingDelegate alloc] init];
+ _modernDelegate.notificationManager = _notificationManager;
+ _legacyDelegate = [[FIRAuthLegacyForwardingDelegate alloc] init];
+ _legacyDelegate.notificationManager = _notificationManager;
+}
+
+/** @fn testForwardingModernDelegate
+ @brief Tests checking notification forwarding on modern fake delegate.
+ */
+- (void)testForwardingModernDelegate {
+ [self verifyForwarding:YES delegate:_modernDelegate];
+}
+
+/** @fn testForwardingLegacyDelegate
+ @brief Tests checking notification forwarding on legacy fake delegate.
+ */
+- (void)testForwardingLegacyDelegate {
+ [self verifyForwarding:YES delegate:_legacyDelegate];
+}
+
+/** @fn testNotForwardingModernDelegate
+ @brief Tests checking notification not forwarding on modern fake delegate.
+ */
+- (void)testNotForwardingModernDelegate {
+ [self verifyForwarding:NO delegate:_modernDelegate];
+}
+
+/** @fn testNotForwardingLegacyDelegate
+ @brief Tests checking notification not forwarding on legacy fake delegate.
+ */
+- (void)testNotForwardingLegacyDelegate {
+ [self verifyForwarding:NO delegate:_legacyDelegate];
+}
+
+/** @fn verifyForwarding:delegate:
+ @brief Tests checking notification forwarding on a particular delegate.
+ @param forwarding Whether the notification is being forwarded or not.
+ @param delegate The fake UIApplicationDelegate used for testing.
+ */
+- (void)verifyForwarding:(BOOL)forwarding
+ delegate:(FIRAuthFakeForwardingDelegate *)delegate {
+ delegate.forwardsNotification = forwarding;
+ OCMStub([_mockApplication delegate]).andReturn(delegate);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [_notificationManager
+ checkNotificationForwardingWithCallback:^(BOOL isNotificationBeingForwarded) {
+ XCTAssertEqual(isNotificationBeingForwarded, forwarding);
+ [expectation fulfill];
+ }];
+ XCTAssertFalse(delegate.notificationReceived);
+ NSTimeInterval timeout = _notificationManager.timeout * (1.5 - forwarding);
+ [self waitForExpectationsWithTimeout:timeout handler:nil];
+ XCTAssertTrue(delegate.notificationReceived);
+ XCTAssertEqual(delegate.notificationhandled, forwarding);
+}
+
+/** @fn testCachedResult
+ @brief Test notification forwarding is only checked once.
+ */
+- (void)testCachedResult {
+ FIRAuthFakeForwardingDelegate *delegate = _modernDelegate;
+ [self verifyForwarding:NO delegate:delegate];
+ delegate.notificationReceived = NO;
+ __block BOOL calledBack = NO;
+ [_notificationManager
+ checkNotificationForwardingWithCallback:^(BOOL isNotificationBeingForwarded) {
+ XCTAssertFalse(isNotificationBeingForwarded);
+ calledBack = YES;
+ }];
+ XCTAssertTrue(calledBack);
+ XCTAssertFalse(delegate.notificationReceived);
+}
+
+/** @fn testMultipleCallbacks
+ @brief Test multiple callbacks are handled correctly.
+ */
+- (void)testMultipleCallbacks {
+ FIRAuthFakeForwardingDelegate *delegate = _legacyDelegate;
+ delegate.forwardsNotification = YES;
+ OCMStub([_mockApplication delegate]).andReturn(delegate);
+ XCTestExpectation *expectation1 = [self expectationWithDescription:@"callback1"];
+ [_notificationManager
+ checkNotificationForwardingWithCallback:^(BOOL isNotificationBeingForwarded) {
+ XCTAssertTrue(isNotificationBeingForwarded);
+ [expectation1 fulfill];
+ }];
+ XCTestExpectation *expectation2 = [self expectationWithDescription:@"callback2"];
+ [_notificationManager
+ checkNotificationForwardingWithCallback:^(BOOL isNotificationBeingForwarded) {
+ XCTAssertTrue(isNotificationBeingForwarded);
+ [expectation2 fulfill];
+ }];
+ XCTAssertFalse(delegate.notificationReceived);
+ [self waitForExpectationsWithTimeout:_notificationManager.timeout * .5 handler:nil];
+ XCTAssertTrue(delegate.notificationReceived);
+ XCTAssertTrue(delegate.notificationhandled);
+}
+
+/** @fn testPassingToCredentialManager
+ @brief Test notification with the right structure is passed to credential manager.
+ */
+- (void)testPassingToCredentialManager {
+ NSDictionary *payload = @{ @"receipt" : kReceipt, @"secret" : kSecret };
+ NSDictionary *notification = @{ @"com.google.firebase.auth" : payload };
+ OCMExpect([_mockAppCredentialManager canFinishVerificationWithReceipt:kReceipt secret:kSecret])
+ .andReturn(YES);
+ XCTAssertTrue([_notificationManager canHandleNotification:notification]);
+ OCMVerifyAll(_mockAppCredentialManager);
+
+ // JSON string form
+ NSData *data = [NSJSONSerialization dataWithJSONObject:payload options:0 error:NULL];
+ NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ notification = @{ @"com.google.firebase.auth" : string };
+ OCMExpect([_mockAppCredentialManager canFinishVerificationWithReceipt:kReceipt secret:kSecret])
+ .andReturn(YES);
+ XCTAssertTrue([_notificationManager canHandleNotification:notification]);
+ OCMVerifyAll(_mockAppCredentialManager);
+}
+
+/** @fn testNotHandling
+ @brief Test unrecognized notifications are not handled.
+ */
+- (void)testNotHandling {
+ XCTAssertFalse([_notificationManager canHandleNotification:@{
+ @"random" : @"string"
+ }]);
+ XCTAssertFalse([_notificationManager canHandleNotification:@{
+ @"com.google.firebase.auth" : @"something wrong"
+ }]);
+ XCTAssertFalse([_notificationManager canHandleNotification:@{
+ @"com.google.firebase.auth" : @{
+ @"receipt" : kReceipt
+ // missing secret
+ }
+ }]);
+ XCTAssertFalse([_notificationManager canHandleNotification:@{
+ @"com.google.firebase.auth" : @{
+ // missing receipt
+ @"secret" : kSecret
+ }
+ }]);
+ XCTAssertFalse([_notificationManager canHandleNotification:@{
+ @"com.google.firebase.auth" : @{
+ // probing notification does not belong to this instance
+ @"warning" : @"asdf"
+ }
+ }]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/FIRAuthSerialTaskQueueTests.m b/Example/Auth/Tests/FIRAuthSerialTaskQueueTests.m
new file mode 100644
index 0000000..26164d6
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthSerialTaskQueueTests.m
@@ -0,0 +1,113 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthGlobalWorkQueue.h"
+#import "FIRAuthSerialTaskQueue.h"
+
+/** @var kTimeout
+ @brief Time-out in seconds waiting for tasks to be executed.
+ */
+static const NSTimeInterval kTimeout = 1;
+
+/** @class FIRAuthSerialTaskQueueTests
+ @brief Tests for @c FIRAuthSerialTaskQueue .
+ */
+@interface FIRAuthSerialTaskQueueTests : XCTestCase
+@end
+@implementation FIRAuthSerialTaskQueueTests
+
+- (void)testExecution {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"executed"];
+ FIRAuthSerialTaskQueue *queue = [[FIRAuthSerialTaskQueue alloc] init];
+ [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) {
+ completionArg();
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kTimeout handler:nil];
+}
+
+- (void)testCompletion {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"executed"];
+ FIRAuthSerialTaskQueue *queue = [[FIRAuthSerialTaskQueue alloc] init];
+ __block FIRAuthSerialTaskCompletionBlock completion = nil;
+ [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) {
+ completion = completionArg;
+ [expectation fulfill];
+ }];
+ __block XCTestExpectation *nextExpectation = nil;
+ __block BOOL executed = NO;
+ [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) {
+ executed = YES;
+ completionArg();
+ [nextExpectation fulfill];
+ }];
+ // The second task should not be executed until the first is completed.
+ [self waitForExpectationsWithTimeout:kTimeout handler:nil];
+ XCTAssertNotNil(completion);
+ XCTAssertFalse(executed);
+ nextExpectation = [self expectationWithDescription:@"executed next"];
+ completion();
+ [self waitForExpectationsWithTimeout:kTimeout handler:nil];
+ XCTAssertTrue(executed);
+}
+
+- (void)testTargetQueue {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"executed"];
+ FIRAuthSerialTaskQueue *queue = [[FIRAuthSerialTaskQueue alloc] init];
+ __block BOOL executed = NO;
+ dispatch_suspend(FIRAuthGlobalWorkQueue());
+ [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) {
+ executed = YES;
+ completionArg();
+ [expectation fulfill];
+ }];
+ // The task should not executed until the global work queue is resumed.
+ usleep(kTimeout * USEC_PER_SEC);
+ XCTAssertFalse(executed);
+ dispatch_resume(FIRAuthGlobalWorkQueue());
+ [self waitForExpectationsWithTimeout:kTimeout handler:nil];
+}
+
+- (void)testTaskQueueNoAffectTargetQueue {
+ FIRAuthSerialTaskQueue *queue = [[FIRAuthSerialTaskQueue alloc] init];
+ __block FIRAuthSerialTaskCompletionBlock completion = nil;
+ [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) {
+ completion = completionArg;
+ }];
+ __block XCTestExpectation *nextExpectation = nil;
+ __block BOOL executed = NO;
+ [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) {
+ executed = YES;
+ completionArg();
+ [nextExpectation fulfill];
+ }];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"executed"];
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [expectation fulfill];
+ });
+ // The task queue waiting for completion should not affect the global work queue.
+ [self waitForExpectationsWithTimeout:kTimeout handler:nil];
+ XCTAssertNotNil(completion);
+ XCTAssertFalse(executed);
+ nextExpectation = [self expectationWithDescription:@"executed next"];
+ completion();
+ [self waitForExpectationsWithTimeout:kTimeout handler:nil];
+ XCTAssertTrue(executed);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRAuthTests.m b/Example/Auth/Tests/FIRAuthTests.m
new file mode 100644
index 0000000..3a6f717
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthTests.m
@@ -0,0 +1,1743 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAppInternal.h"
+#import "EmailPassword/FIREmailAuthProvider.h"
+#import "Google/FIRGoogleAuthProvider.h"
+#import "Phone/FIRPhoneAuthCredential.h"
+#import "Phone/FIRPhoneAuthProvider.h"
+#import "FIRAdditionalUserInfo.h"
+#import "FIRAuth_Internal.h"
+#import "FIRAuthErrorUtils.h"
+#import "FIRAuthDispatcher.h"
+#import "FIRAuthGlobalWorkQueue.h"
+#import "FIRUser_Internal.h"
+#import "FIRAuthBackend.h"
+#import "FIRCreateAuthURIRequest.h"
+#import "FIRCreateAuthURIResponse.h"
+#import "FIRGetAccountInfoRequest.h"
+#import "FIRGetAccountInfoResponse.h"
+#import "FIRGetOOBConfirmationCodeRequest.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRSecureTokenRequest.h"
+#import "FIRSecureTokenResponse.h"
+#import "FIRResetPasswordRequest.h"
+#import "FIRResetPasswordResponse.h"
+#import "FIRSetAccountInfoRequest.h"
+#import "FIRSetAccountInfoResponse.h"
+#import "FIRSignUpNewUserRequest.h"
+#import "FIRSignUpNewUserResponse.h"
+#import "FIRVerifyCustomTokenRequest.h"
+#import "FIRVerifyCustomTokenResponse.h"
+#import "FIRVerifyAssertionRequest.h"
+#import "FIRVerifyAssertionResponse.h"
+#import "FIRVerifyPasswordRequest.h"
+#import "FIRVerifyPasswordResponse.h"
+#import "FIRVerifyPhoneNumberRequest.h"
+#import "FIRVerifyPhoneNumberResponse.h"
+#import "FIRApp+FIRAuthUnitTests.h"
+#import "OCMStubRecorder+FIRAuthUnitTests.h"
+#import <OCMock/OCMock.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 kAccessToken
+ @brief The fake access token.
+ */
+static NSString *const kAccessToken = @"ACCESS_TOKEN";
+
+/** @var kNewAccessToken
+ @brief Another fake access token used to simulate token refreshed via automatic token refresh.
+ */
+NSString *kNewAccessToken = @"NewAccessToken";
+
+/** @var kAccessTokenValidInterval
+ @brief The time to live for the fake access token.
+ */
+static const NSTimeInterval kAccessTokenTimeToLive = 60 * 60;
+
+/** @var kTestTokenExpirationTimeInterval
+ @brief The fake time interval that it takes a token to expire.
+ */
+static const NSTimeInterval kTestTokenExpirationTimeInterval = 55 * 60;
+
+/** @var kRefreshToken
+ @brief The fake refresh token.
+ */
+static NSString *const kRefreshToken = @"REFRESH_TOKEN";
+
+/** @var kEmail
+ @brief The fake user email.
+ */
+static NSString *const kEmail = @"user@company.com";
+
+/** @var kPassword
+ @brief The fake user password.
+ */
+static NSString *const kPassword = @"!@#$%^";
+
+/** @var kPasswordHash
+ @brief The fake user password hash.
+ */
+static NSString *const kPasswordHash = @"UkVEQUNURUQ=";
+
+/** @var kLocalID
+ @brief The fake local user ID.
+ */
+static NSString *const kLocalID = @"LOCAL_ID";
+
+/** @var kDisplayName
+ @brief The fake user display name.
+ */
+static NSString *const kDisplayName = @"User Doe";
+
+/** @var kGoogleUD
+ @brief The fake user ID under Google Sign-In.
+ */
+static NSString *const kGoogleID = @"GOOGLE_ID";
+
+/** @var kGoogleEmail
+ @brief The fake user email under Google Sign-In.
+ */
+static NSString *const kGoogleEmail = @"user@gmail.com";
+
+/** @var kGoogleDisplayName
+ @brief The fake user display name under Google Sign-In.
+ */
+static NSString *const kGoogleDisplayName = @"Google Doe";
+
+/** @var kGoogleAccessToken
+ @brief The fake access token from Google Sign-In.
+ */
+static NSString *const kGoogleAccessToken = @"GOOGLE_ACCESS_TOKEN";
+
+/** @var kGoogleIDToken
+ @brief The fake ID token from Google Sign-In.
+ */
+static NSString *const kGoogleIDToken = @"GOOGLE_ID_TOKEN";
+
+/** @var kCustomToken
+ @brief The fake custom token to sign in.
+ */
+static NSString *const kCustomToken = @"CUSTOM_TOKEN";
+
+/** @var kVerificationCode
+ @brief Fake verification code used for testing.
+ */
+static NSString *const kVerificationCode = @"12345678";
+
+/** @var kVerificationID
+ @brief Fake verification ID for testing.
+ */
+static NSString *const kVerificationID = @"55432";
+
+/** @var kExpectationTimeout
+ @brief The maximum time waiting for expectations to fulfill.
+ */
+static const NSTimeInterval kExpectationTimeout = 1;
+
+/** @var kWaitInterval
+ @brief The time waiting for background tasks to finish before continue when necessary.
+ */
+static const NSTimeInterval kWaitInterval = .5;
+
+/** @class FIRAuthTests
+ @brief Tests for @c FIRAuth.
+ */
+@interface FIRAuthTests : XCTestCase
+@end
+@implementation FIRAuthTests {
+
+ /** @var _mockBackend
+ @brief The mock @c FIRAuthBackendImplementation .
+ */
+ id _mockBackend;
+
+ /** @var _FIRAuthDispatcherCallback
+ @brief Used to save a task from FIRAuthDispatcher to be executed later.
+ */
+ __block void (^_Nonnull _FIRAuthDispatcherCallback)(void);
+}
+
+/** @fn googleProfile
+ @brief The fake user profile under additional user data in @c FIRVerifyAssertionResponse.
+ */
++ (NSDictionary *)googleProfile {
+ static NSDictionary *kGoogleProfile = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ kGoogleProfile = @{
+ @"iss": @"https://accounts.google.com\\",
+ @"email": kGoogleEmail,
+ @"given_name": @"User",
+ @"family_name": @"Doe"
+ };
+ });
+ return kGoogleProfile;
+}
+
+- (void)setUp {
+ [super setUp];
+ _mockBackend = OCMProtocolMock(@protocol(FIRAuthBackendImplementation));
+ [FIRAuthBackend setBackendImplementation:_mockBackend];
+ [FIRApp resetAppForAuthUnitTests];
+
+ // Set FIRAuthDispatcher implementation in order to save the token refresh task for later
+ // execution.
+ [[FIRAuthDispatcher sharedInstance]
+ setDispatchAfterImplementation:^(NSTimeInterval delay,
+ dispatch_queue_t _Nonnull queue,
+ void (^task)(void)) {
+ XCTAssertNotNil(task);
+ XCTAssert(delay > 0);
+ XCTAssertEqualObjects(FIRAuthGlobalWorkQueue(), queue);
+ _FIRAuthDispatcherCallback = task;
+ }];
+}
+
+- (void)tearDown {
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [[FIRAuthDispatcher sharedInstance] setDispatchAfterImplementation:nil];
+ [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.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 testGetUID
+ @brief Verifies that FIRApp's getUIDImplementation is correctly set by FIRAuth.
+ */
+- (void)testGetUID {
+ FIRApp *app = [FIRApp defaultApp];
+ XCTAssertNotNil(app.getUIDImplementation);
+ [[FIRAuth auth] signOut:NULL];
+ XCTAssertNil(app.getUIDImplementation());
+ [self waitForSignIn];
+ XCTAssertEqualObjects(app.getUIDImplementation(), kLocalID);
+}
+
+#pragma mark - Server API Tests
+
+/** @fn testFetchProvidersForEmailSuccess
+ @brief Tests the flow of a successful @c fetchProvidersForEmail:completion: call.
+ */
+- (void)testFetchProvidersForEmailSuccess {
+ NSArray<NSString *> *allProviders =
+ @[ FIRGoogleAuthProviderID, FIREmailAuthProviderID ];
+ OCMExpect([_mockBackend createAuthURI:[OCMArg any]
+ callback:[OCMArg any]])
+ .andCallBlock2(^(FIRCreateAuthURIRequest *_Nullable request,
+ FIRCreateAuthURIResponseCallback callback) {
+ XCTAssertEqualObjects(request.identifier, kEmail);
+ XCTAssertNotNil(request.endpoint);
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockCreateAuthURIResponse = OCMClassMock([FIRCreateAuthURIResponse class]);
+ OCMStub([mockCreateAuthURIResponse allProviders]).andReturn(allProviders);
+ callback(mockCreateAuthURIResponse, nil);
+ });
+ });
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] fetchProvidersForEmail:kEmail
+ completion:^(NSArray<NSString *> *_Nullable providers,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertEqualObjects(providers, allProviders);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testFetchProvidersForEmailSuccessDeprecatedProviderID
+ @brief Tests the flow of a successful @c fetchProvidersForEmail:completion: call using the
+ deprecated FIREmailPasswordAuthProviderID.
+ */
+- (void)testFetchProvidersForEmailSuccessDeprecatedProviderID {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ NSArray<NSString *> *allProviders =
+ @[ FIRGoogleAuthProviderID, FIREmailPasswordAuthProviderID ];
+#pragma clang diagnostic pop
+ OCMExpect([_mockBackend createAuthURI:[OCMArg any]
+ callback:[OCMArg any]])
+ .andCallBlock2(^(FIRCreateAuthURIRequest *_Nullable request,
+ FIRCreateAuthURIResponseCallback callback) {
+ XCTAssertEqualObjects(request.identifier, kEmail);
+ XCTAssertNotNil(request.endpoint);
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockCreateAuthURIResponse = OCMClassMock([FIRCreateAuthURIResponse class]);
+ OCMStub([mockCreateAuthURIResponse allProviders]).andReturn(allProviders);
+ callback(mockCreateAuthURIResponse, nil);
+ });
+ });
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] fetchProvidersForEmail:kEmail
+ completion:^(NSArray<NSString *> *_Nullable providers,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertEqualObjects(providers, allProviders);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testFetchProvidersForEmailFailure
+ @brief Tests the flow of a failed @c fetchProvidersForEmail:completion: call.
+ */
+- (void)testFetchProvidersForEmailFailure {
+ OCMExpect([_mockBackend createAuthURI:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils tooManyRequestsErrorWithMessage:nil]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] fetchProvidersForEmail:kEmail
+ completion:^(NSArray<NSString *> *_Nullable providers,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(providers);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeTooManyRequests);
+ XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testPhoneAuthSuccess
+ @brief Tests the flow of a successful @c signInWithCredential:completion for phone auth.
+ */
+- (void)testPhoneAuthSuccess {
+ OCMExpect([_mockBackend verifyPhoneNumber:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyPhoneNumberRequest *_Nullable request,
+ FIRVerifyPhoneNumberResponseCallback callback) {
+ XCTAssertEqualObjects(request.verificationCode, kVerificationCode);
+ XCTAssertEqualObjects(request.verificationID, kVerificationID);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVerifyPhoneResponse = OCMClassMock([FIRVerifyPhoneNumberResponse class]);
+ [self stubTokensWithMockResponse:mockVerifyPhoneResponse];
+ callback(mockVerifyPhoneResponse, nil);
+ });
+ });
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode];
+
+ [[FIRAuth auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUser:user];
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUser:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testPhoneAuthMissingVerificationCode
+ @brief Tests the flow of an unsuccessful @c signInWithCredential:completion for phone auth due
+ to an empty verification code
+ */
+- (void)testPhoneAuthMissingVerificationCode {
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID
+ verificationCode:@""];
+
+ [[FIRAuth auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(user);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeMissingVerificationCode);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+}
+
+/** @fn testPhoneAuthMissingVerificationID
+ @brief Tests the flow of an unsuccessful @c signInWithCredential:completion for phone auth due
+ to an empty verification ID.
+ */
+- (void)testPhoneAuthMissingVerificationID {
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:@""
+ verificationCode:kVerificationCode];
+
+ [[FIRAuth auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(user);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeMissingVerificationID);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+}
+
+/** @fn testSignInWithEmailPasswordSuccess
+ @brief Tests the flow of a successful @c signInWithEmail:password:completion: call.
+ */
+- (void)testSignInWithEmailPasswordSuccess {
+ OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request,
+ FIRVerifyPasswordResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.email, kEmail);
+ XCTAssertEqualObjects(request.password, kPassword);
+ XCTAssertTrue(request.returnSecureToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVerifyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]);
+ [self stubTokensWithMockResponse:mockVerifyPasswordResponse];
+ callback(mockVerifyPasswordResponse, nil);
+ });
+ });
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] signInWithEmail:kEmail password:kPassword completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUser:user];
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUser:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInWithEmailPasswordFailure
+ @brief Tests the flow of a failed @c signInWithEmail:password:completion: call.
+ */
+- (void)testSignInWithEmailPasswordFailure {
+ OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils wrongPasswordErrorWithMessage:nil]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] signInWithEmail:kEmail password:kPassword completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(user);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeWrongPassword);
+ XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNil([FIRAuth auth].currentUser);
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testResetPasswordSuccess
+ @brief Tests the flow of a successful @c confirmPasswordResetWithCode:newPassword:completion:
+ call.
+ */
+- (void)testResetPasswordSuccess {
+ NSString *fakeEmail = @"fakeEmail";
+ NSString *fakeCode = @"fakeCode";
+ NSString *fakeNewPassword = @"fakeNewPassword";
+ OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRResetPasswordRequest *_Nullable request,
+ FIRResetPasswordCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.oobCode, fakeCode);
+ XCTAssertEqualObjects(request.updatedPassword, fakeNewPassword);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockResetPasswordResponse = OCMClassMock([FIRResetPasswordResponse class]);
+ OCMStub([mockResetPasswordResponse email]).andReturn(fakeEmail);
+ callback(mockResetPasswordResponse, nil);
+ });
+ });
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] confirmPasswordResetWithCode:fakeCode
+ newPassword:fakeNewPassword
+ completion:^(NSError *_Nullable error) {
+
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testResetPasswordFailure
+ @brief Tests the flow of a failed @c confirmPasswordResetWithCode:newPassword:completion:
+ call.
+ */
+- (void)testResetPasswordFailure {
+ NSString *fakeCode = @"fakeCode";
+ NSString *fakeNewPassword = @"fakeNewPassword";
+ OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]])
+ ._andDispatchError2([FIRAuthErrorUtils invalidActionCodeErrorWithMessage:nil]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] confirmPasswordResetWithCode:fakeCode
+ newPassword:fakeNewPassword
+ completion:^(NSError *_Nullable error) {
+
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidActionCode);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testCheckActionCodeSuccess
+ @brief Tests the flow of a successful @c checkActionCode:completion call.
+ */
+- (void)testCheckActionCodeSuccess {
+ NSString *verifyEmailRequestType = @"VERIFY_EMAIL";
+ NSString *fakeEmail = @"fakeEmail";
+ NSString *fakeNewEmail = @"fakeNewEmail";
+ NSString *fakeCode = @"fakeCode";
+ OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRResetPasswordRequest *_Nullable request,
+ FIRResetPasswordCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.oobCode, fakeCode);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockResetPasswordResponse = OCMClassMock([FIRResetPasswordResponse class]);
+ OCMStub([mockResetPasswordResponse email]).andReturn(fakeEmail);
+ OCMStub([mockResetPasswordResponse verifiedEmail]).andReturn(fakeNewEmail);
+ OCMStubRecorder *stub =
+ OCMStub([(FIRResetPasswordResponse *) mockResetPasswordResponse requestType]);
+ stub.andReturn(verifyEmailRequestType);
+ callback(mockResetPasswordResponse, nil);
+ });
+ });
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] checkActionCode:fakeCode completion:^(FIRActionCodeInfo *_Nullable info,
+ NSError *_Nullable error) {
+
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(error);
+ XCTAssertEqual(info.operation, FIRActionCodeOperationVerifyEmail);
+ XCTAssert([fakeNewEmail isEqualToString:[info dataForKey:FIRActionCodeEmailKey]]);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testCheckActionCodeFailure
+ @brief Tests the flow of a failed @c checkActionCode:completion call.
+ */
+- (void)testCheckActionCodeFailure {
+ NSString *fakeCode = @"fakeCode";
+ OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]])
+ ._andDispatchError2([FIRAuthErrorUtils expiredActionCodeErrorWithMessage:nil]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] checkActionCode:fakeCode completion:^(FIRActionCodeInfo *_Nullable info,
+ NSError *_Nullable error) {
+
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNotNil(error);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeExpiredActionCode);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testApplyActionCodeSuccess
+ @brief Tests the flow of a successful @c applyActionCode:completion call.
+ */
+- (void)testApplyActionCodeSuccess {
+ NSString *fakeCode = @"fakeCode";
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request,
+ FIRSetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.OOBCode, fakeCode);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]);
+ callback(mockSetAccountInfoResponse, nil);
+ });
+ });
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] applyActionCode:fakeCode completion:^(NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testApplyActionCodeFailure
+ @brief Tests the flow of a failed @c checkActionCode:completion call.
+ */
+- (void)testApplyActionCodeFailure {
+ NSString *fakeCode = @"fakeCode";
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ ._andDispatchError2([FIRAuthErrorUtils invalidActionCodeErrorWithMessage:nil]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] applyActionCode:fakeCode completion:^(NSError *_Nullable error) {
+
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNotNil(error);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidActionCode);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testVerifyPasswordResetCodeSuccess
+ @brief Tests the flow of a successful @c verifyPasswordResetCode:completion call.
+ */
+- (void)testVerifyPasswordResetCodeSuccess {
+ NSString *passwordResetRequestType = @"PASSWORD_RESET";
+ NSString *fakeEmail = @"fakeEmail";
+ NSString *fakeCode = @"fakeCode";
+ OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRResetPasswordRequest *_Nullable request,
+ FIRResetPasswordCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.oobCode, fakeCode);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockResetPasswordResponse = OCMClassMock([FIRResetPasswordResponse class]);
+ OCMStub([mockResetPasswordResponse email]).andReturn(fakeEmail);
+ OCMStubRecorder *stub =
+ OCMStub([(FIRResetPasswordResponse *) mockResetPasswordResponse requestType]);
+ stub.andReturn(passwordResetRequestType);
+ callback(mockResetPasswordResponse, nil);
+ });
+ });
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] verifyPasswordResetCode:fakeCode completion:^(NSString *_Nullable email,
+ NSError *_Nullable error) {
+
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(error);
+ XCTAssertEqual(email, fakeEmail);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testVerifyPasswordResetCodeFailure
+ @brief Tests the flow of a failed @c verifyPasswordResetCode:completion call.
+ */
+- (void)testVeridyPasswordResetCodeFailure {
+ NSString *fakeCode = @"fakeCode";
+ OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]])
+ ._andDispatchError2([FIRAuthErrorUtils invalidActionCodeErrorWithMessage:nil]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] verifyPasswordResetCode:fakeCode completion:^(NSString *_Nullable email,
+ NSError *_Nullable error) {
+
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNotNil(error);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidActionCode);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInWithEmailCredentialSuccess
+ @brief Tests the flow of a successfully @c signInWithCredential:completion: call with an
+ email-password credential.
+ */
+- (void)testSignInWithEmailCredentialSuccess {
+ OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request,
+ FIRVerifyPasswordResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.email, kEmail);
+ XCTAssertEqualObjects(request.password, kPassword);
+ XCTAssertTrue(request.returnSecureToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]);
+ [self stubTokensWithMockResponse:mockVeriyPasswordResponse];
+ callback(mockVeriyPasswordResponse, nil);
+ });
+ });
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *emailCredential =
+ [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword];
+ [[FIRAuth auth] signInWithCredential:emailCredential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUser:user];
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUser:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInWithEmailCredentialSuccess
+ @brief Tests the flow of a successfully @c signInWithCredential:completion: call with an
+ email-password credential using the deprecated FIREmailPasswordAuthProvider.
+ */
+- (void)testSignInWithEmailCredentialSuccessWithDepricatedProvider {
+ OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request,
+ FIRVerifyPasswordResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.email, kEmail);
+ XCTAssertEqualObjects(request.password, kPassword);
+ XCTAssertTrue(request.returnSecureToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]);
+ [self stubTokensWithMockResponse:mockVeriyPasswordResponse];
+ callback(mockVeriyPasswordResponse, nil);
+ });
+ });
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ FIRAuthCredential *emailCredential =
+ [FIREmailPasswordAuthProvider credentialWithEmail:kEmail password:kPassword];
+#pragma clang diagnostic pop
+ [[FIRAuth auth] signInWithCredential:emailCredential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUser:user];
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUser:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInWithEmailCredentialFailure
+ @brief Tests the flow of a failed @c signInWithCredential:completion: call with an
+ email-password credential.
+ */
+- (void)testSignInWithEmailCredentialFailure {
+ OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils userDisabledErrorWithMessage:nil]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *emailCredential =
+ [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword];
+ [[FIRAuth auth] signInWithCredential:emailCredential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(user);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeUserDisabled);
+ XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNil([FIRAuth auth].currentUser);
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInWithEmailCredentialEmptyPassword
+ @brief Tests the flow of a failed @c signInWithCredential:completion: call with an
+ email-password credential using an empty password. This error occurs on the client side,
+ so there is no need to fake an RPC response.
+ */
+- (void)testSignInWithEmailCredentialEmptyPassword {
+ NSString *emptyString = @"";
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *emailCredential =
+ [FIREmailAuthProvider credentialWithEmail:kEmail password:emptyString];
+ [[FIRAuth auth] signInWithCredential:emailCredential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeWrongPassword);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+}
+
+/** @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
+ FIRAuthErrorCodeAccountExistsWithDifferentCredential error should be thrown.
+ */
+- (void)testSignInWithGoogleAccountExistsError {
+ OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request,
+ FIRVerifyAssertionResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.providerID, FIRGoogleAuthProviderID);
+ XCTAssertEqualObjects(request.providerIDToken, kGoogleIDToken);
+ XCTAssertEqualObjects(request.providerAccessToken, kGoogleAccessToken);
+ XCTAssertTrue(request.returnSecureToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVeriyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]);
+ OCMStub([mockVeriyAssertionResponse needConfirmation]).andReturn(YES);
+ OCMStub([mockVeriyAssertionResponse email]).andReturn(kEmail);
+ [self stubTokensWithMockResponse:mockVeriyAssertionResponse];
+ callback(mockVeriyAssertionResponse, nil);
+ });
+ });
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *googleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [[FIRAuth auth] signInWithCredential:googleCredential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeAccountExistsWithDifferentCredential);
+ XCTAssertEqualObjects(error.userInfo[FIRAuthErrorUserInfoEmailKey], kEmail);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInWithGoogleCredentialSuccess
+ @brief Tests the flow of a successful @c signInWithCredential:completion: call with an
+ Google Sign-In credential.
+ */
+- (void)testSignInWithGoogleCredentialSuccess {
+ OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request,
+ FIRVerifyAssertionResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.providerID, FIRGoogleAuthProviderID);
+ XCTAssertEqualObjects(request.providerIDToken, kGoogleIDToken);
+ XCTAssertEqualObjects(request.providerAccessToken, kGoogleAccessToken);
+ 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];
+ FIRAuthCredential *googleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [[FIRAuth auth] signInWithCredential:googleCredential 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);
+}
+
+/** @fn testSignInAndRetrieveDataWithCredentialSuccess
+ @brief Tests the flow of a successful @c signInAndRetrieveDataWithCredential:completion: call
+ with an Google Sign-In credential.
+ */
+- (void)testSignInAndRetrieveDataWithCredentialSuccess {
+ OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request,
+ FIRVerifyAssertionResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.providerID, FIRGoogleAuthProviderID);
+ XCTAssertEqualObjects(request.providerIDToken, kGoogleIDToken);
+ XCTAssertEqualObjects(request.providerAccessToken, kGoogleAccessToken);
+ 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);
+ OCMStub([mockVeriyAssertionResponse profile]).andReturn([[self class] googleProfile]);
+ OCMStub([mockVeriyAssertionResponse username]).andReturn(kDisplayName);
+ [self stubTokensWithMockResponse:mockVeriyAssertionResponse];
+ callback(mockVeriyAssertionResponse, nil);
+ });
+ });
+ [self expectGetAccountInfoGoogle];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *googleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:googleCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserGoogle:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kDisplayName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRGoogleAuthProviderID);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUserGoogle:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInWithGoogleCredentialFailure
+ @brief Tests the flow of a failed @c signInWithCredential:completion: call with an
+ Google Sign-In credential.
+ */
+- (void)testSignInWithGoogleCredentialFailure {
+ OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils emailAlreadyInUseErrorWithEmail:kGoogleEmail]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *googleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [[FIRAuth auth] signInWithCredential:googleCredential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(user);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeEmailAlreadyInUse);
+ XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNil([FIRAuth auth].currentUser);
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInAnonymouslySuccess
+ @brief Tests the flow of a successful @c signInAnonymously:completion: call.
+ */
+- (void)testSignInAnonymouslySuccess {
+ OCMExpect([_mockBackend signUpNewUser:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSignUpNewUserRequest *_Nullable request,
+ FIRSignupNewUserCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertNil(request.email);
+ XCTAssertNil(request.password);
+ XCTAssertTrue(request.returnSecureToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSignUpNewUserResponse = OCMClassMock([FIRSignUpNewUserResponse class]);
+ [self stubTokensWithMockResponse:mockSignUpNewUserResponse];
+ callback(mockSignUpNewUserResponse, nil);
+ });
+ });
+ [self expectGetAccountInfoAnonymous];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] signInAnonymouslyWithCompletion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserAnonymous:user];
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUserAnonymous:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInAnonymouslyFailure
+ @brief Tests the flow of a failed @c signInAnonymously:completion: call.
+ */
+- (void)testSignInAnonymouslyFailure {
+ OCMExpect([_mockBackend signUpNewUser:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils operationNotAllowedErrorWithMessage:nil]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] signInAnonymouslyWithCompletion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(user);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeOperationNotAllowed);
+ XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNil([FIRAuth auth].currentUser);
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInWithCustomTokenSuccess
+ @brief Tests the flow of a successful @c signInWithCustomToken:completion: call.
+ */
+- (void)testSignInWithCustomTokenSuccess {
+ OCMExpect([_mockBackend verifyCustomToken:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyCustomTokenRequest *_Nullable request,
+ FIRVerifyCustomTokenResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.token, kCustomToken);
+ XCTAssertTrue(request.returnSecureToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVeriyCustomTokenResponse = OCMClassMock([FIRVerifyCustomTokenResponse class]);
+ [self stubTokensWithMockResponse:mockVeriyCustomTokenResponse];
+ callback(mockVeriyCustomTokenResponse, nil);
+ });
+ });
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] signInWithCustomToken:kCustomToken completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUser:user];
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUser:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignInWithCustomTokenFailure
+ @brief Tests the flow of a failed @c signInWithCustomToken:completion: call.
+ */
+- (void)testSignInWithCustomTokenFailure {
+ OCMExpect([_mockBackend verifyCustomToken:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils invalidCustomTokenErrorWithMessage:nil]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] signInWithCustomToken:kCustomToken completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(user);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidCustomToken);
+ XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNil([FIRAuth auth].currentUser);
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testCreateUserWithEmailPasswordSuccess
+ @brief Tests the flow of a successful @c createUserWithEmail:password:completion: call.
+ */
+- (void)testCreateUserWithEmailPasswordSuccess {
+ OCMExpect([_mockBackend signUpNewUser:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSignUpNewUserRequest *_Nullable request,
+ FIRSignupNewUserCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.email, kEmail);
+ XCTAssertEqualObjects(request.password, kPassword);
+ XCTAssertTrue(request.returnSecureToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSignUpNewUserResponse = OCMClassMock([FIRSignUpNewUserResponse class]);
+ [self stubTokensWithMockResponse:mockSignUpNewUserResponse];
+ callback(mockSignUpNewUserResponse, nil);
+ });
+ });
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] createUserWithEmail:kEmail
+ password:kPassword
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUser:user];
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUser:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testCreateUserWithEmailPasswordFailure
+ @brief Tests the flow of a failed @c createUserWithEmail:password:completion: call.
+ */
+- (void)testCreateUserWithEmailPasswordFailure {
+ NSString *reason = @"Password shouldn't be a common word.";
+ OCMExpect([_mockBackend signUpNewUser:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils weakPasswordErrorWithServerResponseReason:reason]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] createUserWithEmail:kEmail
+ password:kPassword
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(user);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeWeakPassword);
+ XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]);
+ XCTAssertEqualObjects(error.userInfo[NSLocalizedFailureReasonErrorKey], reason);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ XCTAssertNil([FIRAuth auth].currentUser);
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testCreateUserEmptyPasswordFailure
+ @brief Tests the flow of a failed @c createUserWithEmail:password:completion: call due to an
+ empty password. This error occurs on the client side, so there is no need to fake an RPC
+ response.
+ */
+- (void)testCreateUserEmptyPasswordFailure {
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] createUserWithEmail:kEmail
+ password:@""
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeWeakPassword);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+}
+
+/** @fn testSendPasswordResetEmailSuccess
+ @brief Tests the flow of a successful @c sendPasswordResetWithEmail:completion: call.
+ */
+- (void)testSendPasswordResetEmailSuccess {
+ OCMExpect([_mockBackend getOOBConfirmationCode:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRGetOOBConfirmationCodeRequest *_Nullable request,
+ FIRGetOOBConfirmationCodeResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.email, kEmail);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback([[FIRGetOOBConfirmationCodeResponse alloc] init], nil);
+ });
+ });
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] sendPasswordResetWithEmail:kEmail completion:^(NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSendPasswordResetEmailFailure
+ @brief Tests the flow of a failed @c sendPasswordResetWithEmail:completion: call.
+ */
+- (void)testSendPasswordResetEmailFailure {
+ OCMExpect([_mockBackend getOOBConfirmationCode:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils appNotAuthorizedError]);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] sendPasswordResetWithEmail:kEmail completion:^(NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeAppNotAuthorized);
+ XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testSignOut
+ @brief Tests the @c signOut: method.
+ */
+- (void)testSignOut {
+ [self waitForSignIn];
+ // Verify signing out succeeds and clears the current user.
+ NSError *error;
+ XCTAssertTrue([[FIRAuth auth] signOut:&error]);
+ XCTAssertNil([FIRAuth auth].currentUser);
+}
+
+/** @fn testAuthStateChanges
+ @brief Tests @c addAuthStateDidChangeListener: and @c removeAuthStateDidChangeListener: methods.
+ */
+- (void)testAuthStateChanges {
+ // Set up listener.
+ __block XCTestExpectation *expectation;
+ __block BOOL shouldHaveUser;
+ FIRAuthStateDidChangeListenerBlock listener = ^(FIRAuth *auth, FIRUser *_Nullable user) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertEqual(auth, [FIRAuth auth]);
+ XCTAssertEqual(user, [FIRAuth auth].currentUser);
+ if (shouldHaveUser) {
+ XCTAssertNotNil(user);
+ } else {
+ XCTAssertNil(user);
+ }
+ // `expectation` being nil means the listener is not expected to be fired at this moment.
+ XCTAssertNotNil(expectation);
+ [expectation fulfill];
+ };
+ [[FIRAuth auth] signOut:NULL];
+ [self waitForTimeIntervel:kWaitInterval]; // Wait until dust settled from previous tests.
+
+ // Listener should fire immediately when attached.
+ expectation = [self expectationWithDescription:@"initial"];
+ shouldHaveUser = NO;
+ FIRAuthStateDidChangeListenerHandle handle =
+ [[FIRAuth auth] addAuthStateDidChangeListener:listener];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+
+ // Listener should fire for signing in.
+ expectation = [self expectationWithDescription:@"sign-in"];
+ shouldHaveUser = YES;
+ [self waitForSignIn];
+
+ // Listener should not fire for signing in again.
+ shouldHaveUser = YES;
+ [self waitForSignIn];
+ [self waitForTimeIntervel:kWaitInterval]; // make sure listener is not called
+
+ // Listener should fire for signing out.
+ expectation = [self expectationWithDescription:@"sign-out"];
+ shouldHaveUser = NO;
+ [[FIRAuth auth] signOut:NULL];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+
+ // Listener should no longer fire once detached.
+ expectation = nil;
+ [[FIRAuth auth] removeAuthStateDidChangeListener:handle];
+ [self waitForSignIn];
+ [self waitForTimeIntervel:kWaitInterval]; // make sure listener is no longer called
+}
+
+/** @fn testIDTokenChanges
+ @brief Tests @c addIDTokenDidChangeListener: and @c removeIDTokenDidChangeListener: methods.
+ */
+- (void)testIDTokenChanges {
+ // Set up listener.
+ __block XCTestExpectation *expectation;
+ __block BOOL shouldHaveUser;
+ FIRIDTokenDidChangeListenerBlock listener = ^(FIRAuth *auth, FIRUser *_Nullable user) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertEqual(auth, [FIRAuth auth]);
+ XCTAssertEqual(user, [FIRAuth auth].currentUser);
+ if (shouldHaveUser) {
+ XCTAssertNotNil(user);
+ } else {
+ XCTAssertNil(user);
+ }
+ // `expectation` being nil means the listener is not expected to be fired at this moment.
+ XCTAssertNotNil(expectation);
+ [expectation fulfill];
+ };
+ [[FIRAuth auth] signOut:NULL];
+ [self waitForTimeIntervel:kWaitInterval]; // Wait until dust settled from previous tests.
+
+ // Listener should fire immediately when attached.
+ expectation = [self expectationWithDescription:@"initial"];
+ shouldHaveUser = NO;
+ FIRIDTokenDidChangeListenerHandle handle =
+ [[FIRAuth auth] addIDTokenDidChangeListener:listener];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+
+ // Listener should fire for signing in.
+ expectation = [self expectationWithDescription:@"sign-in"];
+ shouldHaveUser = YES;
+ [self waitForSignIn];
+
+ // Listener should fire for signing in again as the same user.
+ expectation = [self expectationWithDescription:@"sign-in again"];
+ shouldHaveUser = YES;
+ [self waitForSignIn];
+
+ // Listener should fire for signing out.
+ expectation = [self expectationWithDescription:@"sign-out"];
+ shouldHaveUser = NO;
+ [[FIRAuth auth] signOut:NULL];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+
+ // Listener should no longer fire once detached.
+ expectation = nil;
+ [[FIRAuth auth] removeIDTokenDidChangeListener:handle];
+ [self waitForSignIn];
+ [self waitForTimeIntervel:kWaitInterval]; // make sure listener is no longer called
+}
+
+#pragma mark - Automatic Token Refresh Tests.
+
+/** @fn testAutomaticTokenRefresh
+ @brief Tests a successful flow to automatically refresh tokens for a signed in user.
+ */
+- (void)testAutomaticTokenRefresh {
+ [[FIRAuth auth] signOut:NULL];
+
+ // Enable auto refresh
+ [self enableAutoTokenRefresh];
+
+ // Sign in a user.
+ [self waitForSignIn];
+
+ // Set up expectation for secureToken RPC made by token refresh task.
+ [self mockSecureTokenResponseWithError:nil];
+
+ // Verify that the current user's access token is the "old" access token before automatic token
+ // refresh.
+ XCTAssertEqualObjects(kAccessToken, [FIRAuth auth].currentUser.rawAccessToken);
+
+ // Execute saved token refresh task.
+ XCTestExpectation *dispatchAfterExpectation =
+ [self expectationWithDescription:@"dispatchAfterExpectation"];
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ XCTAssertNotNil(_FIRAuthDispatcherCallback);
+ _FIRAuthDispatcherCallback();
+ [dispatchAfterExpectation fulfill];
+ });
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+
+ // Verify that current user's access token is the "new" access token provided in the mock secure
+ // token response during automatic token refresh.
+ XCTAssertEqualObjects(kNewAccessToken, [FIRAuth auth].currentUser.rawAccessToken);
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testAutomaticTokenRefreshInvalidTokenFailure
+ @brief Tests an unsuccessful flow to auto refresh tokens with an "invalid token" error.
+ This error should cause the user to be signed out.
+ */
+- (void)testAutomaticTokenRefreshInvalidTokenFailure {
+ [[FIRAuth auth] signOut:NULL];
+ // Enable auto refresh
+ [self enableAutoTokenRefresh];
+
+ // Sign in a user.
+ [self waitForSignIn];
+
+ // Set up expectation for secureToken RPC made by a failed attempt to refresh tokens.
+ [self mockSecureTokenResponseWithError:[FIRAuthErrorUtils invalidUserTokenErrorWithMessage:nil]];
+
+ // Verify that current user is still valid.
+ XCTAssertEqualObjects(kAccessToken, [FIRAuth auth].currentUser.rawAccessToken);
+
+ // Execute saved token refresh task.
+ XCTestExpectation *dispatchAfterExpectation =
+ [self expectationWithDescription:@"dispatchAfterExpectation"];
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ XCTAssertNotNil(_FIRAuthDispatcherCallback);
+ _FIRAuthDispatcherCallback();
+ [dispatchAfterExpectation fulfill];
+ });
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+
+ //Verify that the user is nil after failed attempt to refresh tokens caused signed out.
+ XCTAssertNil([FIRAuth auth].currentUser);
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testAutomaticTokenRefreshRetry
+ @brief Tests that a retry is attempted for a automatic token refresh task (which is not due to
+ invalid tokens). The initial attempt to refresh the access token fails, but the second
+ attempt is successful.
+ */
+- (void)testAutomaticTokenRefreshRetry {
+ [[FIRAuth auth] signOut:NULL];
+ // Enable auto refresh
+ [self enableAutoTokenRefresh];
+
+ // Sign in a user.
+ [self waitForSignIn];
+
+ // Set up expectation for secureToken RPC made by a failed attempt to refresh tokens.
+ [self mockSecureTokenResponseWithError:[NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil]];
+
+ // Execute saved token refresh task.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ XCTAssertNotNil(_FIRAuthDispatcherCallback);
+ _FIRAuthDispatcherCallback();
+ _FIRAuthDispatcherCallback = nil;
+ [expectation fulfill];
+ });
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+
+ // The old access token should still be the current user's access token and not the new access
+ // token (kNewAccessToken).
+ XCTAssertEqualObjects(kAccessToken, [FIRAuth auth].currentUser.rawAccessToken);
+
+ // Set up expectation for secureToken RPC made by a successful attempt to refresh tokens.
+ [self mockSecureTokenResponseWithError:nil];
+
+ // Execute saved token refresh task.
+ XCTestExpectation *dispatchAfterExpectation =
+ [self expectationWithDescription:@"dispatchAfterExpectation"];
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ XCTAssertNotNil(_FIRAuthDispatcherCallback);
+ _FIRAuthDispatcherCallback();
+ [dispatchAfterExpectation fulfill];
+ });
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+
+ // Verify that current user's access token is the "new" access token provided in the mock secure
+ // token response during automatic token refresh.
+ XCTAssertEqualObjects([FIRAuth auth].currentUser.rawAccessToken, kNewAccessToken);
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testAutomaticTokenRefreshInvalidTokenFailure
+ @brief Tests that app foreground notification triggers the scheduling of an automatic token
+ refresh task.
+ */
+- (void)testAutoRefreshAppForegroundedNotification {
+ [[FIRAuth auth] signOut:NULL];
+ // Enable auto refresh
+ [self enableAutoTokenRefresh];
+
+ // Sign in a user.
+ [self waitForSignIn];
+
+ // Post "UIApplicationDidBecomeActiveNotification" to trigger scheduling token refresh task.
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];
+
+ // Verify that current user is still valid with old access token.
+ XCTAssertEqualObjects(kAccessToken, [FIRAuth auth].currentUser.rawAccessToken);
+
+ // Set up expectation for secureToken RPC made by a successful attempt to refresh tokens.
+ [self mockSecureTokenResponseWithError:nil];
+
+ // Execute saved token refresh task.
+ XCTestExpectation *dispatchAfterExpectation =
+ [self expectationWithDescription:@"dispatchAfterExpectation"];
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ XCTAssertNotNil(_FIRAuthDispatcherCallback);
+ _FIRAuthDispatcherCallback();
+ [dispatchAfterExpectation fulfill];
+ });
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ // Verify that current user is still valid with new access token.
+ XCTAssertEqualObjects(kNewAccessToken, [FIRAuth auth].currentUser.rawAccessToken);
+ OCMVerifyAll(_mockBackend);
+}
+
+#pragma mark - Helpers
+
+/** @fn mockSecureTokenResponseWithError:
+ @brief Set up expectation for secureToken RPC.
+ @param error The error that the mock should return if any.
+ */
+- (void)mockSecureTokenResponseWithError:(nullable NSError *)error {
+ // Set up expectation for secureToken RPC made by a successful attempt to refresh tokens.
+ XCTestExpectation *secureTokenResponseExpectation =
+ [self expectationWithDescription:@"secureTokenResponseExpectation"];
+ OCMExpect([_mockBackend secureToken:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSecureTokenRequest *_Nullable request,
+ FIRSecureTokenResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.refreshToken, kRefreshToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ if (error) {
+ callback(nil, error);
+ [secureTokenResponseExpectation fulfill];
+ return;
+ }
+ id mockSecureTokenResponse = OCMClassMock([FIRSecureTokenResponse class]);
+ OCMStub([mockSecureTokenResponse accessToken]).andReturn(kNewAccessToken);
+ NSDate *futureDate =
+ [[NSDate date] dateByAddingTimeInterval:kTestTokenExpirationTimeInterval];
+ OCMStub([mockSecureTokenResponse approximateExpirationDate]).andReturn(futureDate);
+ callback(mockSecureTokenResponse, nil);
+ [secureTokenResponseExpectation fulfill];
+ });
+ });
+}
+
+/** @fn enableAutoTokenRefresh
+ @brief Enables automatic token refresh by invoking FIRAuth's implementation of FIRApp's
+ |getTokenWithImplementation|.
+ */
+- (void)enableAutoTokenRefresh {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"autoTokenRefreshcallback"];
+ [[FIRAuth auth].app getTokenForcingRefresh:NO withCallback:^(NSString *_Nullable token,
+ NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+ [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.
+ */
+- (void)stubTokensWithMockResponse:(id)mockResponse {
+ OCMStub([mockResponse IDToken]).andReturn(kAccessToken);
+ OCMStub([mockResponse approximateExpirationDate])
+ .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]);
+ OCMStub([mockResponse refreshToken]).andReturn(kRefreshToken);
+}
+
+/** @fn expectGetAccountInfo
+ @brief Expects a GetAccountInfo request on the mock backend and calls back with fake account
+ data.
+ */
+- (void)expectGetAccountInfo {
+ OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request,
+ FIRGetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]);
+ OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockGetAccountInfoResponseUser ]);
+ callback(mockGetAccountInfoResponse, nil);
+ });
+ });
+}
+
+/** @fn assertUser
+ @brief Asserts the given FIRUser matching the fake data returned by @c expectGetAccountInfo.
+ @param user The user object to be verified.
+ */
+- (void)assertUser:(FIRUser *)user {
+ XCTAssertNotNil(user);
+ XCTAssertEqualObjects(user.uid, kLocalID);
+ XCTAssertEqualObjects(user.displayName, kDisplayName);
+ XCTAssertEqualObjects(user.email, kEmail);
+ XCTAssertFalse(user.anonymous);
+ XCTAssertEqual(user.providerData.count, 0u);
+}
+
+/** @fn expectGetAccountInfoGoogle
+ @brief Expects a GetAccountInfo request on the mock backend and calls back with fake account
+ data for a Google Sign-In user.
+ */
+- (void)expectGetAccountInfoGoogle {
+ OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request,
+ FIRGetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockGoogleUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]);
+ OCMStub([mockGoogleUserInfo providerID]).andReturn(FIRGoogleAuthProviderID);
+ OCMStub([mockGoogleUserInfo displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGoogleUserInfo federatedID]).andReturn(kGoogleID);
+ OCMStub([mockGoogleUserInfo email]).andReturn(kGoogleEmail);
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser providerUserInfo])
+ .andReturn((@[ mockGoogleUserInfo ]));
+ id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]);
+ OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockGetAccountInfoResponseUser ]);
+ callback(mockGetAccountInfoResponse, nil);
+ });
+ });
+}
+
+/** @fn assertUserGoogle
+ @brief Asserts the given FIRUser matching the fake data returned by
+ @c expectGetAccountInfoGoogle.
+ @param user The user object to be verified.
+ */
+- (void)assertUserGoogle:(FIRUser *)user {
+ XCTAssertNotNil(user);
+ XCTAssertEqualObjects(user.uid, kLocalID);
+ XCTAssertEqualObjects(user.displayName, kDisplayName);
+ XCTAssertEqual(user.providerData.count, 1u);
+ id<FIRUserInfo> googleUserInfo = user.providerData[0];
+ XCTAssertEqualObjects(googleUserInfo.providerID, FIRGoogleAuthProviderID);
+ XCTAssertEqualObjects(googleUserInfo.uid, kGoogleID);
+ XCTAssertEqualObjects(googleUserInfo.displayName, kGoogleDisplayName);
+ XCTAssertEqualObjects(googleUserInfo.email, kGoogleEmail);
+}
+
+/** @fn expectGetAccountInfoAnonymous
+ @brief Expects a GetAccountInfo request on the mock backend and calls back with fake anonymous
+ account data.
+ */
+- (void)expectGetAccountInfoAnonymous {
+ OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request,
+ FIRGetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]);
+ OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockGetAccountInfoResponseUser ]);
+ callback(mockGetAccountInfoResponse, nil);
+ });
+ });
+}
+
+/** @fn assertUserAnonymous
+ @brief Asserts the given FIRUser matching the fake data returned by
+ @c expectGetAccountInfoAnonymous.
+ @param user The user object to be verified.
+ */
+- (void)assertUserAnonymous:(FIRUser *)user {
+ XCTAssertNotNil(user);
+ XCTAssertEqualObjects(user.uid, kLocalID);
+ XCTAssertNil(user.displayName);
+ XCTAssertTrue(user.anonymous);
+ XCTAssertEqual(user.providerData.count, 0u);
+}
+
+/** @fn waitForSignIn
+ @brief Signs in a user to prepare for tests.
+ @remarks This method also waits for all other pending @c XCTestExpectation instances.
+ */
+- (void)waitForSignIn {
+ OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request,
+ FIRVerifyPasswordResponseCallback callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]);
+ [self stubTokensWithMockResponse:mockVeriyPasswordResponse];
+ callback(mockVeriyPasswordResponse, nil);
+ });
+ });
+ [self expectGetAccountInfo];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signInWithEmail:kEmail password:kPassword completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+ XCTAssertNotNil([FIRAuth auth].currentUser);
+}
+
+/** @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/FIRAuthUserDefaultsStorageTests.m b/Example/Auth/Tests/FIRAuthUserDefaultsStorageTests.m
new file mode 100644
index 0000000..07493d5
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthUserDefaultsStorageTests.m
@@ -0,0 +1,155 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthUserDefaultsStorage.h"
+
+#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kKey
+ @brief The key used in tests.
+ */
+static NSString *const kKey = @"ACCOUNT";
+
+/** @var kService
+ @brief The keychain service used in tests.
+ */
+static NSString *const kService = @"SERVICE";
+
+/** @var kOtherService
+ @brief Another keychain service used in tests.
+ */
+static NSString *const kOtherService = @"OTHER_SERVICE";
+
+/** @var kData
+ @brief A piece of keychain data used in tests.
+ */
+static NSString *const kData = @"DATA";
+
+/** @var kOtherData
+ @brief Another piece of keychain data used in tests.
+ */
+static NSString *const kOtherData = @"OTHER_DATA";
+
+/** @fn dataFromString
+ @brief Converts a NSString to NSData.
+ @param string The NSString to be converted from.
+ @return The NSData being the conversion result.
+ */
+static NSData *dataFromString(NSString *string) {
+ return [string dataUsingEncoding:NSUTF8StringEncoding];
+}
+
+/** @fn fakeError
+ @brief Creates a fake error object.
+ @return a non-nil NSError instance.
+ */
+static NSError *fakeError() {
+ return [NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil];
+}
+
+/** @class FIRAuthUserDefaultsStorageTests
+ @brief Tests for @c FIRAuthUserDefaultsStorage .
+ */
+@interface FIRAuthUserDefaultsStorageTests : XCTestCase
+@end
+
+@implementation FIRAuthUserDefaultsStorageTests {
+ /** @var _storage
+ @brief The @c FIRAuthUserDefaultsStorage object under test.
+ */
+ FIRAuthUserDefaultsStorage *_storage;
+}
+
+- (void)setUp {
+ [super setUp];
+ _storage = [[FIRAuthUserDefaultsStorage alloc] initWithService:kService];
+ [_storage clear];
+}
+
+/** @fn testReadNonexisting
+ @brief Tests reading non-existing storage item.
+ */
+- (void)testReadNonExisting {
+ NSError *error = fakeError();
+ XCTAssertNil([_storage dataForKey:kKey error:&error]);
+ XCTAssertNil(error);
+}
+
+/** @fn testWriteRead
+ @brief Tests writing and reading a storage item.
+ */
+- (void)testWriteRead {
+ XCTAssertTrue([_storage setData:dataFromString(kData) forKey:kKey error:NULL]);
+ NSError *error = fakeError();
+ XCTAssertEqualObjects([_storage dataForKey:kKey error:&error], dataFromString(kData));
+ XCTAssertNil(error);
+}
+
+/** @fn testOverwrite
+ @brief Tests overwriting a storage item.
+ */
+- (void)testOverwrite {
+ XCTAssertTrue([_storage setData:dataFromString(kData) forKey:kKey error:NULL]);
+ XCTAssertTrue([_storage setData:dataFromString(kOtherData) forKey:kKey error:NULL]);
+ NSError *error = fakeError();
+ XCTAssertEqualObjects([_storage dataForKey:kKey error:&error], dataFromString(kOtherData));
+ XCTAssertNil(error);
+}
+
+/** @fn testRemove
+ @brief Tests removing a storage item.
+ */
+- (void)testRemove {
+ XCTAssertTrue([_storage setData:dataFromString(kData) forKey:kKey error:NULL]);
+ XCTAssertTrue([_storage removeDataForKey:kKey error:NULL]);
+ NSError *error = fakeError();
+ XCTAssertNil([_storage dataForKey:kKey error:&error]);
+ XCTAssertNil(error);
+}
+
+/** @fn testServices
+ @brief Tests storage items belonging to different services doesn't affect each other.
+ */
+- (void)testServices {
+ XCTAssertTrue([_storage setData:dataFromString(kData) forKey:kKey error:NULL]);
+ _storage = [[FIRAuthUserDefaultsStorage alloc] initWithService:kOtherService];
+ NSError *error = fakeError();
+ XCTAssertNil([_storage dataForKey:kKey error:&error]);
+ XCTAssertNil(error);
+}
+
+/** @fn testStandardUserDefaults
+ @brief Tests standard user defaults are not affected by FIRAuthUserDefaultsStorage operations,
+ */
+- (void)testStandardUserDefaults {
+ NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
+ NSUInteger count =
+ [userDefaults persistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]].count;
+ XCTAssertTrue([_storage setData:dataFromString(kData) forKey:kKey error:NULL]);
+ XCTAssertNil([userDefaults dataForKey:kKey]);
+ XCTAssertEqual([userDefaults persistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]]
+ .count, count);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
diff --git a/Example/Auth/Tests/FIRCreateAuthURIRequestTests.m b/Example/Auth/Tests/FIRCreateAuthURIRequestTests.m
new file mode 100644
index 0000000..409c232
--- /dev/null
+++ b/Example/Auth/Tests/FIRCreateAuthURIRequestTests.m
@@ -0,0 +1,103 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRCreateAuthURIRequest.h"
+#import "FIRCreateAuthURIResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kTestAuthUri
+ @brief The test value of the "authURI" property in the json response.
+ */
+static NSString *const kTestAuthUri = @"AuthURI";
+
+/** @var kTestIdentifier
+ @brief Fake identifier key used for testing.
+ */
+static NSString *const kTestIdentifier = @"Identifier";
+
+/** @var kContinueURITestKey
+ @brief The key for the "continueUri" value in the request.
+ */
+static NSString *const kContinueURITestKey = @"continueUri";
+
+/** @var kTestContinueURI
+ @brief Fake Continue URI key used for testing.
+ */
+static NSString *const kTestContinueURI = @"ContinueUri";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for the test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/createAuthUri?key=APIKey";
+
+/** @class FIRCreateAuthURIRequestTests
+ @brief Tests for @c CreateAuthURIRequest.
+ */
+@interface FIRCreateAuthURIRequestTests : XCTestCase
+@end
+@implementation FIRCreateAuthURIRequestTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testCreateAuthUriRequest
+ @brief Tests the encoding of an create auth URI request.
+ */
+- (void)testEmailVerificationRequest {
+ FIRCreateAuthURIRequest *request =
+ [[FIRCreateAuthURIRequest alloc]initWithIdentifier:kTestIdentifier
+ continueURI:kTestContinueURI
+ APIKey:kTestAPIKey];
+
+ [FIRAuthBackend createAuthURI:request
+ callback:^(FIRCreateAuthURIResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kContinueURITestKey], kTestContinueURI);
+}
+
+
+@end
diff --git a/Example/Auth/Tests/FIRCreateAuthURIResponseTests.m b/Example/Auth/Tests/FIRCreateAuthURIResponseTests.m
new file mode 100644
index 0000000..11cab9d
--- /dev/null
+++ b/Example/Auth/Tests/FIRCreateAuthURIResponseTests.m
@@ -0,0 +1,205 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRCreateAuthURIRequest.h"
+#import "FIRCreateAuthURIResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kAuthUriKey
+ @brief The name of the "authURI" property in the json response.
+ */
+static NSString *const kAuthUriKey = @"authUri";
+
+/** @var kTestAuthUri
+ @brief The test value of the "authURI" property in the json response.
+ */
+static NSString *const kTestAuthUri = @"AuthURI";
+
+/** @var kTestIdentifier
+ @brief Fake identifier key used for testing.
+ */
+static NSString *const kTestIdentifier = @"Identifier";
+
+/** @var kTestContinueURI
+ @brief Fake Continue URI key used for testing.
+ */
+static NSString *const kTestContinueURI = @"ContinueUri";
+
+/** @var kMissingContinueURIErrorMessage
+ @brief The error returned by the server if continue Uri is invalid.
+ */
+static NSString *const kMissingContinueURIErrorMessage = @"MISSING_CONTINUE_URI:";
+
+/** @var kInvalidEmailErrorMessage
+ @brief The error returned by the server if the email is invalid.
+ */
+static NSString *const kInvalidIdentifierErrorMessage = @"INVALID_IDENTIFIER :";
+
+/** @var kInvalidEmailErrorMessage
+ @brief The error returned by the server if the email is invalid.
+ */
+static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL:";
+
+/** @class CreateAuthURIResponseTests
+ @brief Tests for @c FIRCreateAuthURIResponse.
+ */
+@interface FIRCreateAuthURIResponseTests : XCTestCase
+@end
+@implementation FIRCreateAuthURIResponseTests{
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testMissingContinueURIError
+ @brief This test checks for invalid continue URI in the response.
+ */
+- (void)testMissingContinueURIError {
+ FIRCreateAuthURIRequest *request =
+ [[FIRCreateAuthURIRequest alloc]initWithIdentifier:kTestIdentifier
+ continueURI:kTestContinueURI
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRCreateAuthURIResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend createAuthURI:request
+ callback:^(FIRCreateAuthURIResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kMissingContinueURIErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInternalError);
+ XCTAssertNil(RPCResponse);
+}
+
+/** @fn testInvalidIdentifierError
+ @brief This test checks for the INVALID_IDENTIFIER error message from the backend.
+ */
+- (void)testInvalidIdentifierError {
+ FIRCreateAuthURIRequest *request =
+ [[FIRCreateAuthURIRequest alloc]initWithIdentifier:kTestIdentifier
+ continueURI:kTestContinueURI
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRCreateAuthURIResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend createAuthURI:request
+ callback:^(FIRCreateAuthURIResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidIdentifierErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail);
+ XCTAssertNil(RPCResponse);
+}
+
+/** @fn testInvalidEmailError
+ @brief This test checks for INVALID_EMAIL error message from the backend.
+ */
+- (void)testInvalidEmailError {
+ FIRCreateAuthURIRequest *request =
+ [[FIRCreateAuthURIRequest alloc]initWithIdentifier:kTestIdentifier
+ continueURI:kTestContinueURI
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRCreateAuthURIResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend createAuthURI:request
+ callback:^(FIRCreateAuthURIResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail);
+ XCTAssertNil(RPCResponse);
+}
+
+/** @fn testSuccessfulCreateAuthURI
+ @brief This test checks for invalid email identifier error.
+ */
+- (void)testSuccessfulCreateAuthURIResponse {
+ FIRCreateAuthURIRequest *request =
+ [[FIRCreateAuthURIRequest alloc]initWithIdentifier:kTestIdentifier
+ continueURI:kTestContinueURI
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRCreateAuthURIResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend createAuthURI:request
+ callback:^(FIRCreateAuthURIResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ kAuthUriKey : kTestAuthUri
+ }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.authURI, kTestAuthUri);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRDeleteAccountRequestTests.m b/Example/Auth/Tests/FIRDeleteAccountRequestTests.m
new file mode 100644
index 0000000..05f1d47
--- /dev/null
+++ b/Example/Auth/Tests/FIRDeleteAccountRequestTests.m
@@ -0,0 +1,98 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRDeleteAccountRequest.h"
+#import "FIRDeleteAccountResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kLocalID
+ @brief Fake LocalID used for testing.
+ */
+static NSString *const kLocalID = @"LocalID";
+
+/** @var kLocalIDKey
+ @brief The name of the "localID" property in the request.
+ */
+static NSString *const kLocalIDKey = @"localId";
+
+/** @var kAccessToken
+ @brief The name of the "AccessToken" property in the request.
+ */
+static NSString *const kAccessToken = @"AccessToken";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount?key=APIKey";
+
+/** @class FIRDeleteUserRequestTests
+ @brief Tests for @c FIRDeleteAccountRequest.
+ */
+@interface FIRDeleteAccountRequestTests : XCTestCase
+@end
+@implementation FIRDeleteAccountRequestTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testDeleteAccountRequest
+ @brief Tests the delete account request.
+ */
+- (void)testDeleteAccountRequest {
+
+ FIRDeleteAccountRequest *request = [[FIRDeleteAccountRequest alloc] initWithAPIKey:kTestAPIKey
+ localID:kLocalID
+ accessToken:kAccessToken];
+ __block BOOL callbackInvoked;
+ __block NSError *RPCError;
+ [FIRAuthBackend deleteAccount:request
+ callback:^(NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCError = error;
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest[kLocalIDKey]);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRDeleteAccountResponseTests.m b/Example/Auth/Tests/FIRDeleteAccountResponseTests.m
new file mode 100644
index 0000000..f75735e
--- /dev/null
+++ b/Example/Auth/Tests/FIRDeleteAccountResponseTests.m
@@ -0,0 +1,172 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRDeleteAccountRequest.h"
+#import "FIRDeleteAccountResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kLocalID
+ @brief Fake LocalID used for testing.
+ */
+static NSString *const kLocalID = @"LocalID";
+
+/** @var kAccessToken
+ @brief Fake AccessToken used for testing.
+ */
+static NSString *const kAccessToken = @"AccessToken";
+
+/** @var kUserDisabledErrorMessage
+ @brief The error returned by the server if the user account is diabled.
+ */
+static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED";
+
+/** @var kinvalidUserTokenErrorMessage
+ @brief This is the error message the server responds with if user's saved auth credential is
+ invalid, and the user needs to sign in again.
+ */
+static NSString *const kinvalidUserTokenErrorMessage = @"INVALID_ID_TOKEN:";
+
+/** @var kCredentialTooOldErrorMessage
+ @brief This is the error message the server responds with if account change is attempted 5
+ minutes after signing in.
+ */
+static NSString *const kCredentialTooOldErrorMessage = @"CREDENTIAL_TOO_OLD_LOGIN_AGAIN:";
+
+/** @class FIRDeleteUserResponseTests
+ @brief Tests for @c FIRDeleteAccountResponse.
+ */
+@interface FIRDeleteAccountResponseTests : XCTestCase
+@end
+@implementation FIRDeleteAccountResponseTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testUserDisabledError
+ @brief This test simulates the occurrence of a @c userDisabled error.
+ */
+- (void)testUserDisabledError {
+ FIRDeleteAccountRequest *request = [[FIRDeleteAccountRequest alloc] initWithAPIKey:kTestAPIKey
+ localID:kLocalID
+ accessToken:kAccessToken];
+
+ __block BOOL callbackInvoked;
+ __block NSError *RPCError;
+ [FIRAuthBackend deleteAccount:request
+ callback:^(NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kUserDisabledErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled);
+}
+
+/** @fn testinvalidUserTokenError
+ @brief This test simulates the occurrence of a @c invalidUserToken error.
+ */
+- (void)testinvalidUserTokenError {
+ FIRDeleteAccountRequest *request = [[FIRDeleteAccountRequest alloc] initWithAPIKey:kTestAPIKey
+ localID:kLocalID
+ accessToken:kAccessToken];
+
+ __block BOOL callbackInvoked;
+ __block NSError *RPCError;
+ [FIRAuthBackend deleteAccount:request
+ callback:^(NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kinvalidUserTokenErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidUserToken);
+}
+
+/** @fn testrequiredRecentLoginError
+ @brief This test simulates the occurrence of a @c credentialTooOld error.
+ */
+- (void)testrequiredRecentLoginError {
+ FIRDeleteAccountRequest *request = [[FIRDeleteAccountRequest alloc] initWithAPIKey:kTestAPIKey
+ localID:kLocalID
+ accessToken:kAccessToken];
+ __block BOOL callbackInvoked;
+ __block NSError *RPCError;
+ [FIRAuthBackend deleteAccount:request
+ callback:^(NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kCredentialTooOldErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeRequiresRecentLogin);
+}
+
+/** @fn testSuccessfulDeleteAccount
+ @brief This test simulates a completed succesful deleteAccount operation.
+ */
+- (void)testSuccessfulDeleteAccountResponse {
+ FIRDeleteAccountRequest *request = [[FIRDeleteAccountRequest alloc] initWithAPIKey:kTestAPIKey
+ localID:kLocalID
+ accessToken:kAccessToken];
+ __block BOOL callbackInvoked;
+ __block NSError *RPCError;
+ [FIRAuthBackend deleteAccount:request
+ callback:^(NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{}];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRFakeBackendRPCIssuer.h b/Example/Auth/Tests/FIRFakeBackendRPCIssuer.h
new file mode 100644
index 0000000..d192cda
--- /dev/null
+++ b/Example/Auth/Tests/FIRFakeBackendRPCIssuer.h
@@ -0,0 +1,100 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthBackend.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRFakeBackendRPCIssuer
+ @brief An implementation of @c FIRAuthBackendRPCIssuer which is used to test backend request,
+ response, and glue logic.
+ */
+@interface FIRFakeBackendRPCIssuer : NSObject <FIRAuthBackendRPCIssuer>
+
+/** @property requestURL
+ @brief The URL which was requested.
+ */
+@property(nonatomic, readonly) NSURL *requestURL;
+
+/** @property requestData
+ @brief The raw data in the POST body.
+ */
+@property(nonatomic, readonly) NSData *requestData;
+
+/** @property decodedRequest
+ @brief The raw data in the POST body decoded as JSON.
+ */
+@property(nonatomic, readonly) NSDictionary *decodedRequest;
+
+/** @property contentType
+ @brief The value of the content type HTTP header in the request.
+ */
+@property(nonatomic, readonly) NSString *contentType;
+
+/** @fn respondWithData:error:
+ @brief Responds to a pending RPC request with data and an error.
+ @remarks This is useful for simulating an error response with bogus data or unexpected data
+ (like unexpectedly receiving an HTML body.)
+ @param data The data to return as the body of an HTTP response.
+ @param error The simulated error to return from GTM.
+ */
+- (void)respondWithData:(nullable NSData *)data error:(nullable NSError *)error;
+
+/** @fn respondWithJSON:error:
+ @brief Responds to a pending RPC request with JSON and an error.
+ @remarks This is useful for simulating an error response with error JSON.
+ @param JSON The JSON to return.
+ @param error The simulated error to return from GTM.
+ */
+- (NSData *)respondWithJSON:(nullable NSDictionary *)JSON error:(nullable NSError *)error;
+
+/** @fn respondWithJSONError:
+ @brief Responds to a pending RPC request with a JSON server error.
+ @param JSON A dictionary which should be a server error encoded as JSON for fake response.
+ */
+- (NSData *)respondWithJSONError:(NSDictionary *)JSON;
+
+/** @fn respondWithError:
+ @brief Responds to a pending RPC request with an error. This is useful for simulating things
+ like a network timeout or unreachable host.
+ @param error The simulated error to return from GTM.
+ */
+- (NSData *)respondWithError:(NSError *)error;
+
+/** @fn respondWithServerErrorMessage:error:
+ @brief Responds to a pending RPC request with a server error message.
+ @param errorMessage The simulated error message to return from the server.
+ @param error The simulated error to return from GTM.
+ */
+- (NSData *)respondWithServerErrorMessage:(NSString *)errorMessage error:(NSError *)error;
+
+/** @fn respondWithServerErrorMessage:
+ @brief Responds to a pending RPC request with a server error message.
+ @param errorMessage The simulated error message to return from the server.
+ */
+- (NSData *)respondWithServerErrorMessage:(NSString *)errorMessage;
+
+/** @fn respondWithJSON:
+ @brief Responds to a pending RPC request with JSON.
+ @param JSON A dictionary which should be encoded as JSON for a fake response.
+ */
+- (NSData *)respondWithJSON:(NSDictionary *)JSON;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/FIRFakeBackendRPCIssuer.m b/Example/Auth/Tests/FIRFakeBackendRPCIssuer.m
new file mode 100644
index 0000000..93589e7
--- /dev/null
+++ b/Example/Auth/Tests/FIRFakeBackendRPCIssuer.m
@@ -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 "FIRFakeBackendRPCIssuer.h"
+
+/** @var kFakeErrorDomain
+ @brief Fake error domain used for testing.
+ */
+static NSString *const kFakeErrorDomain = @"fake domain";
+
+@implementation FIRFakeBackendRPCIssuer {
+ /** @var _handler
+ @brief A block we must invoke when @c respondWithError or @c respondWithJSON are called.
+ */
+ FIRAuthBackendRPCIssuerCompletionHandler _handler;
+}
+
+- (void)asyncPostToURL:(NSURL *)URL
+ body:(NSData *)body
+ contentType:(NSString *)contentType
+ completionHandler:(FIRAuthBackendRPCIssuerCompletionHandler)handler {
+ _requestURL = [URL copy];
+ _requestData = body;
+ NSDictionary *JSON = [NSJSONSerialization JSONObjectWithData:body options:0 error:nil];
+ _decodedRequest = JSON;
+ _contentType = contentType;
+ _handler = handler;
+}
+
+- (void)respondWithData:(NSData *)data error:(NSError *)error {
+ NSAssert(_handler, @"There is no pending RPC request.");
+ NSAssert(data || error, @"At least one of: data or error should be been non-nil.");
+ FIRAuthBackendRPCIssuerCompletionHandler handler = _handler;
+ _handler = nil;
+ handler(data, error);
+}
+
+- (NSData *)respondWithServerErrorMessage:(NSString *)errorMessage error:(NSError *)error {
+ return [self respondWithJSON:@{ @"error" : @{ @"message" : errorMessage } } error:error];
+}
+
+- (NSData *)respondWithServerErrorMessage:(NSString *)errorMessage {
+ NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil];
+ return [self respondWithServerErrorMessage:errorMessage error:error];
+}
+
+- (NSData *)respondWithJSON:(NSDictionary *)JSON error:(NSError *)error {
+ NSError *JSONEncodingError;
+ NSData *data;
+ if (JSON) {
+ data = [NSJSONSerialization dataWithJSONObject:JSON
+ options:NSJSONWritingPrettyPrinted
+ error:&JSONEncodingError];
+ }
+ NSAssert(!JSONEncodingError, @"An error occurred encoding the JSON response.");
+ [self respondWithData:data error:error];
+ return data;
+}
+
+- (NSData *)respondWithJSONError:(NSDictionary *)JSONError {
+ return [self respondWithJSON:JSONError
+ error:[NSError errorWithDomain:kFakeErrorDomain code:0 userInfo:nil]];
+}
+
+- (NSData *)respondWithError:(NSError *)error {
+ return [self respondWithJSON:nil error:error];
+}
+
+- (NSData *)respondWithJSON:(NSDictionary *)JSON {
+ return [self respondWithJSON:JSON error:nil];
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRGetAccountInfoRequestTests.m b/Example/Auth/Tests/FIRGetAccountInfoRequestTests.m
new file mode 100644
index 0000000..6f713b0
--- /dev/null
+++ b/Example/Auth/Tests/FIRGetAccountInfoRequestTests.m
@@ -0,0 +1,87 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthBackend.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRGetAccountInfoRequest.h"
+#import "FIRGetAccountInfoResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kTestAccessToken
+ @brief testing token.
+ */
+static NSString *const kTestAccessToken = @"testAccessToken";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=APIKey";
+
+@interface FIRGetAccountInfoRequestTests : XCTestCase
+@end
+@implementation FIRGetAccountInfoRequestTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testGetAccountInfoRequest
+ @brief Tests the set account info request.
+ */
+- (void)testGetAccountInfoRequest {
+ FIRGetAccountInfoRequest *request =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey accessToken:kTestAccessToken];
+
+ [FIRAuthBackend getAccountInfo:request callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+
+ }];
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest[kIDTokenKey]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestAccessToken);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRGetAccountInfoResponseTests.m b/Example/Auth/Tests/FIRGetAccountInfoResponseTests.m
new file mode 100644
index 0000000..b6c261e
--- /dev/null
+++ b/Example/Auth/Tests/FIRGetAccountInfoResponseTests.m
@@ -0,0 +1,248 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthInternalErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRGetAccountInfoRequest.h"
+#import "FIRGetAccountInfoResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kUsersKey
+ @brief the name of the "users" property in the response.
+ */
+static NSString *const kUsersKey = @"users";
+
+/** @var kVerifiedProviderKey
+ @brief The name of the "VerifiedProvider" property in the response.
+ */
+static NSString *const kProviderUserInfoKey = @"providerUserInfo";
+
+/** @var kPhotoUrlKey
+ @brief The name of the "photoURL" property in the response.
+ */
+static NSString *const kPhotoUrlKey = @"photoUrl";
+
+/** @var kTestPhotoURL
+ @brief The fake photoUrl property value in the response.
+ */
+static NSString *const kTestPhotoURL = @"testPhotoURL";
+
+/** @var kTestAccessToken
+ @brief testing token.
+ */
+static NSString *const kTestAccessToken = @"testAccessToken";
+
+/** @var kProviderIDkey
+ @brief The name of the "provider ID" property in the response.
+ */
+static NSString *const kProviderIDkey = @"providerId";
+
+/** @var kTestProviderID
+ @brief The fake providerID property value in the response.
+ */
+static NSString *const kTestProviderID = @"testProviderID";
+
+/** @var kDisplayNameKey
+ @brief The name of the "Display Name" property in the response.
+ */
+static NSString *const kDisplayNameKey = @"displayName";
+
+/** @var kTestDisplayName
+ @brief The fake DisplayName property value in the response.
+ */
+static NSString *const kTestDisplayName = @"DisplayName";
+
+/** @var kFederatedIDKey
+ @brief The name of the "federated Id" property in the response.
+ */
+static NSString *const kFederatedIDKey = @"federatedId";
+
+/** @var kTestFederatedID
+ @brief The fake federated Id property value in the response.
+ */
+static NSString *const kTestFederatedID = @"testFederatedId";
+
+/** @var kEmailKey
+ @brief The name of the "Email" property in the response.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kTestEmail
+ @brief The fake email property value in the response.
+ */
+static NSString *const kTestEmail = @"testEmail";
+
+/** @var kPasswordHashKey
+ @brief The name of the "password hash" property in the response.
+ */
+static NSString *const kPasswordHashKey = @"passwordHash";
+
+/** @var kTestPasswordHash
+ @brief The fake password hash property value in the response.
+ */
+static NSString *const kTestPasswordHash = @"testPasswordHash";
+
+/** @var kLocalIDKey
+ @brief The key for the "localID" value in the response.
+ */
+static NSString *const kLocalIDKey = @"localId";
+
+/** @var kTestLocalID
+ @brief The fake @c localID for testing in the response.
+ */
+static NSString *const kTestLocalID = @"testLocalId";
+
+/** @var kEmailVerifiedKey
+ @brief The key for the "emailVerified" value in the response.
+ */
+static NSString *const kEmailVerifiedKey = @"emailVerified";
+
+/** @class FIRGetAccountInfoResponseTests
+ @brief Tests for @c FIRGetAccountInfoResponse.
+ */
+@interface FIRGetAccountInfoResponseTests : XCTestCase
+@end
+@implementation FIRGetAccountInfoResponseTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testGetAccountInfoUnexpectedResponseError
+ @brief This test simulates an unexpected response returned from server in @c GetAccountInfo
+ flow.
+ */
+- (void)testGetAccountInfoUnexpectedResponseError {
+ FIRGetAccountInfoRequest *request =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey accessToken:kTestAccessToken];
+
+ __block BOOL callbackInvoked;
+ __block FIRGetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getAccountInfo:request
+ callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ NSArray *erroneousUserData = @[@"user1Data", @"user2Data"];
+ [_RPCIssuer respondWithJSON:@{
+ kUsersKey : erroneousUserData
+ }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertEqualObjects(RPCError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInternalError);
+ XCTAssertNotNil(RPCError.userInfo[NSUnderlyingErrorKey]);
+ NSError *underlyingError = RPCError.userInfo[NSUnderlyingErrorKey];
+ XCTAssertNotNil(underlyingError);
+ XCTAssertNotNil(underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]);
+ XCTAssertNil(RPCResponse);
+}
+
+/** @fn testSuccessfulGetAccountInfoResponse
+ @brief This test simulates a successful @c GetAccountInfo flow.
+ */
+- (void)testSuccessfulGetAccountInfoResponse {
+ FIRGetAccountInfoRequest *request =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey accessToken:kTestAccessToken];
+
+ __block BOOL callbackInvoked;
+ __block FIRGetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getAccountInfo:request
+ callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ NSArray *users = @[
+ @{
+ kProviderUserInfoKey:@[
+ @{
+ kProviderIDkey : kTestProviderID,
+ kDisplayNameKey: kTestDisplayName,
+ kPhotoUrlKey : kTestPhotoURL,
+ kFederatedIDKey : kTestFederatedID,
+ kEmailKey : kTestEmail,
+ }
+ ],
+ kLocalIDKey : kTestLocalID,
+ kDisplayNameKey : kTestDisplayName,
+ kEmailKey : kTestEmail,
+ kPhotoUrlKey : kTestPhotoURL,
+ kEmailVerifiedKey : @YES,
+ kPasswordHashKey : kTestPasswordHash
+ }
+ ];
+ [_RPCIssuer respondWithJSON:@{
+ @"users" : users,
+ }];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertNotNil(RPCResponse.users);
+ if ([RPCResponse.users count]) {
+ NSURL *responsePhotoUrl = RPCResponse.users[0].photoURL;
+ XCTAssertEqualObjects(responsePhotoUrl.absoluteString, kTestPhotoURL);
+ XCTAssertEqualObjects(RPCResponse.users[0].displayName, kTestDisplayName);
+ XCTAssertEqualObjects(RPCResponse.users[0].email, kTestEmail);
+ XCTAssertEqualObjects(RPCResponse.users[0].localID, kTestLocalID);
+ XCTAssertEqual(RPCResponse.users[0].emailVerified, YES);
+ XCTAssertEqualObjects(RPCResponse.users[0].passwordHash, kTestPasswordHash);
+ NSArray<FIRGetAccountInfoResponseProviderUserInfo *> *providerUserInfo =
+ RPCResponse.users[0].providerUserInfo;
+ if ([providerUserInfo count]) {
+ NSURL *providerInfoPhotoUrl = providerUserInfo[0].photoURL;
+ XCTAssertEqualObjects(providerInfoPhotoUrl.absoluteString, kTestPhotoURL);
+ XCTAssertEqualObjects(providerUserInfo[0].providerID, kTestProviderID);
+ XCTAssertEqualObjects(providerUserInfo[0].displayName, kTestDisplayName);
+ XCTAssertEqualObjects(providerUserInfo[0].federatedID, kTestFederatedID);
+ XCTAssertEqualObjects(providerUserInfo[0].email, kTestEmail);
+ }
+ }
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m b/Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m
new file mode 100644
index 0000000..d5a22aa
--- /dev/null
+++ b/Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m
@@ -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.
+ */
+
+#import <XCTest/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRGetOOBConfirmationCodeRequest.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for the test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key=APIKey";
+
+/** @var kRequestTypeKey
+ @brief The name of the required "requestType" property in the request.
+ */
+static NSString *const kRequestTypeKey = @"requestType";
+
+/** @var kPasswordResetRequestTypeValue
+ @brief The value for the "PASSWORD_RESET" request type.
+ */
+static NSString *const kPasswordResetRequestTypeValue = @"PASSWORD_RESET";
+
+/** @var kVerifyEmailRequestTypeValue
+ @brief The value for the "VERIFY_EMAIL" request type.
+ */
+static NSString *const kVerifyEmailRequestTypeValue = @"VERIFY_EMAIL";
+
+/** @var kEmailKey
+ @brief The name of the "email" property in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kTestEmail
+ @brief Testing user email adadress.
+ */
+static NSString *const kTestEmail = @"test@gmail.com";
+
+/** @var kAccessTokenKey
+ @brief The name of the "accessToken" property in the request.
+ */
+static NSString *const kAccessTokenKey = @"idToken";
+
+/** @var kTestAccessToken
+ @brief Testing access token.
+ */
+static NSString *const kTestAccessToken = @"ACCESS_TOKEN";
+
+/** @class FIRGetOOBConfirmationCodeRequestTests
+ @brief Tests for @c FIRGetOOBConfirmationCodeRequest.
+ */
+@interface FIRGetOOBConfirmationCodeRequestTests : XCTestCase
+@end
+@implementation FIRGetOOBConfirmationCodeRequestTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testPasswordResetRequest
+ @brief Tests the encoding of a password reset request.
+ */
+- (void)testPasswordResetRequest {
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRGetOOBConfirmationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kRequestTypeKey], kPasswordResetRequestTypeValue);
+}
+
+/** @fn testEmailVerificationRequest
+ @brief Tests the encoding of an email verification request.
+ */
+- (void)testEmailVerificationRequest {
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest verifyEmailRequestWithAccessToken:kTestAccessToken
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRGetOOBConfirmationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAccessTokenKey], kTestAccessToken);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kRequestTypeKey], kVerifyEmailRequestTypeValue);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRGetOOBConfirmationCodeResponseTests.m b/Example/Auth/Tests/FIRGetOOBConfirmationCodeResponseTests.m
new file mode 100644
index 0000000..98c9d8e
--- /dev/null
+++ b/Example/Auth/Tests/FIRGetOOBConfirmationCodeResponseTests.m
@@ -0,0 +1,320 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRGetOOBConfirmationCodeRequest.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestEmail
+ @brief Testing user email adadress.
+ */
+static NSString *const kTestEmail = @"test@gmail.com";
+
+/** @var kTestAccessToken
+ @brief Testing access token.
+ */
+static NSString *const kTestAccessToken = @"ACCESS_TOKEN";
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kOOBCodeKey
+ @brief The name of the field in the response JSON for the OOB code.
+ */
+static NSString *const kOOBCodeKey = @"oobCode";
+
+/** @var kTestOOBCode
+ @brief Fake OOB Code used for testing.
+ */
+static NSString *const kTestOOBCode = @"OOBCode";
+
+/** @var kEmailNotFoundMessage
+ @brief The value of the "message" field returned for an "email not found" error.
+ */
+static NSString *const kEmailNotFoundMessage = @"EMAIL_NOT_FOUND: fake custom message";
+
+/** @var kInvalidEmailErrorMessage
+ @brief The error returned by the server if the email is invalid.
+ */
+static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL:";
+
+/** @var kInvalidMessagePayloadErrorMessage
+ @brief This is the prefix for the error message the server responds with if an invalid message
+ payload was sent.
+ */
+static NSString *const kInvalidMessagePayloadErrorMessage = @"INVALID_MESSAGE_PAYLOAD";
+
+/** @var kInvalidSenderErrorMessage
+ @brief This is the prefix for the error message the server responds with if invalid sender is
+ used to send the email for updating user's email address.
+ */
+static NSString *const kInvalidSenderErrorMessage = @"INVALID_SENDER";
+
+
+/** @var kInvalidRecipientEmailErrorMessage
+ @brief This is the prefix for the error message the server responds with if the recipient email
+ is invalid.
+ */
+static NSString *const kInvalidRecipientEmailErrorMessage = @"INVALID_RECIPIENT_EMAIL";
+
+/** @class FIRGetOOBConfirmationCodeResponseTests
+ @brief Tests for @c FIRGetOOBConfirmationCodeResponse.
+ */
+@interface FIRGetOOBConfirmationCodeResponseTests : XCTestCase
+@end
+@implementation FIRGetOOBConfirmationCodeResponseTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testSuccessfulPasswordResetResponse
+ @brief This test simulates a complete password reset response (with OOB Code) and makes sure
+ it succeeds, and we get the OOB Code decoded correctly.
+ */
+- (void)testSuccessfulPasswordResetResponse {
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRGetOOBConfirmationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ kOOBCodeKey : kTestOOBCode
+ }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.OOBCode, kTestOOBCode);
+}
+
+/** @fn testSuccessfulPasswordResetResponseWithoutOOBCode
+ @brief This test simulates a password reset request where we don't receive the optional OOBCode
+ response value. It should still succeed.
+ */
+- (void)testSuccessfulPasswordResetResponseWithoutOOBCode {
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRGetOOBConfirmationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{}];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertNil(RPCResponse.OOBCode);
+}
+
+/** @fn testEmailNotFoundError
+ @brief This test checks for email not found responses, and makes sure they are decoded to the
+ correct error response.
+ */
+- (void)testEmailNotFoundError {
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRGetOOBConfirmationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kEmailNotFoundMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertEqualObjects(RPCError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserNotFound);
+ XCTAssertNil(RPCResponse);
+}
+
+/** @fn testInvalidEmailError
+ @brief This test checks for the INVALID_EMAIL error message from the backend.
+ */
+- (void)testInvalidEmailError {
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRGetOOBConfirmationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertEqualObjects(RPCError.domain, FIRAuthErrorDomain);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail);
+ XCTAssertNil(RPCResponse);
+}
+
+/** @fn testInvalidMessagePayloadError
+ @brief Tests for @c FIRAuthErrorCodeInvalidMessagePayload.
+ */
+- (void)testInvalidMessagePayloadError {
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRGetOOBConfirmationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidMessagePayloadErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidMessagePayload);
+}
+
+/** @fn testInvalidSenderError
+ @brief Tests for @c FIRAuthErrorCodeInvalidSender.
+ */
+- (void)testInvalidSenderError {
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRGetOOBConfirmationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidSenderErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidSender);
+}
+
+/** @fn testInvalidRecipientEmailError
+ @brief Tests for @c FIRAuthErrorCodeInvalidRecipientEmail.
+ */
+- (void)testInvalidRecipientEmailError {
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail
+ APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRGetOOBConfirmationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidRecipientEmailErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidRecipientEmail);
+}
+
+/** @fn testSuccessfulEmailVerificationResponse
+ @brief This test is really not much different than the original test for password reset. But
+ it's here for completeness sake.
+ */
+- (void)testSuccessfulEmailVerificationResponse {
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRGetOOBConfirmationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ kOOBCodeKey : kTestOOBCode
+ }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.OOBCode, kTestOOBCode);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRGitHubAuthProviderTests.m b/Example/Auth/Tests/FIRGitHubAuthProviderTests.m
new file mode 100644
index 0000000..dfcd87f
--- /dev/null
+++ b/Example/Auth/Tests/FIRGitHubAuthProviderTests.m
@@ -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 <XCTest/XCTest.h>
+
+#import "GitHub/FIRGitHubAuthProvider.h"
+#import "FIRAuthCredential_Internal.h"
+#import "FIRVerifyAssertionRequest.h"
+
+/** @var kGitHubToken
+ @brief A testing GitHub token.
+ */
+static NSString *const kGitHubToken = @"Token";
+
+/** @var kAPIKey
+ @brief A testing API Key.
+ */
+static NSString *const kAPIKey = @"APIKey";
+
+/** @class FIRGitHubAuthProviderTests
+ @brief Tests for @c FIRGitHubAuthProvider
+ */
+@interface FIRGitHubAuthProviderTests : XCTestCase
+@end
+@implementation FIRGitHubAuthProviderTests
+
+/** @fn testCredentialWithToken
+ @brief Tests the @c credentialWithToken method to make sure the credential it produces populates
+ the appropriate fields in a verify assertion request.
+ */
+- (void)testCredentialWithToken {
+ FIRAuthCredential *credential = [FIRGitHubAuthProvider credentialWithToken:kGitHubToken];
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kAPIKey providerID:FIRGitHubAuthProviderID];
+ [credential prepareVerifyAssertionRequest:request];
+ XCTAssertEqualObjects(request.providerAccessToken, kGitHubToken);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRPhoneAuthProviderTests.m b/Example/Auth/Tests/FIRPhoneAuthProviderTests.m
new file mode 100644
index 0000000..f907601
--- /dev/null
+++ b/Example/Auth/Tests/FIRPhoneAuthProviderTests.m
@@ -0,0 +1,550 @@
+/*
+ * 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/XCTest.h>
+
+#import "Phone/FIRPhoneAuthProvider.h"
+#import "Phone/FIRPhoneAuthCredential_Internal.h"
+#import "Phone/NSString+FIRAuth.h"
+#import "FIRAuthAPNSToken.h"
+#import "FIRAuthAPNSTokenManager.h"
+#import "FIRAuthAppCredential.h"
+#import "FIRAuthAppCredentialManager.h"
+#import "FIRAuthNotificationManager.h"
+#import "FIRAuth_Internal.h"
+#import "FIRAuthCredential_Internal.h"
+#import "FIRAuthErrorUtils.h"
+#import "FIRAuthGlobalWorkQueue.h"
+#import "FIRAuthBackend.h"
+#import "FIRSendVerificationCodeRequest.h"
+#import "FIRSendVerificationCodeResponse.h"
+#import "FIRVerifyClientRequest.h"
+#import "FIRVerifyClientResponse.h"
+#import "FIRApp+FIRAuthUnitTests.h"
+#import "OCMStubRecorder+FIRAuthUnitTests.h"
+#import <OCMock/OCMock.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kTestPhoneNumber
+ @brief A testing phone number.
+ */
+static NSString *const kTestPhoneNumber = @"55555555";
+
+/** @var kTestInvalidPhoneNumber
+ @brief An invalid testing phone number.
+ */
+static NSString *const kTestInvalidPhoneNumber = @"555+!*55555";
+
+/** @var kTestVerificationID
+ @brief A testing verfication ID.
+ */
+static NSString *const kTestVerificationID = @"verificationID";
+
+/** @var kTestReceipt
+ @brief A fake receipt for testing.
+ */
+static NSString *const kTestReceipt = @"receipt";
+
+/** @var kTestSecret
+ @brief A fake secret for testing.
+ */
+static NSString *const kTestSecret = @"secret";
+
+/** @var kTestOldReceipt
+ @brief A fake old receipt for testing.
+ */
+static NSString *const kTestOldReceipt = @"old_receipt";
+
+/** @var kTestOldSecret
+ @brief A fake old secret for testing.
+ */
+static NSString *const kTestOldSecret = @"old_secret";
+
+
+/** @var kTestVerificationCode
+ @brief A fake verfication code.
+ */
+static NSString *const kTestVerificationCode = @"verificationCode";
+
+/** @var kTestTimeout
+ @brief A fake timeout value for waiting for push notification.
+ */
+static const NSTimeInterval kTestTimeout = 5;
+
+/** @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 = 1;
+
+/** @class FIRPhoneAuthProviderTests
+ @brief Tests for @c FIRPhoneAuthProvider
+ */
+@interface FIRPhoneAuthProviderTests : XCTestCase
+@end
+
+@implementation FIRPhoneAuthProviderTests {
+ /** @var _mockBackend
+ @brief The mock @c FIRAuthBackendImplementation .
+ */
+ id _mockBackend;
+
+ /** @var _provider
+ @brief The @c FIRPhoneAuthProvider instance under test.
+ */
+ FIRPhoneAuthProvider *_provider;
+
+ /** @var _mockAuth
+ @brief The mock @c FIRAuth instance associated with @c _provider .
+ */
+ id _mockAuth;
+
+ /** @var _mockAPNSTokenManager
+ @brief The mock @c FIRAuthAPNSTokenManager instance associated with @c _mockAuth .
+ */
+ id _mockAPNSTokenManager;
+
+ /** @var _mockAppCredentialManager
+ @brief The mock @c FIRAuthAppCredentialManager instance associated with @c _mockAuth .
+ */
+ id _mockAppCredentialManager;
+
+ /** @var _mockNotificationManager
+ @brief The mock @c FIRAuthNotificationManager instance associated with @c _mockAuth .
+ */
+ id _mockNotificationManager;
+}
+
+- (void)setUp {
+ [super setUp];
+ _mockBackend = OCMProtocolMock(@protocol(FIRAuthBackendImplementation));
+ [FIRAuthBackend setBackendImplementation:_mockBackend];
+ _mockAuth = OCMClassMock([FIRAuth class]);
+ _mockAPNSTokenManager = OCMClassMock([FIRAuthAPNSTokenManager class]);
+ OCMStub([_mockAuth tokenManager]).andReturn(_mockAPNSTokenManager);
+ _mockAppCredentialManager = OCMClassMock([FIRAuthAppCredentialManager class]);
+ OCMStub([_mockAuth appCredentialManager]).andReturn(_mockAppCredentialManager);
+ _mockNotificationManager = OCMClassMock([FIRAuthNotificationManager class]);
+ OCMStub([_mockAuth notificationManager]).andReturn(_mockNotificationManager);
+ _provider = [FIRPhoneAuthProvider providerWithAuth:_mockAuth];
+}
+
+- (void)tearDown {
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testCredentialWithVerificationID
+ @brief Tests the @c credentialWithToken method to make sure that it returns a valid
+ FIRAuthCredential instance.
+ */
+- (void)testCredentialWithVerificationID {
+ FIRPhoneAuthCredential *credential =
+ [_provider credentialWithVerificationID:kTestVerificationID
+ verificationCode:kTestVerificationCode];
+ XCTAssertEqualObjects(credential.verificationID, kTestVerificationID);
+ XCTAssertEqualObjects(credential.verificationCode, kTestVerificationCode);
+ XCTAssertNil(credential.temporaryProof);
+ XCTAssertNil(credential.phoneNumber);
+}
+
+/** @fn testVerifyEmptyPhoneNumber
+ @brief Tests a failed invocation @c verifyPhoneNumber:completion: because an empty phone
+ number was provided.
+ */
+- (void)testVerifyEmptyPhoneNumber {
+ // Empty phone number is checked on the client side so no backend RPC is mocked.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [_provider verifyPhoneNumber:@""
+ completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeMissingPhoneNumber);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+}
+
+/** @fn testVerifyInvalidPhoneNumber
+ @brief Tests a failed invocation @c verifyPhoneNumber:completion: because an invalid phone
+ number was provided.
+ */
+- (void)testVerifyInvalidPhoneNumber {
+ OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); });
+ OCMStub([_mockAppCredentialManager credential])
+ .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:kTestSecret]);
+ OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSendVerificationCodeRequest *request,
+ FIRSendVerificationCodeResponseCallback callback) {
+ XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber);
+ XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt);
+ XCTAssertEqualObjects(request.appCredential.secret, kTestSecret);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback(nil, [FIRAuthErrorUtils invalidPhoneNumberErrorWithMessage:nil]);
+ });
+ });
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [_provider verifyPhoneNumber:kTestPhoneNumber
+ completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(verificationID);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidPhoneNumber);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+ OCMVerifyAll(_mockNotificationManager);
+ OCMVerifyAll(_mockAppCredentialManager);
+}
+
+/** @fn testVerifyPhoneNumber
+ @brief Tests a successful invocation of @c verifyPhoneNumber:completion:.
+ */
+- (void)testVerifyPhoneNumber {
+ OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); });
+ OCMStub([_mockAppCredentialManager credential])
+ .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:kTestSecret]);
+ OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSendVerificationCodeRequest *request,
+ FIRSendVerificationCodeResponseCallback callback) {
+ XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber);
+ XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt);
+ XCTAssertEqualObjects(request.appCredential.secret, kTestSecret);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSendVerificationCodeResponse = OCMClassMock([FIRSendVerificationCodeResponse class]);
+ OCMStub([mockSendVerificationCodeResponse verificationID]).andReturn(kTestVerificationID);
+ callback(mockSendVerificationCodeResponse, nil);
+ });
+ });
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [_provider verifyPhoneNumber:kTestPhoneNumber
+ completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(verificationID, kTestVerificationID);
+ XCTAssertEqualObjects(verificationID.fir_authPhoneNumber, kTestPhoneNumber);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+ OCMVerifyAll(_mockNotificationManager);
+ OCMVerifyAll(_mockAppCredentialManager);
+}
+
+/** @fn testNotForwardingNotification
+ @brief Tests returning an error for the app failing to forward notification.
+ */
+- (void)testNotForwardingNotification {
+ OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(NO); });
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [_provider verifyPhoneNumber:kTestPhoneNumber
+ completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(verificationID);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeNotificationNotForwarded);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockNotificationManager);
+}
+
+/** @fn testMissingAPNSToken
+ @brief Tests returning an error for the app failing to provide an APNS device token.
+ */
+- (void)testMissingAPNSToken {
+ OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); });
+ OCMExpect([_mockAppCredentialManager credential]).andReturn(nil);
+ OCMExpect([_mockAPNSTokenManager getTokenWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthAPNSTokenCallback callback) { callback(nil); });
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [_provider verifyPhoneNumber:kTestPhoneNumber
+ completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(verificationID);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeMissingAppToken);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockNotificationManager);
+ OCMVerifyAll(_mockAppCredentialManager);
+ OCMVerifyAll(_mockAPNSTokenManager);
+}
+
+/** @fn testVerifyClient
+ @brief Tests verifying client before sending verification code.
+ */
+- (void)testVerifyClient {
+ OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); });
+ OCMExpect([_mockAppCredentialManager credential]).andReturn(nil);
+ NSData *data = [@"!@#$%^" dataUsingEncoding:NSUTF8StringEncoding];
+ FIRAuthAPNSToken *token = [[FIRAuthAPNSToken alloc] initWithData:data
+ type:FIRAuthAPNSTokenTypeProd];
+ OCMExpect([_mockAPNSTokenManager getTokenWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthAPNSTokenCallback callback) { callback(token); });
+ // Expect verify client request to the backend.
+ OCMExpect([_mockBackend verifyClient:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyClientRequest *request,
+ FIRVerifyClientResponseCallback callback) {
+ XCTAssertEqualObjects(request.appToken, @"21402324255E");
+ XCTAssertFalse(request.isSandbox);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVerifyClientResponse = OCMClassMock([FIRVerifyClientResponse class]);
+ OCMStub([mockVerifyClientResponse receipt]).andReturn(kTestReceipt);
+ OCMStub([mockVerifyClientResponse suggestedTimeOutDate])
+ .andReturn([NSDate dateWithTimeIntervalSinceNow:kTestTimeout]);
+ callback(mockVerifyClientResponse, nil);
+ });
+ });
+ // Mock receiving of push notification.
+ OCMExpect([[_mockAppCredentialManager ignoringNonObjectArgs]
+ didStartVerificationWithReceipt:OCMOCK_ANY timeout:0 callback:OCMOCK_ANY])
+ .andCallIdDoubleIdBlock(^(NSString *receipt,
+ NSTimeInterval timeout,
+ FIRAuthAppCredentialCallback callback) {
+ XCTAssertEqualObjects(receipt, kTestReceipt);
+ // Unfortunately 'ignoringNonObjectArgs' means the real value for 'timeout' doesn't get passed
+ // into the block either, so we can't verify it here.
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:kTestSecret]);
+ });
+ });
+ // Expect send verification code request to the backend.
+ OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSendVerificationCodeRequest *request,
+ FIRSendVerificationCodeResponseCallback callback) {
+ XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber);
+ XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt);
+ XCTAssertEqualObjects(request.appCredential.secret, kTestSecret);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSendVerificationCodeResponse = OCMClassMock([FIRSendVerificationCodeResponse class]);
+ OCMStub([mockSendVerificationCodeResponse verificationID]).andReturn(kTestVerificationID);
+ callback(mockSendVerificationCodeResponse, nil);
+ });
+ });
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [_provider verifyPhoneNumber:kTestPhoneNumber
+ completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(verificationID, kTestVerificationID);
+ XCTAssertEqualObjects(verificationID.fir_authPhoneNumber, kTestPhoneNumber);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+ OCMVerifyAll(_mockNotificationManager);
+ OCMVerifyAll(_mockAppCredentialManager);
+ OCMVerifyAll(_mockAPNSTokenManager);
+}
+
+/** @fn testSendVerificationCodeFailedRetry
+ @brief Tests failed retry after failing to send verification code.
+ */
+- (void)testSendVerificationCodeFailedRetry {
+ OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); });
+
+ // Expect twice due to null check consumes one expectation.
+ OCMExpect([_mockAppCredentialManager credential])
+ .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestOldReceipt
+ secret:kTestOldSecret]);
+ OCMExpect([_mockAppCredentialManager credential])
+ .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestOldReceipt
+ secret:kTestOldSecret]);
+ NSData *data = [@"!@#$%^" dataUsingEncoding:NSUTF8StringEncoding];
+ FIRAuthAPNSToken *token = [[FIRAuthAPNSToken alloc] initWithData:data
+ type:FIRAuthAPNSTokenTypeProd];
+
+ // Expect first sendVerificationCode request to the backend, with request containing old app
+ // credential.
+ OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSendVerificationCodeRequest *request,
+ FIRSendVerificationCodeResponseCallback callback) {
+ XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber);
+ XCTAssertEqualObjects(request.appCredential.receipt, kTestOldReceipt);
+ XCTAssertEqualObjects(request.appCredential.secret, kTestOldSecret);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback(nil, [FIRAuthErrorUtils invalidAppCredentialWithMessage:nil]);
+ });
+ });
+
+ // Expect send verification code request to the backend, with request containing new app
+ // credential data.
+ OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSendVerificationCodeRequest *request,
+ FIRSendVerificationCodeResponseCallback callback) {
+ XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber);
+ XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt);
+ XCTAssertEqualObjects(request.appCredential.secret, kTestSecret);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback(nil, [FIRAuthErrorUtils invalidAppCredentialWithMessage:nil]);
+ });
+ });
+
+ OCMExpect([_mockAPNSTokenManager getTokenWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthAPNSTokenCallback callback) { callback(token); });
+ // Expect verify client request to the backend.
+ OCMExpect([_mockBackend verifyClient:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyClientRequest *request,
+ FIRVerifyClientResponseCallback callback) {
+ XCTAssertEqualObjects(request.appToken, @"21402324255E");
+ XCTAssertFalse(request.isSandbox);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVerifyClientResponse = OCMClassMock([FIRVerifyClientResponse class]);
+ OCMStub([mockVerifyClientResponse receipt]).andReturn(kTestReceipt);
+ OCMStub([mockVerifyClientResponse suggestedTimeOutDate])
+ .andReturn([NSDate dateWithTimeIntervalSinceNow:kTestTimeout]);
+ callback(mockVerifyClientResponse, nil);
+ });
+ });
+
+ // Mock receiving of push notification.
+ OCMStub([[_mockAppCredentialManager ignoringNonObjectArgs]
+ didStartVerificationWithReceipt:OCMOCK_ANY timeout:0 callback:OCMOCK_ANY])
+ .andCallIdDoubleIdBlock(^(NSString *receipt,
+ NSTimeInterval timeout,
+ FIRAuthAppCredentialCallback callback) {
+ XCTAssertEqualObjects(receipt, kTestReceipt);
+ // Unfortunately 'ignoringNonObjectArgs' means the real value for 'timeout' doesn't get passed
+ // into the block either, so we can't verify it here.
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:kTestSecret]);
+ });
+ });
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [_provider verifyPhoneNumber:kTestPhoneNumber
+ completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ XCTAssertNil(verificationID);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeInternalError);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+ OCMVerifyAll(_mockNotificationManager);
+ OCMVerifyAll(_mockAppCredentialManager);
+ OCMVerifyAll(_mockAPNSTokenManager);
+}
+
+/** @fn testSendVerificationCodeSuccessFulRetry
+ @brief Tests successful retry after failing to send verification code.
+ */
+- (void)testSendVerificationCodeSuccessFulRetry {
+ OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); });
+
+ // Expect twice due to null check consumes one expectation.
+ OCMExpect([_mockAppCredentialManager credential])
+ .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestOldReceipt
+ secret:kTestOldSecret]);
+ OCMExpect([_mockAppCredentialManager credential])
+ .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestOldReceipt
+ secret:kTestOldSecret]);
+ NSData *data = [@"!@#$%^" dataUsingEncoding:NSUTF8StringEncoding];
+ FIRAuthAPNSToken *token = [[FIRAuthAPNSToken alloc] initWithData:data
+ type:FIRAuthAPNSTokenTypeProd];
+
+ // Expect first sendVerificationCode request to the backend, with request containing old app
+ // credential.
+ OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSendVerificationCodeRequest *request,
+ FIRSendVerificationCodeResponseCallback callback) {
+ XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber);
+ XCTAssertEqualObjects(request.appCredential.receipt, kTestOldReceipt);
+ XCTAssertEqualObjects(request.appCredential.secret, kTestOldSecret);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback(nil, [FIRAuthErrorUtils invalidAppCredentialWithMessage:nil]);
+ });
+ });
+
+ // Expect send verification code request to the backend, with request containing new app
+ // credential data.
+ OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSendVerificationCodeRequest *request,
+ FIRSendVerificationCodeResponseCallback callback) {
+ XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber);
+ XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt);
+ XCTAssertEqualObjects(request.appCredential.secret, kTestSecret);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSendVerificationCodeResponse = OCMClassMock([FIRSendVerificationCodeResponse class]);
+ OCMStub([mockSendVerificationCodeResponse verificationID]).andReturn(kTestVerificationID);
+ callback(mockSendVerificationCodeResponse, nil);
+ });
+ });
+
+ OCMExpect([_mockAPNSTokenManager getTokenWithCallback:OCMOCK_ANY])
+ .andCallBlock1(^(FIRAuthAPNSTokenCallback callback) { callback(token); });
+ // Expect verify client request to the backend.
+ OCMExpect([_mockBackend verifyClient:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyClientRequest *request,
+ FIRVerifyClientResponseCallback callback) {
+ XCTAssertEqualObjects(request.appToken, @"21402324255E");
+ XCTAssertFalse(request.isSandbox);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVerifyClientResponse = OCMClassMock([FIRVerifyClientResponse class]);
+ OCMStub([mockVerifyClientResponse receipt]).andReturn(kTestReceipt);
+ OCMStub([mockVerifyClientResponse suggestedTimeOutDate])
+ .andReturn([NSDate dateWithTimeIntervalSinceNow:kTestTimeout]);
+ callback(mockVerifyClientResponse, nil);
+ });
+ });
+
+ // Mock receiving of push notification.
+ OCMStub([[_mockAppCredentialManager ignoringNonObjectArgs]
+ didStartVerificationWithReceipt:OCMOCK_ANY timeout:0 callback:OCMOCK_ANY])
+ .andCallIdDoubleIdBlock(^(NSString *receipt,
+ NSTimeInterval timeout,
+ FIRAuthAppCredentialCallback callback) {
+ XCTAssertEqualObjects(receipt, kTestReceipt);
+ // Unfortunately 'ignoringNonObjectArgs' means the real value for 'timeout' doesn't get passed
+ // into the block either, so we can't verify it here.
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:kTestSecret]);
+ });
+ });
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [_provider verifyPhoneNumber:kTestPhoneNumber
+ completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(verificationID, kTestVerificationID);
+ XCTAssertEqualObjects(verificationID.fir_authPhoneNumber, kTestPhoneNumber);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+ OCMVerifyAll(_mockNotificationManager);
+ OCMVerifyAll(_mockAppCredentialManager);
+ OCMVerifyAll(_mockAPNSTokenManager);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/FIRResetPasswordRequestTests.m b/Example/Auth/Tests/FIRResetPasswordRequestTests.m
new file mode 100644
index 0000000..d0ccc5d
--- /dev/null
+++ b/Example/Auth/Tests/FIRResetPasswordRequestTests.m
@@ -0,0 +1,101 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRResetPasswordRequest.h"
+#import "FIRResetPasswordResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kTestOOBCode
+ @brief Fake OOBCode used for testing.
+ */
+static NSString *const kTestOOBCode = @"OOBCode";
+
+/** @var kTestNewPassword
+ @brief Fake new password used for testing.
+ */
+static NSString *const kTestNewPassword = @"newPassword:-)";
+
+/** @var kOOBCodeKey
+ @brief The "resetPassword" key.
+ */
+static NSString *const kOOBCodeKey = @"oobCode";
+
+/** @var knewPasswordKey
+ @brief The "newPassword" key.
+ */
+static NSString *const knewPasswordKey = @"newPassword";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPassword?key=APIKey";
+
+/** @class FIRResetPasswordRequestTests
+ @brief Tests for @c FIRResetPasswordRequest.
+ */
+@interface FIRResetPasswordRequestTest : XCTestCase
+@end
+
+@implementation FIRResetPasswordRequestTest {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testResetPasswordRequest
+ @brief Tests the reset password reqeust.
+ */
+- (void)testResetPasswordRequest {
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey
+ oobCode:kTestOOBCode
+ newPassword:kTestNewPassword];
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+
+ }];
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[knewPasswordKey], kTestNewPassword);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kOOBCodeKey], kTestOOBCode);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRResetPasswordResponseTests.m b/Example/Auth/Tests/FIRResetPasswordResponseTests.m
new file mode 100644
index 0000000..51f1155
--- /dev/null
+++ b/Example/Auth/Tests/FIRResetPasswordResponseTests.m
@@ -0,0 +1,257 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRResetPasswordRequest.h"
+#import "FIRResetPasswordResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kUserDisabledErrorMessage
+ @brief This is the error message the server will respond with if the user's account has been
+ disabled.
+ */
+static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED";
+
+/** @var kOperationNotAllowedErrorMessage
+ @brief This is the error message the server will respond with if Admin disables IDP specified by
+ provider.
+ */
+static NSString *const kOperationNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED";
+
+/** @var kExpiredActionCodeErrorMessage
+ @brief This is the error message the server will respond with if the action code is expired.
+ */
+static NSString *const kExpiredActionCodeErrorMessage = @"EXPIRED_OOB_CODE";
+
+/** @var kInvalidActionCodeErrorMessage
+ @brief This is the error message the server will respond with if the action code is invalid.
+ */
+static NSString *const kInvalidActionCodeErrorMessage = @"INVALID_OOB_CODE";
+
+/** @var kWeakPasswordErrorMessagePrefix
+ @brief This is the prefix for the error message the server responds with if user's new password
+ to be set is too weak.
+ */
+static NSString *const kWeakPasswordErrorMessagePrefix = @"WEAK_PASSWORD : ";
+
+/** @var kTestOOBCode
+ @brief Fake OOBCode used for testing.
+ */
+static NSString *const kTestOOBCode = @"OOBCode";
+
+/** @var kTestNewPassword
+ @brief Fake new password used for testing.
+ */
+static NSString *const kTestNewPassword = @"newPassword";
+
+/** @var kEmailKey
+ @brief The key for the email returned in the response.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kRequestTypeKey
+ @brief The key for the request type returned in the response.
+ */
+static NSString *const kRequestTypeKey = @"requestType";
+
+/** @var kTestEmail
+ @brief The email returned in the response.
+ */
+static NSString *const kTestEmail = @"test@email.com";
+
+/** @var kResetPasswordExpectedRequestType.
+ @brief The expected request type returned for reset password request.
+ */
+static NSString *const kExpectedResetPasswordRequestType = @"PASSWORD_RESET";
+
+/** @class FIRResetPasswordRequestTests
+ @brief Tests for @c FIRResetPasswordRequest.
+ */
+@interface FIRResetPasswordResponseTests : XCTestCase
+@end
+
+@implementation FIRResetPasswordResponseTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testUserDisabledError
+ @brief Tests for @c FIRAuthErrorCodeUserDisabled.
+ */
+- (void)testUserDisabledError {
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey
+ oobCode:kTestOOBCode
+ newPassword:kTestNewPassword];
+ __block BOOL callbackInvoked;
+ __block FIRResetPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kUserDisabledErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled);
+}
+
+/** @fn testOperationNotAllowedError
+ @brief Tests for @c FIRAuthErrorCodeOperationNotAllowed.
+ */
+- (void)testOperationNotAllowedError {
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey
+ oobCode:kTestOOBCode
+ newPassword:kTestNewPassword];
+ __block BOOL callbackInvoked;
+ __block FIRResetPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kOperationNotAllowedErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed);
+}
+
+/** @fn testOOBExpiredError
+ @brief Tests for @c FIRAuthErrorCodeExpiredActionCode.
+ */
+- (void)testOOBExpiredError {
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey
+ oobCode:kTestOOBCode
+ newPassword:kTestNewPassword];
+ __block BOOL callbackInvoked;
+ __block FIRResetPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kExpiredActionCodeErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeExpiredActionCode);
+}
+
+/** @fn testOOBInvalidError
+ @brief Tests for @c FIRAuthErrorCodeInvalidActionCode.
+ */
+- (void)testOOBInvalidError {
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey
+ oobCode:kTestOOBCode
+ newPassword:kTestNewPassword];
+ __block BOOL callbackInvoked;
+ __block FIRResetPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidActionCodeErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidActionCode);
+}
+
+/** @fn testWeakPasswordError
+ @brief Tests for @c FIRAuthErrorCodeWeakPassword.
+ */
+- (void)testWeakPasswordError {
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey
+ oobCode:kTestOOBCode
+ newPassword:kTestNewPassword];
+ __block BOOL callbackInvoked;
+ __block FIRResetPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kWeakPasswordErrorMessagePrefix];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeWeakPassword);
+}
+
+/** @fn testSuccessfulResetPassword
+ @brief Tests a successful reset password flow.
+ */
+- (void)testSuccessfulResetPassword {
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey
+ oobCode:kTestOOBCode
+ newPassword:kTestNewPassword];
+ __block BOOL callbackInvoked;
+ __block FIRResetPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithJSON:@{
+ kEmailKey : kTestEmail,
+ kRequestTypeKey : kExpectedResetPasswordRequestType
+ }];
+ XCTAssert(callbackInvoked);
+ XCTAssertEqualObjects(RPCResponse.email, kTestEmail);
+ XCTAssertEqualObjects(RPCResponse.requestType, kExpectedResetPasswordRequestType);
+ XCTAssertNil(RPCError);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRSendVerificationCodeRequestTests.m b/Example/Auth/Tests/FIRSendVerificationCodeRequestTests.m
new file mode 100644
index 0000000..5582d32
--- /dev/null
+++ b/Example/Auth/Tests/FIRSendVerificationCodeRequestTests.m
@@ -0,0 +1,119 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthAppCredential.h"
+#import "FIRAuthBackend.h"
+#import "FIRSendVerificationCodeRequest.h"
+#import "FIRSendVerificationCodeResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kTestPhoneNumber
+ @brief Fake phone number used for testing.
+ */
+static NSString *const kTestPhoneNumber = @"12345678";
+
+/** @var kTestSecret
+ @brief Fake secret used for testing.
+ */
+static NSString *const kTestSecret = @"secret";
+
+/** @var kTestReceipt
+ @brief Fake receipt used for testing.
+ */
+static NSString *const kTestReceipt = @"receipt";
+
+/** @var kPhoneNumberKey
+ @brief The key for the "phone number" value in the request.
+ */
+static NSString *const kPhoneNumberKey = @"phoneNumber";
+
+/** @var kReceiptKey
+ @brief The key for the receipt parameter in the request.
+ */
+static NSString *const kReceiptKey = @"iosReceipt";
+
+/** @var kSecretKey
+ @brief The key for the Secret parameter in the request.
+ */
+static NSString *const kSecretKey = @"iosSecret";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for the test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/sendVerificationCode?key=APIKey";
+
+/** @class FIRSendVerificationCodeRequestTests
+ @brief Tests for @c FIRSendVerificationCodeRequest.
+ */
+@interface FIRSendVerificationCodeRequestTests : XCTestCase
+@end
+
+@implementation FIRSendVerificationCodeRequestTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testSendVerificationCodeRequest
+ @brief Tests the sendVerificationCode request.
+ */
+- (void)testSendVerificationCodeRequest {
+ FIRAuthAppCredential *credential =
+ [[FIRAuthAppCredential alloc]initWithReceipt:kTestReceipt secret:kTestSecret];
+ FIRSendVerificationCodeRequest *request =
+ [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:kTestPhoneNumber
+ appCredential:credential
+ APIKey:kTestAPIKey];
+ XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber);
+ XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt);
+ XCTAssertEqualObjects(request.appCredential.secret, kTestSecret);
+
+ [FIRAuthBackend sendVerificationCode:request
+ callback:^(FIRSendVerificationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPhoneNumberKey], kTestPhoneNumber);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPhoneNumberKey], kTestPhoneNumber);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kReceiptKey], kTestReceipt);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kSecretKey], kTestSecret);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRSendVerificationCodeResponseTests.m b/Example/Auth/Tests/FIRSendVerificationCodeResponseTests.m
new file mode 100644
index 0000000..5a1244b
--- /dev/null
+++ b/Example/Auth/Tests/FIRSendVerificationCodeResponseTests.m
@@ -0,0 +1,221 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthAppCredential.h"
+#import "FIRAuthErrors.h"
+#import "FIRAuthErrorUtils.h"
+#import "FIRAuthBackend.h"
+#import "FIRSendVerificationCodeRequest.h"
+#import "FIRSendVerificationCodeResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kTestPhoneNumber
+ @brief Fake phone number used for testing.
+ */
+static NSString *const kTestPhoneNumber = @"12345678";
+
+/** @var kTestInvalidPhoneNumber
+ @brief An invalid testing phone number.
+ */
+static NSString *const kTestInvalidPhoneNumber = @"555+!*55555";
+
+/** @var kVerificationIDKey
+ @brief Fake key for the test verification ID.
+ */
+static NSString *const kVerificationIDKey = @"sessionInfo";
+
+/** @var kFakeVerificationID
+ @brief Fake verification ID for testing.
+ */
+static NSString *const kFakeVerificationID = @"testVerificationID";
+
+/** @var kTestSecret
+ @brief Fake secret used for testing.
+ */
+static NSString *const kTestSecret = @"secret";
+
+/** @var kTestReceipt
+ @brief Fake receipt used for testing.
+ */
+static NSString *const kTestReceipt = @"receipt";
+
+/** @var kInvalidPhoneNumberErrorMessage
+ @brief This is the error message the server will respond with if an incorrectly formatted phone
+ number is provided.
+ */
+static NSString *const kInvalidPhoneNumberErrorMessage = @"INVALID_PHONE_NUMBER";
+
+/** @var kQuotaExceededErrorMessage
+ @brief This is the error message the server will respond with if the quota for SMS text messages
+ has been exceeded for the project.
+ */
+static NSString *const kQuotaExceededErrorMessage = @"QUOTA_EXCEEDED";
+
+/** @var kAppNotVerifiedErrorMessage
+ @brief This is the error message the server will respond with if Firebase could not verify the
+ app during a phone authentication flow.
+ */
+static NSString *const kAppNotVerifiedErrorMessage = @"APP_NOT_VERIFIED";
+
+/** @class FIRSendVerificationCodeResponseTests
+ @brief Tests for @c FIRSendVerificationCodeResponseTests.
+ */
+@interface FIRSendVerificationCodeResponseTests : XCTestCase
+@end
+
+@implementation FIRSendVerificationCodeResponseTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testSendVerificationCodeResponseInvalidPhoneNumber
+ @brief Tests a failed attempt to send a verification code with an invalid phone number.
+ */
+- (void)testSendVerificationCodeResponseInvalidPhoneNumber {
+ FIRAuthAppCredential *credential =
+ [[FIRAuthAppCredential alloc]initWithReceipt:kTestReceipt secret:kTestSecret];
+ FIRSendVerificationCodeRequest *request =
+ [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:kTestInvalidPhoneNumber
+ appCredential:credential
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRSendVerificationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend sendVerificationCode:request
+ callback:^(FIRSendVerificationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidPhoneNumberErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidPhoneNumber);
+}
+
+/** @fn testSendVerificationCodeResponseQuotaExceededError
+ @brief Tests a failed attempt to send a verification code due to SMS quota having been exceeded.
+ */
+- (void)testSendVerificationCodeResponseQuotaExceededError {
+ FIRAuthAppCredential *credential =
+ [[FIRAuthAppCredential alloc]initWithReceipt:kTestReceipt secret:kTestSecret];
+ FIRSendVerificationCodeRequest *request =
+ [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:kTestPhoneNumber
+ appCredential:credential
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRSendVerificationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend sendVerificationCode:request
+ callback:^(FIRSendVerificationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kQuotaExceededErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeQuotaExceeded);
+}
+
+/** @fn testSendVerificationCodeResponseAppNotVerifiedError
+ @brief Tests a failed attempt to send a verification code due to Firebase not being able to
+ verify the app.
+ */
+- (void)testSendVerificationCodeResponseAppNotVerifiedError {
+ FIRAuthAppCredential *credential =
+ [[FIRAuthAppCredential alloc]initWithReceipt:kTestReceipt secret:kTestSecret];
+ FIRSendVerificationCodeRequest *request =
+ [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:kTestPhoneNumber
+ appCredential:credential
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRSendVerificationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend sendVerificationCode:request
+ callback:^(FIRSendVerificationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kAppNotVerifiedErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeAppNotVerified);
+}
+
+/** @fn testSuccessfulSendVerificationCodeResponse
+ @brief Tests a succesful to send a verification code.
+ */
+- (void)testSuccessfulSendVerificationCodeResponse {
+ FIRAuthAppCredential *credential =
+ [[FIRAuthAppCredential alloc]initWithReceipt:kTestReceipt secret:kTestSecret];
+ FIRSendVerificationCodeRequest *request =
+ [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:kTestPhoneNumber
+ appCredential:credential
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRSendVerificationCodeResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend sendVerificationCode:request
+ callback:^(FIRSendVerificationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ kVerificationIDKey : kFakeVerificationID
+ }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.verificationID, kFakeVerificationID);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRSetAccountInfoRequestTests.m b/Example/Auth/Tests/FIRSetAccountInfoRequestTests.m
new file mode 100644
index 0000000..54d8ff0
--- /dev/null
+++ b/Example/Auth/Tests/FIRSetAccountInfoRequestTests.m
@@ -0,0 +1,285 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRSetAccountInfoRequest.h"
+#import "FIRSetAccountInfoResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kTestAccessToken
+ @bried Fake acess token for testing.
+ */
+ static NSString *const kTestAccessToken = @"accessToken";
+
+/** @var kDisplayNameKey
+ @brief The key for the "displayName" value in the request.
+ */
+static NSString *const kDisplayNameKey = @"displayName";
+
+/** @var kTestDisplayName
+ @brief The fake @c displayName for testing.
+ */
+static NSString *const kTestDisplayName = @"testDisplayName";
+
+/** @var kLocalIDKey
+ @brief The key for the "localID" value in the request.
+ */
+static NSString *const kLocalIDKey = @"localId";
+
+/** @var kTestLocalID
+ @brief The fake @c localID for testing in the request.
+ */
+static NSString *const kTestLocalID = @"testLocalId";
+
+/** @var kEmailKey
+ @brief The key for the "email" value in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kTestEmail
+ @brief The fake @c email used for testing in the request.
+ */
+static NSString *const ktestEmail = @"testEmail";
+
+/** @var kPasswordKey
+ @brief The key for the "password" value in the request.
+ */
+static NSString *const kPasswordKey = @"password";
+
+/** @var kTestPassword
+ @brief The fake @c password used for testing in the request.
+ */
+static NSString *const kTestPassword = @"testPassword";
+
+/** @var kPhotoURLKey
+ @brief The key for the "photoURL" value in the request.
+ */
+static NSString *const kPhotoURLKey = @"photoUrl";
+
+/** @var kTestPhotoURL
+ @brief The fake photoUrl for testing in the request.
+ */
+static NSString *const kTestPhotoURL = @"testPhotoUrl";
+
+/** @var kProvidersKey
+ @brief The key for the "providers" value in the request.
+ */
+static NSString *const kProvidersKey = @"provider";
+
+/** @var kTestProviders
+ @brief The fake @c providers value used for testing in the request.
+ */
+static NSString *const kTestProviders = @"testProvider";
+
+/** @var kOOBCodeKey
+ @brief The key for the "OOBCode" value in the request.
+ */
+static NSString *const kOOBCodeKey = @"oobCode";
+
+/** @var kTestOOBCode
+ @brief The fake @c OOBCode used for testing the request.
+ */
+static NSString *const kTestOOBCode = @"testOobCode";
+
+/** @var kEmailVerifiedKey
+ @brief The key for the "emailVerified" value in the request.
+ */
+static NSString *const kEmailVerifiedKey = @"emailVerified";
+
+/** @var kTestEmailVerified
+ @brief The fake @c emailVerified value used for testing the request.
+ */
+static const BOOL kTestEmailVerified = YES;
+
+/** @var kUpgradeToFederatedLoginKey
+ @brief The key for the "upgradeToFederatedLogin" value in the request.
+ */
+static NSString *const kUpgradeToFederatedLoginKey = @"upgradeToFederatedLogin";
+
+/** @var kTestUpgradeToFederatedLogin
+ @brief The fake @c upgradeToFederatedLogin value for testing the request.
+ */
+static const BOOL kTestUpgradeToFederatedLogin = YES;
+
+/** @var kCaptchaChallengeKey
+ @brief The key for the "captchaChallenge" value in the request.
+ */
+static NSString *const kCaptchaChallengeKey = @"captchaChallenge";
+
+/** @var kTestCaptchaChallenge
+ @brief The fake @c captchaChallenge for testing in the request.
+ */
+static NSString *const kTestCaptchaChallenge = @"TestCaptchaChallenge";
+
+/** @var kCaptchaResponseKey
+ @brief The key for the "captchaResponse" value the request.
+ */
+static NSString *const kCaptchaResponseKey = @"captchaResponse";
+
+/** @var kTestCaptchaResponse
+ @brief The fake @c captchaResponse for testing the request.
+ */
+static NSString *const kTestCaptchaResponse = @"TestCaptchaResponse";
+
+/** @var kDeleteAttributesKey
+ @brief The key for the "deleteAttribute" value in the request.
+ */
+static NSString *const kDeleteAttributesKey = @"deleteAttribute";
+
+/** @var kTestDeleteAttributes
+ @brief The fake @c deleteAttribute value for testing the request.
+ */
+static NSString *const kTestDeleteAttributes = @"TestDeleteAttributes";
+
+/** @var kDeleteProvidersKey
+ @brief The key for the "deleteProvider" value in the request.
+ */
+static NSString *const kDeleteProvidersKey = @"deleteProvider";
+
+/** @var kTestDeleteProviders
+ @brief The fake @c deleteProviders for testing the request.
+ */
+static NSString *const kTestDeleteProviders = @"TestDeleteProviders";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccountInfo?key=APIKey";
+
+/** @class FIRSetAccountInfoRequestTests
+ @brief Tests for @c FIRSetAccountInfoRequest.
+ */
+@interface FIRSetAccountInfoRequestTests : XCTestCase
+@end
+@implementation FIRSetAccountInfoRequestTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testSetAccountInfoRequest
+ @brief Tests the set account info request.
+ */
+- (void)testSetAccountInfoRequest {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+ request.returnSecureToken = NO;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kIDTokenKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kDisplayNameKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kLocalIDKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kEmailKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kPasswordKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kPhotoURLKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kProvidersKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kOOBCodeKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kEmailVerifiedKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kUpgradeToFederatedLoginKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kCaptchaChallengeKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kCaptchaResponseKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kDeleteAttributesKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kDeleteProvidersKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kReturnSecureTokenKey]);
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+}
+
+/** @fn testSetAccountInfoRequestOptionalFields
+ @brief Tests the set account info request with optional fields.
+ */
+- (void)testSetAccountInfoRequestOptionalFields {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+ request.accessToken = kTestAccessToken;
+ request.displayName = kTestDisplayName;
+ request.localID = kTestLocalID;
+ request.email = ktestEmail;
+ request.password = kTestPassword;
+ request.providers = @[ kTestProviders ];
+ request.OOBCode = kTestOOBCode;
+ request.emailVerified = kTestEmailVerified;
+ request.photoURL = [NSURL URLWithString:kTestPhotoURL];
+ request.upgradeToFederatedLogin = kTestUpgradeToFederatedLogin;
+ request.captchaChallenge = kTestCaptchaChallenge;
+ request.captchaResponse = kTestCaptchaResponse;
+ request.deleteAttributes = @[ kTestDeleteAttributes ];
+ request.deleteProviders = @[ kTestDeleteProviders ];
+
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestAccessToken);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDisplayNameKey], kTestDisplayName);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kLocalIDKey], kTestLocalID);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], ktestEmail);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPasswordKey], kTestPassword);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPhotoURLKey], kTestPhotoURL);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kProvidersKey], @[ kTestProviders ]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kOOBCodeKey], kTestOOBCode);
+ XCTAssert(_RPCIssuer.decodedRequest[kEmailVerifiedKey]);
+ XCTAssert(_RPCIssuer.decodedRequest[kUpgradeToFederatedLoginKey]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kCaptchaChallengeKey], kTestCaptchaChallenge);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kCaptchaResponseKey], kTestCaptchaResponse);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDeleteAttributesKey],
+ @[ kTestDeleteAttributes ]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDeleteProvidersKey], @[ kTestDeleteProviders ]);
+ XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]);
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRSetAccountInfoResponseTests.m b/Example/Auth/Tests/FIRSetAccountInfoResponseTests.m
new file mode 100644
index 0000000..d650f13
--- /dev/null
+++ b/Example/Auth/Tests/FIRSetAccountInfoResponseTests.m
@@ -0,0 +1,530 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRSetAccountInfoRequest.h"
+#import "FIRSetAccountInfoResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kEmailExistsErrorMessage
+ @brief This is the error message the server will respond with if the user entered an invalid
+ email address.
+ */
+static NSString *const kEmailExistsErrorMessage = @"EMAIL_EXISTS";
+
+/** @var kVerifiedProviderKey
+ @brief The name of the "VerifiedProvider" property in the response.
+ */
+static NSString *const kProviderUserInfoKey = @"providerUserInfo";
+
+/** @var kPhotoUrlKey
+ @brief The name of the "photoURL" property in the response.
+ */
+static NSString *const kPhotoUrlKey = @"photoUrl";
+
+/** @var kTestPhotoURL
+ @brief The fake photoUrl property value in the response.
+ */
+static NSString *const kTestPhotoURL = @"testPhotoURL";
+
+/** @var kIDTokenKey
+ @brief The name of the "IDToken" property in the response.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kTestIDToken
+ @brief Testing ID token for verifying assertion.
+ */
+static NSString *const kTestIDToken = @"ID_TOKEN";
+
+/** @var kExpiresInKey
+ @brief The name of the "expiresIn" property in the response.
+ */
+static NSString *const kExpiresInKey = @"expiresIn";
+
+/** @var kTestExpiresIn
+ @brief Fake token expiration time.
+ */
+static NSString *const kTestExpiresIn = @"12345";
+
+/** @var kRefreshTokenKey
+ @brief The name of the "refreshToken" property in the response.
+ */
+static NSString *const kRefreshTokenKey = @"refreshToken";
+
+/** @var kTestRefreshToken
+ @brief Fake refresh token.
+ */
+static NSString *const kTestRefreshToken = @"REFRESH_TOKEN";
+
+/** @var kEmailSignUpNotAllowedErrorMessage
+ @brief This is the error message the server will respond with if admin disables password
+ account.
+ */
+static NSString *const kEmailSignUpNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED";
+
+/** @var kPasswordLoginDisabledErrorMessage
+ @brief This is the error message the server responds with if password login is disabled.
+ */
+static NSString *const kPasswordLoginDisabledErrorMessage = @"PASSWORD_LOGIN_DISABLED";
+
+/** @var kCredentialTooOldErrorMessage
+ @brief This is the error message the server responds with if account change is attempted 5
+ minutes after signing in.
+ */
+static NSString *const kCredentialTooOldErrorMessage = @"CREDENTIAL_TOO_OLD_LOGIN_AGAIN";
+
+/** @var kinvalidUserTokenErrorMessage
+ @brief This is the error message the server will respond with if the user's saved auth
+ credential is invalid, the user has to sign-in again.
+ */
+static NSString *const kinvalidUserTokenErrorMessage = @"INVALID_ID_TOKEN";
+
+/** @var kUserDisabledErrorMessage
+ @brief This is the error message the server will respond with if the user's account has been
+ disabled.
+ */
+static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED";
+
+/** @var kInvalidEmailErrorMessage
+ @brief The error returned by the server if the email is invalid.
+ */
+static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL";
+
+/** @var kWeakPasswordErrorMessage
+ @brief This is the error message the server will respond with if the user's new password
+ is too weak that it is too short.
+ */
+static NSString *const kWeakPasswordErrorMessage =
+ @"WEAK_PASSWORD : Password should be at least 6 characters";
+
+/** @var kWeakPasswordClientErrorMessage
+ @brief This is the error message the client will see if the user's new password is too weak
+ that it is too short.
+ @remarks This message should be derived from @c kWeakPasswordErrorMessage .
+ */
+static NSString *const kWeakPasswordClientErrorMessage =
+ @"Password should be at least 6 characters";
+
+/** @var kExpiredActionCodeErrorMessage
+ @brief This is the error message the server will respond with if the action code is expired.
+ */
+static NSString *const kExpiredActionCodeErrorMessage = @"EXPIRED_OOB_CODE:";
+
+/** @var kInvalidActionCodeErrorMessage
+ @brief This is the error message the server will respond with if the action code is invalid.
+ */
+static NSString *const kInvalidActionCodeErrorMessage = @"INVALID_OOB_CODE";
+
+/** @var kInvalidMessagePayloadErrorMessage
+ @brief This is the prefix for the error message the server responds with if an invalid message
+ payload was sent.
+ */
+static NSString *const kInvalidMessagePayloadErrorMessage = @"INVALID_MESSAGE_PAYLOAD";
+
+/** @var kInvalidSenderErrorMessage
+ @brief This is the prefix for the error message the server responds with if invalid sender is
+ used to send the email for updating user's email address.
+ */
+static NSString *const kInvalidSenderErrorMessage = @"INVALID_SENDER";
+
+/** @var kInvalidRecipientEmailErrorMessage
+ @brief This is the prefix for the error message the server responds with if the recipient email
+ is invalid.
+ */
+static NSString *const kInvalidRecipientEmailErrorMessage = @"INVALID_RECIPIENT_EMAIL";
+
+/** @var kEpsilon
+ @brief Allowed difference when comparing floating point numbers.
+ */
+static const double kEpsilon = 1e-3;
+
+/** @class FIRSetAccountInfoResponseTests
+ @brief Tests for @c FIRSetAccountInfoResponse.
+ */
+@interface FIRSetAccountInfoResponseTests : XCTestCase
+@end
+@implementation FIRSetAccountInfoResponseTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testEmailExistsError
+ @brief This test simulates @c testSignUpNewUserEmailExistsError with @c
+ FIRAuthErrorCodeEmailExists error.
+ */
+- (void)testEmailExistsError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kEmailExistsErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeEmailAlreadyInUse);
+}
+
+/** @fn testEmailSignUpNotAllowedError
+ @brief This test simulates @c testEmailSignUpNotAllowedError with @c
+ FIRAuthErrorCodeOperationNotAllowed error.
+ */
+- (void)testEmailSignUpNotAllowedError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kEmailSignUpNotAllowedErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed);
+}
+
+/** @fn testPasswordLoginDisabledError
+ @brief This test simulates @c passwordLoginDisabledError with @c
+ FIRAuthErrorCodeOperationNotAllowed error.
+ */
+- (void)testPasswordLoginDisabledError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kPasswordLoginDisabledErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed);
+}
+
+/** @fn testUserDisabledError
+ @brief This test simulates @c testUserDisabledError with @c FIRAuthErrorCodeUserDisabled error.
+ */
+- (void)testUserDisabledError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kUserDisabledErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled);
+}
+
+/** @fn testInvalidUserTokenError
+ @brief This test simulates @c testinvalidUserTokenError with @c
+ FIRAuthErrorCodeCredentialTooOld error.
+ */
+- (void)testInvalidUserTokenError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kinvalidUserTokenErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidUserToken);
+}
+
+/** @fn testrequiresRecentLogin
+ @brief This test simulates @c testCredentialTooOldError with @c
+ FIRAuthErrorCodeRequiresRecentLogin error.
+ */
+- (void)testrequiresRecentLogin {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kCredentialTooOldErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeRequiresRecentLogin);
+}
+
+/** @fn testWeakPasswordError
+ @brief This test simulates @c FIRAuthErrorCodeWeakPassword error.
+ */
+- (void)testWeakPasswordError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kWeakPasswordErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeWeakPassword);
+ XCTAssertEqualObjects(RPCError.userInfo[NSLocalizedFailureReasonErrorKey],
+ kWeakPasswordClientErrorMessage);
+}
+
+/** @fn testInvalidEmailError
+ @brief This test simulates @c FIRAuthErrorCodeInvalidEmail error code.
+ */
+- (void)testInvalidEmailError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail);
+}
+
+/** @fn testInvalidActionCodeError
+ @brief This test simulates @c FIRAuthErrorCodeInvalidActionCode error code.
+ */
+- (void)testInvalidActionCodeError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidActionCodeErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidActionCode);
+}
+
+/** @fn testExpiredActionCodeError
+ @brief This test simulates @c FIRAuthErrorCodeExpiredActionCode error code.
+ */
+- (void)testExpiredActionCodeError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kExpiredActionCodeErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeExpiredActionCode);
+}
+
+/** @fn testInvalidMessagePayloadError
+ @brief Tests for @c FIRAuthErrorCodeInvalidMessagePayload.
+ */
+- (void)testInvalidMessagePayloadError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidMessagePayloadErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidMessagePayload);
+}
+
+/** @fn testInvalidSenderError
+ @brief Tests for @c FIRAuthErrorCodeInvalidSender.
+ */
+- (void)testInvalidSenderError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidSenderErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidSender);
+}
+
+/** @fn testInvalidRecipientEmailError
+ @brief Tests for @c FIRAuthErrorCodeInvalidRecipientEmail.
+ */
+- (void)testInvalidRecipientEmailError {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidRecipientEmailErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidRecipientEmail);
+}
+
+/** @fn testSuccessfulSetAccountInfoResponse
+ @brief This test simulates a successful @c SetAccountInfo flow.
+ */
+- (void)testSuccessfulSetAccountInfoResponse {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSetAccountInfoResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend setAccountInfo:request
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ kProviderUserInfoKey:@[
+ @{ kPhotoUrlKey : kTestPhotoURL }
+ ],
+ kIDTokenKey : kTestIDToken,
+ kExpiresInKey : kTestExpiresIn,
+ kRefreshTokenKey : kTestRefreshToken
+ }];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+ XCTAssertNotNil(RPCResponse);
+ if ([RPCResponse.providerUserInfo count]) {
+ NSURL *responsePhotoUrl = RPCResponse.providerUserInfo[0].photoURL;
+ XCTAssertEqualObjects(responsePhotoUrl.absoluteString, kTestPhotoURL);
+ }
+ XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken);
+ NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow];
+ XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon);
+ XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRSignUpNewUserRequestTests.m b/Example/Auth/Tests/FIRSignUpNewUserRequestTests.m
new file mode 100644
index 0000000..622ec7c
--- /dev/null
+++ b/Example/Auth/Tests/FIRSignUpNewUserRequestTests.m
@@ -0,0 +1,140 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRSignUpNewUserRequest.h"
+#import "FIRSignUpNewUserResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for the test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=APIKey";
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kEmailKey
+ @brief The name of the "email" property in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kTestEmail
+ @brief Testing user email adadress.
+ */
+static NSString *const kTestEmail = @"test@gmail.com";
+
+/** @var kDisplayNameKey
+ @brief the name of the "displayName" property in the request.
+ */
+static NSString *const kDisplayNameKey = @"displayName";
+
+/** @var kTestDisplayName
+ @brief Testing display name.
+ */
+static NSString *const kTestDisplayName = @"DisplayName";
+
+/** @var kPasswordKey
+ @brief the name of the "password" property in the request.
+ */
+static NSString *const kPasswordKey = @"password";
+
+/** @var kTestPassword
+ @brief Testing password.
+ */
+static NSString *const kTestPassword = @"Password";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@interface FIRSignUpNewUserRequestTests : XCTestCase
+
+@end
+
+@implementation FIRSignUpNewUserRequestTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testSignUpNewUserRequestAnonymous
+ @brief Tests the encoding of a sign up new user request when user is signed in anonymously.
+ */
+- (void)testSignUpNewUserRequestAnonymous {
+ FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey];
+ request.returnSecureToken = NO;
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kEmailKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kDisplayNameKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kPasswordKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kReturnSecureTokenKey]);
+}
+
+/** @fn testSignUpNewUserRequestNotAnonymous
+ @brief Tests the encoding of a sign up new user request when user is not signed in anonymously.
+ */
+- (void)testSignUpNewUserRequestNotAnonymous {
+ FIRSignUpNewUserRequest *request =
+ [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey
+ email:kTestEmail
+ password:kTestPassword
+ displayName:kTestDisplayName];
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPasswordKey], kTestPassword);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDisplayNameKey], kTestDisplayName);
+ XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRSignUpNewUserResponseTests.m b/Example/Auth/Tests/FIRSignUpNewUserResponseTests.m
new file mode 100644
index 0000000..89479f7
--- /dev/null
+++ b/Example/Auth/Tests/FIRSignUpNewUserResponseTests.m
@@ -0,0 +1,291 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRSignUpNewUserRequest.h"
+#import "FIRSignUpNewUserResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kIDTokenKey
+ @brief The name of the "IDToken" property in the response.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kTestIDToken
+ @brief Testing ID token for verifying assertion.
+ */
+static NSString *const kTestIDToken = @"ID_TOKEN";
+
+/** @var kExpiresInKey
+ @brief The name of the "expiresIn" property in the response.
+ */
+static NSString *const kExpiresInKey = @"expiresIn";
+
+/** @var kTestExpiresIn
+ @brief Fake token expiration time.
+ */
+static NSString *const kTestExpiresIn = @"12345";
+
+/** @var kRefreshTokenKey
+ @brief The name of the "refreshToken" property in the response.
+ */
+static NSString *const kRefreshTokenKey = @"refreshToken";
+
+/** @var kTestRefreshToken
+ @brief Fake refresh token.
+ */
+static NSString *const kTestRefreshToken = @"REFRESH_TOKEN";
+
+/** @var kTestEmail
+ @brief Testing user email adadress.
+ */
+static NSString *const kTestEmail = @"test@gmail.com";
+
+/** @var kTestDisplayName
+ @brief Testing display name.
+ */
+static NSString *const kTestDisplayName = @"DisplayName";
+
+/** @var kTestPassword
+ @brief Testing password.
+ */
+static NSString *const kTestPassword = @"Password";
+
+/** @var kEmailAlreadyInUseErrorMessage
+ @brief This is the error message the server will respond with if the user entered an invalid
+ email address.
+ */
+static NSString *const kEmailAlreadyInUseErrorMessage = @"EMAIL_EXISTS";
+
+/** @var kOperationNotAllowedErrorMessage
+ @brief This is the error message the server will respond with if user/password account was
+ disabled by the administrator.
+ */
+static NSString *const kEmailSignUpNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED";
+
+/** @var kPasswordLoginDisabledErrorMessage
+ @brief This is the error message the server responds with if password login is disabled.
+ */
+static NSString *const kPasswordLoginDisabledErrorMessage = @"PASSWORD_LOGIN_DISABLED:";
+
+/** @var kInvalidEmailErrorMessage
+ @brief The error returned by the server if the email is invalid.
+ */
+static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL";
+
+/** @var kWeakPasswordErrorMessage
+ @brief This is the error message the server will respond with if the new user's password
+ is too weak that it is too short.
+ */
+static NSString *const kWeakPasswordErrorMessage =
+ @"WEAK_PASSWORD : Password should be at least 6 characters";
+
+/** @var kWeakPasswordClientErrorMessage
+ @brief This is the error message the client will see if the new user's password is too weak
+ that it is too short.
+ @remarks This message should be derived from @c kWeakPasswordErrorMessage .
+ */
+static NSString *const kWeakPasswordClientErrorMessage =
+ @"Password should be at least 6 characters";
+
+/** @var kEpsilon
+ @brief Allowed difference when comparing floating point numbers.
+ */
+static const double kEpsilon = 1e-3;
+
+@interface FIRSignUpNewUserResponseTests : XCTestCase
+@end
+@implementation FIRSignUpNewUserResponseTests
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testSuccessfulSignUp
+ @brief This test simulates a complete sign up flow with no errors.
+ */
+- (void)testSuccessfulSignUp {
+ FIRSignUpNewUserRequest *request =
+ [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey
+ email:kTestEmail
+ password:kTestPassword
+ displayName:kTestDisplayName];
+
+ __block BOOL callbackInvoked;
+ __block FIRSignUpNewUserResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ kIDTokenKey : kTestIDToken,
+ kExpiresInKey : kTestExpiresIn,
+ kRefreshTokenKey : kTestRefreshToken
+ }];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken);
+ NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow];
+ XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon);
+ XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken);
+ XCTAssertNil(RPCError, "There should be no error");
+}
+
+/** @fn testSignUpNewUserEmailAlreadyInUseError
+ @brief This test simulates @c testSignUpNewUserEmailAlreadyInUseError with @c
+ FIRAuthErrorCodeEmailAlreadyInUse error.
+ */
+- (void)testSignUpNewUserEmailAlreadyInUseError {
+ FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSignUpNewUserResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kEmailAlreadyInUseErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeEmailAlreadyInUse);
+}
+
+/** @fn testSignUpNewUserOperationNotAllowedError
+ @brief This test simulates @c testSignUpNewUserEmailExistsError with @c
+ FIRAuthErrorCodeOperationNotAllowed error.
+ */
+- (void)testSignUpNewUserOperationNotAllowedError {
+ FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSignUpNewUserResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kEmailSignUpNotAllowedErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed);
+}
+
+/** @fn testSignUpNewUserPasswordLoginDisabledError
+ @brief This test simulates @c signUpNewUserPasswordLoginDisabledError with @c
+ FIRAuthErrorCodeOperationNotAllowed error.
+ */
+- (void)testSignUpNewUserPasswordLoginDisabledError {
+ FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSignUpNewUserResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kPasswordLoginDisabledErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed);
+}
+
+/** @fn testinvalidEmailError
+ @brief This test simulates making a request containing an invalid email address and receiving @c
+ FIRAuthErrorInvalidEmail error as a result.
+ */
+- (void)testinvalidEmailError {
+ FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSignUpNewUserResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail);
+}
+
+/** @fn testSignUpNewUserWeakPasswordError
+ @brief This test simulates @c FIRAuthErrorCodeWeakPassword error.
+ */
+- (void)testSignUpNewUserWeakPasswordError {
+ FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRSignUpNewUserResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kWeakPasswordErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeWeakPassword);
+ XCTAssertEqualObjects(RPCError.userInfo[NSLocalizedFailureReasonErrorKey],
+ kWeakPasswordClientErrorMessage);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRTwitterAuthProviderTests.m b/Example/Auth/Tests/FIRTwitterAuthProviderTests.m
new file mode 100644
index 0000000..da02c43
--- /dev/null
+++ b/Example/Auth/Tests/FIRTwitterAuthProviderTests.m
@@ -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 <XCTest/XCTest.h>
+
+#import "Twitter/FIRTwitterAuthProvider.h"
+#import "FIRAuthCredential_Internal.h"
+#import "FIRVerifyAssertionRequest.h"
+
+/** @var kTwitterToken
+ @brief A testing Twitter token.
+ */
+static NSString *const kTwitterToken = @"Token";
+
+/** @var kTwitterSecret
+ @brief A testing Twitter secret.
+ */
+static NSString *const kTwitterSecret = @"Secret";
+
+/** @var kAPIKey
+ @brief A testing API Key.
+ */
+static NSString *const kAPIKey = @"APIKey";
+
+/** @class FIRTwitterAuthProviderTests
+ @brief Tests for @c FIRTwitterAuthProvider
+ */
+@interface FIRTwitterAuthProviderTests : XCTestCase
+@end
+@implementation FIRTwitterAuthProviderTests
+
+/** @fn testCredentialWithToken
+ @brief Tests the @c credentialWithToken method to make sure the credential it produces populates
+ the appropriate fields in a verify assertion request.
+ */
+- (void)testCredentialWithToken {
+ FIRAuthCredential *credential =
+ [FIRTwitterAuthProvider credentialWithToken:kTwitterToken secret:kTwitterSecret];
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kAPIKey
+ providerID:FIRTwitterAuthProviderID];
+ [credential prepareVerifyAssertionRequest:request];
+ XCTAssertEqualObjects(request.providerAccessToken, kTwitterToken);
+ XCTAssertEqualObjects(request.providerOAuthTokenSecret, kTwitterSecret);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRUserTests.m b/Example/Auth/Tests/FIRUserTests.m
new file mode 100644
index 0000000..5a4c00a
--- /dev/null
+++ b/Example/Auth/Tests/FIRUserTests.m
@@ -0,0 +1,1801 @@
+/*
+ * 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/XCTest.h>
+
+#import "EmailPassword/FIREmailAuthProvider.h"
+#import "Facebook/FIRFacebookAuthProvider.h"
+#import "Google/FIRGoogleAuthProvider.h"
+#import "Phone/FIRPhoneAuthCredential_Internal.h"
+#import "Phone/FIRPhoneAuthProvider.h"
+#import "FIRAdditionalUserInfo.h"
+#import "FIRAuth.h"
+#import "FIRAuthErrorUtils.h"
+#import "FIRAuthGlobalWorkQueue.h"
+#import "FIRUser.h"
+#import "FIRUserInfo.h"
+#import "FIRAuthBackend.h"
+#import "FIRGetAccountInfoRequest.h"
+#import "FIRGetAccountInfoResponse.h"
+#import "FIRSetAccountInfoRequest.h"
+#import "FIRSetAccountInfoResponse.h"
+#import "FIRVerifyAssertionResponse.h"
+#import "FIRVerifyAssertionRequest.h"
+#import "FIRVerifyPasswordRequest.h"
+#import "FIRVerifyPasswordResponse.h"
+#import "FIRVerifyPhoneNumberRequest.h"
+#import "FIRVerifyPhoneNumberResponse.h"
+#import "FIRApp+FIRAuthUnitTests.h"
+#import "OCMStubRecorder+FIRAuthUnitTests.h"
+#import <OCMock/OCMock.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kAPIKey
+ @brief The fake API key.
+ */
+static NSString *const kAPIKey = @"FAKE_API_KEY";
+
+/** @var kAccessToken
+ @brief The fake access token.
+ */
+static NSString *const kAccessToken = @"ACCESS_TOKEN";
+
+/** @var kNewAccessToken
+ @brief A new value for the fake access token.
+ */
+static NSString *const kNewAccessToken = @"NEW_ACCESS_TOKEN";
+
+/** @var kAccessTokenValidInterval
+ @brief The time to live for the fake access token.
+ */
+static const NSTimeInterval kAccessTokenTimeToLive = 60 * 60;
+
+/** @var kRefreshToken
+ @brief The fake refresh token.
+ */
+static NSString *const kRefreshToken = @"REFRESH_TOKEN";
+
+/** @var kLocalID
+ @brief The fake local user ID.
+ */
+static NSString *const kLocalID = @"LOCAL_ID";
+
+/** @var kAnotherLocalID
+ @brief The fake local ID of another user.
+ */
+static NSString *const kAnotherLocalID = @"ANOTHER_LOCAL_ID";
+
+/** @var kGoogleIDToken
+ @brief The fake ID token from Google Sign-In.
+ */
+static NSString *const kGoogleIDToken = @"GOOGLE_ID_TOKEN";
+
+/** @var kFacebookIDToken
+ @brief The fake ID token from Facebook Sign-In. Facebook provider ID token is always nil.
+ */
+static NSString *const kFacebookIDToken = nil;
+
+/** @var kGoogleAccessToken
+ @brief The fake access token from Google Sign-In.
+ */
+static NSString *const kGoogleAccessToken = @"GOOGLE_ACCESS_TOKEN";
+
+/** @var kFacebookAccessToken
+ @brief The fake access token from Facebook Sign-In.
+ */
+static NSString *const kFacebookAccessToken = @"FACEBOOK_ACCESS_TOKEN";
+
+/** @var kEmail
+ @brief The fake user email.
+ */
+static NSString *const kEmail = @"user@company.com";
+
+/** @var kPhoneNumber
+ @brief The fake user phone number.
+ */
+static NSString *const kPhoneNumber = @"12345658";
+
+/** @var kTemporaryProof
+ @brief The fake temporary proof.
+ */
+static NSString *const kTemporaryProof = @"12345658";
+
+/** @var kNewEmail
+ @brief A new value for the fake user email.
+ */
+static NSString *const kNewEmail = @"newuser@company.com";
+
+/** @var kUserName
+ @brief The fake user name.
+ */
+static NSString *const kUserName = @"User Doe";
+
+/** @var kNewDisplayName
+ @brief A new value for the fake user display name.
+ */
+static NSString *const kNewDisplayName = @"New User Doe";
+
+/** @var kPhotoURL
+ @brief The fake user profile image URL string.
+ */
+static NSString *const kPhotoURL = @"https://host.domain/image";
+
+/** @var kNewPhotoURL
+ @brief A new value for the fake user profile image URL string..
+ */
+static NSString *const kNewPhotoURL = @"https://host.domain/new/image";
+
+/** @var kPassword
+ @brief The fake user password.
+ */
+static NSString *const kPassword = @"123456";
+
+/** @var kNewPassword
+ @brief The fake new user password.
+ */
+static NSString *const kNewPassword = @"1234567";
+
+/** @var kPasswordHash
+ @brief The fake user password hash.
+ */
+static NSString *const kPasswordHash = @"UkVEQUNURUQ=";
+
+/** @var kGoogleUD
+ @brief The fake user ID under Google Sign-In.
+ */
+static NSString *const kGoogleID = @"GOOGLE_ID";
+
+/** @var kGoogleEmail
+ @brief The fake user email under Google Sign-In.
+ */
+static NSString *const kGoogleEmail = @"user@gmail.com";
+
+/** @var kGoogleDisplayName
+ @brief The fake user display name under Google Sign-In.
+ */
+static NSString *const kGoogleDisplayName = @"Google Doe";
+
+/** @var kEmailDisplayName
+ @brief The fake user display name for email password user.
+ */
+static NSString *const kEmailDisplayName = @"Email Doe";
+
+/** @var kFacebookDisplayName
+ @brief The fake user display name under Facebook Sign-In.
+ */
+static NSString *const kFacebookDisplayName = @"Facebook Doe";
+
+/** @var kGooglePhotoURL
+ @brief The fake user profile image URL string under Google Sign-In.
+ */
+static NSString *const kGooglePhotoURL = @"https://googleusercontents.com/user/profile";
+
+/** @var kFacebookID
+ @brief The fake user ID under Facebook Login.
+ */
+static NSString *const kFacebookID = @"FACEBOOK_ID";
+
+/** @var kFacebookEmail
+ @brief The fake user email under Facebook Login.
+ */
+static NSString *const kFacebookEmail = @"user@facebook.com";
+
+/** @var kVerificationCode
+ @brief Fake verification code used for testing.
+ */
+static NSString *const kVerificationCode = @"12345678";
+
+/** @var kVerificationID
+ @brief Fake verification ID for testing.
+ */
+static NSString *const kVerificationID = @"55432";
+
+/** @var kExpectationTimeout
+ @brief The maximum time waiting for expectations to fulfill.
+ */
+static const NSTimeInterval kExpectationTimeout = 1;
+
+/** @class FIRUserTests
+ @brief Tests for @c FIRUser .
+ */
+@interface FIRUserTests : XCTestCase
+@end
+@implementation FIRUserTests {
+
+ /** @var _mockBackend
+ @brief The mock @c FIRAuthBackendImplementation .
+ */
+ id _mockBackend;
+}
+
+/** @fn googleProfile
+ @brief The fake user profile under additional user data in @c FIRVerifyAssertionResponse.
+ */
++ (NSDictionary *)googleProfile {
+ static NSDictionary *kGoogleProfile = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ kGoogleProfile = @{
+ @"email": kGoogleEmail,
+ @"given_name": @"User",
+ @"family_name": @"Doe"
+ };
+ });
+ return kGoogleProfile;
+}
+
+- (void)setUp {
+ [super setUp];
+ _mockBackend = OCMProtocolMock(@protocol(FIRAuthBackendImplementation));
+ [FIRAuthBackend setBackendImplementation:_mockBackend];
+ [FIRApp resetAppForAuthUnitTests];
+}
+
+- (void)tearDown {
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+#pragma mark - Tests
+
+/** @fn testUserProperties
+ @brief Tests properties of the @c FIRUser instance.
+ */
+- (void)testUserProperties {
+ // Mock auth provider user info for email/password for GetAccountInfo.
+ id mockPasswordUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]);
+ OCMStub([mockPasswordUserInfo providerID]).andReturn(FIREmailAuthProviderID);
+ OCMStub([mockPasswordUserInfo federatedID]).andReturn(kEmail);
+ OCMStub([mockPasswordUserInfo email]).andReturn(kEmail);
+
+ // Mock auth provider user info from Google for GetAccountInfo.
+ id mockGoogleUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]);
+ OCMStub([mockGoogleUserInfo providerID]).andReturn(FIRGoogleAuthProviderID);
+ OCMStub([mockGoogleUserInfo displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGoogleUserInfo photoURL]).andReturn([NSURL URLWithString:kGooglePhotoURL]);
+ OCMStub([mockGoogleUserInfo federatedID]).andReturn(kGoogleID);
+ OCMStub([mockGoogleUserInfo email]).andReturn(kGoogleEmail);
+
+ // Mock auth provider user info from Facebook for GetAccountInfo.
+ id mockFacebookUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]);
+ OCMStub([mockFacebookUserInfo providerID]).andReturn(FIRFacebookAuthProviderID);
+ OCMStub([mockFacebookUserInfo federatedID]).andReturn(kFacebookID);
+ OCMStub([mockFacebookUserInfo email]).andReturn(kFacebookEmail);
+
+ // Mock auth provider user info from Phone auth provider for GetAccountInfo.
+ id mockPhoneUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]);
+ OCMStub([mockPhoneUserInfo providerID]).andReturn(FIRPhoneAuthProviderID);
+ OCMStub([mockPhoneUserInfo phoneNumber]).andReturn(kPhoneNumber);
+
+ // Mock the root user info object for GetAccountInfo.
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser emailVerified]).andReturn(YES);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser photoURL]).andReturn([NSURL URLWithString:kPhotoURL]);
+ OCMStub([mockGetAccountInfoResponseUser providerUserInfo])
+ .andReturn((@[ mockPasswordUserInfo,
+ mockGoogleUserInfo,
+ mockFacebookUserInfo,
+ mockPhoneUserInfo ]));
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ // Verify FIRUserInfo properties on FIRUser itself.
+ XCTAssertEqualObjects(user.providerID, @"Firebase");
+ XCTAssertEqualObjects(user.uid, kLocalID);
+ XCTAssertEqualObjects(user.displayName, kGoogleDisplayName);
+ XCTAssertEqualObjects(user.photoURL, [NSURL URLWithString:kPhotoURL]);
+ XCTAssertEqualObjects(user.email, kEmail);
+
+ // Verify FIRUser properties besides providerData contents.
+ XCTAssertFalse(user.anonymous);
+ XCTAssertTrue(user.emailVerified);
+ XCTAssertEqualObjects(user.refreshToken, kRefreshToken);
+ XCTAssertEqual(user.providerData.count, 4u);
+
+ NSDictionary<NSString *, id<FIRUserInfo>> *providerMap =
+ [self dictionaryWithUserInfoArray:user.providerData];
+
+ // Verify FIRUserInfo properties from email/password.
+ id<FIRUserInfo> passwordUserInfo = providerMap[FIREmailAuthProviderID];
+ XCTAssertNotNil(passwordUserInfo);
+ XCTAssertEqualObjects(passwordUserInfo.uid, kEmail);
+ XCTAssertNil(passwordUserInfo.displayName);
+ XCTAssertNil(passwordUserInfo.photoURL);
+ XCTAssertEqualObjects(passwordUserInfo.email, kEmail);
+
+ // Verify FIRUserInfo properties from the Google auth provider.
+ id<FIRUserInfo> googleUserInfo = providerMap[FIRGoogleAuthProviderID];
+ XCTAssertNotNil(googleUserInfo);
+ XCTAssertEqualObjects(googleUserInfo.uid, kGoogleID);
+ XCTAssertEqualObjects(googleUserInfo.displayName, kGoogleDisplayName);
+ XCTAssertEqualObjects(googleUserInfo.photoURL, [NSURL URLWithString:kGooglePhotoURL]);
+ XCTAssertEqualObjects(googleUserInfo.email, kGoogleEmail);
+
+ // Verify FIRUserInfo properties from the Facebook auth provider.
+ id<FIRUserInfo> facebookUserInfo = providerMap[FIRFacebookAuthProviderID];
+ XCTAssertNotNil(facebookUserInfo);
+ XCTAssertEqualObjects(facebookUserInfo.uid, kFacebookID);
+ XCTAssertNil(facebookUserInfo.displayName);
+ XCTAssertNil(facebookUserInfo.photoURL);
+ XCTAssertEqualObjects(facebookUserInfo.email, kFacebookEmail);
+
+ // Verify FIRUserInfo properties from the phone auth provider.
+ id<FIRUserInfo> phoneUserInfo = providerMap[FIRPhoneAuthProviderID];
+ XCTAssertNotNil(phoneUserInfo);
+ XCTAssertEqualObjects(phoneUserInfo.phoneNumber, kPhoneNumber);
+
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testUpdateEmailSuccess
+ @brief Tests the flow of a successful @c updateEmail:completion: call.
+ */
+- (void)testUpdateEmailSuccess {
+ id (^mockUserInfoWithDisplayName)(NSString *) = ^(NSString *displayName) {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(displayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ return mockGetAccountInfoResponseUser;
+ };
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ id userInfoResponse = mockUserInfoWithDisplayName(kGoogleDisplayName);
+ [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse
+ completion:^(FIRUser *user) {
+ // Pretend that the display name on the server has been changed since last request.
+ [self
+ expectGetAccountInfoWithMockUserInfoResponse:mockUserInfoWithDisplayName(kNewDisplayName)];
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request,
+ FIRSetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ XCTAssertEqualObjects(request.email, kNewEmail);
+ XCTAssertNil(request.localID);
+ XCTAssertNil(request.displayName);
+ XCTAssertNil(request.photoURL);
+ XCTAssertNil(request.password);
+ XCTAssertNil(request.providers);
+ XCTAssertNil(request.deleteAttributes);
+ XCTAssertNil(request.deleteProviders);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]);
+ OCMStub([mockSetAccountInfoResponse email]).andReturn(kNewEmail);
+ OCMStub([mockSetAccountInfoResponse displayName]).andReturn(kNewDisplayName);
+ callback(mockSetAccountInfoResponse, nil);
+ });
+ });
+ [user updateEmail:kNewEmail completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(user.email, kNewEmail);
+ XCTAssertEqualObjects(user.displayName, kNewDisplayName);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testUpdateEmailFailure
+ @brief Tests the flow of a failed @c updateEmail:completion: call.
+ */
+- (void)testUpdateEmailFailure {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser];
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils invalidEmailErrorWithMessage:nil]);
+ [user updateEmail:kNewEmail completion:^(NSError *_Nullable error) {
+ XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidEmail);
+ // Email should not have changed on the client side.
+ XCTAssertEqualObjects(user.email, kEmail);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testUpdatePhoneSuccess
+ @brief Tests the flow of a successful @c updatePhoneNumberCredential:completion: call.
+ */
+- (void)testUpdatePhoneSuccess {
+ id (^mockUserInfoWithPhoneNumber)(NSString *) = ^(NSString *phoneNumber) {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ if (phoneNumber.length) {
+ OCMStub([mockGetAccountInfoResponseUser phoneNumber]).andReturn(phoneNumber);
+ }
+ return mockGetAccountInfoResponseUser;
+ };
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ id userInfoResponse = mockUserInfoWithPhoneNumber(nil);
+ [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse
+ completion:^(FIRUser *user) {
+ [self expectVerifyPhoneNumberRequestWithPhoneNumber:kPhoneNumber error:nil];
+ id userInfoResponseUpdate = mockUserInfoWithPhoneNumber(kPhoneNumber);
+ [self expectGetAccountInfoWithMockUserInfoResponse:userInfoResponseUpdate];
+
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode];
+ [user updatePhoneNumberCredential:credential
+ completion:^(NSError * _Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects([FIRAuth auth].currentUser.phoneNumber, kPhoneNumber);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testUpdatePhoneNumberFailure
+ @brief Tests the flow of a failed @c updatePhoneNumberCredential:completion: call.
+ */
+- (void)testUpdatePhoneNumberFailure {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ OCMExpect([_mockBackend verifyPhoneNumber:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils invalidPhoneNumberErrorWithMessage:nil]);
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode];
+ [user updatePhoneNumberCredential:credential completion:^(NSError *_Nullable error) {
+ XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidPhoneNumber);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testUpdatePasswordSuccess
+ @brief Tests the flow of a successful @c updatePassword:completion: call.
+ */
+- (void)testUpdatePasswordSuccess {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser];
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request,
+ FIRSetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ XCTAssertEqualObjects(request.password, kNewPassword);
+ XCTAssertNil(request.localID);
+ XCTAssertNil(request.displayName);
+ XCTAssertNil(request.photoURL);
+ XCTAssertNil(request.email);
+ XCTAssertNil(request.providers);
+ XCTAssertNil(request.deleteAttributes);
+ XCTAssertNil(request.deleteProviders);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]);
+ OCMStub([mockSetAccountInfoResponse displayName]).andReturn(kNewDisplayName);
+ callback(mockSetAccountInfoResponse, nil);
+ });
+ });
+ [user updatePassword:kNewPassword completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertFalse(user.isAnonymous);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testUpdatePasswordFailure
+ @brief Tests the flow of a failed @c updatePassword:completion: call.
+ */
+- (void)testUpdatePasswordFailure {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser];
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils userDisabledErrorWithMessage:nil]);
+ [user updatePassword:kNewPassword completion:^(NSError *_Nullable error) {
+ XCTAssertEqual(error.code, FIRAuthErrorCodeUserDisabled);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testUpdateEmptyPasswordFailure
+ @brief Tests the flow of a failed @c updatePassword:completion: call due to an empty password.
+ */
+- (void)testUpdateEmptyPasswordFailure {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ [user updatePassword:@"" completion:^(NSError *_Nullable error) {
+ XCTAssertEqual(error.code, FIRAuthErrorCodeWeakPassword);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+}
+
+/** @fn testChangeProfileSuccess
+ @brief Tests a successful user profile change flow.
+ */
+- (void)testChangeProfileSuccess {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser photoURL]).andReturn(kPhotoURL);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser];
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request,
+ FIRSetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ XCTAssertEqualObjects(request.displayName, kNewDisplayName);
+ XCTAssertEqualObjects(request.photoURL, [NSURL URLWithString:kNewPhotoURL]);
+ XCTAssertNil(request.localID);
+ XCTAssertNil(request.email);
+ XCTAssertNil(request.password);
+ XCTAssertNil(request.providers);
+ XCTAssertNil(request.deleteAttributes);
+ XCTAssertNil(request.deleteProviders);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]);
+ OCMStub([mockSetAccountInfoResponse displayName]).andReturn(kNewDisplayName);
+ callback(mockSetAccountInfoResponse, nil);
+ });
+ });
+ FIRUserProfileChangeRequest *profileChange = [user profileChangeRequest];
+ profileChange.photoURL = [NSURL URLWithString:kNewPhotoURL];
+ profileChange.displayName = kNewDisplayName;
+ [profileChange commitChangesWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(user.displayName, kNewDisplayName);
+ XCTAssertEqualObjects(user.photoURL, [NSURL URLWithString:kNewPhotoURL]);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testChangeProfileFailure
+ @brief Tests a failed user profile change flow.
+ */
+- (void)testChangeProfileFailure {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser];
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils tooManyRequestsErrorWithMessage:nil]);
+ FIRUserProfileChangeRequest *profileChange = [user profileChangeRequest];
+ profileChange.displayName = kNewDisplayName;
+ [profileChange commitChangesWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertEqual(error.code, FIRAuthErrorCodeTooManyRequests);
+ XCTAssertEqualObjects(user.displayName, kGoogleDisplayName);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testReloadSuccess
+ @brief Tests the flow of a successful @c reloadWithCompletion: call.
+ */
+- (void)testReloadSuccess {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ id mockGetAccountInfoResponseUserNew = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUserNew localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUserNew email]).andReturn(kNewEmail);
+ OCMStub([mockGetAccountInfoResponseUserNew displayName]).andReturn(kNewDisplayName);
+ OCMStub([mockGetAccountInfoResponseUserNew passwordHash]).andReturn(kPasswordHash);
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUserNew];
+ [user reloadWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(user.email, kNewEmail);
+ XCTAssertEqualObjects(user.displayName, kNewDisplayName);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testReloadFailure
+ @brief Tests the flow of a failed @c reloadWithCompletion: call.
+ */
+- (void)testReloadFailure {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andDispatchError2([FIRAuthErrorUtils userTokenExpiredErrorWithMessage:nil]);
+ [user reloadWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertEqual(error.code, FIRAuthErrorCodeUserTokenExpired);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testReauthenticateSuccess
+ @brief Tests the flow of a successful @c reauthenticateWithCredential:completion: call.
+ */
+- (void)testReauthenticateSuccess {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request,
+ FIRVerifyPasswordResponseCallback callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]);
+ // New authentication comes back with new access token.
+ OCMStub([mockVeriyPasswordResponse IDToken]).andReturn(kNewAccessToken);
+ OCMStub([mockVeriyPasswordResponse approximateExpirationDate])
+ .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]);
+ OCMStub([mockVeriyPasswordResponse refreshToken]).andReturn(kRefreshToken);
+ callback(mockVeriyPasswordResponse, nil);
+ });
+ });
+ OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request,
+ FIRGetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ // Verify that the new access token is being used for subsequent requests.
+ XCTAssertEqualObjects(request.accessToken, kNewAccessToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]);
+ OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockGetAccountInfoResponseUser ]);
+ callback(mockGetAccountInfoResponse, nil);
+ });
+ });
+ FIRAuthCredential *emailCredential =
+ [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword];
+ [user reauthenticateWithCredential:emailCredential completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ // Verify that the current user is unchanged.
+ XCTAssertEqual([FIRAuth auth].currentUser, user);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testReauthenticateAndRetrieveDataSuccess
+ @brief Tests the flow of a successful @c reauthenticateAndRetrieveDataWithCredential:completion:
+ call.
+ */
+- (void)testReauthenticateAndRetrieveDataSuccess {
+ [self expectVerifyAssertionRequest:FIRGoogleAuthProviderID
+ federatedID:kGoogleID
+ displayName:kGoogleDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kGoogleIDToken
+ providerAccessToken:kGoogleAccessToken];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *googleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:googleCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserGoogle:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRGoogleAuthProviderID);
+ XCTAssertNil(error);
+
+ [self expectVerifyAssertionRequest:FIRGoogleAuthProviderID
+ federatedID:kGoogleID
+ displayName:kGoogleDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kGoogleIDToken
+ providerAccessToken:kGoogleAccessToken];
+
+ FIRAuthCredential *reauthenticateGoogleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [authResult.user
+ reauthenticateAndRetrieveDataWithCredential:reauthenticateGoogleCredential
+ completion:^(FIRAuthDataResult *_Nullable
+ reauthenticateAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(error);
+ // Verify that the current user is unchanged.
+ XCTAssertEqual([FIRAuth auth].currentUser, authResult.user);
+ // Verify that the current user and reauthenticated user are not same pointers.
+ XCTAssertNotEqualObjects(authResult.user, reauthenticateAuthResult.user);
+ // Verify that anyway the current user and reauthenticated user have same IDs.
+ XCTAssertEqualObjects(authResult.user.uid, reauthenticateAuthResult.user.uid);
+ XCTAssertEqualObjects(authResult.user.displayName, reauthenticateAuthResult.user.displayName);
+ XCTAssertEqualObjects(reauthenticateAuthResult.additionalUserInfo.profile,
+ [[self class] googleProfile]);
+ XCTAssertEqualObjects(reauthenticateAuthResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(reauthenticateAuthResult.additionalUserInfo.providerID,
+ FIRGoogleAuthProviderID);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUserGoogle:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testReauthenticateFailure
+ @brief Tests the flow of a failed @c reauthenticateWithCredential:completion: call.
+ */
+- (void)testReauthenticateFailure {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request,
+ FIRVerifyPasswordResponseCallback callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]);
+ OCMStub([mockVeriyPasswordResponse IDToken]).andReturn(kNewAccessToken);
+ OCMStub([mockVeriyPasswordResponse approximateExpirationDate])
+ .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]);
+ OCMStub([mockVeriyPasswordResponse refreshToken]).andReturn(kRefreshToken);
+ callback(mockVeriyPasswordResponse, nil);
+ });
+ });
+ OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request,
+ FIRGetAccountInfoResponseCallback callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockGetAccountInfoResponseUserNew = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ // The newly-signed-in user has a different ID.
+ OCMStub([mockGetAccountInfoResponseUserNew localID]).andReturn(kAnotherLocalID);
+ OCMStub([mockGetAccountInfoResponseUserNew email]).andReturn(kNewEmail);
+ OCMStub([mockGetAccountInfoResponseUserNew displayName]).andReturn(kNewDisplayName);
+ OCMStub([mockGetAccountInfoResponseUserNew passwordHash]).andReturn(kPasswordHash);
+ id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]);
+ OCMStub([mockGetAccountInfoResponse users])
+ .andReturn(@[ mockGetAccountInfoResponseUserNew ]);
+ callback(mockGetAccountInfoResponse, nil);
+ });
+ });
+ FIRAuthCredential *emailCredential =
+ [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword];
+ [user reauthenticateWithCredential:emailCredential completion:^(NSError *_Nullable error) {
+ // Verify user mismatch error.
+ XCTAssertEqual(error.code, FIRAuthErrorCodeUserMismatch);
+ // Verify that the current user is unchanged.
+ XCTAssertEqual([FIRAuth auth].currentUser, user);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testReauthenticateUserMismatchFailure
+ @brief Tests the flow of a failed @c reauthenticateWithCredential:completion: call due to trying
+ to reauthenticate a user that does not exist.
+ */
+- (void)testReauthenticateUserMismatchFailure {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser
+ completion:^(FIRUser *user) {
+ OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request,
+ FIRVerifyAssertionResponseCallback callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback(nil, [FIRAuthErrorUtils userNotFoundErrorWithMessage:nil]);
+ });
+ });
+ FIRAuthCredential *googleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [user reauthenticateWithCredential:googleCredential completion:^(NSError *_Nullable error) {
+ // Verify user mismatch error.
+ XCTAssertEqual(error.code, FIRAuthErrorCodeUserMismatch);
+ // Verify that the current user is unchanged.
+ XCTAssertEqual([FIRAuth auth].currentUser, user);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkAndRetrieveDataSuccess
+ @brief Tests the flow of a successful @c linkAndRetrieveDataWithCredential:completion:
+ call.
+ */
+- (void)testlinkAndRetrieveDataSuccess {
+ [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID
+ federatedID:kFacebookID
+ displayName:kFacebookDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kFacebookIDToken
+ providerAccessToken:kFacebookAccessToken];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *facebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserFacebook:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID);
+ XCTAssertNil(error);
+
+ [self expectVerifyAssertionRequest:FIRGoogleAuthProviderID
+ federatedID:kGoogleID
+ displayName:kGoogleDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kGoogleIDToken
+ providerAccessToken:kGoogleAccessToken];
+
+ FIRAuthCredential *linkGoogleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [authResult.user linkAndRetrieveDataWithCredential:linkGoogleCredential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(error);
+ // Verify that the current user is unchanged.
+ XCTAssertEqual([FIRAuth auth].currentUser, authResult.user);
+ // Verify that the current user and reauthenticated user are same pointers.
+ XCTAssertEqualObjects(authResult.user, linkAuthResult.user);
+ // Verify that anyway the current user and linked user have same IDs.
+ XCTAssertEqualObjects(authResult.user.uid, linkAuthResult.user.uid);
+ XCTAssertEqualObjects(authResult.user.displayName, linkAuthResult.user.displayName);
+ XCTAssertEqualObjects(linkAuthResult.additionalUserInfo.profile,
+ [[self class] googleProfile]);
+ XCTAssertEqualObjects(linkAuthResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(linkAuthResult.additionalUserInfo.providerID,
+ FIRGoogleAuthProviderID);
+ [expectation fulfill];
+ }];
+
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUserGoogle:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkAndRetrieveDataError
+ @brief Tests the flow of an unsuccessful @c linkAndRetrieveDataWithCredential:completion:
+ call with an error from the backend.
+ */
+- (void)testlinkAndRetrieveDataError {
+ [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID
+ federatedID:kFacebookID
+ displayName:kFacebookDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kFacebookIDToken
+ providerAccessToken:kFacebookAccessToken];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *facebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserFacebook:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID);
+ XCTAssertNil(error);
+
+ OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request,
+ FIRVerifyAssertionResponseCallback callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback(nil, [FIRAuthErrorUtils userDisabledErrorWithMessage:nil]);
+ });
+ });
+
+ FIRAuthCredential *linkGoogleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [authResult.user linkAndRetrieveDataWithCredential:linkGoogleCredential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(linkAuthResult);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeUserDisabled);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkAndRetrieveDataProviderAlreadyLinked
+ @brief Tests the flow of an unsuccessful @c linkAndRetrieveDataWithCredential:completion:
+ call with FIRAuthErrorCodeProviderAlreadyLinked, which is a client side error.
+ */
+- (void)testlinkAndRetrieveDataProviderAlreadyLinked {
+ [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID
+ federatedID:kFacebookID
+ displayName:kFacebookDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kFacebookIDToken
+ providerAccessToken:kFacebookAccessToken];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *facebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserFacebook:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID);
+ XCTAssertNil(error);
+
+ FIRAuthCredential *linkFacebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken];
+ [authResult.user linkAndRetrieveDataWithCredential:linkFacebookCredential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(linkAuthResult);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeProviderAlreadyLinked);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkEmailAndRetrieveDataSuccess
+ @brief Tests the flow of a successful @c linkAndRetrieveDataWithCredential:completion:
+ invocation for email credential.
+ */
+- (void)testlinkEmailAndRetrieveDataSuccess {
+ [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID
+ federatedID:kFacebookID
+ displayName:kFacebookDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kFacebookIDToken
+ providerAccessToken:kFacebookAccessToken];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *facebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserFacebook:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID);
+ XCTAssertNil(error);
+
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kEmailDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ // Get account info is expected to be invoked twice.
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser];
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser];
+
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request,
+ FIRSetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ XCTAssertEqualObjects(request.password, kPassword);
+ XCTAssertNil(request.localID);
+ XCTAssertNil(request.displayName);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]);
+ callback(mockSetAccountInfoResponse, nil);
+ });
+ });
+
+ FIRAuthCredential *linkEmailCredential =
+ [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword];
+ [authResult.user linkAndRetrieveDataWithCredential:linkEmailCredential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(linkAuthResult.user.email, kEmail);
+ XCTAssertEqualObjects(linkAuthResult.user.displayName, kEmailDisplayName);
+ [expectation fulfill];
+ }];
+
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkEmailProviderAlreadyLinkedError
+ @brief Tests the flow of an unsuccessful @c linkAndRetrieveDataWithCredential:completion:
+ invocation for email credential and FIRAuthErrorCodeProviderAlreadyLinked which is a client
+ side error.
+ */
+- (void)testlinkEmailProviderAlreadyLinkedError {
+ [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID
+ federatedID:kFacebookID
+ displayName:kFacebookDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kFacebookIDToken
+ providerAccessToken:kFacebookAccessToken];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *facebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserFacebook:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID);
+ XCTAssertNil(error);
+
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kEmailDisplayName);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ // Get account info is expected to be invoked twice.
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser];
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser];
+
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request,
+ FIRSetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ XCTAssertEqualObjects(request.password, kPassword);
+ XCTAssertNil(request.localID);
+ XCTAssertNil(request.displayName);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]);
+ callback(mockSetAccountInfoResponse, nil);
+ });
+ });
+
+ FIRAuthCredential *linkEmailCredential =
+ [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword];
+ [authResult.user linkAndRetrieveDataWithCredential:linkEmailCredential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(linkAuthResult.user.email, kEmail);
+ XCTAssertEqualObjects(linkAuthResult.user.displayName, kEmailDisplayName);
+
+ // Try linking same credential a second time to trigger client side error.
+ [authResult.user linkAndRetrieveDataWithCredential:linkEmailCredential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(linkAuthResult);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeProviderAlreadyLinked);
+ [expectation fulfill];
+ }];
+ }];
+
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkEmailAndRetrieveDataError
+ @brief Tests the flow of an unsuccessful @c linkAndRetrieveDataWithCredential:completion:
+ invocation for email credential and an error from the backend.
+ */
+- (void)testlinkEmailAndRetrieveDataError {
+ [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID
+ federatedID:kFacebookID
+ displayName:kFacebookDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kFacebookIDToken
+ providerAccessToken:kFacebookAccessToken];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *facebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserFacebook:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID);
+ XCTAssertNil(error);
+
+ OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request,
+ FIRGetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback(nil, [FIRAuthErrorUtils tooManyRequestsErrorWithMessage:nil]);
+ });
+ });
+
+ FIRAuthCredential *linkEmailCredential =
+ [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword];
+ [authResult.user linkAndRetrieveDataWithCredential:linkEmailCredential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(linkAuthResult);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeTooManyRequests);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkCredentialSuccess
+ @brief Tests the flow of a successful @c linkWithCredential:completion: call, without additional
+ IDP data.
+ */
+- (void)testlinkCredentialSuccess {
+ [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID
+ federatedID:kFacebookID
+ displayName:kFacebookDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kFacebookIDToken
+ providerAccessToken:kFacebookAccessToken];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *facebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserFacebook:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID);
+ XCTAssertNil(error);
+
+ [self expectVerifyAssertionRequest:FIRGoogleAuthProviderID
+ federatedID:kGoogleID
+ displayName:kGoogleDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kGoogleIDToken
+ providerAccessToken:kGoogleAccessToken];
+
+ FIRAuthCredential *linkGoogleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [authResult.user linkWithCredential:linkGoogleCredential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertNil(error);
+ id<FIRUserInfo> userInfo = user.providerData.firstObject;
+ XCTAssertEqual(userInfo.providerID, FIRGoogleAuthProviderID);
+ XCTAssertEqual([FIRAuth auth].currentUser, authResult.user);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ [self assertUserGoogle:[FIRAuth auth].currentUser];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkCredentialError
+ @brief Tests the flow of an unsuccessful @c linkWithCredential:completion: call, with an error
+ from the backend.
+ */
+- (void)testlinkCredentialError {
+ [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID
+ federatedID:kFacebookID
+ displayName:kFacebookDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kFacebookIDToken
+ providerAccessToken:kFacebookAccessToken];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *facebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserFacebook:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID);
+ XCTAssertNil(error);
+
+ OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request,
+ FIRVerifyAssertionResponseCallback callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback(nil, [FIRAuthErrorUtils userDisabledErrorWithMessage:nil]);
+ });
+ });
+
+ FIRAuthCredential *linkGoogleCredential =
+ [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken];
+ [authResult.user linkWithCredential:linkGoogleCredential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertNil(user);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeUserDisabled);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkCredentialProviderAlreadyLinkedError
+ @brief Tests the flow of an unsuccessful @c linkWithCredential:completion: call, with a client
+ side error.
+ */
+- (void)testlinkCredentialProviderAlreadyLinkedError {
+ [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID
+ federatedID:kFacebookID
+ displayName:kFacebookDisplayName
+ profile:[[self class] googleProfile]
+ providerIDToken:kFacebookIDToken
+ providerAccessToken:kFacebookAccessToken];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ [[FIRAuth auth] signOut:NULL];
+ FIRAuthCredential *facebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken];
+ [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ XCTAssertTrue([NSThread isMainThread]);
+ [self assertUserFacebook:authResult.user];
+ XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName);
+ XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID);
+ XCTAssertNil(error);
+
+ FIRAuthCredential *linkFacebookCredential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:kGoogleAccessToken];
+ [authResult.user linkWithCredential:linkFacebookCredential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertNil(user);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeProviderAlreadyLinked);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkPhoneAuthCredentialSuccess
+ @brief Tests the flow of a successful @c linkAndRetrieveDataWithCredential:completion:
+ call using a phoneAuthCredential.
+ */
+- (void)testlinkPhoneAuthCredentialSuccess {
+ id (^mockUserInfoWithPhoneNumber)(NSString *) = ^(NSString *phoneNumber) {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ if (phoneNumber.length) {
+ NSDictionary *userInfoDictionary = @{ @"providerId" : FIRPhoneAuthProviderID };
+ FIRGetAccountInfoResponseProviderUserInfo *userInfo =
+ [[FIRGetAccountInfoResponseProviderUserInfo alloc] initWithDictionary:userInfoDictionary];
+ OCMStub([mockGetAccountInfoResponseUser providerUserInfo]).andReturn(@[ userInfo ]);
+ OCMStub([mockGetAccountInfoResponseUser phoneNumber]).andReturn(phoneNumber);
+ }
+ return mockGetAccountInfoResponseUser;
+ };
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ id userInfoResponse = mockUserInfoWithPhoneNumber(nil);
+ [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse
+ completion:^(FIRUser *user) {
+ [self expectVerifyPhoneNumberRequestWithPhoneNumber:kPhoneNumber error:nil];
+ id userInfoResponseUpdate = mockUserInfoWithPhoneNumber(kPhoneNumber);
+ [self expectGetAccountInfoWithMockUserInfoResponse:userInfoResponseUpdate];
+
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode];
+ [user linkAndRetrieveDataWithCredential:credential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects([FIRAuth auth].currentUser.providerData.firstObject.providerID,
+ FIRPhoneAuthProviderID);
+ XCTAssertEqualObjects([FIRAuth auth].currentUser.phoneNumber, kPhoneNumber);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testUnlinkPhoneAuthCredentialSuccess
+ @brief Tests the flow of a successful @c unlinkFromProvider:completion: call using a
+ @c FIRPhoneAuthProvider.
+ */
+- (void)testUnlinkPhoneAuthCredentialSuccess {
+ id (^mockUserInfoWithPhoneNumber)(NSString *) = ^(NSString *phoneNumber) {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ if (phoneNumber.length) {
+ NSDictionary *userInfoDictionary = @{ @"providerId" : FIRPhoneAuthProviderID };
+ FIRGetAccountInfoResponseProviderUserInfo *userInfo =
+ [[FIRGetAccountInfoResponseProviderUserInfo alloc] initWithDictionary:userInfoDictionary];
+ OCMStub([mockGetAccountInfoResponseUser providerUserInfo]).andReturn(@[ userInfo ]);
+ OCMStub([mockGetAccountInfoResponseUser phoneNumber]).andReturn(phoneNumber);
+ }
+ return mockGetAccountInfoResponseUser;
+ };
+
+ OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request,
+ FIRSetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ XCTAssertNotNil(request.deleteProviders);
+ XCTAssertNil(request.email);
+ XCTAssertNil(request.localID);
+ XCTAssertNil(request.displayName);
+ XCTAssertNil(request.photoURL);
+ XCTAssertNil(request.password);
+ XCTAssertNil(request.providers);
+ XCTAssertNil(request.deleteAttributes);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]);
+ callback(mockSetAccountInfoResponse, nil);
+ });
+ });
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ id userInfoResponse = mockUserInfoWithPhoneNumber(nil);
+ [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse
+ completion:^(FIRUser *user) {
+ [self expectVerifyPhoneNumberRequestWithPhoneNumber:kPhoneNumber error:nil];
+ id userInfoResponseUpdate = mockUserInfoWithPhoneNumber(kPhoneNumber);
+ [self expectGetAccountInfoWithMockUserInfoResponse:userInfoResponseUpdate];
+
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode];
+ // Link phone credential.
+ [user linkAndRetrieveDataWithCredential:credential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects([FIRAuth auth].currentUser.providerData.firstObject.providerID,
+ FIRPhoneAuthProviderID);
+ XCTAssertEqualObjects([FIRAuth auth].currentUser.phoneNumber, kPhoneNumber);
+ // Immediately unlink the phone auth provider.
+ [user unlinkFromProvider:FIRPhoneAuthProviderID
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertNil([FIRAuth auth].currentUser.phoneNumber);
+ [expectation fulfill];
+ }];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkPhoneAuthCredentialFailure
+ @brief Tests the flow of a failed call to @c linkAndRetrieveDataWithCredential:completion: due
+ to a phone provider already being linked.
+ */
+- (void)testlinkPhoneAuthCredentialFailure {
+ id (^mockUserInfoWithPhoneNumber)(NSString *) = ^(NSString *phoneNumber) {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ if (phoneNumber.length) {
+ OCMStub([mockGetAccountInfoResponseUser phoneNumber]).andReturn(phoneNumber);
+ }
+ return mockGetAccountInfoResponseUser;
+ };
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ id userInfoResponse = mockUserInfoWithPhoneNumber(nil);
+ [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse
+ completion:^(FIRUser *user) {
+ NSError *error = [FIRAuthErrorUtils providerAlreadyLinkedError];
+ [self expectVerifyPhoneNumberRequestWithPhoneNumber:nil error:error];
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode];
+ [user linkAndRetrieveDataWithCredential:credential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeProviderAlreadyLinked);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+/** @fn testlinkPhoneCredentialAlreadyExistsError
+ @brief Tests the flow of @c linkAndRetrieveDataWithCredential:completion:
+ call using a phoneAuthCredential and a credential already exisits error. In this case we
+ should get a FIRAuthCredential in the error object.
+ */
+- (void)testlinkPhoneCredentialAlreadyExistsError {
+ id (^mockUserInfoWithPhoneNumber)(NSString *) = ^(NSString *phoneNumber) {
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
+ OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash);
+ if (phoneNumber.length) {
+ OCMStub([mockGetAccountInfoResponseUser phoneNumber]).andReturn(phoneNumber);
+ }
+ return mockGetAccountInfoResponseUser;
+ };
+
+ void (^expectVerifyPhoneNumberRequest)(NSString *) = ^(NSString *phoneNumber) {
+ OCMExpect([_mockBackend verifyPhoneNumber:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyPhoneNumberRequest *_Nullable request,
+ FIRVerifyPhoneNumberResponseCallback callback) {
+ XCTAssertEqualObjects(request.verificationID, kVerificationID);
+ XCTAssertEqualObjects(request.verificationCode, kVerificationCode);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthCredential alloc] initWithTemporaryProof:kTemporaryProof
+ phoneNumber:kPhoneNumber
+ providerID:FIRPhoneAuthProviderID];
+ callback(nil,
+ [FIRAuthErrorUtils credentialAlreadyInUseErrorWithMessage:nil
+ credential:credential]);
+ });
+ });
+ };
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"callback"];
+ id userInfoResponse = mockUserInfoWithPhoneNumber(nil);
+ [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse
+ completion:^(FIRUser *user) {
+ expectVerifyPhoneNumberRequest(kPhoneNumber);
+
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode];
+ [user linkAndRetrieveDataWithCredential:credential
+ completion:^(FIRAuthDataResult *_Nullable
+ linkAuthResult,
+ NSError *_Nullable error) {
+ XCTAssertNil(linkAuthResult);
+ XCTAssertEqual(error.code, FIRAuthErrorCodeCredentialAlreadyInUse);
+ FIRPhoneAuthCredential *credential = error.userInfo[FIRAuthUpdatedCredentialKey];
+ XCTAssertEqual(credential.temporaryProof, kTemporaryProof);
+ XCTAssertEqual(credential.phoneNumber, kPhoneNumber);
+ [expectation fulfill];
+ }];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(_mockBackend);
+}
+
+#pragma mark - Helpers
+
+/** @fn signInWithEmailPasswordWithMockGetAccountInfoResponse:completion:
+ @brief Signs in with an email and password account with mocked backend end calls.
+ @param mockUserInfoResponse A mocked FIRGetAccountInfoResponseUser object.
+ @param completion The completion block that takes the newly signed-in user as the only
+ parameter.
+ */
+- (void)signInWithEmailPasswordWithMockUserInfoResponse:(id)mockUserInfoResponse
+ completion:(void (^)(FIRUser *user))completion {
+ OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request,
+ FIRVerifyPasswordResponseCallback callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]);
+ OCMStub([mockVeriyPasswordResponse IDToken]).andReturn(kAccessToken);
+ OCMStub([mockVeriyPasswordResponse approximateExpirationDate])
+ .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]);
+ OCMStub([mockVeriyPasswordResponse refreshToken]).andReturn(kRefreshToken);
+ callback(mockVeriyPasswordResponse, nil);
+ });
+ });
+ [self expectGetAccountInfoWithMockUserInfoResponse:mockUserInfoResponse];
+ [[FIRAuth auth] signOut:NULL];
+ [[FIRAuth auth] signInWithEmail:kEmail password:kPassword completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ XCTAssertNotNil(user);
+ XCTAssertNil(error);
+ completion(user);
+ }];
+}
+
+/** @fn expectGetAccountInfoWithMockUserInfoResponse:
+ @brief Expects a GetAccountInfo request on the mock backend and calls back with provided
+ fake account data.
+ @param mockUserInfoResponse A mock @c FIRGetAccountInfoResponseUser object containing user info.
+ */
+- (void)expectGetAccountInfoWithMockUserInfoResponse:(id)mockUserInfoResponse {
+ OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request,
+ FIRGetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]);
+ OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockUserInfoResponse ]);
+ callback(mockGetAccountInfoResponse, nil);
+ });
+ });
+}
+
+/** @fn dictionaryWithUserInfoArray:
+ @brief Converts an array of @c FIRUserInfo into a dictionary that indexed by provider IDs.
+ @param userInfoArray An array of @c FIRUserInfo objects.
+ @return A dictionary contains same values as @c userInfoArray does but keyed by their
+ @c providerID .
+ */
+- (NSDictionary<NSString *, id<FIRUserInfo>> *)
+ dictionaryWithUserInfoArray:(NSArray<id<FIRUserInfo>> *)userInfoArray {
+ NSMutableDictionary<NSString *, id<FIRUserInfo>> *map =
+ [NSMutableDictionary dictionaryWithCapacity:userInfoArray.count];
+ for (id<FIRUserInfo> userInfo in userInfoArray) {
+ XCTAssertNil(map[userInfo.providerID]);
+ map[userInfo.providerID] = userInfo;
+ }
+ return map;
+}
+
+/** @fn stubSecureTokensWithMockResponse
+ @brief Creates stubs on the mock response object with access and refresh tokens
+ @param mockResponse The mock response object.
+ */
+- (void)stubTokensWithMockResponse:(id)mockResponse {
+ OCMStub([mockResponse IDToken]).andReturn(kAccessToken);
+ OCMStub([mockResponse approximateExpirationDate])
+ .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]);
+ OCMStub([mockResponse refreshToken]).andReturn(kRefreshToken);
+}
+
+/** @fn assertUserGoogle
+ @brief Asserts the given FIRUser matching the fake data returned by
+ @c expectGetAccountInfo:federatedID:displayName: .
+ @param user The user object to be verified.
+ */
+- (void)assertUserGoogle:(FIRUser *)user {
+ XCTAssertNotNil(user);
+ XCTAssertEqualObjects(user.uid, kLocalID);
+ XCTAssertEqualObjects(user.displayName, kGoogleDisplayName);
+ XCTAssertEqual(user.providerData.count, 1u);
+ id<FIRUserInfo> googleUserInfo = user.providerData[0];
+ XCTAssertEqualObjects(googleUserInfo.providerID, FIRGoogleAuthProviderID);
+ XCTAssertEqualObjects(googleUserInfo.uid, kGoogleID);
+ XCTAssertEqualObjects(googleUserInfo.displayName, kGoogleDisplayName);
+ XCTAssertEqualObjects(googleUserInfo.email, kGoogleEmail);
+}
+
+/** @fn assertUserFacebook
+ @brief Asserts the given FIRUser matching the fake data returned by
+ @c expectGetAccountInfo:federatedID:displayName: .
+ @param user The user object to be verified.
+ */
+- (void)assertUserFacebook:(FIRUser *)user {
+ XCTAssertNotNil(user);
+ XCTAssertEqualObjects(user.uid, kLocalID);
+ XCTAssertEqualObjects(user.displayName, kFacebookDisplayName);
+ XCTAssertEqual(user.providerData.count, 1u);
+ id<FIRUserInfo> googleUserInfo = user.providerData[0];
+ XCTAssertEqualObjects(googleUserInfo.providerID, FIRFacebookAuthProviderID);
+ XCTAssertEqualObjects(googleUserInfo.uid, kFacebookID);
+ XCTAssertEqualObjects(googleUserInfo.displayName, kFacebookDisplayName);
+ XCTAssertEqualObjects(googleUserInfo.email, kGoogleEmail);
+}
+
+/** @fn expectGetAccountInfo:federatedID:displayName:
+ @brief Expects a GetAccountInfo request on the mock backend and calls back with fake account
+ data for a Google Sign-In user.
+ */
+- (void)expectGetAccountInfo:(NSString *)providerId
+ federatedID:(NSString *)federatedID
+ displayName:(NSString *)displayName {
+ OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request,
+ FIRGetAccountInfoResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockGoogleUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]);
+ OCMStub([mockGoogleUserInfo providerID]).andReturn(providerId);
+ OCMStub([mockGoogleUserInfo displayName]).andReturn(displayName);
+ OCMStub([mockGoogleUserInfo federatedID]).andReturn(federatedID);
+ OCMStub([mockGoogleUserInfo email]).andReturn(kGoogleEmail);
+ id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
+ OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
+ OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(displayName);
+ OCMStub([mockGetAccountInfoResponseUser providerUserInfo])
+ .andReturn((@[ mockGoogleUserInfo ]));
+ id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]);
+ OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockGetAccountInfoResponseUser ]);
+ callback(mockGetAccountInfoResponse, nil);
+ });
+ });
+}
+
+/** @fn expectVerifyAssertionRequest:federatedID:displayName:profile:providerAccessToken:
+ @brief Expects a Verify Assertion request on the mock backend and calls back with fake account
+ data.
+ */
+- (void)expectVerifyAssertionRequest:(NSString *)providerId
+ federatedID:(NSString *)federatedID
+ displayName:(NSString *)displayName
+ profile:(NSDictionary *)profile
+ providerIDToken:(nullable NSString *)providerIDToken
+ providerAccessToken:(NSString *)providerAccessToken {
+ OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request,
+ FIRVerifyAssertionResponseCallback callback) {
+ XCTAssertEqualObjects(request.APIKey, kAPIKey);
+ XCTAssertEqualObjects(request.providerID, providerId);
+ XCTAssertEqualObjects(request.providerIDToken, providerIDToken);
+ XCTAssertEqualObjects(request.providerAccessToken, providerAccessToken);
+ XCTAssertTrue(request.returnSecureToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ id mockVeriyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]);
+ OCMStub([mockVeriyAssertionResponse federatedID]).andReturn(federatedID);
+ OCMStub([mockVeriyAssertionResponse providerID]).andReturn(providerId);
+ OCMStub([mockVeriyAssertionResponse localID]).andReturn(kLocalID);
+ OCMStub([mockVeriyAssertionResponse displayName]).andReturn(displayName);
+ OCMStub([mockVeriyAssertionResponse profile]).andReturn(profile);
+ OCMStub([mockVeriyAssertionResponse username]).andReturn(kUserName);
+ [self stubTokensWithMockResponse:mockVeriyAssertionResponse];
+ callback(mockVeriyAssertionResponse, nil);
+ });
+ });
+ [self expectGetAccountInfo:providerId federatedID:federatedID displayName:displayName];
+}
+
+/** @fn expectVerifyPhoneNumberRequestWithPhoneNumber:error:
+ @brief Expects a verify phone numner request on the mock backend and calls back with fake
+ account data or an error.
+ @param phoneNumber Optionally; The phone number to use in the mocked response.
+ @param error Optionally; The error to return in the mocked response.
+ */
+- (void)expectVerifyPhoneNumberRequestWithPhoneNumber:(nullable NSString *)phoneNumber
+ error:(nullable NSError*)error {
+ OCMExpect([_mockBackend verifyPhoneNumber:[OCMArg any] callback:[OCMArg any]])
+ .andCallBlock2(^(FIRVerifyPhoneNumberRequest *_Nullable request,
+ FIRVerifyPhoneNumberResponseCallback callback) {
+ XCTAssertEqualObjects(request.verificationID, kVerificationID);
+ XCTAssertEqualObjects(request.verificationCode, kVerificationCode);
+ XCTAssertEqualObjects(request.accessToken, kAccessToken);
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ id mockVerifyPhoneNumberResponse = OCMClassMock([FIRVerifyPhoneNumberResponse class]);
+ OCMStub([mockVerifyPhoneNumberResponse phoneNumber]).andReturn(phoneNumber);
+ callback(mockVerifyPhoneNumberResponse, nil);
+ });
+ });
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/FIRVerifyAssertionRequestTests.m b/Example/Auth/Tests/FIRVerifyAssertionRequestTests.m
new file mode 100644
index 0000000..becc420
--- /dev/null
+++ b/Example/Auth/Tests/FIRVerifyAssertionRequestTests.m
@@ -0,0 +1,232 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRVerifyAssertionRequest.h"
+#import "FIRVerifyAssertionResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+#import <GoogleToolboxForMac/GTMNSDictionary+URLArguments.h>
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kTestPostBodyKey
+ @brief The name of the "postBody" property in the response.
+ */
+static NSString *const kPostBodyKey = @"postBody";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAssertion?key=APIKey";
+
+/** @var kIDTokenKey
+ @brief The name of the "idToken" property in the response.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kTestAccessToken
+ @brief Fake access token used for testing.
+ */
+static NSString *const kTestAccessToken = @"ACCESS_TOKEN";
+
+/** @var kProviderIDKey
+ @brief The key for the "providerId" value in the request.
+ */
+static NSString *const kProviderIDKey = @"providerId";
+
+/** @var kTestProviderID
+ @brief Fake provider ID used for testing.
+ */
+static NSString *const kTestProviderID = @"ProviderID";
+
+/** @var kProviderIDTokenKey
+ @brief The key for the "id_token" value in the request.
+ */
+static NSString *const kProviderIDTokenKey = @"id_token";
+
+/** @var kTestProviderIDToken
+ @brief Fake provider ID token used for testing.
+ */
+static NSString *const kTestProviderIDToken = @"ProviderIDToken";
+
+/** @var kInputEmailKey
+ @brief The key for the "inputEmail" value in the request.
+ */
+static NSString *const kInputEmailKey = @"identifier";
+
+/** @var kTestInputEmail
+ @brief Fake input email used for testing.
+ */
+static NSString *const kTestInputEmail = @"testInputEmail";
+
+/** @var kPendingIDTokenKey
+ @brief The key for the "pendingIdToken" value in the request.
+ */
+static NSString *const kPendingIDTokenKey = @"pendingIdToken";
+
+/** @var kTestPendingToken
+ @brief Fake pending token used for testing.
+ */
+static NSString *const kTestPendingToken = @"testPendingToken";
+
+/** @var kProviderAccessTokenKey
+ @brief The key for the "access_token" value in the request.
+ */
+static NSString *const kProviderAccessTokenKey = @"access_token";
+
+/** @var kTestProviderAccessToken
+ @brief Fake @c providerAccessToken used for testing the request.
+ */
+static NSString *const kTestProviderAccessToken = @"testProviderAccessToken";
+
+/** @var kProviderOAuthTokenSecretKey
+ @brief The key for the "oauth_token_secret" value in the request.
+ */
+static NSString *const kProviderOAuthTokenSecretKey = @"oauth_token_secret";
+
+/** @var kTestProviderOAuthTokenSecret
+ @brief Fake @c providerOAuthTokenSecret used for testing the request.
+ */
+static NSString *const kTestProviderOAuthTokenSecret = @"testProviderOAuthTokenSecret";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+/** @var kAutoCreateKey
+ @brief The key for the "auto-create" value in the request.
+ */
+static NSString *const kAutoCreateKey = @"autoCreate";
+
+/** @class FIRVerifyAssertionRequestTests
+ @brief Tests for @c FIRVerifyAssertionReuqest
+ */
+@interface FIRVerifyAssertionRequestTests : XCTestCase
+@end
+@implementation FIRVerifyAssertionRequestTests{
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testVerifyAssertionRequestMissingTokens
+ @brief Tests the request with missing @c providerAccessToken and @c provideIDToken.
+ @remarks The request creation will raise an @c NSInvalidArgumentException exception when both
+ these tokens are missing.
+ */
+- (void)testVerifyAssertionRequestMissingTokens {
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID];
+
+ FIRVerifyAssertionResponseCallback callback =
+ ^(FIRVerifyAssertionResponse *_Nullable response, NSError *_Nullable error) {};
+ void (^verifyAssertionBlock)(void) = ^{
+ [FIRAuthBackend verifyAssertion:request callback:callback];
+ };
+ XCTAssertThrowsSpecificNamed(verifyAssertionBlock(), NSException, NSInvalidArgumentException,
+ @"Either IDToken or accessToken must be supplied.");
+ XCTAssertNil(_RPCIssuer.decodedRequest[kPostBodyKey]);
+}
+
+/** @fn testVerifyAssertionRequestProviderAccessToken
+ @brief Tests the verify assertion request with the @c providerAccessToken field set.
+ @remarks The presence of the @c providerAccessToken will prevent an @c
+ NSInvalidArgumentException exception from being raised.
+ */
+- (void)testVerifyAssertionRequestProviderAccessToken {
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID];
+ request.providerAccessToken = kTestProviderAccessToken;
+ request.returnSecureToken = NO;
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ NSDictionary *postBody = @{
+ kProviderIDKey : kTestProviderID,
+ kProviderAccessTokenKey : kTestProviderAccessToken
+ };
+ NSString *postBodyArgs = [postBody gtm_httpArgumentsString];
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest[kPostBodyKey]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPostBodyKey], postBodyArgs);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kIDTokenKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kReturnSecureTokenKey]);
+ // Auto-create flag Should be true by default.
+ XCTAssertTrue([_RPCIssuer.decodedRequest[kAutoCreateKey] boolValue]);
+}
+
+/** @fn testVerifyAssertionRequestOptionalFields
+ @brief Tests the verify assertion request with all optinal fields set.
+ */
+- (void)testVerifyAssertionRequestOptionalFields {
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID];
+ request.providerIDToken = kTestProviderIDToken;
+ request.providerAccessToken = kTestProviderAccessToken;
+ request.accessToken = kTestAccessToken;
+ request.inputEmail = kTestInputEmail;
+ request.pendingIDToken = kTestPendingToken;
+ request.providerOAuthTokenSecret = kTestProviderOAuthTokenSecret;
+ request.autoCreate = NO;
+
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ NSDictionary *postBody = @{
+ kProviderIDKey : kTestProviderID,
+ kProviderIDTokenKey : kTestProviderIDToken,
+ kProviderAccessTokenKey : kTestProviderAccessToken,
+ kProviderOAuthTokenSecretKey : kTestProviderOAuthTokenSecret,
+ kInputEmailKey : kTestInputEmail
+ };
+ NSString *postBodyArgs = [postBody gtm_httpArgumentsString];
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest[kPostBodyKey]);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPostBodyKey], postBodyArgs);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestAccessToken);
+ XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]);
+ XCTAssertFalse([_RPCIssuer.decodedRequest[kAutoCreateKey] boolValue]);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRVerifyAssertionResponseTests.m b/Example/Auth/Tests/FIRVerifyAssertionResponseTests.m
new file mode 100644
index 0000000..cd9e771
--- /dev/null
+++ b/Example/Auth/Tests/FIRVerifyAssertionResponseTests.m
@@ -0,0 +1,426 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRVerifyAssertionRequest.h"
+#import "FIRVerifyAssertionResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kProviderIDKey
+ @brief The name of the "providerId" property in the response.
+ */
+static NSString *const kProviderIDKey = @"providerId";
+
+/** @var kIDTokenKey
+ @brief The name of the "IDToken" property in the response.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kExpiresInKey
+ @brief The name of the "expiresIn" property in the response.
+ */
+static NSString *const kExpiresInKey = @"expiresIn";
+
+/** @var kRefreshTokenKey
+ @brief The name of the "refreshToken" property in the response.
+ */
+static NSString *const kRefreshTokenKey = @"refreshToken";
+
+/** @var kVerifiedProviderKey
+ @brief The name of the "VerifiedProvider" property in the response.
+ */
+static NSString *const kVerifiedProviderKey = @"verifiedProvider";
+
+/** @var kRawUserInfoKey
+ @brief The name of the "rawUserInfo" property in the response.
+ */
+static NSString *const kRawUserInfoKey = @"rawUserInfo";
+
+/** @var kUsernameKey
+ @brief The name of the "username" property in the response.
+ */
+static NSString *const kUsernameKey = @"username";
+
+/** @var kIsNewUserKey
+ @brief The name of the "isNewUser" property in the response.
+ */
+static NSString *const kIsNewUserKey = @"isNewUser";
+
+/** @var kTestProviderID
+ @brief Fake provider ID used for testing.
+ */
+static NSString *const kTestProviderID = @"ProviderID";
+
+/** @var kTestProviderIDToken
+ @brief Fake provider ID token used for testing.
+ */
+static NSString *const kTestProviderIDToken = @"ProviderIDToken";
+
+/** @var kTestIDToken
+ @brief Testing ID token for verifying assertion.
+ */
+static NSString *const kTestIDToken = @"ID_TOKEN";
+
+/** @var kTestExpiresIn
+ @brief Fake token expiration time.
+ */
+static NSString *const kTestExpiresIn = @"12345";
+
+/** @var kTestRefreshToken
+ @brief Fake refresh token.
+ */
+static NSString *const kTestRefreshToken = @"REFRESH_TOKEN";
+
+/** @var kTestProvider
+ @brief Fake provider used for testing.
+ */
+static NSString *const kTestProvider = @"Provider";
+
+/** @var kPhotoUrlKey
+ @brief The name of the "PhotoUrl" property in the response.
+ */
+static NSString *const kPhotoUrlKey = @"photoUrl";
+
+/** @var kTestPhotoUrl
+ @brief The "PhotoUrl" value for testing the response.
+ */
+static NSString *const kTestPhotoUrl = @"www.example.com";
+
+/** @var kUsername
+ @brief The "username" value for testing the response.
+ */
+static NSString *const kUsername = @"Joe Doe";
+
+/** @var testInvalidCredentialError
+ @brief This is the error message the server will respond with if the IDP token or requestUri is
+ invalid.
+ */
+static NSString *const ktestInvalidCredentialError = @"INVALID_IDP_RESPONSE";
+
+/** @var kUserDisabledErrorMessage
+ @brief This is the error message the server will respond with if the user's account has been
+ disabled.
+ */
+static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED";
+
+/** @var kOperationNotAllowedErrorMessage
+ @brief This is the error message the server will respond with if Admin disables IDP specified by
+ provider.
+ */
+static NSString *const kOperationNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED";
+
+/** @var kPasswordLoginDisabledErrorMessage
+ @brief This is the error message the server responds with if password login is disabled.
+ */
+static NSString *const kPasswordLoginDisabledErrorMessage = @"PASSWORD_LOGIN_DISABLED";
+
+/** @var kFederatedUserIDAlreadyLinkedMessage
+ @brief This is the error message the server will respond with if the federated user ID has been
+ already linked with another account.
+ */
+static NSString *const kFederatedUserIDAlreadyLinkedMessage = @"FEDERATED_USER_ID_ALREADY_LINKED:";
+
+/** @var kEpsilon
+ @brief Allowed difference when comparing floating point numbers.
+ */
+static const double kEpsilon = 1e-3;
+
+/** @class FIRVerifyAssertionResponseTests
+ @brief Tests for @c FIRVerifyAssertionResponse
+ */
+@interface FIRVerifyAssertionResponseTests : XCTestCase
+@end
+@implementation FIRVerifyAssertionResponseTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+/** @fn profile
+ @brief The "rawUserInfo" value for testing the response.
+ */
++ (NSDictionary *)profile {
+ static NSDictionary *kGoogleProfile = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ kGoogleProfile = @{
+ @"iss": @"https://accounts.google.com\\",
+ @"email": @"test@email.com",
+ @"given_name": @"User",
+ @"family_name": @"Doe"
+ };
+ });
+ return kGoogleProfile;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testInvalidIDPResponseError
+ @brief This test simulates @c invalidIDPResponseError with @c FIRAuthErrorCodeInvalidIDPResponse
+ error code.
+ */
+- (void)testInvalidIDPResponseError {
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID];
+ request.providerIDToken = kTestProviderIDToken;
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyAssertionResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:ktestInvalidCredentialError];
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidCredential);
+}
+
+/** @fn testUserDisabledError
+ @brief This test simulates @c userDisabledError with @c
+ FIRAuthErrorCodeUserDisabled error code.
+ */
+- (void)testUserDisabledError {
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID];
+ request.providerIDToken = kTestProviderIDToken;
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyAssertionResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kUserDisabledErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled);
+}
+
+/** @fn testCredentialAlreadyInUseError
+ @brief This test simulates a @c FIRAuthErrorCodeCredentialAlreadyInUse error.
+ */
+- (void)testCredentialAlreadyInUseError {
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID];
+ request.providerIDToken = kTestProviderIDToken;
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyAssertionResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kFederatedUserIDAlreadyLinkedMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeCredentialAlreadyInUse);
+}
+
+/** @fn testOperationNotAllowedError
+ @brief This test simulates a @c FIRAuthErrorCodeOperationNotAllowed error.
+ */
+- (void)testOperationNotAllowedError {
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID];
+ request.providerIDToken = kTestProviderIDToken;
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyAssertionResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kOperationNotAllowedErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed);
+}
+
+/** @fn testPasswordLoginDisabledError
+ @brief This test simulates a @c FIRAuthErrorCodeOperationNotAllowed error.
+ */
+- (void)testPasswordLoginDisabledError {
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID];
+ request.providerIDToken = kTestProviderIDToken;
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyAssertionResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kPasswordLoginDisabledErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed);
+}
+
+/** @fn testSuccessfulVerifyAssertionResponse
+ @brief This test simulates a successful verify assertion flow.
+ */
+- (void)testSuccessfulVerifyAssertionResponse {
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID];
+ request.providerIDToken = kTestProviderIDToken;
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyAssertionResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ kProviderIDKey : kTestProviderID,
+ kIDTokenKey : kTestIDToken,
+ kExpiresInKey : kTestExpiresIn,
+ kRefreshTokenKey : kTestRefreshToken,
+ kVerifiedProviderKey : @[ kTestProvider ],
+ kPhotoUrlKey : kTestPhotoUrl,
+ kUsernameKey : kUsername,
+ kIsNewUserKey : @YES,
+ kRawUserInfoKey : [[self class] profile]
+ }];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken);
+ NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow];
+ XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon);
+ XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken);
+ XCTAssertEqualObjects(RPCResponse.verifiedProvider, @[ kTestProvider ]);
+ XCTAssertEqualObjects(RPCResponse.photoURL, [NSURL URLWithString:kTestPhotoUrl]);
+ XCTAssertEqualObjects(RPCResponse.username, kUsername);
+ XCTAssertEqualObjects(RPCResponse.profile, [[self class] profile]);
+ XCTAssertEqualObjects(RPCResponse.providerID, kTestProviderID);
+ XCTAssertTrue(RPCResponse.isNewUser);
+}
+
+/** @fn testSuccessfulVerifyAssertionResponseWithTextData
+ @brief This test simulates a successful verify assertion flow when response collection
+ fields are sent as text values.
+ */
+- (void)testSuccessfulVerifyAssertionResponseWithTextData {
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID];
+ request.providerIDToken = kTestProviderIDToken;
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyAssertionResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+
+ [_RPCIssuer respondWithJSON:@{
+ kProviderIDKey : kTestProviderID,
+ kIDTokenKey : kTestIDToken,
+ kExpiresInKey : kTestExpiresIn,
+ kRefreshTokenKey : kTestRefreshToken,
+ kVerifiedProviderKey : [[self class] convertToJSONString:@[ kTestProvider ]],
+ kPhotoUrlKey : kTestPhotoUrl,
+ kUsernameKey : kUsername,
+ kIsNewUserKey : @NO,
+ kRawUserInfoKey : [[self class] convertToJSONString:[[self class] profile]]
+ }];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken);
+ NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow];
+ XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon);
+ XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken);
+ XCTAssertEqualObjects(RPCResponse.verifiedProvider, @[ kTestProvider ]);
+ XCTAssertEqualObjects(RPCResponse.photoURL, [NSURL URLWithString:kTestPhotoUrl]);
+ XCTAssertEqualObjects(RPCResponse.username, kUsername);
+ XCTAssertEqualObjects(RPCResponse.profile, [[self class] profile]);
+ XCTAssertEqualObjects(RPCResponse.providerID, kTestProviderID);
+ XCTAssertFalse(RPCResponse.isNewUser);
+}
+
+#pragma mark - Helpers
+
++ (NSString *)convertToJSONString:(NSObject *)object {
+ NSData *objectAsData = [NSJSONSerialization dataWithJSONObject:object
+ options:0
+ error:nil];
+ return [[NSString alloc] initWithData:objectAsData encoding:NSUTF8StringEncoding];
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRVerifyClientRequestTest.m b/Example/Auth/Tests/FIRVerifyClientRequestTest.m
new file mode 100644
index 0000000..dcb00f6
--- /dev/null
+++ b/Example/Auth/Tests/FIRVerifyClientRequestTest.m
@@ -0,0 +1,94 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthBackend.h"
+#import "FIRVerifyClientRequest.h"
+#import "FIRVerifyClientResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kFakeAppToken
+ @brief The fake app token to use in the test request.
+ */
+static NSString *const kFakeAppToken = @"appToken";
+
+/** @var kFakeAPIKey
+ @brief The fake API key to use in the test request.
+ */
+static NSString *const kFakeAPIKey = @"APIKey";
+
+/** @var kAppTokenKey
+ @brief The key for the appToken request paramenter.
+ */
+static NSString *const kAPPTokenKey = @"appToken";
+
+/** @var kIsSandboxKey
+ @brief The key for the isSandbox request parameter
+ */
+static NSString *const kIsSandboxKey = @"isSandbox";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for the test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyClient?key=APIKey";
+
+/** @class FIRVerifyClientRequestTest
+ @brief Tests for @c FIRVerifyClientRequests.
+ */
+@interface FIRVerifyClientRequestTest : XCTestCase
+@end
+
+@implementation FIRVerifyClientRequestTest {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testVerifyClientRequest
+ @brief Tests the verify client request.
+ */
+- (void)testVerifyClientRequest {
+ FIRVerifyClientRequest *request =
+ [[FIRVerifyClientRequest alloc] initWithAppToken:kFakeAppToken
+ isSandbox:YES
+ APIKey:kFakeAPIKey];
+ [FIRAuthBackend verifyClient:request callback:^(FIRVerifyClientResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAPPTokenKey], kFakeAppToken);
+ XCTAssertTrue(_RPCIssuer.decodedRequest[kIsSandboxKey]);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRVerifyClientResponseTests.m b/Example/Auth/Tests/FIRVerifyClientResponseTests.m
new file mode 100644
index 0000000..68b8feb
--- /dev/null
+++ b/Example/Auth/Tests/FIRVerifyClientResponseTests.m
@@ -0,0 +1,178 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRVerifyClientRequest.h"
+#import "FIRVerifyClientResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kFakeAppToken
+ @brief The fake app token to use in the test request.
+ */
+static NSString *const kFakeAppToken = @"appToken";
+
+/** @var kFakeAPIKey
+ @brief The fake API key to use in the test request.
+ */
+static NSString *const kFakeAPIKey = @"APIKey";
+
+/** @var kAppTokenKey
+ @brief The key for the appToken request paramenter.
+ */
+static NSString *const kAPPTokenKey = @"appToken";
+
+/** @var kIsSandboxKey
+ @brief The key for the isSandbox request parameter
+ */
+static NSString *const kIsSandboxKey = @"isSandbox";
+
+/** @var kReceiptKey
+ @brief The key for the receipt response paramenter.
+ */
+static NSString *const kReceiptKey = @"receipt";
+
+/** @var kFakeReceipt
+ @brief The fake receipt returned in the response.
+ */
+static NSString *const kFakeReceipt = @"receipt";
+
+/** @var kSuggestedTimeOutKey
+ @brief The key for the suggested timeout response parameter
+ */
+static NSString *const kSuggestedTimeOutKey = @"suggestedTimeout";
+
+/** @var kFakeSuggestedTimeout
+ @brief The fake suggested timeout returned in the response.
+ */
+static NSString *const kFakeSuggestedTimeout = @"1234";
+
+/** @var kEpsilon
+ @brief Allowed difference when comparing floating point numbers.
+ */
+static const double kEpsilon = 1e-3;
+
+/** @var kMissingAppCredentialErrorMessage
+ @brief This is the error message the server will respond with if the APNS token is missing in a
+ verifyClient request is missing.
+ */
+static NSString *const kMissingAppCredentialErrorMessage = @"MISSING_APP_CREDENTIAL";
+
+/** @var kMissingAppCredentialErrorMessage
+ @brief This is the error message the server will respond with if the APNS token is missing in a
+ verifyClient request is invalid.
+ */
+static NSString *const kInvalidAppCredentialErrorMessage = @"INVALID_APP_CREDENTIAL";
+
+@interface FIRVerifyClientResponseTests : XCTestCase
+@end
+
+@implementation FIRVerifyClientResponseTests{
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+/** @fn testMissingAppCredentialError
+ @brief Tests that @c FIRAuthErrorCodeMissingAppCredential error.
+ */
+- (void)testMissingAppCredentialError {
+ FIRVerifyClientRequest *request =
+ [[FIRVerifyClientRequest alloc] initWithAppToken:kFakeAppToken
+ isSandbox:YES
+ APIKey:kFakeAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyClientResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyClient:request
+ callback:^(FIRVerifyClientResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kMissingAppCredentialErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeMissingAppCredential);
+}
+
+/** @fn testInvalidAppCredentialError
+ @brief Tests that @c FIRAuthErrorCodeInvalidAppCredential error.
+ */
+- (void)testInvalidAppCredentialError {
+ FIRVerifyClientRequest *request =
+ [[FIRVerifyClientRequest alloc] initWithAppToken:kFakeAppToken
+ isSandbox:YES
+ APIKey:kFakeAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyClientResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyClient:request
+ callback:^(FIRVerifyClientResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidAppCredentialErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidAppCredential);
+}
+
+/** @fn testSuccessfulVerifyClientResponse
+ @brief Tests a succesful attempt of the verify password flow.
+ */
+- (void)testSuccessfulVerifyPasswordResponse {
+ FIRVerifyClientRequest *request =
+ [[FIRVerifyClientRequest alloc] initWithAppToken:kFakeAppToken
+ isSandbox:YES
+ APIKey:kFakeAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyClientResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyClient:request
+ callback:^(FIRVerifyClientResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ kReceiptKey : kFakeReceipt,
+ kSuggestedTimeOutKey : kFakeSuggestedTimeout
+ }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.receipt, kFakeReceipt);
+ NSTimeInterval suggestedTimeout = [RPCResponse.suggestedTimeOutDate timeIntervalSinceNow];
+ XCTAssertLessThanOrEqual(fabs(suggestedTimeout - [kFakeSuggestedTimeout doubleValue]), kEpsilon);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRVerifyCustomTokenRequestTests.m b/Example/Auth/Tests/FIRVerifyCustomTokenRequestTests.m
new file mode 100644
index 0000000..9f65f73
--- /dev/null
+++ b/Example/Auth/Tests/FIRVerifyCustomTokenRequestTests.m
@@ -0,0 +1,110 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRVerifyCustomTokenRequest.h"
+#import "FIRVerifyCustomTokenResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kTestTokenKey
+ @brief The name of the "token" property in the response.
+ */
+static NSString *const kTestTokenKey = @"token";
+
+/** @var kTestToken
+ @brief testing token.
+ */
+static NSString *const kTestToken = @"test token";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=APIKey";
+
+@interface FIRVerifyCustomTokenRequestTests : XCTestCase
+@end
+@implementation FIRVerifyCustomTokenRequestTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testVerifyCustomTokenRequest
+ @brief Tests the verify custom token request.
+ */
+- (void)testVerifyCustomTokenRequest {
+ FIRVerifyCustomTokenRequest *request =
+ [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey];
+ request.returnSecureToken = NO;
+ [FIRAuthBackend verifyCustomToken:request
+ callback:^(FIRVerifyCustomTokenResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest[kTestTokenKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kReturnSecureTokenKey]);
+}
+
+/** @fn testVerifyCustomTokenRequestOptionalFields
+ @brief Tests the verify custom token request with optional fields.
+ */
+- (void)testVerifyCustomTokenRequestOptionalFields {
+ FIRVerifyCustomTokenRequest *request =
+ [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey];
+ [FIRAuthBackend verifyCustomToken:request
+ callback:^(FIRVerifyCustomTokenResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest[kTestTokenKey]);
+ XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRVerifyCustomTokenResponseTests.m b/Example/Auth/Tests/FIRVerifyCustomTokenResponseTests.m
new file mode 100644
index 0000000..7a634ed
--- /dev/null
+++ b/Example/Auth/Tests/FIRVerifyCustomTokenResponseTests.m
@@ -0,0 +1,274 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRVerifyCustomTokenRequest.h"
+#import "FIRVerifyCustomTokenResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestToken
+ @brief testing token.
+ */
+static NSString *const kTestToken = @"test token";
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kIDTokenKey
+ @brief The name of the "IDToken" property in the response.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kExpiresInKey
+ @brief The name of the "expiresIn" property in the response.
+ */
+static NSString *const kExpiresInKey = @"expiresIn";
+
+/** @var kRefreshTokenKey
+ @brief The name of the "refreshToken" property in the response.
+ */
+static NSString *const kRefreshTokenKey = @"refreshToken";
+
+/** @var kTestIDToken
+ @brief Testing ID token for verifying assertion.
+ */
+static NSString *const kTestIDToken = @"ID_TOKEN";
+
+/** @var kTestExpiresIn
+ @brief Fake token expiration time.
+ */
+static NSString *const kTestExpiresIn = @"12345";
+
+/** @var kTestRefreshToken
+ @brief Fake refresh token.
+ */
+static NSString *const kTestRefreshToken = @"REFRESH_TOKEN";
+
+/** @var kMissingTokenCustomErrorMessage
+ @brief This is the error message the server will respond with if token field is missing in
+ request.
+ */
+static NSString *const kMissingCustomTokenErrorMessage = @"MISSING_CUSTOM_TOKEN";
+
+/** @var kInvalidTokenCustomErrorMessage
+ @brief This is the error message the server will respond with if there is a validation error
+ with the custom token.
+ */
+static NSString *const kInvalidCustomTokenErrorMessage = @"INVALID_CUSTOM_TOKEN";
+
+/** @var kInvalidCustomTokenServerErrorMessage
+ @brief This is the error message the server will respond with if there is a validation error
+ with the custom token. This message contains error details from the server.
+ */
+static NSString *const kInvalidCustomTokenServerErrorMessage =
+ @"INVALID_CUSTOM_TOKEN : Detailed Error";
+
+/** @var kInvalidCustomTokenEmptyServerErrorMessage
+ @brief This is the error message the server will respond with if there is a validation error
+ with the custom token.
+ @remarks This message deliberately has no content where it should contain
+ error details.
+ */
+static NSString *const kInvalidCustomTokenEmptyServerErrorMessage =
+ @"INVALID_CUSTOM_TOKEN :";
+
+/** @var kInvalidCustomTokenErrorDetails
+ @brief This is the test detailed error message that could be returned by the backend.
+ */
+static NSString *const kInvalidCustomTokenErrorDetails = @"Detailed Error";
+
+/** @var kCredentialMismatchErrorMessage
+ @brief This is the error message the server will respond with if the service API key belongs to
+ different projects.
+ */
+static NSString *const kCredentialMismatchErrorMessage = @"CREDENTIAL_MISMATCH:";
+
+/** @var kEpsilon
+ @brief Allowed difference when comparing floating point numbers.
+ */
+static const double kEpsilon = 1e-3;
+
+@interface FIRVerifyCustomTokenResponseTests : XCTestCase
+@end
+@implementation FIRVerifyCustomTokenResponseTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testInvalidCustomTokenError
+ @brief This test simulates @c invalidCustomTokenError with @c
+ FIRAuthErrorCodeINvalidCustomToken error code.
+ */
+- (void)testInvalidCustomTokenError {
+ FIRVerifyCustomTokenRequest *request =
+ [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyCustomTokenResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyCustomToken:request
+ callback:^(FIRVerifyCustomTokenResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidCustomTokenErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidCustomToken);
+}
+
+/** @fn testInvalidCustomTokenServerError
+ @brief This test simulates @c invalidCustomTokenError with @c
+ FIRAuthErrorCodeINvalidCustomToken error code, with a custom message from the server.
+ */
+- (void)testInvalidCustomTokenServerError {
+ FIRVerifyCustomTokenRequest *request =
+ [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyCustomTokenResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyCustomToken:request
+ callback:^(FIRVerifyCustomTokenResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidCustomTokenServerErrorMessage];
+ NSString *errorDescription = [RPCError.userInfo valueForKey:NSLocalizedDescriptionKey];
+ XCTAssertTrue([errorDescription isEqualToString:kInvalidCustomTokenErrorDetails]);
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidCustomToken);
+}
+
+/** @fn testEmptyServerDetailMessage
+ @brief This test simulates @c invalidCustomTokenError with @c
+ FIRAuthErrorCodeINvalidCustomToken error code, with an empty custom message from the server.
+ @remarks An empty error message is not valid and therefore should not be added as an error
+ description.
+ */
+- (void)testEmptyServerDetailMessage {
+ FIRVerifyCustomTokenRequest *request =
+ [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyCustomTokenResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyCustomToken:request
+ callback:^(FIRVerifyCustomTokenResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidCustomTokenEmptyServerErrorMessage];
+ NSString *errorDescription = [RPCError.userInfo valueForKey:NSLocalizedDescriptionKey];
+ XCTAssertFalse([errorDescription isEqualToString:@""]);
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidCustomToken);
+}
+
+/** @fn testInvalidCredentialMismatchError
+ @brief This test simulates @c credentialMistmatchTokenError with @c
+ FIRAuthErrorCodeCredetialMismatch error code.
+ */
+- (void)testInvalidCredentialMismatchError {
+ FIRVerifyCustomTokenRequest *request =
+ [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyCustomTokenResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyCustomToken:request
+ callback:^(FIRVerifyCustomTokenResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kCredentialMismatchErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeCustomTokenMismatch);
+}
+
+/** @fn testSuccessfulVerifyCustomTokenResponse
+ @brief This test simulates a successful @c VerifyCustomToken flow.
+ */
+- (void)testSuccessfulVerifyCustomTokenResponse {
+ FIRVerifyCustomTokenRequest *request =
+ [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey];
+
+ __block BOOL callbackInvoked;
+ __block FIRVerifyCustomTokenResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyCustomToken:request
+ callback:^(FIRVerifyCustomTokenResponse*_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ kIDTokenKey : kTestIDToken,
+ kExpiresInKey : kTestExpiresIn,
+ kRefreshTokenKey : kTestRefreshToken,
+ }];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCError);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken);
+ NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow];
+ XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon);
+ XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRVerifyPasswordRequestTest.m b/Example/Auth/Tests/FIRVerifyPasswordRequestTest.m
new file mode 100644
index 0000000..f07afdc
--- /dev/null
+++ b/Example/Auth/Tests/FIRVerifyPasswordRequestTest.m
@@ -0,0 +1,163 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRVerifyPasswordRequest.h"
+#import "FIRVerifyPasswordResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kEmailKey
+ @brief The key for the "email" value in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kPasswordKey
+ @brief The key for the "password" value in the request.
+ */
+static NSString *const kPasswordKey = @"password";
+
+/** @var kTestEmail
+ @brief Fake email address for testing the request.
+ */
+static NSString *const kTestEmail = @"testEmail.";
+
+/** @var kTestPassword
+ @brief Fake password for testing the request.
+ */
+static NSString *const kTestPassword = @"testPassword";
+
+/** @var kPendingIDTokenKey
+ @brief The key for the "pendingIdToken" value in the request.
+ */
+static NSString *const kPendingIDTokenKey = @"pendingIdToken";
+
+/** @var kTestPendingToken
+ @brief Fake pendingToken for testing the request.
+ */
+static NSString *const kTestPendingToken = @"testPendingToken";
+
+/** @var kCaptchaChallengeKey
+ @brief The key for the "captchaChallenge" value in the request.
+ */
+static NSString *const kCaptchaChallengeKey = @"captchaChallenge";
+
+/** @var kTestCaptchaChallenge
+ @brief Fake captchaChallenge for testing the request.
+ */
+static NSString *const kTestCaptchaChallenge = @"testCaptchaChallenge";
+
+/** @var kCaptchaResponseKey
+ @brief The key for the "captchaResponse" value in the request.
+ */
+static NSString *const kCaptchaResponseKey = @"captchaResponse";
+
+/** @var kTestCaptchaResponse
+ @brief Fake captchaResponse for testing the request.
+ */
+static NSString *const kTestCaptchaResponse = @"captchaResponse";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=APIKey";
+
+/** @class FIRVerifyPasswordRequestTest
+ @brief Tests for @c FIRVerifyPasswordRequestTest.
+ */
+@interface FIRVerifyPasswordRequestTest : XCTestCase
+@end
+@implementation FIRVerifyPasswordRequestTest {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testVerifyPasswordRequest
+ @brief Tests the verify password request.
+ */
+- (void)testVerifyPasswordRequest {
+ FIRVerifyPasswordRequest * request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ request.returnSecureToken = NO;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPasswordKey], kTestPassword);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kCaptchaChallengeKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kCaptchaResponseKey]);
+ XCTAssertNil(_RPCIssuer.decodedRequest[kReturnSecureTokenKey]);
+}
+
+/** @fn testVerifyPasswordRequestOptionalFields
+ @brief Tests the verify password request with optional fields.
+ */
+- (void)testVerifyPasswordRequestOptionalFields {
+ FIRVerifyPasswordRequest * request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ request.pendingIDToken = kTestPendingToken;
+ request.captchaChallenge = kTestCaptchaChallenge;
+ request.captchaResponse = kTestCaptchaResponse;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPasswordKey], kTestPassword);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kCaptchaChallengeKey], kTestCaptchaChallenge);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kCaptchaResponseKey], kTestCaptchaResponse);
+ XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRVerifyPasswordResponseTests.m b/Example/Auth/Tests/FIRVerifyPasswordResponseTests.m
new file mode 100644
index 0000000..949969b
--- /dev/null
+++ b/Example/Auth/Tests/FIRVerifyPasswordResponseTests.m
@@ -0,0 +1,454 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthErrors.h"
+#import "FIRAuthBackend.h"
+#import "FIRVerifyPasswordRequest.h"
+#import "FIRVerifyPasswordResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestPassword
+ @brief Testing user password.
+ */
+static NSString *const kTestPassword = @"testpassword";
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"_test_API_key_";
+
+/** @var kLocalIDKey
+ @brief The name of the 'localID' property in the response.
+ */
+static NSString *const kLocalIDKey = @"localId";
+
+/** @var kTestLocalID
+ @brief The fake localID for testing the response.
+ */
+static NSString *const kTestLocalID = @"testLocalId";
+
+/** @var kEmailKey
+ @brief The name of the 'email' property in the response.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kTestEmail
+ @brief Fake user email for testing the response.
+ */
+static NSString *const kTestEmail = @"test@gmail.com";
+
+/** @var kDisplayNameKey
+ @brief The name of the 'displayName' property in the response.
+ */
+static NSString *const kDisplayNameKey = @"displayName";
+
+/** @var kTestDisplayName
+ @brief Fake displayName for testing the response.
+ */
+static NSString *const kTestDisplayName = @"testDisplayName";
+
+/** @var kIDTokenKey
+ @brief The name of the "IDToken" property in the response.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kTestIDToken
+ @brief Testing ID token for verifying assertion.
+ */
+static NSString *const kTestIDToken = @"ID_TOKEN";
+
+/** @var kExpiresInKey
+ @brief The name of the "expiresIn" property in the response.
+ */
+static NSString *const kExpiresInKey = @"expiresIn";
+
+/** @var kTestExpiresIn
+ @brief Fake token expiration time.
+ */
+static NSString *const kTestExpiresIn = @"12345";
+
+/** @var kRefreshTokenKey
+ @brief The name of the "refreshToken" property in the response.
+ */
+static NSString *const kRefreshTokenKey = @"refreshToken";
+
+/** @var kTestRefreshToken
+ @brief Fake refresh token.
+ */
+static NSString *const kTestRefreshToken = @"REFRESH_TOKEN";
+
+/** @var kOperationNotAllowedErrorMessage
+ @brief This is the error message the server will respond with if Admin disables IDP specified by
+ provider.
+ */
+static NSString *const kOperationNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED";
+
+/** @var kPasswordLoginDisabledErrorMessage
+ @brief This is the error message the server responds with if password login is disabled.
+ */
+static NSString *const kPasswordLoginDisabledErrorMessage = @"PASSWORD_LOGIN_DISABLED";
+
+/** @var kPhotoUrlKey
+ @brief The name of the 'photoUrl' property in the response.
+ */
+static NSString *const kPhotoUrlKey = @"photoUrl";
+
+/** @var kTestPhotoUrl
+ @brief Fake photoUrl for testing the response.
+ */
+static NSString *const kTestPhotoUrl = @"www.example.com";
+
+/** @var kUserDisabledErrorMessage
+ @brief This is the error message the server will respond with if the user's account has been
+ disabled.
+ */
+static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED";
+
+/** @var kEmailNotFoundErrorMessage
+ @brief This is the error message the server will respond with if the email entered is not
+ found.
+ */
+static NSString *const kEmailNotFoundErrorMessage = @"EMAIL_NOT_FOUND";
+
+/** @var kWrongPasswordErrorMessage
+ @brief This is the error message the server will respond with if the user entered a wrong
+ password.
+ */
+static NSString *const kWrongPasswordErrorMessage = @"INVALID_PASSWORD";
+
+/** @var kInvalidEmailErrorMessage
+ @brief The error returned by the server if the email is invalid.
+ */
+static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL";
+
+/** @var kBadRequestErrorMessage
+ @brief This is the error message returned when a bad request is made; often due to a bad API
+ Key.
+ */
+static NSString *const kBadRequestErrorMessage = @"Bad Request";
+
+/** @var kInvalidKeyReasonValue
+ @brief The value for the "reason" key indicating an invalid API Key was received by the server.
+ */
+static NSString *const kInvalidKeyReasonValue = @"keyInvalid";
+
+/** @var kAppNotAuthorizedReasonValue
+ @brief The value for the "reason" key indicating the App is not authorized to use Firebase
+ Authentication.
+ */
+static NSString *const kAppNotAuthorizedReasonValue = @"ipRefererBlocked";
+
+/** @var kTooManyAttemptsErrorMessage
+ @brief This is the error message the server will respond with if a user has tried (and failed)
+ to sign in too many times.
+ */
+static NSString *const kTooManyAttemptsErrorMessage = @"TOO_MANY_ATTEMPTS_TRY_LATER:";
+
+/** @var kEpsilon
+ @brief Allowed difference when comparing floating point numbers.
+ */
+static const double kEpsilon = 1e-3;
+
+/** @class FIRVerifyPasswordResponseTests
+ @brief Tests for @c FIRVerifyPasswordResponse.
+ */
+@interface FIRVerifyPasswordResponseTests : XCTestCase
+@end
+@implementation FIRVerifyPasswordResponseTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+/** @fn testUserDisabledError
+ @brief Tests that @c FIRAuthErrorCodeUserDisabled error is received if the email is disabled.
+ */
+- (void)testUserDisabledError {
+ FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kUserDisabledErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled);
+}
+
+/** @fn testEmailNotFoundError
+ @brief Tests that @c FIRAuthErrorCodeEmailNotFound error is received if the email is not found.
+ */
+- (void)testEmailNotFoundError {
+ FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kEmailNotFoundErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserNotFound);
+}
+
+/** @fn testInvalidPasswordError
+ @brief Tests that @c FIRAuthErrorCodeInvalidPassword error is received if the password is
+ invalid.
+ */
+- (void)testInvalidPasswordError {
+ FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kWrongPasswordErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeWrongPassword);
+}
+
+/** @fn testInvalidEmailError
+ @brief Tests that @c FIRAuthErrorCodeInvalidEmail error is received if the email address has an
+ incorrect format.
+ */
+- (void)testInvalidEmailError {
+ FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail);
+}
+
+/** @fn testTooManyAttemptsError
+ @brief Tests that @c FIRAuthErrorCodeTooManyRequests error is received if too many sign-in
+ attempts were made.
+ */
+- (void)testTooManySignInAttemptsError {
+ FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+ [_RPCIssuer respondWithServerErrorMessage:kTooManyAttemptsErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeTooManyRequests);
+}
+
+/** @fn testKeyInvalid
+ @brief Tests that @c FIRAuthErrorCodeInvalidApiKey error is received from the server.
+ */
+- (void)testKeyInvalid {
+ FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ NSDictionary *errorDictionary = @{
+ @"error" : @{
+ @"message" : kBadRequestErrorMessage,
+ @"errors" : @[ @{ @"reason" : kInvalidKeyReasonValue } ]
+ }
+ };
+ [_RPCIssuer respondWithJSONError:errorDictionary];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidAPIKey);
+}
+
+/** @fn testOperationNotAllowedError
+ @brief This test simulates a @c FIRAuthErrorCodeOperationNotAllowed error.
+ */
+- (void)testOperationNotAllowedError {
+ FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kOperationNotAllowedErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed);
+}
+
+/** @fn testPasswordLoginDisabledError
+ @brief This test simulates a @c FIRAuthErrorCodeOperationNotAllowed error.
+ */
+- (void)testPasswordLoginDisabledError {
+ FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ callbackInvoked = YES;
+ RPCResponse = response;
+ RPCError = error;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kPasswordLoginDisabledErrorMessage];
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCError);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed);
+}
+
+/** @fn testAppNotAuthorized
+ @brief Tests that @c FIRAuthErrorCodeAppNotAuthorized error is received from the server.
+ */
+- (void)testAppNotAuthorized {
+ FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ NSDictionary *errorDictionary = @{
+ @"error" : @{
+ @"message" : kBadRequestErrorMessage,
+ @"errors" : @[ @{ @"reason" : kAppNotAuthorizedReasonValue } ]
+ }
+ };
+ [_RPCIssuer respondWithJSONError:errorDictionary];
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeAppNotAuthorized);
+}
+
+/** @fn testSuccessfulVerifyPasswordResponse
+ @brief Tests a succesful attempt of the verify password flow.
+ */
+- (void)testSuccessfulVerifyPasswordResponse {
+ FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail
+ password:kTestPassword
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPasswordResponse *RPCResponse;
+ __block NSError *RPCError;
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ kLocalIDKey : kTestLocalID,
+ kEmailKey : kTestEmail,
+ kDisplayNameKey : kTestDisplayName,
+ kIDTokenKey : kTestIDToken,
+ kExpiresInKey : kTestExpiresIn,
+ kRefreshTokenKey : kTestRefreshToken,
+ kPhotoUrlKey : kTestPhotoUrl
+ }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.email, kTestEmail);
+ XCTAssertEqualObjects(RPCResponse.localID, kTestLocalID);
+ XCTAssertEqualObjects(RPCResponse.displayName, kTestDisplayName);
+ XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken);
+ NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow];
+ XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon);
+ XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken);
+ XCTAssertEqualObjects(RPCResponse.photoURL.absoluteString, kTestPhotoUrl );
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRVerifyPhoneNumberRequestTests.m b/Example/Auth/Tests/FIRVerifyPhoneNumberRequestTests.m
new file mode 100644
index 0000000..f51d102
--- /dev/null
+++ b/Example/Auth/Tests/FIRVerifyPhoneNumberRequestTests.m
@@ -0,0 +1,154 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRAuthBackend.h"
+#import "FIRVerifyPhoneNumberRequest.h"
+#import "FIRVerifyPhoneNumberResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kVerificationCode
+ @brief Fake verification code used for testing.
+ */
+static NSString *const kVerificationCode = @"12345678";
+
+/** @var kVerificationID
+ @brief Fake verification ID for testing.
+ */
+static NSString *const kVerificationID = @"55432";
+
+/** @var kPhoneNumber
+ @brief The fake user phone number.
+ */
+static NSString *const kPhoneNumber = @"12345658";
+
+/** @var kTemporaryProof
+ @brief The fake temporary proof.
+ */
+static NSString *const kTemporaryProof = @"12345658";
+
+/** @var kVerificationCodeKey
+ @brief The key for the verification code" value in the request.
+ */
+static NSString *const kVerificationCodeKey = @"code";
+
+/** @var kVerificationIDKey
+ @brief The key for the verification ID" value in the request.
+ */
+static NSString *const kVerificationIDKey = @"sessionInfo";
+
+/** @var kIDTokenKey
+ @brief The key for the "ID Token" value in the request.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kTestAccessToken
+ @bried Fake acess token for testing.
+ */
+ static NSString *const kTestAccessToken = @"accessToken";
+
+ /** @var kTemporaryProofKey
+ @brief The key for the temporary proof value in the request.
+ */
+static NSString *const kTemporaryProofKey = @"temporaryProof";
+
+/** @var kPhoneNumberKey
+ @brief The key for the phone number value in the request.
+ */
+static NSString *const kPhoneNumberKey = @"phoneNumber";
+
+/** @var kExpectedAPIURL
+ @brief The expected URL for the test calls.
+ */
+static NSString *const kExpectedAPIURL =
+ @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhoneNumber?key=APIKey";
+
+/** @class FIRVerifyPhoneNumberRequestTests
+ @brief Tests for @c FIRVerifyPhoneNumberRequest.
+ */
+@interface FIRVerifyPhoneNumberRequestTests : XCTestCase
+@end
+
+@implementation FIRVerifyPhoneNumberRequestTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ [super setUp];
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testVerifyPhoneNumberRequest
+ @brief Tests the verifyPhoneNumber request.
+ */
+- (void)testVerifyPhoneNumberRequest {
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc] initWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode
+ APIKey:kTestAPIKey];
+ request.accessToken = kTestAccessToken;
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kVerificationIDKey], kVerificationID);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kVerificationCodeKey], kVerificationCode);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestAccessToken);
+}
+
+/** @fn testVerifyPhoneNumberRequestWithTemporaryProof
+ @brief Tests the verifyPhoneNumber request when created using a temporary proof.
+ */
+- (void)testVerifyPhoneNumberRequestWithTemporaryProof {
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc] initWithTemporaryProof:kTemporaryProof
+ phoneNumber:kPhoneNumber
+ APIKey:kTestAPIKey];
+ request.accessToken = kTestAccessToken;
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ }];
+
+ XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL);
+ XCTAssertNotNil(_RPCIssuer.decodedRequest);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kTemporaryProofKey], kTemporaryProof);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPhoneNumberKey], kPhoneNumber);
+ XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestAccessToken);
+}
+
+@end
diff --git a/Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m b/Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m
new file mode 100644
index 0000000..c647c3d
--- /dev/null
+++ b/Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m
@@ -0,0 +1,271 @@
+/*
+ * 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/XCTest.h>
+
+#import "Phone/FIRPhoneAuthCredential_Internal.h"
+#import "FIRAuthBackend.h"
+#import "FIRAuthErrors.h"
+#import "FIRVerifyPhoneNumberRequest.h"
+#import "FIRVerifyPhoneNumberResponse.h"
+#import "FIRFakeBackendRPCIssuer.h"
+
+/** @var kTestAPIKey
+ @brief Fake API key used for testing.
+ */
+static NSString *const kTestAPIKey = @"APIKey";
+
+/** @var kVerificationCode
+ @brief Fake verification code used for testing.
+ */
+static NSString *const kVerificationCode = @"12345678";
+
+/** @var kVerificationID
+ @brief Fake verification ID for testing.
+ */
+static NSString *const kVerificationID = @"55432";
+
+/** @var kfakeRefreshToken
+ @brief Fake refresh token for testing.
+ */
+static NSString *const kfakeRefreshToken = @"refreshtoken";
+
+/** @var klocalID
+ @brief Fake local ID for testing.
+ */
+static NSString *const klocalID = @"localID";
+
+/** @var kfakeIDToken
+ @brief Fake ID Token for testing.
+ */
+static NSString *const kfakeIDToken = @"idtoken";
+
+/** @var kTestExpiresIn
+ @brief Fake token expiration time.
+ */
+static NSString *const kTestExpiresIn = @"12345";
+
+/** @var kInvalidVerificationCodeErrorMessage
+ @brief This is the error message the server will respond with if an invalid verification code
+ provided.
+ */
+static NSString *const kInvalidVerificationCodeErrorMessage = @"INVALID_CODE";
+
+/** @var kInvalidSessionInfoErrorMessage
+ @brief This is the error message the server will respond with if an invalid verification ID
+ provided.
+ */
+static NSString *const kInvalidSessionInfoErrorMessage = @"INVALID_SESSION_INFO";
+
+/** @var kSessionExpiredErrorMessage
+ @brief This is the error message the server will respond with if the SMS code has expired before
+ it is used.
+ */
+static NSString *const kSessionExpiredErrorMessage = @"SESSION_EXPIRED";
+
+/** @var kFakePhoneNumber
+ @brief The fake user phone number.
+ */
+static NSString *const kFakePhoneNumber = @"12345658";
+
+/** @var kFakeTemporaryProof
+ @brief The fake temporary proof.
+ */
+static NSString *const kFakeTemporaryProof = @"12345658";
+
+/** @var kEpsilon
+ @brief Allowed difference when comparing floating point numbers.
+ */
+static const double kEpsilon = 1e-3;
+
+/** @class FIRVerifyPhoneNumberResponseTests
+ @brief Tests for @c FIRVerifyPhoneNumberResponse.
+ */
+@interface FIRVerifyPhoneNumberResponseTests : XCTestCase
+
+@end
+
+@implementation FIRVerifyPhoneNumberResponseTests {
+ /** @var _RPCIssuer
+ @brief This backend RPC issuer is used to fake network responses for each test in the suite.
+ In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it.
+ */
+ FIRFakeBackendRPCIssuer *_RPCIssuer;
+}
+
+- (void)setUp {
+ FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init];
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer];
+ _RPCIssuer = RPCIssuer;
+}
+
+- (void)tearDown {
+ _RPCIssuer = nil;
+ [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil];
+ [super tearDown];
+}
+
+/** @fn testInvalidVerificationCodeError
+ @brief Tests invalid verification code error.
+ */
+- (void)testInvalidVerificationCodeError {
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc] initWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPhoneNumberResponse *RPCResponse;
+ __block NSError *RPCError;
+
+
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidVerificationCodeErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidVerificationCode);
+}
+
+/** @fn testInvalidVerificationIDError
+ @brief Tests invalid verification code error.
+ */
+- (void)testInvalidVerificationIDError {
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc] initWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPhoneNumberResponse *RPCResponse;
+ __block NSError *RPCError;
+
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kInvalidSessionInfoErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidVerificationID);
+}
+
+/** @fn testSessionExpiredError
+ @brief Tests session expired error code.
+ */
+- (void)testSessionExpiredError {
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc] initWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPhoneNumberResponse *RPCResponse;
+ __block NSError *RPCError;
+
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithServerErrorMessage:kSessionExpiredErrorMessage];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ XCTAssertEqual(RPCError.code, FIRAuthErrorCodeSessionExpired);
+}
+
+/** @fn testSuccessfulVerifyPhoneNumberResponse
+ @brief Tests a succesful to verify phone number flow.
+ */
+- (void)testSuccessfulVerifyPhoneNumberResponse {
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc] initWithVerificationID:kVerificationID
+ verificationCode:kVerificationCode
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPhoneNumberResponse *RPCResponse;
+ __block NSError *RPCError;
+
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ @"idToken" : kfakeIDToken,
+ @"refreshToken" : kfakeRefreshToken,
+ @"localID" : klocalID,
+ @"expiresIn" : kTestExpiresIn,
+ @"isNewUser" : @YES // Set new user flag to true.
+ }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNotNil(RPCResponse);
+ XCTAssertEqualObjects(RPCResponse.IDToken, kfakeIDToken);
+ XCTAssertEqualObjects(RPCResponse.refreshToken, kfakeRefreshToken);
+ NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow];
+ XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon);
+ XCTAssertTrue(RPCResponse.isNewUser);
+}
+
+/** @fn testSuccessfulVerifyPhoneNumberResponseWithTemporaryProof
+ @brief Tests a succesful to verify phone number flow with temporary proof response.
+ */
+- (void)testSuccessfulVerifyPhoneNumberResponseWithTemporaryProof {
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc] initWithTemporaryProof:kFakeTemporaryProof
+ phoneNumber:kFakePhoneNumber
+ APIKey:kTestAPIKey];
+ __block BOOL callbackInvoked;
+ __block FIRVerifyPhoneNumberResponse *RPCResponse;
+ __block NSError *RPCError;
+
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ RPCResponse = response;
+ RPCError = error;
+ callbackInvoked = YES;
+ }];
+
+ [_RPCIssuer respondWithJSON:@{
+ @"temporaryProof" : kFakeTemporaryProof,
+ @"phoneNumber" : kFakePhoneNumber
+ }];
+
+ XCTAssert(callbackInvoked);
+ XCTAssertNil(RPCResponse);
+ FIRPhoneAuthCredential *credential = RPCError.userInfo[FIRAuthUpdatedCredentialKey];
+ XCTAssertEqualObjects(credential.temporaryProof, kFakeTemporaryProof);
+ XCTAssertEqualObjects(credential.phoneNumber, kFakePhoneNumber);
+}
+
+@end
diff --git a/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.h b/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.h
new file mode 100644
index 0000000..61f9935
--- /dev/null
+++ b/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.h
@@ -0,0 +1,112 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import <OCMock/OCMStubRecorder.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthGeneralBlock1
+ @brief A general block that takes one id and returns nothing.
+ */
+typedef void (^FIRAuthGeneralBlock1)(id);
+
+/** @typedef FIRAuthGeneralBlock2
+ @brief A general block that takes two nullable ids and returns nothing.
+ */
+typedef void (^FIRAuthGeneralBlock2)(id _Nullable, id _Nullable);
+
+/** @typedef FIRAuthIdDoubleIdBlock
+ @brief A block that takes third parameters with types @c id, @c double, and @c id .
+ */
+typedef void (^FIRAuthIdDoubleIdBlock)(id, double, id);
+
+/** @category OCMStubRecorder(FIRAuthUnitTests)
+ @brief Utility methods and properties use by Firebase Auth unit tests.
+ */
+@interface OCMStubRecorder (FIRAuthUnitTests)
+
+/** @fn andCallBlock1
+ @brief Calls a general block that takes one parameter as the action of the stub.
+ @param block1 A block that takes exactly one 'id'-compatible parameter.
+ @remarks The method being stubbed must take exactly one parameter, which must be
+ compatible with type 'id'.
+ */
+- (id)andCallBlock1:(FIRAuthGeneralBlock1)block1;
+
+/** @fn andCallBlock2
+ @brief Calls a general block that takes two parameters as the action of the stub.
+ @param block2 A block that takes exactly two 'id'-compatible parameters.
+ @remarks The method being stubbed must take exactly two parameters, both of which must be
+ compatible with type 'id'.
+ */
+- (id)andCallBlock2:(FIRAuthGeneralBlock2)block2;
+
+/** @fn andDispatchError2
+ @brief Dispatchs an error to the second callback parameter in the global auth work queue.
+ @param error The error to call back as the second argument to the second parameter block.
+ @remarks The method being stubbed must take exactly two parameters, the first of which must be
+ compatible with type 'id' and the second of which must be a block that takes an
+ 'id'-compatible parameter and an NSError* parameter.
+ */
+- (id)andDispatchError2:(NSError *)error;
+
+/** @fn andCallIdDoubleIdBlock:
+ @brief Calls a block that takes three parameters as the action of the stub.
+ @param block A block that takes exactly three parameters as described.
+ @remarks The method being stubbed must take exactly three parameters. Its first and the third
+ parameters must be compatible with type 'id' and its second parameter must be a 'double'.
+ */
+- (id)andCallIdDoubleIdBlock:(FIRAuthIdDoubleIdBlock)block;
+
+// This macro allows .andCallBlock1 shorthand to match established style of OCMStubRecorder.
+#define andCallBlock1(block1) _andCallBlock1(block1)
+
+// This macro allows .andCallBlock2 shorthand to match established style of OCMStubRecorder.
+#define andCallBlock2(block2) _andCallBlock2(block2)
+
+// This macro allows .andDispatchError2 shorthand to match established style of OCMStubRecorder.
+#define andDispatchError2(block2) _andDispatchError2(block2)
+
+// This macro allows .andCallIdDoubleIdBlock shorthand to match established style of
+// OCMStubRecorder.
+#define andCallIdDoubleIdBlock(block) _andCallIdDoubleIdBlock(block)
+
+
+/** @property _andCallBlock1
+ @brief A block that calls @c andCallBlock1: method on self.
+ */
+@property(nonatomic, readonly) OCMStubRecorder *(^ _andCallBlock1)(FIRAuthGeneralBlock1);
+
+/** @property _andCallBlock2
+ @brief A block that calls @c andCallBlock2: method on self.
+ */
+@property(nonatomic, readonly) OCMStubRecorder *(^ _andCallBlock2)(FIRAuthGeneralBlock2);
+
+/** @property _andDispatchError2
+ @brief A block that calls @c andDispatchError2: method on self.
+ */
+@property(nonatomic, readonly) OCMStubRecorder *(^ _andDispatchError2)(NSError *);
+
+/** @property _andCallIdDoubleIdBlock
+ @brief A block that calls @c andCallBlock2: method on self.
+ */
+@property(nonatomic, readonly) OCMStubRecorder *(^ _andCallIdDoubleIdBlock)(FIRAuthIdDoubleIdBlock);
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.m b/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.m
new file mode 100644
index 0000000..bd43303
--- /dev/null
+++ b/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.m
@@ -0,0 +1,103 @@
+/*
+ * 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 "OCMStubRecorder+FIRAuthUnitTests.h"
+
+#import "FIRAuthGlobalWorkQueue.h"
+
+/** @fn argumentOf
+ @brief Retrieves a specific argument from a method invocation.
+ @param invocation The Objective-C method invocation.
+ @param position The position of the argument to retrieve, starting from 0.
+ @return The argument at the given position that the method has been invoked with.
+ @remarks The argument type must be compatible with @c id .
+ */
+static id argumentOf(NSInvocation *invocation, int position) {
+ __unsafe_unretained id unretainedArgument;
+ // Indices 0 and 1 indicate the hidden arguments self and _cmd. Actual arguments starts from 2.
+ [invocation getArgument:&unretainedArgument atIndex:position + 2];
+ // The argument needs to be retained, or it will be released along with the invocation object.
+ id argument = unretainedArgument;
+ return argument;
+}
+
+/** @fn doubleArgumentOf
+ @brief Retrieves a specific argument of type 'double' from a method invocation.
+ @param invocation The Objective-C method invocation.
+ @param position The position of the argument to retrieve, starting from 0.
+ @return The argument at the given position that the method has been invoked with.
+ @remarks The argument type must be @c double .
+ */
+static double doubleArgumentOf(NSInvocation *invocation, int position) {
+ double argument;
+ // Indices 0 and 1 indicate the hidden arguments self and _cmd. Actual arguments starts from 2.
+ [invocation getArgument:&argument atIndex:position + 2];
+ return argument;
+}
+
+@implementation OCMStubRecorder (FIRAuthUnitTests)
+
+- (id)andCallBlock1:(FIRAuthGeneralBlock1)block1 {
+ return [self andDo:^(NSInvocation *invocation) {
+ block1(argumentOf(invocation, 0));
+ }];
+}
+
+- (id)andCallBlock2:(FIRAuthGeneralBlock2)block2 {
+ return [self andDo:^(NSInvocation *invocation) {
+ block2(argumentOf(invocation, 0), argumentOf(invocation, 1));
+ }];
+}
+
+- (id)andDispatchError2:(NSError *)error {
+ return [self andCallBlock2:^(id request, FIRAuthGeneralBlock2 callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
+ callback(nil, error);
+ });
+ }];
+}
+
+- (id)andCallIdDoubleIdBlock:(FIRAuthIdDoubleIdBlock)block {
+ return [self andDo:^(NSInvocation *invocation) {
+ block(argumentOf(invocation, 0), doubleArgumentOf(invocation, 2), argumentOf(invocation, 2));
+ }];
+}
+
+- (OCMStubRecorder *(^)(FIRAuthGeneralBlock1))_andCallBlock1 {
+ return ^(FIRAuthGeneralBlock1 block1) {
+ return [self andCallBlock1:block1];
+ };
+}
+
+- (OCMStubRecorder *(^)(FIRAuthGeneralBlock2))_andCallBlock2 {
+ return ^(FIRAuthGeneralBlock2 block2) {
+ return [self andCallBlock2:block2];
+ };
+}
+
+- (OCMStubRecorder *(^)(NSError *))_andDispatchError2 {
+ return ^(NSError *error) {
+ return [self andDispatchError2:error];
+ };
+}
+
+- (OCMStubRecorder *(^)(FIRAuthIdDoubleIdBlock))_andCallIdDoubleIdBlock {
+ return ^(FIRAuthIdDoubleIdBlock block) {
+ return [self andCallIdDoubleIdBlock:block];
+ };
+}
+
+@end
diff --git a/Example/Auth/Tests/Tests-Info.plist b/Example/Auth/Tests/Tests-Info.plist
new file mode 100644
index 0000000..169b6f7
--- /dev/null
+++ b/Example/Auth/Tests/Tests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Example/Core/App/Base.lproj/LaunchScreen.storyboard b/Example/Core/App/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..66a7681
--- /dev/null
+++ b/Example/Core/App/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Core/App/Base.lproj/Main.storyboard b/Example/Core/App/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..d164a23
--- /dev/null
+++ b/Example/Core/App/Base.lproj/Main.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="whP-gf-Uak">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="wQg-tq-qST">
+ <objects>
+ <viewController id="whP-gf-Uak" customClass="FIRViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="uEw-UM-LJ8"/>
+ <viewControllerLayoutGuide type="bottom" id="Mvr-aV-6Um"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="TpU-gO-2f1">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="tc2-Qw-aMS" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="305" y="433"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Core/App/Core-Info.plist b/Example/Core/App/Core-Info.plist
new file mode 100644
index 0000000..7576a0d
--- /dev/null
+++ b/Example/Core/App/Core-Info.plist
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Core/App/FIRAppDelegate.h b/Example/Core/App/FIRAppDelegate.h
new file mode 100644
index 0000000..e3fba8f
--- /dev/null
+++ b/Example/Core/App/FIRAppDelegate.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 UIKit;
+
+@interface FIRAppDelegate : UIResponder <UIApplicationDelegate>
+
+@property (strong, nonatomic) UIWindow *window;
+
+@end
diff --git a/Example/Core/App/FIRAppDelegate.m b/Example/Core/App/FIRAppDelegate.m
new file mode 100644
index 0000000..0ecfdea
--- /dev/null
+++ b/Example/Core/App/FIRAppDelegate.m
@@ -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 "FIRAppDelegate.h"
+
+@implementation FIRAppDelegate
+
+- (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 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/Core/App/FIRViewController.h b/Example/Core/App/FIRViewController.h
new file mode 100644
index 0000000..64b4b74
--- /dev/null
+++ b/Example/Core/App/FIRViewController.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 UIKit;
+
+@interface FIRViewController : UIViewController
+
+@end
diff --git a/Example/Core/App/FIRViewController.m b/Example/Core/App/FIRViewController.m
new file mode 100644
index 0000000..901accf
--- /dev/null
+++ b/Example/Core/App/FIRViewController.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 "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/Core/App/GoogleService-Info.plist b/Example/Core/App/GoogleService-Info.plist
new file mode 100644
index 0000000..89afffe
--- /dev/null
+++ b/Example/Core/App/GoogleService-Info.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>API_KEY</key>
+ <string>correct_api_key</string>
+ <key>TRACKING_ID</key>
+ <string>correct_tracking_id</string>
+ <key>CLIENT_ID</key>
+ <string>correct_client_id</string>
+ <key>REVERSED_CLIENT_ID</key>
+ <string>correct_reversed_client_id</string>
+ <key>ANDROID_CLIENT_ID</key>
+ <string>correct_android_client_id</string>
+ <key>GOOGLE_APP_ID</key>
+ <string>1:123:ios:123abc</string>
+ <key>GCM_SENDER_ID</key>
+ <string>correct_gcm_sender_id</string>
+ <key>PLIST_VERSION</key>
+ <string>1</string>
+ <key>BUNDLE_ID</key>
+ <string>com.google.FirebaseSDKTests</string>
+ <key>PROJECT_ID</key>
+ <string>abc-xyz-123</string>
+ <key>DATABASE_URL</key>
+ <string>https://abc-xyz-123.firebaseio.com</string>
+ <key>STORAGE_BUCKET</key>
+ <string>project-id-123.storage.firebase.com</string>
+</dict>
+</plist>
diff --git a/Example/Core/App/main.m b/Example/Core/App/main.m
new file mode 100644
index 0000000..03b5c12
--- /dev/null
+++ b/Example/Core/App/main.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 UIKit;
+#import "FIRAppDelegate.h"
+
+int main(int argc, char * argv[])
+{
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class]));
+ }
+}
diff --git a/Example/Core/Tests/FIRAppAssociationRegistrationUnitTests.m b/Example/Core/Tests/FIRAppAssociationRegistrationUnitTests.m
new file mode 100644
index 0000000..9649c99
--- /dev/null
+++ b/Example/Core/Tests/FIRAppAssociationRegistrationUnitTests.m
@@ -0,0 +1,193 @@
+// 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/XCTest.h>
+
+#import "FIRAppAssociationRegistration.h"
+
+/** @var kKey
+ @brief A unique string key.
+ */
+static NSString *kKey = @"key";
+
+/** @var kKey1
+ @brief A unique string key.
+ */
+static NSString *kKey1 = @"key1";
+
+/** @var kKey2
+ @brief A unique string key.
+ */
+static NSString *kKey2 = @"key2";
+
+/** @var gCreateNewObject
+ @brief A block that returns a new object everytime it is called.
+ */
+static id _Nullable (^gCreateNewObject)() = ^id _Nullable() {
+ return [[NSObject alloc] init];
+};
+
+/** @class FIRAppAssociationRegistrationTests
+ @brief Tests for @c FIRAppAssociationRegistration
+ */
+@interface FIRAppAssociationRegistrationTests : XCTestCase
+@end
+
+@implementation FIRAppAssociationRegistrationTests
+
+- (void)testPassObject {
+ id host = gCreateNewObject();
+ id obj = gCreateNewObject();
+ id result = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:^id _Nullable() {
+ return obj;
+ }];
+ XCTAssertEqual(obj, result);
+}
+
+- (void)testPassNil {
+ id host = gCreateNewObject();
+ id obj = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:^id _Nullable() {
+ return nil;
+ }];
+ XCTAssertNil(obj);
+}
+
+- (void)testObjectOwnership {
+ __weak id weakHost;
+ __block __weak id weakObj;
+ @autoreleasepool {
+ id host = gCreateNewObject();
+ weakHost = host;
+ [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:^id _Nullable() {
+ id obj = gCreateNewObject();
+ weakObj = obj;
+ return obj;
+ }];
+ // Verify that neither the host nor the object is released yet, i.e., the host owns the object
+ // because nothing else retains the object.
+ XCTAssertNotNil(weakHost);
+ XCTAssertNotNil(weakObj);
+ }
+ // Verify that both the host and the object are released upon exit of the autorelease pool,
+ // i.e., the host is the sole owner of the object.
+ XCTAssertNil(weakHost);
+ XCTAssertNil(weakObj);
+}
+
+- (void)testSameHostSameKey {
+ id host = gCreateNewObject();
+ id obj1 = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:gCreateNewObject];
+ id obj2 = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:gCreateNewObject];
+ XCTAssertEqual(obj1, obj2);
+}
+
+- (void)testSameHostDifferentKey {
+ id host = gCreateNewObject();
+ id obj1 = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey1
+ creationBlock:gCreateNewObject];
+ id obj2 = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey2
+ creationBlock:gCreateNewObject];
+ XCTAssertNotEqual(obj1, obj2);
+}
+
+- (void)testDifferentHostSameKey {
+ id host1 = gCreateNewObject();
+ id obj1 = [FIRAppAssociationRegistration registeredObjectWithHost:host1
+ key:kKey
+ creationBlock:gCreateNewObject];
+ id host2 = gCreateNewObject();
+ id obj2 = [FIRAppAssociationRegistration registeredObjectWithHost:host2
+ key:kKey
+ creationBlock:gCreateNewObject];
+ XCTAssertNotEqual(obj1, obj2);
+}
+
+- (void)testDifferentHostDifferentKey {
+ id host1 = gCreateNewObject();
+ id obj1 = [FIRAppAssociationRegistration registeredObjectWithHost:host1
+ key:kKey1
+ creationBlock:gCreateNewObject];
+ id host2 = gCreateNewObject();
+ id obj2 = [FIRAppAssociationRegistration registeredObjectWithHost:host2
+ key:kKey2
+ creationBlock:gCreateNewObject];
+ XCTAssertNotEqual(obj1, obj2);
+}
+
+- (void)testReentrySameHostSameKey {
+ id host = gCreateNewObject();
+ XCTAssertThrows([FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:^id _Nullable() {
+ [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:gCreateNewObject];
+ return gCreateNewObject();
+ }]);
+}
+
+- (void)testReentrySameHostDifferentKey {
+ id host = gCreateNewObject();
+ [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey1
+ creationBlock:^id _Nullable() {
+ [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey2
+ creationBlock:gCreateNewObject];
+ return gCreateNewObject();
+ }];
+ // Expect no exception raised.
+}
+
+- (void)testReentryDifferentHostSameKey {
+ id host1 = gCreateNewObject();
+ id host2 = gCreateNewObject();
+ [FIRAppAssociationRegistration registeredObjectWithHost:host1
+ key:kKey
+ creationBlock:^id _Nullable() {
+ [FIRAppAssociationRegistration registeredObjectWithHost:host2
+ key:kKey
+ creationBlock:gCreateNewObject];
+ return gCreateNewObject();
+ }];
+ // Expect no exception raised.
+}
+
+- (void)testReentryDifferentHostDifferentKey {
+ id host1 = gCreateNewObject();
+ id host2 = gCreateNewObject();
+ [FIRAppAssociationRegistration registeredObjectWithHost:host1
+ key:kKey1
+ creationBlock:^id _Nullable() {
+ [FIRAppAssociationRegistration registeredObjectWithHost:host2
+ key:kKey2
+ creationBlock:gCreateNewObject];
+ return gCreateNewObject();
+ }];
+ // Expect no exception raised.
+}
+
+@end
diff --git a/Example/Core/Tests/FIRAppTest.m b/Example/Core/Tests/FIRAppTest.m
new file mode 100644
index 0000000..da97c6c
--- /dev/null
+++ b/Example/Core/Tests/FIRAppTest.m
@@ -0,0 +1,582 @@
+// 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 "FIRAppInternal.h"
+#import "FIROptionsInternal.h"
+#import "FIRTestCase.h"
+
+NSString *const kFIRTestAppName1 = @"test_app_name_1";
+NSString *const kFIRTestAppName2 = @"test-app-name-2";
+
+@interface FIRApp (TestInternal)
+
+@property(nonatomic) BOOL alreadySentConfigureNotification;
+@property(nonatomic) BOOL alreadySentDeleteNotification;
+
++ (void)resetApps;
+- (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options;
+- (BOOL)configureCore;
++ (NSError *)errorForInvalidAppID;
+- (BOOL)isAppIDValid;
++ (NSString *)actualBundleID;
++ (NSNumber *)mapFromServiceStringToTypeEnum:(NSString *)serviceString;
++ (NSString *)deviceModel;
++ (NSString *)installString;
++ (NSURL *)filePathURLWithName:(NSString *)fileName;
++ (NSString *)stringAtURL:(NSURL *)filePathURL;
++ (BOOL)writeString:(NSString *)string toURL:(NSURL *)filePathURL;
++ (void)logAppInfo:(NSNotification *)notification;
++ (BOOL)validateAppID:(NSString *)appID;
++ (BOOL)validateAppIDFormat:(NSString *)appID withVersion:(NSString *)version;
++ (BOOL)validateAppIDFingerprint:(NSString *)appID withVersion:(NSString *)version;
+
+@end
+
+
+@interface FIRAppTest : FIRTestCase
+
+@property(nonatomic) id appClassMock;
+@property(nonatomic) id optionsInstanceMock;
+@property(nonatomic) id notificationCenterMock;
+@property(nonatomic) FIRApp *app;
+
+@end
+
+@implementation FIRAppTest
+
+- (void)setUp {
+ [super setUp];
+ [FIROptions resetDefaultOptions];
+ [FIRApp resetApps];
+ _appClassMock = OCMClassMock([FIRApp class]);
+ _optionsInstanceMock = OCMPartialMock([FIROptions defaultOptions]);
+ _notificationCenterMock = OCMPartialMock([NSNotificationCenter defaultCenter]);
+}
+
+- (void)tearDown {
+ [_appClassMock stopMocking];
+ [_optionsInstanceMock stopMocking];
+ [_notificationCenterMock stopMocking];
+
+ [super tearDown];
+}
+
+- (void)testConfigure {
+ NSDictionary *expectedUserInfo = [self expectedUserInfoWithAppName:kFIRDefaultAppName
+ isDefaultApp:YES];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo]);
+ XCTAssertNoThrow([FIRApp configure]);
+ OCMVerifyAll(self.notificationCenterMock);
+
+ self.app = [FIRApp defaultApp];
+ XCTAssertNotNil(self.app);
+ XCTAssertEqualObjects(self.app.name, kFIRDefaultAppName);
+ XCTAssertEqualObjects(self.app.options.clientID, kClientID);
+ XCTAssertTrue([FIRApp allApps].count == 1);
+ XCTAssertTrue(self.app.alreadySentConfigureNotification);
+
+ // Test if options is nil
+ id optionsClassMock = OCMClassMock([FIROptions class]);
+ OCMStub([optionsClassMock defaultOptions]).andReturn(nil);
+ XCTAssertThrows([FIRApp configure]);
+}
+
+- (void)testConfigureWithOptions {
+ // nil options
+ XCTAssertThrows([FIRApp configureWithOptions:nil]);
+ XCTAssertTrue([FIRApp allApps].count == 0);
+
+ NSDictionary *expectedUserInfo = [self expectedUserInfoWithAppName:kFIRDefaultAppName
+ isDefaultApp:YES];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo]);
+ // default options
+ XCTAssertNoThrow([FIRApp configureWithOptions:[FIROptions defaultOptions]]);
+ OCMVerifyAll(self.notificationCenterMock);
+
+ self.app = [FIRApp defaultApp];
+ XCTAssertNotNil(self.app);
+ XCTAssertEqualObjects(self.app.name, kFIRDefaultAppName);
+ XCTAssertEqualObjects(self.app.options.clientID, kClientID);
+ XCTAssertTrue([FIRApp allApps].count == 1);
+}
+
+- (void)testConfigureWithCustomizedOptions {
+ // valid customized options
+ FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ bundleID:kBundleID
+ GCMSenderID:kGCMSenderID
+ APIKey:kCustomizedAPIKey
+ clientID:nil
+ trackingID:nil
+ androidClientID:nil
+ databaseURL:nil
+ storageBucket:nil
+ deepLinkURLScheme:nil];
+
+ NSDictionary *expectedUserInfo = [self expectedUserInfoWithAppName:kFIRDefaultAppName
+ isDefaultApp:YES];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo]);
+
+ XCTAssertNoThrow([FIRApp configureWithOptions:options]);
+ OCMVerifyAll(self.notificationCenterMock);
+
+ self.app = [FIRApp defaultApp];
+ XCTAssertNotNil(self.app);
+ XCTAssertEqualObjects(self.app.name, kFIRDefaultAppName);
+ XCTAssertEqualObjects(self.app.options.googleAppID, kGoogleAppID);
+ XCTAssertEqualObjects(self.app.options.APIKey, kCustomizedAPIKey);
+ XCTAssertTrue([FIRApp allApps].count == 1);
+}
+
+- (void)testConfigureWithNameAndOptions {
+ XCTAssertThrows([FIRApp configureWithName:nil options:[FIROptions defaultOptions]]);
+ XCTAssertThrows([FIRApp configureWithName:kFIRTestAppName1 options:nil]);
+ XCTAssertThrows([FIRApp configureWithName:@"" options:[FIROptions defaultOptions]]);
+ XCTAssertThrows([FIRApp configureWithName:kFIRDefaultAppName
+ options:[FIROptions defaultOptions]]);
+ XCTAssertTrue([FIRApp allApps].count == 0);
+
+ NSDictionary *expectedUserInfo = [self expectedUserInfoWithAppName:kFIRTestAppName1
+ isDefaultApp:NO];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo]);
+ XCTAssertNoThrow([FIRApp configureWithName:kFIRTestAppName1 options:[FIROptions defaultOptions]]);
+ OCMVerifyAll(self.notificationCenterMock);
+
+ XCTAssertTrue([FIRApp allApps].count == 1);
+ self.app = [FIRApp appNamed:kFIRTestAppName1];
+ XCTAssertNotNil(self.app);
+ XCTAssertEqualObjects(self.app.name, kFIRTestAppName1);
+ XCTAssertEqualObjects(self.app.options.clientID, kClientID);
+
+ // Configure the same app again should throw an exception.
+ XCTAssertThrows([FIRApp configureWithName:kFIRTestAppName1 options:[FIROptions defaultOptions]]);
+}
+
+- (void)testConfigureWithNameAndCustomizedOptions {
+ FIROptions *options = [FIROptions defaultOptions];
+ FIROptions *newOptions = [options copy];
+ newOptions.deepLinkURLScheme = kDeepLinkURLScheme;
+
+ NSDictionary *expectedUserInfo1 = [self expectedUserInfoWithAppName:kFIRTestAppName1
+ isDefaultApp:NO];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo1]);
+ XCTAssertNoThrow([FIRApp configureWithName:kFIRTestAppName1 options:newOptions]);
+ XCTAssertTrue([FIRApp allApps].count == 1);
+ self.app = [FIRApp appNamed:kFIRTestAppName1];
+
+ // Configure a different app with valid customized options
+ FIROptions *customizedOptions = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ bundleID:kBundleID
+ GCMSenderID:kGCMSenderID
+ APIKey:kCustomizedAPIKey
+ clientID:nil
+ trackingID:nil
+ androidClientID:nil
+ databaseURL:nil
+ storageBucket:nil
+ deepLinkURLScheme:nil];
+
+ NSDictionary *expectedUserInfo2 = [self expectedUserInfoWithAppName:kFIRTestAppName2
+ isDefaultApp:NO];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo2]);
+ XCTAssertNoThrow([FIRApp configureWithName:kFIRTestAppName2 options:customizedOptions]);
+ OCMVerifyAll(self.notificationCenterMock);
+
+ XCTAssertTrue([FIRApp allApps].count == 2);
+ self.app = [FIRApp appNamed:kFIRTestAppName2];
+ XCTAssertNotNil(self.app);
+ XCTAssertEqualObjects(self.app.name, kFIRTestAppName2);
+ XCTAssertEqualObjects(self.app.options.googleAppID, kGoogleAppID);
+ XCTAssertEqualObjects(self.app.options.APIKey, kCustomizedAPIKey);
+}
+
+- (void)testValidName {
+ XCTAssertNoThrow([FIRApp configureWithName:@"aA1_" options:[FIROptions defaultOptions]]);
+ XCTAssertThrows([FIRApp configureWithName:@"aA1%" options:[FIROptions defaultOptions]]);
+ XCTAssertThrows([FIRApp configureWithName:@"aA1?" options:[FIROptions defaultOptions]]);
+ XCTAssertThrows([FIRApp configureWithName:@"aA1!" options:[FIROptions defaultOptions]]);
+}
+
+- (void)testDefaultApp {
+ self.app = [FIRApp defaultApp];
+ XCTAssertNil(self.app);
+
+ [FIRApp configure];
+ self.app = [FIRApp defaultApp];
+ XCTAssertEqualObjects(self.app.name, kFIRDefaultAppName);
+ XCTAssertEqualObjects(self.app.options.clientID, kClientID);
+}
+
+- (void)testAppNamed {
+ self.app = [FIRApp appNamed:kFIRTestAppName1];
+ XCTAssertNil(self.app);
+
+ [FIRApp configureWithName:kFIRTestAppName1 options:[FIROptions defaultOptions]];
+ self.app = [FIRApp appNamed:kFIRTestAppName1];
+ XCTAssertEqualObjects(self.app.name, kFIRTestAppName1);
+ XCTAssertEqualObjects(self.app.options.clientID, kClientID);
+}
+
+- (void)testDeleteApp {
+ [FIRApp configure];
+ self.app = [FIRApp defaultApp];
+ XCTAssertTrue([FIRApp allApps].count == 1);
+ [self.app deleteApp:^(BOOL success) {
+ XCTAssertTrue(success);
+ }];
+ OCMVerify([self.notificationCenterMock postNotificationName:kFIRAppDeleteNotification
+ object:[FIRApp class]
+ userInfo:[OCMArg any]]);
+ XCTAssertTrue(self.app.alreadySentDeleteNotification);
+ XCTAssertTrue([FIRApp allApps].count == 0);
+}
+
+- (void)testErrorForSubspecConfigurationFailure {
+ NSError *error = [FIRApp errorForSubspecConfigurationFailureWithDomain:kFirebaseAdMobErrorDomain
+ errorCode:FIRErrorCodeAdMobFailed
+ service:kFIRServiceAdMob
+ reason:@"some reason"];
+ XCTAssertNotNil(error);
+ XCTAssert([error.domain isEqualToString:kFirebaseAdMobErrorDomain]);
+ XCTAssert(error.code == FIRErrorCodeAdMobFailed);
+ XCTAssert([error.description containsString:@"Configuration failed for"]);
+}
+
+- (void)testGetTokenWithCallback {
+ [FIRApp configure];
+ FIRApp *app = [FIRApp defaultApp];
+
+ __block BOOL getTokenImplementationWasCalled = NO;
+ __block BOOL getTokenCallbackWasCalled = NO;
+ __block BOOL passedRefreshValue = NO;
+
+ [app getTokenForcingRefresh:YES
+ withCallback:^(NSString *_Nullable token, NSError *_Nullable error) {
+ getTokenCallbackWasCalled = YES;
+ }];
+
+ XCTAssert(getTokenCallbackWasCalled,
+ @"The callback should be invoked by the base implementation when no block for "
+ "'getTokenImplementation' has been specified.");
+
+ getTokenCallbackWasCalled = NO;
+
+ app.getTokenImplementation = ^(BOOL refresh, FIRTokenCallback callback) {
+ getTokenImplementationWasCalled = YES;
+ passedRefreshValue = refresh;
+ callback(nil, nil);
+ };
+ [app getTokenForcingRefresh:YES
+ withCallback:^(NSString *_Nullable token, NSError *_Nullable error) {
+ getTokenCallbackWasCalled = YES;
+ }];
+
+ XCTAssert(getTokenImplementationWasCalled,
+ @"The 'getTokenImplementation' block was never called.");
+ XCTAssert(passedRefreshValue,
+ @"The value for the 'refresh' parameter wasn't passed to the 'getTokenImplementation' "
+ "block correctly.");
+ XCTAssert(getTokenCallbackWasCalled,
+ @"The 'getTokenImplementation' should have invoked the callback. This could be an "
+ "error in this test, or the callback parameter may not have been passed to the "
+ "implementation correctly.");
+
+ getTokenImplementationWasCalled = NO;
+ getTokenCallbackWasCalled = NO;
+ passedRefreshValue = NO;
+
+ [app getTokenForcingRefresh:NO
+ withCallback:^(NSString *_Nullable token, NSError *_Nullable error) {
+ getTokenCallbackWasCalled = YES;
+ }];
+
+ XCTAssertFalse(passedRefreshValue,
+ @"The value for the 'refresh' parameter wasn't passed to the "
+ "'getTokenImplementation' block correctly.");
+}
+
+- (void)testModifyingOptionsThrows {
+ [FIRApp configure];
+ FIROptions *options = [[FIRApp defaultApp] options];
+ XCTAssertTrue(options.isEditingLocked);
+
+ // Modification to every property should result in an exception.
+ XCTAssertThrows(options.androidClientID = @"should_throw");
+ XCTAssertThrows(options.APIKey = @"should_throw");
+ XCTAssertThrows(options.bundleID = @"should_throw");
+ XCTAssertThrows(options.clientID = @"should_throw");
+ XCTAssertThrows(options.databaseURL = @"should_throw");
+ XCTAssertThrows(options.deepLinkURLScheme = @"should_throw");
+ XCTAssertThrows(options.GCMSenderID = @"should_throw");
+ XCTAssertThrows(options.googleAppID = @"should_throw");
+ XCTAssertThrows(options.projectID = @"should_throw");
+ XCTAssertThrows(options.storageBucket = @"should_throw");
+ XCTAssertThrows(options.trackingID = @"should_throw");
+}
+
+- (void)testOptionsLocking {
+ FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ GCMSenderID:kGCMSenderID];
+ options.projectID = kProjectID;
+ options.databaseURL = kDatabaseURL;
+
+ // Options should not be locked before they are used to configure a `FIRApp`.
+ XCTAssertFalse(options.isEditingLocked);
+
+ // The options returned should be locked after configuring `FIRApp`.
+ [FIRApp configureWithOptions:options];
+ FIROptions *optionsCopy = [[FIRApp defaultApp] options];
+ XCTAssertTrue(optionsCopy.isEditingLocked);
+}
+
+#pragma mark - App ID v1
+
+- (void)testAppIDV1 {
+ // Missing separator between platform:fingerprint.
+ XCTAssertFalse([FIRApp validateAppID:@"1:1337:iosdeadbeef"]);
+
+ // Wrong platform "android".
+ XCTAssertFalse([FIRApp validateAppID:@"1:1337:android:deadbeef"]);
+
+ // The fingerprint, aka 4th field, should only contain hex characters.
+ XCTAssertFalse([FIRApp validateAppID:@"1:1337:ios:123abcxyz"]);
+
+ // The fingerprint, aka 4th field, is not tested in V1, so a bad value shouldn't cause a failure.
+ XCTAssertTrue([FIRApp validateAppID:@"1:1337:ios:deadbeef"]);
+}
+
+#pragma mark - App ID v2
+
+- (void)testAppIDV2 {
+ // Missing separator between platform:fingerprint.
+ XCTAssertTrue([FIRApp validateAppID:@"2:1337:ios5e18052ab54fbfec"]);
+
+ // Unknown versions may contain anything.
+ XCTAssertTrue([FIRApp validateAppID:@"2:1337:ios:123abcxyz"]);
+ XCTAssertTrue([FIRApp validateAppID:@"2:thisdoesn'teven_m:a:t:t:e:r_"]);
+
+ // Known good fingerprint.
+ XCTAssertTrue([FIRApp validateAppID:@"2:1337:ios:5e18052ab54fbfec"]);
+
+ // Unknown fingerprint, not tested so shouldn't cause a failure.
+ XCTAssertTrue([FIRApp validateAppID:@"2:1337:ios:deadbeef"]);
+}
+
+#pragma mark - App ID other
+
+- (void)testAppIDV3 {
+ // Currently there is no specification for v3, so we would not expect it to fail.
+ XCTAssertTrue([FIRApp validateAppID:@"3:1337:ios:deadbeef"]);
+}
+
+- (void)testAppIDEmpty {
+ XCTAssertFalse([FIRApp validateAppID:@""]);
+}
+
+- (void)testAppIDValidationTrue {
+ // Ensure that isAppIDValid matches validateAppID.
+ [FIRApp configure];
+ OCMStub([self.appClassMock validateAppID:[OCMArg any]]).andReturn(YES);
+ XCTAssertTrue([[FIRApp defaultApp] isAppIDValid]);
+}
+
+- (void)testAppIDValidationFalse {
+ // Ensure that isAppIDValid matches validateAppID.
+ [FIRApp configure];
+ OCMStub([self.appClassMock validateAppID:[OCMArg any]]).andReturn(NO);
+ XCTAssertFalse([[FIRApp defaultApp] isAppIDValid]);
+}
+
+- (void)testAppIDPrefix {
+ // Unknown numeric-character prefixes should pass.
+ XCTAssertTrue([FIRApp validateAppID:@"0:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"01:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"10:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"010:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"3:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"123:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"999999999:"]);
+
+ // Non-numeric prefixes should not pass.
+ XCTAssertFalse([FIRApp validateAppID:@"a:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"abcsdf0:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"0aaaa:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"0aaaa0450:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"-1:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"abcsdf:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"ABDCF:"]);
+ XCTAssertFalse([FIRApp validateAppID:@" :"]);
+ XCTAssertFalse([FIRApp validateAppID:@"1 :"]);
+ XCTAssertFalse([FIRApp validateAppID:@" 1:"]);
+ XCTAssertFalse([FIRApp validateAppID:@" 123 :"]);
+ XCTAssertFalse([FIRApp validateAppID:@"1 23:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"&($*&%(*$&:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"abCDSF$%%df:"]);
+
+ // Known version prefixes should never pass without the rest of the app ID string present.
+ XCTAssertFalse([FIRApp validateAppID:@"1:"]);
+
+ // Version must include ":".
+ XCTAssertFalse([FIRApp validateAppID:@"0"]);
+ XCTAssertFalse([FIRApp validateAppID:@"01"]);
+ XCTAssertFalse([FIRApp validateAppID:@"10"]);
+ XCTAssertFalse([FIRApp validateAppID:@"010"]);
+ XCTAssertFalse([FIRApp validateAppID:@"3"]);
+ XCTAssertFalse([FIRApp validateAppID:@"123"]);
+ XCTAssertFalse([FIRApp validateAppID:@"999999999"]);
+ XCTAssertFalse([FIRApp validateAppID:@"com.google.bundleID"]);
+}
+
+- (void)testAppIDFormatInvalid {
+ OCMStub([self.appClassMock actualBundleID]).andReturn(@"com.google.bundleID");
+ // Some direct tests of the validateAppIDFormat:withVersion: method.
+ // Sanity checks first.
+ NSString *const kGoodAppIDV1 = @"1:1337:ios:deadbeef";
+ NSString *const kGoodVersionV1 = @"1:";
+ XCTAssertTrue([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:kGoodVersionV1]);
+
+ NSString *const kGoodAppIDV2 = @"2:1337:ios:5e18052ab54fbfec";
+ NSString *const kGoodVersionV2 = @"2:";
+ XCTAssertTrue([FIRApp validateAppIDFormat:kGoodAppIDV2 withVersion:kGoodVersionV2]);
+
+ // Version mismatch.
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV2 withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:@"999:"]);
+
+ // Nil or empty strings.
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:nil]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:@""]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:nil withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:nil withVersion:nil]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"" withVersion:@""]);
+
+ // App ID contains only the version prefix.
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodVersionV1 withVersion:kGoodVersionV1]);
+ // The version is the entire app ID.
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:kGoodAppIDV1]);
+
+ // Versions digits that may make a partial match.
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"01:1337:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"10:1337:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"11:1337:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"21:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"22:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"02:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"20:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+
+ // Extra fields.
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"ab:1:1337:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"1:ab:1337:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"1:1337:ab:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"1:1337:ios:ab:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"1:1337:ios:deadbeef:ab" withVersion:kGoodVersionV1]);
+}
+
+- (void)testAppIDFingerprintInvalid {
+ OCMStub([self.appClassMock actualBundleID]).andReturn(@"com.google.bundleID");
+ // Some direct tests of the validateAppIDFingerprint:withVersion: method.
+ // Sanity checks first.
+ NSString *const kGoodAppIDV1 = @"1:1337:ios:deadbeef";
+ NSString *const kGoodVersionV1 = @"1:";
+ XCTAssertTrue([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:kGoodVersionV1]);
+
+ NSString *const kGoodAppIDV2 = @"2:1337:ios:5e18052ab54fbfec";
+ 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:@""]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:nil withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:nil withVersion:nil]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"" withVersion:@""]);
+
+ // App ID contains only the version prefix.
+ 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]);
+}
+
+#pragma mark - Internal Methods
+
+- (void)testAuthGetUID {
+ [FIRApp configure];
+
+ [FIRApp defaultApp].getUIDImplementation = ^NSString *{ return @"highlander"; };
+ XCTAssertEqual([[FIRApp defaultApp] getUID], @"highlander");
+}
+
+#pragma mark - private
+
+- (NSDictionary <NSString *, NSObject *> *)expectedUserInfoWithAppName:(NSString *)name
+ isDefaultApp:(BOOL)isDefaultApp {
+ return @{
+ kFIRAppNameKey : name,
+ kFIRAppIsDefaultAppKey : [NSNumber numberWithBool:isDefaultApp],
+ kFIRGoogleAppIDKey : kGoogleAppID
+ };
+}
+
+@end
diff --git a/Example/Core/Tests/FIRBundleUtilTest.m b/Example/Core/Tests/FIRBundleUtilTest.m
new file mode 100644
index 0000000..6a3e20a
--- /dev/null
+++ b/Example/Core/Tests/FIRBundleUtilTest.m
@@ -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 "FIRBundleUtil.h"
+
+#import "FIRTestCase.h"
+
+static NSString *const kResultPath = @"resultPath";
+static NSString *const kResourceName = @"resourceName";
+static NSString *const kFileType = @"fileType";
+
+@interface FIRBundleUtilTest : FIRTestCase
+
+@property(nonatomic, strong) id mockBundle;
+
+@end
+
+@implementation FIRBundleUtilTest
+
+- (void)setUp {
+ [super setUp];
+ self.mockBundle = OCMClassMock([NSBundle class]);
+}
+
+- (void)testRelevantBundles_mainIsFirst {
+ // Pointer compare to same instance of main bundle.
+ XCTAssertEqual([NSBundle mainBundle], [FIRBundleUtil relevantBundles][0]);
+}
+
+// TODO: test that adding a bundle appears in "all bundles"
+// once the use-case is understood.
+
+- (void)testFindOptionsDictionaryPath {
+ [OCMStub([self.mockBundle pathForResource:kResourceName ofType:kFileType]) andReturn:kResultPath];
+ XCTAssertEqualObjects(
+ [FIRBundleUtil optionsDictionaryPathWithResourceName:kResourceName
+ andFileType:kFileType
+ inBundles:@[ self.mockBundle ]],
+ kResultPath);
+}
+
+- (void)testFindOptionsDictionaryPath_notFound {
+ XCTAssertNil([FIRBundleUtil optionsDictionaryPathWithResourceName:kResourceName
+ andFileType:kFileType
+ inBundles:@[ self.mockBundle ]]);
+}
+
+- (void)testFindOptionsDictionaryPath_secondBundle {
+ NSBundle *mockBundleEmpty = OCMClassMock([NSBundle class]);
+ [OCMStub([self.mockBundle pathForResource:kResourceName ofType:kFileType]) andReturn:kResultPath];
+
+ NSArray *bundles = @[ mockBundleEmpty, self.mockBundle ];
+ XCTAssertEqualObjects(
+ [FIRBundleUtil optionsDictionaryPathWithResourceName:kResourceName
+ andFileType:kFileType
+ inBundles:bundles],
+ kResultPath);
+}
+
+- (void)testBundleIdentifierExistsInBundles {
+ NSString *bundleID = @"com.google.test";
+ [OCMStub([self.mockBundle bundleIdentifier]) andReturn:bundleID];
+ XCTAssertTrue([FIRBundleUtil hasBundleIdentifier:bundleID inBundles:@[ self.mockBundle ]]);
+}
+
+- (void)testBundleIdentifierExistsInBundles_notExist {
+ [OCMStub([self.mockBundle bundleIdentifier]) andReturn:@"com.google.test"];
+ XCTAssertFalse([FIRBundleUtil hasBundleIdentifier:@"not-exist" inBundles:@[ self.mockBundle ]]);
+}
+
+- (void)testBundleIdentifierExistsInBundles_emptyBundlesArray {
+ XCTAssertFalse([FIRBundleUtil hasBundleIdentifier:@"com.google.test" inBundles:@[ ]]);
+}
+
+@end
diff --git a/Example/Core/Tests/FIRConfigurationTest.m b/Example/Core/Tests/FIRConfigurationTest.m
new file mode 100644
index 0000000..2b3ff46
--- /dev/null
+++ b/Example/Core/Tests/FIRConfigurationTest.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 "FIRConfiguration.h"
+
+#import "FIRTestCase.h"
+
+@interface FIRConfigurationTest : FIRTestCase
+
+@end
+
+@implementation FIRConfigurationTest
+
+- (void)testSharedInstance {
+ FIRConfiguration *config = [FIRConfiguration sharedInstance];
+ XCTAssertNotNil(config);
+ XCTAssertNotNil(config.analyticsConfiguration);
+}
+
+@end
diff --git a/Example/Core/Tests/FIRLoggerTest.m b/Example/Core/Tests/FIRLoggerTest.m
new file mode 100644
index 0000000..e7031a7
--- /dev/null
+++ b/Example/Core/Tests/FIRLoggerTest.m
@@ -0,0 +1,265 @@
+// 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 "FIRLogger.h"
+#import "FIRTestCase.h"
+
+#import <asl.h>
+
+// The following constants are exposed from FIRLogger for unit tests.
+extern NSString *const kFIRDisableDebugModeApplicationArgument;
+extern NSString *const kFIREnableDebugModeApplicationArgument;
+
+extern NSString *const kFIRPersistedDebugModeKey;
+
+extern const char *kFIRLoggerASLClientFacilityName;
+
+extern const char *kFIRLoggerCustomASLMessageFormat;
+
+extern void FIRResetLogger();
+
+extern aslclient getFIRLoggerClient();
+
+extern dispatch_queue_t getFIRClientQueue();
+
+extern BOOL getFIRLoggerDebugMode();
+
+// Define the message format again to make sure the format doesn't accidentally change.
+static NSString *const kCorrectASLMessageFormat =
+ @"$((Time)(J.3)) $(Sender)[$(PID)] <$((Level)(str))> $Message";
+
+static NSString *const kMessageCode = @"I-COR000001";
+
+@interface FIRLoggerTest : FIRTestCase
+
+@property(nonatomic) NSString *randomLogString;
+
+@end
+
+@implementation FIRLoggerTest
+
+- (void)setUp {
+ [super setUp];
+ FIRResetLogger();
+}
+
+// Test some stable variables to make sure they weren't accidently changed.
+- (void)testStableVariables {
+ // kFIRLoggerCustomASLMessageFormat.
+ XCTAssertEqualObjects(kCorrectASLMessageFormat,
+ [NSString stringWithUTF8String:kFIRLoggerCustomASLMessageFormat]);
+
+ // Strings of type FIRLoggerServices.
+ XCTAssertEqualObjects(kFIRLoggerABTesting, @"[Firebase/ABTesting]");
+ XCTAssertEqualObjects(kFIRLoggerAdMob, @"[Firebase/AdMob]");
+ XCTAssertEqualObjects(kFIRLoggerAnalytics, @"[Firebase/Analytics]");
+ XCTAssertEqualObjects(kFIRLoggerAuth, @"[Firebase/Auth]");
+ XCTAssertEqualObjects(kFIRLoggerCore, @"[Firebase/Core]");
+ XCTAssertEqualObjects(kFIRLoggerCrash, @"[Firebase/Crash]");
+ XCTAssertEqualObjects(kFIRLoggerDatabase, @"[Firebase/Database]");
+ XCTAssertEqualObjects(kFIRLoggerDynamicLinks, @"[Firebase/DynamicLinks]");
+ XCTAssertEqualObjects(kFIRLoggerInstanceID, @"[Firebase/InstanceID]");
+ XCTAssertEqualObjects(kFIRLoggerInvites, @"[Firebase/Invites]");
+ XCTAssertEqualObjects(kFIRLoggerMessaging, @"[Firebase/Messaging]");
+ XCTAssertEqualObjects(kFIRLoggerRemoteConfig, @"[Firebase/RemoteConfig]");
+ XCTAssertEqualObjects(kFIRLoggerStorage, @"[Firebase/Storage]");
+}
+
+- (void)testInitializeASLForNonDebugMode {
+ // Stub.
+ id processInfoMock = [OCMockObject partialMockForObject:[NSProcessInfo processInfo]];
+ NSArray *arguments = @[ kFIRDisableDebugModeApplicationArgument ];
+ [[[processInfoMock stub] andReturn:arguments] arguments];
+
+ // Test.
+ FIRLogError(kFIRLoggerCore, kMessageCode, @"Some error.");
+
+ // Assert.
+ NSNumber *debugMode =
+ [[NSUserDefaults standardUserDefaults] objectForKey:kFIRPersistedDebugModeKey];
+ XCTAssertNil(debugMode);
+ XCTAssertFalse(getFIRLoggerDebugMode());
+
+ // Stop.
+ [processInfoMock stopMocking];
+}
+
+- (void)testInitializeASLForDebugModeWithArgument {
+ // Stub.
+ id processInfoMock = [OCMockObject partialMockForObject:[NSProcessInfo processInfo]];
+ NSArray *arguments = @[ kFIREnableDebugModeApplicationArgument ];
+ [[[processInfoMock stub] andReturn:arguments] arguments];
+
+ // Test.
+ FIRLogError(kFIRLoggerCore, kMessageCode, @"Some error.");
+
+ // Assert.
+ NSNumber *debugMode =
+ [[NSUserDefaults standardUserDefaults] objectForKey:kFIRPersistedDebugModeKey];
+ XCTAssertTrue(debugMode.boolValue);
+ XCTAssertTrue(getFIRLoggerDebugMode());
+
+ // Stop.
+ [processInfoMock stopMocking];
+}
+
+- (void)testInitializeASLForDebugModeWithUserDefaults {
+ // Stub.
+ id userDefaultsMock = [OCMockObject partialMockForObject:[NSUserDefaults standardUserDefaults]];
+ NSNumber *debugMode = @YES;
+ [[[userDefaultsMock stub] andReturnValue:debugMode] boolForKey:kFIRPersistedDebugModeKey];
+
+ // Test.
+ FIRLogError(kFIRLoggerCore, kMessageCode, @"Some error.");
+
+ // Assert.
+ debugMode = [[NSUserDefaults standardUserDefaults] objectForKey:kFIRPersistedDebugModeKey];
+ XCTAssertTrue(debugMode.boolValue);
+ XCTAssertTrue(getFIRLoggerDebugMode());
+
+ // Stop.
+ [userDefaultsMock stopMocking];
+}
+
+- (void)testMessageCodeFormat {
+ // Valid case.
+ XCTAssertNoThrow(FIRLogError(kFIRLoggerCore, @"I-APP000001", @"Message."));
+
+ // An extra dash or missing dash should fail.
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"I-APP-000001", @"Message."));
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"IAPP000001", @"Message."));
+
+ // Wrong number of digits should fail.
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"I-APP00001", @"Message."));
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"I-APP0000001", @"Message."));
+
+ // Lowercase should fail.
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"I-app000001", @"Message."));
+
+ // nil or empty message code should fail.
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, nil, @"Message."));
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"", @"Message."));
+
+ // Android message code should fail.
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"A-APP000001", @"Message."));
+}
+
+
+- (void)testLoggerInterface {
+ XCTAssertNoThrow(FIRLogError(kFIRLoggerCore, kMessageCode, @"Message."));
+ XCTAssertNoThrow(FIRLogError(kFIRLoggerCore, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(FIRLogWarning(kFIRLoggerCore, kMessageCode, @"Message."));
+ XCTAssertNoThrow(FIRLogWarning(kFIRLoggerCore, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(FIRLogNotice(kFIRLoggerCore, kMessageCode, @"Message."));
+ XCTAssertNoThrow(FIRLogNotice(kFIRLoggerCore, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(FIRLogInfo(kFIRLoggerCore, kMessageCode, @"Message."));
+ XCTAssertNoThrow(FIRLogInfo(kFIRLoggerCore, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(FIRLogDebug(kFIRLoggerCore, kMessageCode, @"Message."));
+ XCTAssertNoThrow(FIRLogDebug(kFIRLoggerCore, kMessageCode, @"Configure %@.", @"blah"));
+}
+
+
+// asl_set_filter does not perform as expected in unit test environment with simulator. The
+// following test only checks whether the logs have been sent to system with the default settings in
+// the unit test environment.
+- (void)testSystemLogWithDefaultStatus {
+#if !(TARGET_OS_SIMULATOR)
+ // Test fails on device - b/38130372
+ return;
+#else
+ // Sets the time interval that we need to wait in order to fetch all the logs.
+ NSTimeInterval timeInterval = 0.1f;
+ // Generates a random string each time and check whether it has been logged.
+ // Log messages with Notice level and below should be logged to system/device by default.
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ FIRLogError(kFIRLoggerCore, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertTrue([self logExists]);
+
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ FIRLogWarning(kFIRLoggerCore, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertTrue([self logExists]);
+
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ FIRLogNotice(kFIRLoggerCore, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertTrue([self logExists]);
+
+ // Log messages with Info level and above should NOT be logged to system/device by default.
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ FIRLogInfo(kFIRLoggerCore, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertFalse([self logExists]);
+
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ FIRLogDebug(kFIRLoggerCore, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertFalse([self logExists]);
+#endif
+}
+
+// The FIRLoggerLevel enum must match the ASL_LEVEL_* constants, but we manually redefine
+// them in FIRLoggerLevel.h since we cannot include <asl.h> (see b/34976089 for more details).
+// This test ensures the constants match.
+- (void)testFIRLoggerLevelValues {
+ XCTAssertEqual(FIRLoggerLevelError, ASL_LEVEL_ERR);
+ XCTAssertEqual(FIRLoggerLevelWarning, ASL_LEVEL_WARNING);
+ XCTAssertEqual(FIRLoggerLevelNotice, ASL_LEVEL_NOTICE);
+ XCTAssertEqual(FIRLoggerLevelInfo, ASL_LEVEL_INFO);
+ XCTAssertEqual(FIRLoggerLevelDebug, ASL_LEVEL_DEBUG);
+}
+
+
+// Helper functions.
+- (BOOL)logExists {
+ [self drainFIRClientQueue];
+ NSString *correctMsg = [NSString stringWithFormat:@"%@[%@] %@", kFIRLoggerCore, kMessageCode,
+ self.randomLogString];
+ return [self messageWasLogged:correctMsg];
+}
+
+
+- (void)drainFIRClientQueue {
+ dispatch_semaphore_t workerSemaphore = dispatch_semaphore_create(0);
+ dispatch_async(getFIRClientQueue(), ^{
+ dispatch_semaphore_signal(workerSemaphore);
+ });
+ dispatch_semaphore_wait(workerSemaphore, DISPATCH_TIME_FOREVER);
+}
+
+- (BOOL)messageWasLogged:(NSString *)message {
+ aslmsg query = asl_new(ASL_TYPE_QUERY);
+ asl_set_query(query, ASL_KEY_FACILITY, kFIRLoggerASLClientFacilityName, ASL_QUERY_OP_EQUAL);
+ aslresponse r = asl_search(getFIRLoggerClient(), query);
+ asl_free(query);
+ aslmsg m;
+ const char *val;
+ NSMutableArray *allMsg = [[NSMutableArray alloc] init];
+ while ((m = asl_next(r)) != NULL) {
+ val = asl_get(m, ASL_KEY_MSG);
+ if (val) {
+ [allMsg addObject:[NSString stringWithUTF8String:val]];
+ }
+ }
+ asl_free(m);
+ asl_release(r);
+ return [allMsg containsObject:message];
+}
+
+@end
diff --git a/Example/Core/Tests/FIROptionsTest.m b/Example/Core/Tests/FIROptionsTest.m
new file mode 100644
index 0000000..62b6294
--- /dev/null
+++ b/Example/Core/Tests/FIROptionsTest.m
@@ -0,0 +1,468 @@
+// 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 "FIRAppInternal.h"
+#import "FIRBundleUtil.h"
+#import "FIROptionsInternal.h"
+
+#import "FIRTestCase.h"
+
+extern NSString *const kFIRIsMeasurementEnabled;
+extern NSString *const kFIRIsAnalyticsCollectionEnabled;
+extern NSString *const kFIRIsAnalyticsCollectionDeactivated;
+extern NSString *const kFIRLibraryVersionID;
+
+@interface FIROptions (Test)
+
+@property(nonatomic, readonly) NSDictionary *analyticsOptionsDictionary;
+
+@end
+
+@interface FIROptionsTest : FIRTestCase
+
+@end
+
+@implementation FIROptionsTest
+
+- (void)setUp {
+ [super setUp];
+ [FIROptions resetDefaultOptions];
+}
+
+- (void)testInit {
+ NSDictionary *optionsDictionary = [FIROptions defaultOptionsDictionary];
+ FIROptions *options =
+ [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ [self assertOptionsMatchDefaults:options andProjectID:YES];
+ XCTAssertNil(options.deepLinkURLScheme);
+ XCTAssertTrue(options.usingOptionsFromDefaultPlist);
+
+ options.deepLinkURLScheme = kDeepLinkURLScheme;
+ XCTAssertEqualObjects(options.deepLinkURLScheme, kDeepLinkURLScheme);
+}
+
+- (void)testDefaultOptionsDictionaryWithNilFilePath {
+ id mockBundleUtil = OCMClassMock([FIRBundleUtil class]);
+ [OCMStub([mockBundleUtil optionsDictionaryPathWithResourceName:kServiceInfoFileName
+ andFileType:kServiceInfoFileType
+ inBundles:[FIRBundleUtil relevantBundles]])
+ andReturn:nil];
+ XCTAssertNil([FIROptions defaultOptionsDictionary]);
+}
+
+- (void)testDefaultOptionsDictionaryWithInvalidSourceFile {
+ id mockBundleUtil = OCMClassMock([FIRBundleUtil class]);
+ [OCMStub([mockBundleUtil optionsDictionaryPathWithResourceName:kServiceInfoFileName
+ andFileType:kServiceInfoFileType
+ inBundles:[FIRBundleUtil relevantBundles]])
+ andReturn:@"invalid.plist"];
+ XCTAssertNil([FIROptions defaultOptionsDictionary]);
+}
+
+- (void)testDefaultOptions {
+ FIROptions *options = [FIROptions defaultOptions];
+ [self assertOptionsMatchDefaults:options andProjectID:YES];
+ XCTAssertNil(options.deepLinkURLScheme);
+ XCTAssertTrue(options.usingOptionsFromDefaultPlist);
+
+ options.deepLinkURLScheme = kDeepLinkURLScheme;
+ XCTAssertEqualObjects(options.deepLinkURLScheme, kDeepLinkURLScheme);
+}
+
+- (void)testInitCustomizedOptions {
+ FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ bundleID:kBundleID
+ GCMSenderID:kGCMSenderID
+ APIKey:kAPIKey
+ clientID:kClientID
+ trackingID:kTrackingID
+ androidClientID:kAndroidClientID
+ databaseURL:kDatabaseURL
+ storageBucket:kStorageBucket
+ deepLinkURLScheme:kDeepLinkURLScheme];
+ [self assertOptionsMatchDefaults:options andProjectID:NO];
+ XCTAssertEqualObjects(options.deepLinkURLScheme, kDeepLinkURLScheme);
+ XCTAssertFalse(options.usingOptionsFromDefaultPlist);
+
+ FIROptions *options2 = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ GCMSenderID:kGCMSenderID];
+ options2.androidClientID = kAndroidClientID;
+ options2.APIKey = kAPIKey;
+ options2.bundleID = kBundleID;
+ options2.clientID = kClientID;
+ options2.databaseURL = kDatabaseURL;
+ options2.deepLinkURLScheme = kDeepLinkURLScheme;
+ options2.projectID = kProjectID;
+ options2.storageBucket = kStorageBucket;
+ options2.trackingID = kTrackingID;
+ [self assertOptionsMatchDefaults:options2 andProjectID:YES];
+ XCTAssertEqualObjects(options2.deepLinkURLScheme, kDeepLinkURLScheme);
+ XCTAssertFalse(options.usingOptionsFromDefaultPlist);
+
+ // nil GoogleAppID should throw an exception
+ XCTAssertThrows([[FIROptions alloc] initWithGoogleAppID:nil
+ bundleID:kBundleID
+ GCMSenderID:kGCMSenderID
+ APIKey:kCustomizedAPIKey
+ clientID:nil
+ trackingID:nil
+ androidClientID:nil
+ databaseURL:nil
+ storageBucket:nil
+ deepLinkURLScheme:nil]);
+}
+
+- (void)testinitWithContentsOfFile {
+ NSString *filePath =
+ [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"];
+ FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:filePath];
+ [self assertOptionsMatchDefaults:options andProjectID:YES];
+ XCTAssertNil(options.deepLinkURLScheme);
+ XCTAssertFalse(options.usingOptionsFromDefaultPlist);
+
+ FIROptions *emptyOptions = [[FIROptions alloc] initWithContentsOfFile:nil];
+ XCTAssertNil(emptyOptions);
+
+ FIROptions *invalidOptions = [[FIROptions alloc] initWithContentsOfFile:@"invalid.plist"];
+ XCTAssertNil(invalidOptions);
+}
+
+- (void)assertOptionsMatchDefaults:(FIROptions *)options andProjectID:(BOOL)matchProjectID {
+ XCTAssertEqualObjects(options.googleAppID, kGoogleAppID);
+ XCTAssertEqualObjects(options.APIKey, kAPIKey);
+ XCTAssertEqualObjects(options.clientID, kClientID);
+ XCTAssertEqualObjects(options.trackingID, kTrackingID);
+ XCTAssertEqualObjects(options.GCMSenderID, kGCMSenderID);
+ XCTAssertEqualObjects(options.androidClientID, kAndroidClientID);
+ XCTAssertEqualObjects(options.libraryVersionID, kFIRLibraryVersionID);
+ XCTAssertEqualObjects(options.databaseURL, kDatabaseURL);
+ XCTAssertEqualObjects(options.storageBucket, kStorageBucket);
+ XCTAssertEqualObjects(options.bundleID, kBundleID);
+
+ // Custom `matchProjectID` parameter to be removed once the deprecated `FIROptions` constructor is
+ // removed.
+ if (matchProjectID) {
+ XCTAssertEqualObjects(options.projectID, kProjectID);
+ }
+}
+
+- (void)testCopyingProperties {
+ NSMutableString *mutableString;
+ FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ GCMSenderID:kGCMSenderID];
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.APIKey = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.APIKey, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.bundleID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.bundleID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.clientID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.clientID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.trackingID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.trackingID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.GCMSenderID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.GCMSenderID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.projectID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.projectID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.androidClientID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.androidClientID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.googleAppID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.googleAppID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.databaseURL = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.databaseURL, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.deepLinkURLScheme = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.deepLinkURLScheme, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.storageBucket = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.storageBucket, @"1");
+}
+
+- (void)testCopyWithZone {
+ // default options
+ FIROptions *options = [FIROptions defaultOptions];
+ options.deepLinkURLScheme = kDeepLinkURLScheme;
+ XCTAssertEqualObjects(options.deepLinkURLScheme, kDeepLinkURLScheme);
+
+ FIROptions *newOptions = [options copy];
+ XCTAssertEqualObjects(newOptions.deepLinkURLScheme, kDeepLinkURLScheme);
+
+ [options setDeepLinkURLScheme:kNewDeepLinkURLScheme];
+ XCTAssertEqualObjects(options.deepLinkURLScheme, kNewDeepLinkURLScheme);
+ XCTAssertEqualObjects(newOptions.deepLinkURLScheme, kDeepLinkURLScheme);
+
+ // customized options
+ FIROptions *customizedOptions = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ bundleID:kBundleID
+ GCMSenderID:kGCMSenderID
+ APIKey:kAPIKey
+ clientID:kClientID
+ trackingID:kTrackingID
+ androidClientID:kAndroidClientID
+ databaseURL:kDatabaseURL
+ storageBucket:kStorageBucket
+ deepLinkURLScheme:kDeepLinkURLScheme];
+ FIROptions *copyCustomizedOptions = [customizedOptions copy];
+ [copyCustomizedOptions setDeepLinkURLScheme:kNewDeepLinkURLScheme];
+ XCTAssertEqualObjects(customizedOptions.deepLinkURLScheme, kDeepLinkURLScheme);
+ XCTAssertEqualObjects(copyCustomizedOptions.deepLinkURLScheme, kNewDeepLinkURLScheme);
+}
+
+- (void)testAnalyticsConstants {
+ // The keys are public values and should never change.
+ XCTAssertEqualObjects(kFIRIsMeasurementEnabled, @"IS_MEASUREMENT_ENABLED");
+ XCTAssertEqualObjects(kFIRIsAnalyticsCollectionEnabled, @"FIREBASE_ANALYTICS_COLLECTION_ENABLED");
+ XCTAssertEqualObjects(kFIRIsAnalyticsCollectionDeactivated,
+ @"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED");
+}
+
+- (void)testAnalyticsOptions {
+ id mainBundleMock = OCMPartialMock([NSBundle mainBundle]);
+
+ // No keys anywhere.
+ NSDictionary *optionsDictionary = nil;
+ FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ NSDictionary *mainDictionary = nil;
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ NSDictionary *expectedAnalyticsOptions = @{};
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ optionsDictionary = @{};
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{};
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = @{};
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ // Main has no keys.
+ optionsDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{};
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = optionsDictionary;
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ // Main overrides all the keys.
+ optionsDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @NO,
+ kFIRIsAnalyticsCollectionEnabled : @NO,
+ kFIRIsMeasurementEnabled : @NO
+ };
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = mainDictionary;
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ // Keys exist only in main.
+ optionsDictionary = @{};
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = mainDictionary;
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ // Main overrides single keys.
+ optionsDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{ kFIRIsAnalyticsCollectionDeactivated : @NO };
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = @{
+ kFIRIsAnalyticsCollectionDeactivated : @NO, // override
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ optionsDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{ kFIRIsAnalyticsCollectionEnabled : @NO };
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @NO, // override
+ kFIRIsMeasurementEnabled : @YES
+ };
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ optionsDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{ kFIRIsMeasurementEnabled : @NO };
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @NO // override
+ };
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+}
+
+- (void)testAnalyticsOptions_combinatorial {
+ // Complete combinatorial test.
+ id mainBundleMock = OCMPartialMock([NSBundle mainBundle]);
+ // Possible values for the flags in the plist, where NSNull means the flag is not present.
+ NSArray *values = @[ [NSNull null], @NO, @YES ];
+
+ // Sanity checks for the combination generation.
+ int combinationCount = 0;
+ NSMutableArray *uniqueMainCombinations = [[NSMutableArray alloc] init];
+ NSMutableArray *uniqueOptionsCombinations = [[NSMutableArray alloc] init];
+
+ // Generate all optout flag combinations for { main plist X GoogleService-info options plist }.
+ // Options present in the main plist should override options of the same key in the service plist.
+ for (id mainDeactivated in values) {
+ for (id mainAnalyticsEnabled in values) {
+ for (id mainMeasurementEnabled in values) {
+ for (id optionsDeactivated in values) {
+ for (id optionsAnalyticsEnabled in values) {
+ for (id optionsMeasurementEnabled in values) {
+ @autoreleasepool {
+ // Fill the GoogleService-info options plist dictionary.
+ NSMutableDictionary *optionsDictionary = [[NSMutableDictionary alloc] init];
+ if (![optionsDeactivated isEqual:[NSNull null]]) {
+ optionsDictionary[kFIRIsAnalyticsCollectionDeactivated] = optionsDeactivated;
+ }
+ if (![optionsAnalyticsEnabled isEqual:[NSNull null]]) {
+ optionsDictionary[kFIRIsAnalyticsCollectionEnabled] = optionsAnalyticsEnabled;
+ }
+ if (![optionsMeasurementEnabled isEqual:[NSNull null]]) {
+ optionsDictionary[kFIRIsMeasurementEnabled] = optionsMeasurementEnabled;
+ }
+ FIROptions *options =
+ [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ if (![uniqueOptionsCombinations containsObject:optionsDictionary]) {
+ [uniqueOptionsCombinations addObject:optionsDictionary];
+ }
+
+ // Fill the main plist dictionary.
+ NSMutableDictionary *mainDictionary = [[NSMutableDictionary alloc] init];
+ if (![mainDeactivated isEqual:[NSNull null]]) {
+ mainDictionary[kFIRIsAnalyticsCollectionDeactivated] = mainDeactivated;
+ }
+ if (![mainAnalyticsEnabled isEqual:[NSNull null]]) {
+ mainDictionary[kFIRIsAnalyticsCollectionEnabled] = mainAnalyticsEnabled;
+ }
+ if (![mainMeasurementEnabled isEqual:[NSNull null]]) {
+ mainDictionary[kFIRIsMeasurementEnabled] = mainMeasurementEnabled;
+ }
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ if (![uniqueMainCombinations containsObject:mainDictionary]) {
+ [uniqueMainCombinations addObject:mainDictionary];
+ }
+
+ // Generate the expected options by adding main values on top of the service options
+ // values. The main values will replace any existing options values with the same
+ // key. This is a different way of combining the two sets of flags from the actual
+ // implementation in FIROptions, with equivalent output.
+ NSMutableDictionary *expectedAnalyticsOptions =
+ [[NSMutableDictionary alloc] initWithDictionary:optionsDictionary];
+ [expectedAnalyticsOptions addEntriesFromDictionary:mainDictionary];
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ combinationCount++;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Verify the sanity checks.
+ XCTAssertEqual(combinationCount, 729); // = 3^6.
+
+ XCTAssertEqual(uniqueOptionsCombinations.count, 27);
+ int optionsSizeCount[4] = {0};
+ for (NSDictionary *dictionary in uniqueOptionsCombinations) {
+ optionsSizeCount[dictionary.count]++;
+ }
+ XCTAssertEqual(optionsSizeCount[0], 1);
+ XCTAssertEqual(optionsSizeCount[1], 6);
+ XCTAssertEqual(optionsSizeCount[2], 12);
+ XCTAssertEqual(optionsSizeCount[3], 8);
+
+ XCTAssertEqual(uniqueMainCombinations.count, 27);
+ int mainSizeCount[4] = {0};
+ for (NSDictionary *dictionary in uniqueMainCombinations) {
+ mainSizeCount[dictionary.count]++;
+ }
+ XCTAssertEqual(mainSizeCount[0], 1);
+ XCTAssertEqual(mainSizeCount[1], 6);
+ XCTAssertEqual(mainSizeCount[2], 12);
+ XCTAssertEqual(mainSizeCount[3], 8);
+}
+
+- (void)testVersionFormat {
+ NSRegularExpression *sLibraryVersionRegex =
+ [NSRegularExpression regularExpressionWithPattern:@"^[0-9]{8,}$" options:0 error:NULL];
+ NSUInteger numberOfMatches =
+ [sLibraryVersionRegex numberOfMatchesInString:kFIRLibraryVersionID
+ options:0
+ range:NSMakeRange(0, kFIRLibraryVersionID.length)];
+ XCTAssertEqual(numberOfMatches, 1, @"Incorrect library version format.");
+}
+
+@end
diff --git a/Example/Core/Tests/FIRTestCase.h b/Example/Core/Tests/FIRTestCase.h
new file mode 100644
index 0000000..acc7b16
--- /dev/null
+++ b/Example/Core/Tests/FIRTestCase.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+extern NSString *const kAPIKey;
+extern NSString *const kCustomizedAPIKey;
+extern NSString *const kClientID;
+extern NSString *const kTrackingID;
+extern NSString *const kGCMSenderID;
+extern NSString *const kAndroidClientID;
+extern NSString *const kGoogleAppID;
+extern NSString *const kDatabaseURL;
+extern NSString *const kStorageBucket;
+
+extern NSString *const kDeepLinkURLScheme;
+extern NSString *const kNewDeepLinkURLScheme;
+
+extern NSString *const kBundleID;
+extern NSString *const kProjectID;
+
+/**
+ * Base test case for Firebase Core SDK tests.
+ */
+@interface FIRTestCase : XCTestCase
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Core/Tests/FIRTestCase.m b/Example/Core/Tests/FIRTestCase.m
new file mode 100644
index 0000000..2c68b8d
--- /dev/null
+++ b/Example/Core/Tests/FIRTestCase.m
@@ -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 "FIRTestCase.h"
+
+NSString *const kAPIKey = @"correct_api_key";
+NSString *const kCustomizedAPIKey = @"customized_api_key";
+NSString *const kClientID = @"correct_client_id";
+NSString *const kTrackingID = @"correct_tracking_id";
+NSString *const kGCMSenderID = @"correct_gcm_sender_id";
+NSString *const kAndroidClientID = @"correct_android_client_id";
+NSString *const kGoogleAppID = @"1:123:ios:123abc";
+NSString *const kDatabaseURL = @"https://abc-xyz-123.firebaseio.com";
+NSString *const kStorageBucket = @"project-id-123.storage.firebase.com";
+
+NSString *const kDeepLinkURLScheme = @"comgoogledeeplinkurl";
+NSString *const kNewDeepLinkURLScheme = @"newdeeplinkurlfortest";
+
+NSString *const kBundleID = @"com.google.FirebaseSDKTests";
+NSString *const kProjectID = @"abc-xyz-123";
+
+@interface FIRTestCase ()
+
+@end
+
+@implementation FIRTestCase
+
+- (void)setUp {
+ [super setUp];
+}
+
+- (void)tearDown {
+ [super tearDown];
+}
+
+@end
diff --git a/Example/Core/Tests/Tests-Info.plist b/Example/Core/Tests/Tests-Info.plist
new file mode 100644
index 0000000..169b6f7
--- /dev/null
+++ b/Example/Core/Tests/Tests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Example/Database/App/Base.lproj/LaunchScreen.storyboard b/Example/Database/App/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..66a7681
--- /dev/null
+++ b/Example/Database/App/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Database/App/Base.lproj/Main.storyboard b/Example/Database/App/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..d164a23
--- /dev/null
+++ b/Example/Database/App/Base.lproj/Main.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="whP-gf-Uak">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="wQg-tq-qST">
+ <objects>
+ <viewController id="whP-gf-Uak" customClass="FIRViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="uEw-UM-LJ8"/>
+ <viewControllerLayoutGuide type="bottom" id="Mvr-aV-6Um"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="TpU-gO-2f1">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="tc2-Qw-aMS" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="305" y="433"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Database/App/Database-Info.plist b/Example/Database/App/Database-Info.plist
new file mode 100644
index 0000000..7576a0d
--- /dev/null
+++ b/Example/Database/App/Database-Info.plist
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Database/App/FIRAppDelegate.h b/Example/Database/App/FIRAppDelegate.h
new file mode 100644
index 0000000..e3fba8f
--- /dev/null
+++ b/Example/Database/App/FIRAppDelegate.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 UIKit;
+
+@interface FIRAppDelegate : UIResponder <UIApplicationDelegate>
+
+@property (strong, nonatomic) UIWindow *window;
+
+@end
diff --git a/Example/Database/App/FIRAppDelegate.m b/Example/Database/App/FIRAppDelegate.m
new file mode 100644
index 0000000..0ecfdea
--- /dev/null
+++ b/Example/Database/App/FIRAppDelegate.m
@@ -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 "FIRAppDelegate.h"
+
+@implementation FIRAppDelegate
+
+- (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 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/Database/App/FIRViewController.h b/Example/Database/App/FIRViewController.h
new file mode 100644
index 0000000..64b4b74
--- /dev/null
+++ b/Example/Database/App/FIRViewController.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 UIKit;
+
+@interface FIRViewController : UIViewController
+
+@end
diff --git a/Example/Database/App/FIRViewController.m b/Example/Database/App/FIRViewController.m
new file mode 100644
index 0000000..901accf
--- /dev/null
+++ b/Example/Database/App/FIRViewController.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 "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/Database/App/main.m b/Example/Database/App/main.m
new file mode 100644
index 0000000..03b5c12
--- /dev/null
+++ b/Example/Database/App/main.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 UIKit;
+#import "FIRAppDelegate.h"
+
+int main(int argc, char * argv[])
+{
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class]));
+ }
+}
diff --git a/Example/Database/Tests/FirebaseTests-Info.plist b/Example/Database/Tests/FirebaseTests-Info.plist
new file mode 100644
index 0000000..42887ee
--- /dev/null
+++ b/Example/Database/Tests/FirebaseTests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.firebase.mobile.ios.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Example/Database/Tests/Helpers/FDevice.h b/Example/Database/Tests/Helpers/FDevice.h
new file mode 100644
index 0000000..c32aea0
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FDevice.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 <Foundation/Foundation.h>
+
+@class FIRDatabaseReference;
+@class SenTest;
+
+@interface FDevice : NSObject
+- (id)initOnline;
+- (id)initOffline;
+- (id)initOnlineWithUrl:(NSString *)firebaseUrl;
+- (id)initOfflineWithUrl:(NSString *)firebaseUrl;
+- (void)goOffline;
+- (void)goOnline;
+- (void)restartOnline;
+- (void)restartOffline;
+- (void)waitForIdleUsingWaiter:(XCTest*)waiter;
+- (void)do:(void (^)(FIRDatabaseReference *))action;
+
+- (void)dispose;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FDevice.m b/Example/Database/Tests/Helpers/FDevice.m
new file mode 100644
index 0000000..f9667df
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FDevice.m
@@ -0,0 +1,133 @@
+/*
+ * 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/XCTest.h>
+#import "FDevice.h"
+#import "FIRDatabaseReference.h"
+#import "FRepoManager.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "SenTest+FWaiter.h"
+#import "FTestHelpers.h"
+
+@interface FDevice() {
+ FIRDatabaseConfig * config;
+ NSString *url;
+ BOOL isOnline;
+ BOOL disposed;
+}
+@end
+
+@implementation FDevice
+
+- (id)initOnline {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ return [self initOnlineWithUrl:[ref description]];
+}
+
+- (id)initOffline {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ return [self initOfflineWithUrl:[ref description]];
+}
+
+- (id)initOnlineWithUrl:(NSString *)firebaseUrl {
+ return [self initWithUrl:firebaseUrl andOnline:YES];
+}
+
+- (id)initOfflineWithUrl:(NSString *)firebaseUrl {
+ return [self initWithUrl:firebaseUrl andOnline:NO];
+}
+
+static NSUInteger deviceId = 0;
+
+- (id)initWithUrl:(NSString *)firebaseUrl andOnline:(BOOL)online {
+ self = [super init];
+ if (self) {
+ config = [FIRDatabaseConfig configForName:[NSString stringWithFormat:@"device-%lu", deviceId++]];
+ config.persistenceEnabled = YES;
+ url = firebaseUrl;
+ isOnline = online;
+ }
+ return self;
+}
+
+- (void) dealloc
+{
+ if (!self->disposed) {
+ [NSException raise:NSInternalInconsistencyException format:@"Forgot to dispose device"];
+ }
+}
+
+- (void) dispose {
+ // TODO: clear persistence
+ [FRepoManager disposeRepos:self->config];
+ self->disposed = YES;
+}
+
+- (void)goOffline {
+ isOnline = NO;
+ [FRepoManager interrupt:config];
+}
+
+- (void)goOnline {
+ isOnline = YES;
+ [FRepoManager resume:config];
+}
+
+- (void)restartOnline {
+ @autoreleasepool {
+ [FRepoManager disposeRepos:config];
+ isOnline = YES;
+ }
+}
+
+- (void)restartOffline {
+ @autoreleasepool {
+ [FRepoManager disposeRepos:config];
+ isOnline = NO;
+ }
+}
+
+// Waits for us to connect and then does an extra round-trip to make sure all initial state restoration is completely done.
+- (void)waitForIdleUsingWaiter:(XCTest*)waiter {
+ [self do:^(FIRDatabaseReference *ref) {
+ __block BOOL connected = NO;
+ FIRDatabaseHandle handle = [[ref.root child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ connected = [snapshot.value boolValue];
+ }];
+ [waiter waitUntil:^BOOL { return connected; }];
+ [ref.root removeObserverWithHandle:handle];
+
+ // HACK: Do a deep setPriority (which we expect to fail because there's no data there) to do a no-op roundtrip.
+ __block BOOL done = NO;
+ [[ref.root child:@"ENTOHTNUHOE/ONTEHNUHTOE"] setPriority:@"blah" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ [waiter waitUntil:^BOOL { return done; }];
+ }];
+}
+
+- (void)do:(void (^)(FIRDatabaseReference *))action {
+ @autoreleasepool {
+ FIRDatabaseReference *ref = [[[[FIRDatabaseReference alloc] initWithConfig:self->config] database] referenceFromURL:self->url];
+ if (!isOnline) {
+ [FRepoManager interrupt:config];
+ }
+ action(ref);
+ }
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FEventTester.h b/Example/Database/Tests/Helpers/FEventTester.h
new file mode 100644
index 0000000..b3503b9
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FEventTester.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 <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+@interface FEventTester : XCTestCase
+
+- (id)initFrom:(XCTestCase *)elsewhere;
+- (void) addLookingFor:(NSArray *)l;
+- (void) wait;
+- (void) waitForInitialization;
+- (void) unregister;
+
+@property (nonatomic, strong) NSMutableArray* lookingFor;
+@property (readwrite) int callbacksCalled;
+@property (nonatomic, strong) NSMutableDictionary* seenFirebaseLocations;
+//@property (nonatomic, strong) NSMutableDictionary* initializationEvents;
+@property (nonatomic, strong) XCTestCase* from;
+@property (nonatomic, strong) NSMutableArray* errors;
+@property (nonatomic, strong) NSMutableArray* actualPathsAndEvents;
+@property (nonatomic) int initializationEvents;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FEventTester.m b/Example/Database/Tests/Helpers/FEventTester.m
new file mode 100644
index 0000000..fa7c081
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FEventTester.m
@@ -0,0 +1,172 @@
+/*
+ * 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 "FEventTester.h"
+#import "FIRDatabaseReference.h"
+#import "FTupleBoolBlock.h"
+#import "FTupleEventTypeString.h"
+#import "FTestHelpers.h"
+#import "SenTest+FWaiter.h"
+
+@implementation FEventTester
+
+@synthesize lookingFor;
+@synthesize callbacksCalled;
+@synthesize from;
+@synthesize errors;
+@synthesize seenFirebaseLocations;
+@synthesize initializationEvents;
+@synthesize actualPathsAndEvents;
+
+- (id)initFrom:(XCTestCase *)elsewhere
+{
+ self = [super init];
+ if (self) {
+ self.seenFirebaseLocations = [[NSMutableDictionary alloc] init];
+ self.initializationEvents = 0;
+ self.lookingFor = [[NSMutableArray alloc] init];
+ self.actualPathsAndEvents = [[NSMutableArray alloc] init];
+ self.from = elsewhere;
+ self.callbacksCalled = 0;
+ }
+ return self;
+}
+
+- (void) addLookingFor:(NSArray *)l {
+
+ // expect them in the order they're given to us
+ [self.lookingFor addObjectsFromArray:l];
+
+
+ // see notes on ordering of listens in init.spec.js
+ NSArray* toListen = [l sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
+ FTupleEventTypeString* a = obj1;
+ FTupleEventTypeString* b = obj2;
+ NSUInteger lenA = [a.firebase description].length;
+ NSUInteger lenB = [b.firebase description].length;
+ if (lenA < lenB) {
+ return NSOrderedAscending;
+ } else if (lenA == lenB) {
+ return NSOrderedSame;
+ } else {
+ return NSOrderedDescending;
+ }
+ }];
+
+ for(FTupleEventTypeString* fevts in toListen) {
+ if(! [self.seenFirebaseLocations objectForKey:[fevts.firebase description]]) {
+ fevts.vvcallback = [self listenOnPath:fevts.firebase];
+ fevts.initialized = NO;
+ [self.seenFirebaseLocations setObject:fevts forKey:[fevts.firebase description]];
+ }
+ }
+}
+
+- (void) unregister {
+ for(FTupleEventTypeString* fevts in self.lookingFor) {
+ if (fevts.vvcallback) {
+ fevts.vvcallback();
+ }
+ }
+ [self.lookingFor removeAllObjects];
+}
+
+- (fbt_void_void) listenOnPath:(FIRDatabaseReference *)path {
+ FIRDatabaseHandle removedHandle = [path observeEventType:FIRDataEventTypeChildRemoved withBlock:[self makeEventCallback:FIRDataEventTypeChildRemoved]];
+ FIRDatabaseHandle addedHandle = [path observeEventType:FIRDataEventTypeChildAdded withBlock:[self makeEventCallback:FIRDataEventTypeChildAdded]];
+ FIRDatabaseHandle movedHandle = [path observeEventType:FIRDataEventTypeChildMoved withBlock:[self makeEventCallback:FIRDataEventTypeChildMoved]];
+ FIRDatabaseHandle changedHandle = [path observeEventType:FIRDataEventTypeChildChanged withBlock:[self makeEventCallback:FIRDataEventTypeChildChanged]];
+ FIRDatabaseHandle valueHandle = [path observeEventType:FIRDataEventTypeValue withBlock:[self makeEventCallback:FIRDataEventTypeValue]];
+
+ fbt_void_void cb = ^() {
+ [path removeObserverWithHandle:removedHandle];
+ [path removeObserverWithHandle:addedHandle];
+ [path removeObserverWithHandle:movedHandle];
+ [path removeObserverWithHandle:changedHandle];
+ [path removeObserverWithHandle:valueHandle];
+ };
+ return [cb copy];
+}
+
+- (void) wait {
+ [self waitUntil:^BOOL{
+ return self.actualPathsAndEvents.count >= self.lookingFor.count;
+ } timeout:kFirebaseTestTimeout];
+
+ for (int i = 0; i < self.lookingFor.count; ++i) {
+ FTupleEventTypeString* target = [self.lookingFor objectAtIndex:i];
+ FTupleEventTypeString* recvd = [self.actualPathsAndEvents objectAtIndex:i];
+ XCTAssertTrue([target isEqualTo:recvd], @"Expected %@ to match %@", target, recvd);
+ }
+
+ if (self.actualPathsAndEvents.count > self.lookingFor.count) {
+ NSLog(@"Too many events: %@", self.actualPathsAndEvents);
+ XCTFail(@"Received too many events");
+ }
+}
+
+- (void) waitForInitialization {
+ [self waitUntil:^BOOL{
+ for (FTupleEventTypeString* evt in [self.seenFirebaseLocations allValues]) {
+ if (!evt.initialized) {
+ return NO;
+ }
+ }
+
+ // splice out all of the initialization events
+ NSRange theRange;
+ theRange.location = 0;
+ theRange.length = self.initializationEvents;
+ [actualPathsAndEvents removeObjectsInRange:theRange];
+
+ return YES;
+ } timeout:kFirebaseTestTimeout];
+}
+
+- (fbt_void_datasnapshot) makeEventCallback:(FIRDataEventType)type {
+
+ fbt_void_datasnapshot cb = ^(FIRDataSnapshot * snap) {
+
+ FIRDatabaseReference * ref = snap.ref;
+ NSString* name = nil;
+ if (type != FIRDataEventTypeValue) {
+ ref = ref.parent;
+ name = snap.key;
+ }
+
+ FTupleEventTypeString* evt = [[FTupleEventTypeString alloc] initWithFirebase:ref withEvent:type withString:name];
+ [actualPathsAndEvents addObject:evt];
+
+ NSLog(@"Adding event: %@ (%@)", evt, [snap value]);
+
+ FTupleEventTypeString* targetEvt = [self.seenFirebaseLocations objectForKey:[ref description]];
+ if (targetEvt && !targetEvt.initialized) {
+ self.initializationEvents++;
+ if (type == FIRDataEventTypeValue) {
+ targetEvt.initialized = YES;
+ }
+ }
+ };
+ return [cb copy];
+}
+
+
+- (void) failWithException:(NSException *) anException {
+ //TODO: FIX
+ @throw anException;
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FIRFakeApp.h b/Example/Database/Tests/Helpers/FIRFakeApp.h
new file mode 100644
index 0000000..afe976a
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FIRFakeApp.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FIRFakeOptions;
+
+@interface FIRFakeApp : NSObject
+
+- (instancetype) initWithName:(NSString *)name URL:(NSString *)url;
+
+@property(nonatomic, readonly) FIRFakeOptions *options;
+@property(nonatomic, copy, readonly) NSString *name;
+@end
diff --git a/Example/Database/Tests/Helpers/FIRFakeApp.m b/Example/Database/Tests/Helpers/FIRFakeApp.m
new file mode 100644
index 0000000..b7abe81
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FIRFakeApp.m
@@ -0,0 +1,48 @@
+/*
+ * 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 "FIRFakeApp.h"
+
+@interface FIRFakeOptions: NSObject
+@property(nonatomic, readonly, copy) NSString *databaseURL;
+- (instancetype) initWithURL:(NSString *)url;
+@end
+
+@implementation FIRFakeOptions
+- (instancetype) initWithURL:(NSString *)url {
+ self = [super init];
+ if (self) {
+ self->_databaseURL = url;
+ }
+ return self;
+}
+@end
+
+@implementation FIRFakeApp
+
+- (instancetype) initWithName:(NSString *)name URL:(NSString *)url {
+ self = [super init];
+ if (self) {
+ self->_name = name;
+ self->_options = [[FIRFakeOptions alloc] initWithURL:url];
+ }
+ return self;
+}
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(void (^)(NSString *_Nullable token, NSError *_Nullable error))callback {
+ callback(nil, nil);
+}
+@end
diff --git a/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.h b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.h
new file mode 100644
index 0000000..e2a5751
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.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 <Foundation/Foundation.h>
+#import "FAuthTokenProvider.h"
+
+@interface FIRTestAuthTokenProvider : NSObject <FAuthTokenProvider>
+
+@property (nonatomic, strong) NSString *token;
+@property (nonatomic, strong) NSString *nextToken;
+
+- (instancetype) initWithToken:(NSString *)token;
+- (instancetype) init NS_UNAVAILABLE;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m
new file mode 100644
index 0000000..4719295
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m
@@ -0,0 +1,61 @@
+/*
+ * 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 "FIRTestAuthTokenProvider.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@interface FIRTestAuthTokenProvider ()
+
+@property (nonatomic, strong) NSMutableArray *listeners;
+
+@end
+
+@implementation FIRTestAuthTokenProvider
+
+- (instancetype) initWithToken:(NSString *)token {
+ self = [super init];
+ if (self != nil) {
+ self.listeners = [NSMutableArray array];
+ self.token = token;
+ }
+ return self;
+}
+
+- (void) setToken:(NSString *)token {
+ self->_token = token;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.listeners enumerateObjectsUsingBlock:^(fbt_void_nsstring _Nonnull listener, NSUInteger idx, BOOL * _Nonnull stop) {
+ listener(token);
+ }];
+ });
+
+}
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback {
+ if (forceRefresh) {
+ self.token = self.nextToken;
+ }
+ // Simulate delay
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_MSEC)), [FIRDatabaseQuery sharedQueue], ^{
+ callback(self.token, nil);
+ });
+}
+
+- (void) listenForTokenChanges:(fbt_void_nsstring)listener {
+ [self.listeners addObject:[listener copy]];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FMockStorageEngine.h b/Example/Database/Tests/Helpers/FMockStorageEngine.h
new file mode 100644
index 0000000..98a7d84
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FMockStorageEngine.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 <Foundation/Foundation.h>
+
+#import "FStorageEngine.h"
+
+@interface FMockStorageEngine : NSObject<FStorageEngine>
+
+@end
diff --git a/Example/Database/Tests/Helpers/FMockStorageEngine.m b/Example/Database/Tests/Helpers/FMockStorageEngine.m
new file mode 100644
index 0000000..98cb596
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FMockStorageEngine.m
@@ -0,0 +1,168 @@
+/*
+ * 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 "FMockStorageEngine.h"
+
+#import "FWriteRecord.h"
+#import "FCompoundWrite.h"
+#import "FNode.h"
+#import "FEmptyNode.h"
+#import "FTrackedQuery.h"
+#import "FPruneForest.h"
+#import "FCompoundWrite.h"
+
+@interface FMockStorageEngine ()
+
+@property (nonatomic) BOOL closed;
+@property (nonatomic, strong) NSMutableDictionary *userWritesDict;
+@property (nonatomic, strong) FCompoundWrite *serverCache;
+@property (nonatomic, strong) NSMutableDictionary *trackedQueries;
+@property (nonatomic, strong) NSMutableDictionary *trackedQueryKeys;
+
+@end
+
+@implementation FMockStorageEngine
+
+- (id)init {
+ self = [super init];
+ if (self != nil) {
+ self->_userWritesDict = [NSMutableDictionary dictionary];
+ self->_serverCache = [FCompoundWrite emptyWrite];
+ self->_trackedQueries = [NSMutableDictionary dictionary];
+ self->_trackedQueryKeys = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+- (void)close {
+ self.closed = YES;
+}
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ FWriteRecord *writeRecord = [[FWriteRecord alloc] initWithPath:path overwrite:node writeId:writeId visible:YES];
+ self.userWritesDict[@(writeId)] = writeRecord;
+}
+
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ FWriteRecord *writeRecord = [[FWriteRecord alloc] initWithPath:path merge:merge writeId:writeId];
+ self.userWritesDict[@(writeId)] = writeRecord;
+}
+
+- (void)removeUserWrite:(NSUInteger)writeId {
+ [self.userWritesDict removeObjectForKey:@(writeId)];
+}
+
+- (void)removeAllUserWrites {
+ [self.userWritesDict removeAllObjects];
+}
+
+- (NSArray *)userWrites {
+ return [[self.userWritesDict allValues] sortedArrayUsingComparator:^NSComparisonResult(FWriteRecord *obj1, FWriteRecord *obj2) {
+ if (obj1.writeId < obj2.writeId) {
+ return NSOrderedAscending;
+ } else if (obj1.writeId > obj2.writeId) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ }];
+}
+
+- (id<FNode>)serverCacheAtPath:(FPath *)path {
+ return [[self.serverCache childCompoundWriteAtPath:path] applyToNode:[FEmptyNode emptyNode]];
+}
+
+- (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path {
+ __block id<FNode> children = [FEmptyNode emptyNode];
+ id<FNode> fullNode = [[self.serverCache childCompoundWriteAtPath:path] applyToNode:[FEmptyNode emptyNode]];
+ [keys enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ children = [children updateImmediateChild:key withNewChild:[fullNode getImmediateChild:key]];
+ }];
+ return children;
+}
+
+- (void)updateServerCache:(id<FNode>)node atPath:(FPath *)path merge:(BOOL)merge {
+ if (merge) {
+ [node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> childNode, BOOL *stop) {
+ self.serverCache = [self.serverCache addWrite:childNode atPath:[path childFromString:key]];
+ }];
+ } else {
+ self.serverCache = [self.serverCache addWrite:node atPath:path];
+ }
+}
+
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path {
+ self.serverCache = [self.serverCache addCompoundWrite:merge atPath:path];
+}
+
+- (NSUInteger)serverCacheEstimatedSizeInBytes {
+ id data = [[self.serverCache applyToNode:[FEmptyNode emptyNode]] valForExport:YES];
+ return [NSJSONSerialization dataWithJSONObject:data options:0 error:nil].length;
+}
+
+- (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)prunePath {
+ [self.serverCache enumerateWrites:^(FPath *absolutePath, id<FNode> node, BOOL *stop) {
+ NSAssert([prunePath isEqual:absolutePath] || ![absolutePath contains:prunePath], @"Pruning at %@ but we found data higher up!", prunePath);
+ if ([prunePath contains:absolutePath]) {
+ FPath *relativePath = [FPath relativePathFrom:prunePath to:absolutePath];
+ if ([pruneForest shouldPruneUnkeptDescendantsAtPath:relativePath]) {
+ __block FCompoundWrite *newCache = [FCompoundWrite emptyWrite];
+ [[pruneForest childAtPath:relativePath] enumarateKeptNodesUsingBlock:^(FPath *keepPath) {
+ newCache = [newCache addWrite:[node getChild:keepPath] atPath:keepPath];
+ }];
+ self.serverCache = [[self.serverCache removeWriteAtPath:absolutePath] addCompoundWrite:newCache atPath:absolutePath];
+ } else {
+ // NOTE: This is technically a valid scenario (e.g. you ask to prune at / but only want to prune
+ // 'foo' and 'bar' and ignore everything else). But currently our pruning will explicitly
+ // prune or keep everything we know about, so if we hit this it means our tracked queries and
+ // the server cache are out of sync.
+ NSAssert([pruneForest shouldKeepPath:relativePath], @"We have data at %@ that is neither pruned nor kept.", relativePath);
+ }
+ }
+ }];
+}
+
+- (NSArray *)loadTrackedQueries {
+ return self.trackedQueries.allValues;
+}
+
+- (void)removeTrackedQuery:(NSUInteger)queryId {
+ [self.trackedQueries removeObjectForKey:@(queryId)];
+ [self.trackedQueryKeys removeObjectForKey:@(queryId)];
+}
+
+- (void)saveTrackedQuery:(FTrackedQuery *)query {
+ self.trackedQueries[@(query.queryId)] = query;
+}
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId {
+ self.trackedQueryKeys[@(queryId)] = keys;
+}
+
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQueryId:(NSUInteger)queryId {
+ NSSet *oldKeys = [self trackedQueryKeysForQuery:queryId];
+ NSMutableSet *newKeys = [NSMutableSet setWithSet:oldKeys];
+ [newKeys minusSet:removed];
+ [newKeys unionSet:added];
+ self.trackedQueryKeys[@(queryId)] = newKeys;
+}
+
+- (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId {
+ NSSet *keys = self.trackedQueryKeys[@(queryId)];
+ return keys != nil ? keys : [NSSet set];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.h b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.h
new file mode 100644
index 0000000..d6d9fd3
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.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 <Foundation/Foundation.h>
+
+@interface FTestAuthTokenGenerator : NSObject
+
++ (NSString *) tokenWithSecret:(NSString *)secret authData:(NSDictionary *)data andOptions:(NSDictionary *)options;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m
new file mode 100644
index 0000000..bd98e82
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m
@@ -0,0 +1,90 @@
+/*
+ * 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 <CommonCrypto/CommonHMAC.h>
+#import "FTestAuthTokenGenerator.h"
+#import "Base64.h"
+
+@implementation FTestAuthTokenGenerator
+
++ (NSString *) jsonStringForData:(id)data {
+ NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data
+ options:kNilOptions error:nil];
+
+ return [[NSString alloc] initWithData:jsonData
+ encoding:NSUTF8StringEncoding];
+}
+
++ (NSNumber *) tokenVersion {
+ return @0;
+}
+
++ (NSMutableDictionary *) createOptionsClaims:(NSDictionary *)options {
+ NSMutableDictionary* claims = [[NSMutableDictionary alloc] init];
+ if (options) {
+ NSDictionary* map = @{
+ @"expires": @"exp",
+ @"notBefore": @"nbf",
+ @"admin": @"admin",
+ @"debug": @"debug",
+ @"simulate": @"simulate"
+ };
+
+ for (NSString* claim in map) {
+ if (options[claim] != nil) {
+ NSString* claimName = [map objectForKey:claim];
+ id val = [options objectForKey:claim];
+ [claims setObject:val forKey:claimName];
+ }
+ }
+ }
+ return claims;
+}
+
++ (NSString *) webSafeBase64:(NSString *)encoded {
+ return [[[encoded stringByReplacingOccurrencesOfString:@"=" withString:@""] stringByReplacingOccurrencesOfString:@"+" withString:@"-"] stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
+}
+
++ (NSString *) base64EncodeString:(NSString *)target {
+ return [self webSafeBase64:[target base64EncodedString]];
+}
+
++ (NSString *) tokenWithClaims:(NSDictionary *)claims andSecret:(NSString *)secret {
+ NSDictionary* headerData = @{@"typ": @"JWT", @"alg": @"HS256"};
+ NSString* encodedHeader = [self base64EncodeString:[self jsonStringForData:headerData]];
+ NSString* encodedClaims = [self base64EncodeString:[self jsonStringForData:claims]];
+
+ NSString* secureBits = [NSString stringWithFormat:@"%@.%@", encodedHeader, encodedClaims];
+
+ const char *cKey = [secret cStringUsingEncoding:NSUTF8StringEncoding];
+ const char *cData = [secureBits cStringUsingEncoding:NSUTF8StringEncoding];
+ unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH];
+ CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cData, strlen(cData), cHMAC);
+ NSData* hmac = [NSData dataWithBytesNoCopy:cHMAC length:CC_SHA256_DIGEST_LENGTH freeWhenDone:NO];
+ NSString* encodedHmac = [self webSafeBase64:[hmac base64EncodedString]];
+ return [NSString stringWithFormat:@"%@.%@.%@", encodedHeader, encodedClaims, encodedHmac];
+}
+
++ (NSString *) tokenWithSecret:(NSString *)secret authData:(NSDictionary *)data andOptions:(NSDictionary *)options {
+ NSMutableDictionary* claims = [self createOptionsClaims:options];
+ [claims setObject:[self tokenVersion] forKey:@"v"];
+ NSNumber* now = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]];
+ [claims setObject:now forKey:@"iat"];
+ [claims setObject:data forKey:@"d"];
+ return [self tokenWithClaims:claims andSecret:secret];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestBase.h b/Example/Database/Tests/Helpers/FTestBase.h
new file mode 100644
index 0000000..8137b94
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestBase.h
@@ -0,0 +1,38 @@
+/*
+ * 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/XCTest.h>
+#import "FTestHelpers.h"
+#import "SenTest+FWaiter.h"
+
+@interface FTestBase : XCTestCase {
+ BOOL runPerfTests;
+}
+
+- (void)snapWaiter:(FIRDatabaseReference *)path withBlock:(fbt_void_datasnapshot)fn;
+- (void)waitUntilConnected:(FIRDatabaseReference *)ref;
+- (void)waitForQueue:(FIRDatabaseReference *)ref;
+- (void)waitForEvents:(FIRDatabaseReference *)ref;
+- (void)waitForRoundTrip:(FIRDatabaseReference *)ref;
+- (void)waitForValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected;
+- (void)waitForExportValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected;
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value;
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value andPriority:(id)priority;
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref updateChildValues:(NSDictionary *)values;
+
+@property(nonatomic, readonly) NSString *databaseURL;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestBase.m b/Example/Database/Tests/Helpers/FTestBase.m
new file mode 100644
index 0000000..f55c73b
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestBase.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 "FIRApp.h"
+#import "FIROptions.h"
+#import "FTestBase.h"
+#import "FTestAuthTokenGenerator.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FIRTestAuthTokenProvider.h"
+
+@implementation FTestBase
+
++ (void)setUp
+{
+ static dispatch_once_t once;
+ dispatch_once(&once, ^ {
+ [FIRApp configure];
+ });
+}
+
+- (void)setUp
+{
+ [super setUp];
+
+ [FIRDatabase setLoggingEnabled:YES];
+ _databaseURL = [[FIRApp defaultApp] options].databaseURL;
+
+ // Disabled normally since they slow down the tests and don't actually assert anything (they just NSLog timings).
+ runPerfTests = NO;
+}
+
+- (void)snapWaiter:(FIRDatabaseReference *)path withBlock:(fbt_void_datasnapshot)fn {
+ __block BOOL done = NO;
+
+ [path observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snap) {
+ fn(snap);
+ done = YES;
+ }];
+
+ NSTimeInterval timeTaken = [self waitUntil:^BOOL{
+ return done;
+ } timeout:kFirebaseTestWaitUntilTimeout];
+
+ NSLog(@"snapWaiter:withBlock: timeTaken:%f", timeTaken);
+
+ XCTAssertTrue(done, @"Properly finished.");
+}
+
+- (void) waitUntilConnected:(FIRDatabaseReference *)ref {
+ __block BOOL connected = NO;
+ FIRDatabaseHandle handle = [[ref.root child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ connected = [snapshot.value boolValue];
+ }];
+ WAIT_FOR(connected);
+ [ref.root removeObserverWithHandle:handle];
+}
+
+- (void) waitForRoundTrip:(FIRDatabaseReference *)ref {
+ // HACK: Do a deep setPriority (which we expect to fail because there's no data there) to do a no-op roundtrip.
+ __block BOOL done = NO;
+ [[ref.root child:@"ENTOHTNUHOE/ONTEHNUHTOE"] setPriority:@"blah" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) waitForQueue:(FIRDatabaseReference *)ref {
+ dispatch_sync([FIRDatabaseQuery sharedQueue], ^{});
+}
+
+- (void) waitForEvents:(FIRDatabaseReference *)ref {
+ [self waitForQueue:ref];
+ __block BOOL done = NO;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ done = YES;
+ });
+ WAIT_FOR(done);
+}
+
+- (void)waitForValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected {
+ __block id value;
+ FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ value = snapshot.value;
+ }];
+
+ @try {
+ [self waitUntil:^BOOL {
+ return [value isEqual:expected];
+ }];
+ } @catch (NSException *exception) {
+ @throw [NSException exceptionWithName:@"DidNotGetValue" reason:@"Did not get expected value"
+ userInfo:@{ @"expected": (!expected ? @"nil" : expected),
+ @"actual": (!value ? @"nil" : value) }];
+ } @finally {
+ [ref removeObserverWithHandle:handle];
+ }
+}
+
+- (void)waitForExportValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected {
+ __block id value;
+ FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ value = snapshot.valueInExportFormat;
+ }];
+
+ @try {
+ [self waitUntil:^BOOL {
+ return [value isEqual:expected];
+ }];
+ } @catch (NSException *exception) {
+ if ([exception.name isEqualToString:@"Timed out"]) {
+ @throw [NSException exceptionWithName:@"DidNotGetValue" reason:@"Did not get expected value"
+ userInfo:@{ @"expected": (!expected ? @"nil" : expected),
+ @"actual": (!value ? @"nil" : value) }]; } else {
+ @throw exception;
+ }
+ } @finally {
+ [ref removeObserverWithHandle:handle];
+ }
+}
+
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value {
+ [self waitForCompletionOf:ref setValue:value andPriority:nil];
+}
+
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value andPriority:(id)priority {
+ __block BOOL done = NO;
+ [ref setValue:value andPriority:priority withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+
+ @try {
+ WAIT_FOR(done);
+ } @catch (NSException *exception) {
+ @throw [NSException exceptionWithName:@"DidNotSetValue" reason:@"Did not complete setting value"
+ userInfo:@{ @"ref": [ref description],
+ @"done": done ? @"true" : @"false",
+ @"value": (!value ? @"nil" : value),
+ @"priority": (!priority ? @"nil" : priority) }];
+ }
+}
+
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref updateChildValues:(NSDictionary *)values {
+ __block BOOL done = NO;
+ [ref updateChildValues:values withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+
+ @try {
+ WAIT_FOR(done);
+ } @catch (NSException *exception) {
+ @throw [NSException exceptionWithName:@"DidNotUpdateChildValues" reason:@"Could not finish updating child values"
+ userInfo:@{ @"ref": [ref description],
+ @"values": (!values ? @"nil" : values)}];
+ }
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestCachePolicy.h b/Example/Database/Tests/Helpers/FTestCachePolicy.h
new file mode 100644
index 0000000..688c21d
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestCachePolicy.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FCachePolicy.h"
+
+@interface FTestCachePolicy : NSObject<FCachePolicy>
+
+- (id)initWithPercent:(float)percent maxQueries:(NSUInteger)maxQueries;
+
+- (void)pruneOnNextCheck;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestCachePolicy.m b/Example/Database/Tests/Helpers/FTestCachePolicy.m
new file mode 100644
index 0000000..aacd010
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestCachePolicy.m
@@ -0,0 +1,65 @@
+/*
+ * 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 "FTestCachePolicy.h"
+
+@interface FTestCachePolicy ()
+
+
+@property (nonatomic) float percentOfQueries;
+@property (nonatomic) NSUInteger maxTrackedQueries;
+@property (nonatomic) BOOL pruneNext;
+
+@end
+
+@implementation FTestCachePolicy
+
+- (id)initWithPercent:(float)percent maxQueries:(NSUInteger)maxQueries {
+ self = [super init];
+ if (self != nil) {
+ self->_maxTrackedQueries = maxQueries;
+ self->_percentOfQueries = percent;
+ self->_pruneNext = NO;
+ }
+ return self;
+}
+
+- (void)pruneOnNextCheck {
+ self.pruneNext = YES;
+}
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries {
+ if (self.pruneNext) {
+ self.pruneNext = NO;
+ return YES;
+ } else {
+ return NO;
+ }
+}
+
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck {
+ return YES;
+}
+
+- (float)percentOfQueriesToPruneAtOnce {
+ return self.percentOfQueries;
+}
+
+- (NSUInteger)maxNumberOfQueriesToKeep {
+ return self.maxTrackedQueries;
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestClock.h b/Example/Database/Tests/Helpers/FTestClock.h
new file mode 100644
index 0000000..5520c6a
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestClock.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 <Foundation/Foundation.h>
+
+#import "FClock.h"
+
+@interface FTestClock : NSObject<FClock>
+
+@property (nonatomic, readonly) NSTimeInterval currentTime;
+
+- (id)init;
+- (void)tick;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestClock.m b/Example/Database/Tests/Helpers/FTestClock.m
new file mode 100644
index 0000000..43599ac
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestClock.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 "FTestClock.h"
+
+@implementation FTestClock
+
+- (id)init {
+ self = [super init];
+ if (self != nil) {
+ self->_currentTime = 0.001;
+ }
+ return self;
+}
+
+- (void)tick {
+ self->_currentTime = self->_currentTime + 0.001;
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestContants.h b/Example/Database/Tests/Helpers/FTestContants.h
new file mode 100644
index 0000000..bc8dd8d
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestContants.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.
+ */
+
+#ifndef Firebase_FTestContants_h
+#define Firebase_FTestContants_h
+
+#define kFirebaseTestTimeout 7
+#define kFirebaseTestWaitUntilTimeout 5
+
+#endif
diff --git a/Example/Database/Tests/Helpers/FTestExpectations.h b/Example/Database/Tests/Helpers/FTestExpectations.h
new file mode 100644
index 0000000..8a797c8
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestExpectations.h
@@ -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 <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+#import "FIRDatabaseQuery.h"
+
+@interface FTestExpectations : XCTestCase {
+ NSMutableArray* expectations;
+ XCTestCase* from;
+}
+
+- (id) initFrom:(XCTestCase *)other;
+- (void)addQuery:(FIRDatabaseQuery *)query withExpectation:(id)expectation;
+- (void) validate;
+
+@property (readonly) BOOL isReady;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestExpectations.m b/Example/Database/Tests/Helpers/FTestExpectations.m
new file mode 100644
index 0000000..d0f84d7
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestExpectations.m
@@ -0,0 +1,88 @@
+/*
+ * 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 "FTestExpectations.h"
+#import "FIRDataSnapshot.h"
+
+@interface FExpectation : NSObject
+
+@property (strong, nonatomic) FIRDatabaseQuery * query;
+@property (strong, nonatomic) id expectation;
+@property (strong, nonatomic) FIRDataSnapshot * snap;
+
+@end
+
+@implementation FExpectation
+
+@synthesize query;
+@synthesize expectation;
+@synthesize snap;
+
+@end
+
+@implementation FTestExpectations
+
+- (id) initFrom:(XCTestCase *)other {
+ self = [super init];
+ if (self) {
+ expectations = [[NSMutableArray alloc] init];
+ from = other;
+ }
+ return self;
+}
+
+- (void)addQuery:(FIRDatabaseQuery *)query withExpectation:(id)expectation {
+ FExpectation* exp = [[FExpectation alloc] init];
+ exp.query = query;
+ exp.expectation = expectation;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ exp.snap = snapshot;
+ }];
+ [expectations addObject:exp];
+}
+
+- (BOOL) isReady {
+ for (FExpectation* exp in expectations) {
+ if (!exp.snap) {
+ return NO;
+ }
+ // Note that a failure here will end up triggering the timeout
+ FIRDataSnapshot * snap = exp.snap;
+ NSDictionary* result = snap.value;
+ NSDictionary* expected = exp.expectation;
+ if ([result isEqual:[NSNull null]] || ![result isEqualToDictionary:expected]) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+- (void) validate {
+ for (FExpectation* exp in expectations) {
+ FIRDataSnapshot * snap = exp.snap;
+ NSDictionary* result = [snap value];
+ NSDictionary* expected = exp.expectation;
+ XCTAssertTrue([result isEqualToDictionary:expected], @"Expectation mismatch: %@ should be %@", result, expected);
+ }
+}
+
+- (void) failWithException:(NSException *) anException {
+ @throw anException;
+ // TODO: fix
+ //[from failWithException:anException];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestHelpers.h b/Example/Database/Tests/Helpers/FTestHelpers.h
new file mode 100644
index 0000000..679be7e
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestHelpers.h
@@ -0,0 +1,38 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+#import "FTupleFirebase.h"
+#import "FRepoManager.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FTestContants.h"
+#import "FSnapshotUtilities.h"
+
+#define WAIT_FOR(x) [self waitUntil:^{ return (BOOL)(x); }];
+
+#define NODE(__node) [FSnapshotUtilities nodeFrom:(__node)]
+#define PATH(__path) [FPath pathWithString:(__path)]
+
+@interface FTestHelpers : XCTestCase
++ (FIRDatabaseReference *) getRandomNode;
++ (FIRDatabaseReference *) getRandomNodeWithoutPersistence;
++ (FTupleFirebase *) getRandomNodePair;
++ (FTupleFirebase *) getRandomNodePairWithoutPersistence;
++ (FTupleFirebase *) getRandomNodeTriple;
++ (id<FNode>)leafNodeOfSize:(NSUInteger)size;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestHelpers.m b/Example/Database/Tests/Helpers/FTestHelpers.m
new file mode 100644
index 0000000..8ffdc7d
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestHelpers.m
@@ -0,0 +1,132 @@
+/*
+ * 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 "FTestHelpers.h"
+#import "FConstants.h"
+#import "FIRApp.h"
+#import "FIROptions.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FTestAuthTokenGenerator.h"
+
+@implementation FTestHelpers
+
++ (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds {
+ NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
+ NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:seconds];
+ NSTimeInterval timeoutTime = [timeoutDate timeIntervalSinceReferenceDate];
+ NSTimeInterval currentTime;
+
+ for (currentTime = [NSDate timeIntervalSinceReferenceDate];
+ !predicate() && currentTime < timeoutTime;
+ currentTime = [NSDate timeIntervalSinceReferenceDate]) {
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
+ }
+
+ NSTimeInterval finish = [NSDate timeIntervalSinceReferenceDate];
+
+ NSAssert(currentTime <= timeoutTime, @"Timed out");
+
+ return (finish - start);
+}
+
++ (NSArray*) getRandomNodes:(int)num persistence:(BOOL)persistence {
+ static dispatch_once_t pred = 0;
+ static NSMutableArray *persistenceRefs = nil;
+ static NSMutableArray *noPersistenceRefs = nil;
+ dispatch_once(&pred, ^{
+ persistenceRefs = [[NSMutableArray alloc] init];
+ noPersistenceRefs = [[NSMutableArray alloc] init];
+ // Uncomment the following line to run tests against a background thread
+ //[Firebase setDispatchQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
+ });
+
+ NSMutableArray *refs = (persistence) ? persistenceRefs : noPersistenceRefs;
+
+ id<FAuthTokenProvider> authTokenProvider = [FAuthTokenProvider authTokenProviderForApp:[FIRApp defaultApp]];
+
+ while (num > refs.count) {
+ NSString *sessionIdentifier = [NSString stringWithFormat:@"test-config-%@persistence-%lu", (persistence) ? @"" : @"no-", refs.count];
+ FIRDatabaseConfig *config = [[FIRDatabaseConfig alloc] initWithSessionIdentifier:sessionIdentifier authTokenProvider:authTokenProvider];
+ config.persistenceEnabled = persistence;
+ FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithConfig:config];
+ [refs addObject:ref];
+ }
+
+ NSMutableArray* results = [[NSMutableArray alloc] init];
+ NSString* name = nil;
+ for (int i = 0; i < num; ++i) {
+ FIRDatabaseReference * ref = [refs objectAtIndex:i];
+ if (!name) {
+ name = [ref childByAutoId].key;
+ }
+ [results addObject:[ref child:name]];
+ }
+ return results;
+}
+
+// Helpers
++ (FIRDatabaseReference *) getRandomNode {
+ NSArray* refs = [self getRandomNodes:1 persistence:YES];
+ return [refs objectAtIndex:0];
+}
+
++ (FIRDatabaseReference *) getRandomNodeWithoutPersistence {
+ NSArray* refs = [self getRandomNodes:1 persistence:NO];
+ return refs[0];
+}
+
++ (FTupleFirebase *) getRandomNodePair {
+ NSArray* refs = [self getRandomNodes:2 persistence:YES];
+
+ FTupleFirebase* tuple = [[FTupleFirebase alloc] init];
+ tuple.one = [refs objectAtIndex:0];
+ tuple.two = [refs objectAtIndex:1];
+
+ return tuple;
+}
+
++ (FTupleFirebase *) getRandomNodePairWithoutPersistence {
+ NSArray* refs = [self getRandomNodes:2 persistence:NO];
+
+ FTupleFirebase* tuple = [[FTupleFirebase alloc] init];
+ tuple.one = refs[0];
+ tuple.two = refs[1];
+
+ return tuple;
+}
+
++ (FTupleFirebase *) getRandomNodeTriple {
+ NSArray* refs = [self getRandomNodes:3 persistence:YES];
+ FTupleFirebase* triple = [[FTupleFirebase alloc] init];
+ triple.one = [refs objectAtIndex:0];
+ triple.two = [refs objectAtIndex:1];
+ triple.three = [refs objectAtIndex:2];
+
+ return triple;
+}
+
++ (id<FNode>)leafNodeOfSize:(NSUInteger)size {
+ NSMutableString *string = [NSMutableString string];
+ NSString *pattern = @"abdefghijklmopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ for (NSUInteger i = 0; i < size - pattern.length; i = i + pattern.length) {
+ [string appendString:pattern];
+ }
+ NSUInteger remainingLength = size - string.length;
+ [string appendString:[pattern substringToIndex:remainingLength]];
+ return [FSnapshotUtilities nodeFrom:string];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTupleEventTypeString.h b/Example/Database/Tests/Helpers/FTupleEventTypeString.h
new file mode 100644
index 0000000..adcb4a0
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTupleEventTypeString.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 <Foundation/Foundation.h>
+#import "FIRDataEventType.h"
+#import "FIRDatabaseReference.h"
+#import "FTypedefs.h"
+
+@interface FTupleEventTypeString : NSObject
+
+- (id)initWithFirebase:(FIRDatabaseReference *)f withEvent:(FIRDataEventType)evt withString:(NSString *)str;
+- (BOOL) isEqualTo:(FTupleEventTypeString *)other;
+
+@property (nonatomic, strong) FIRDatabaseReference * firebase;
+@property (readwrite) FIRDataEventType eventType;
+@property (nonatomic, strong) NSString* string;
+@property (nonatomic, copy) fbt_void_void vvcallback;
+@property (nonatomic) BOOL initialized;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTupleEventTypeString.m b/Example/Database/Tests/Helpers/FTupleEventTypeString.m
new file mode 100644
index 0000000..4cb3df2
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTupleEventTypeString.m
@@ -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 "FTupleEventTypeString.h"
+
+@implementation FTupleEventTypeString
+
+@synthesize firebase;
+@synthesize eventType;
+@synthesize string;
+@synthesize vvcallback;
+@synthesize initialized;
+
+- (id)initWithFirebase:(FIRDatabaseReference *)f withEvent:(FIRDataEventType)evt withString:(NSString *)str;
+{
+ self = [super init];
+ if (self) {
+ self.firebase = f;
+ self.eventType = evt;
+ self.string = str;
+ self.initialized = NO;
+ }
+ return self;
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"%@ %@ (%zd)", self.firebase, self.string, self.eventType];
+}
+
+- (BOOL) isEqualTo:(FTupleEventTypeString *)other {
+ BOOL stringsEqual = NO;
+ if (self.string == nil && other.string == nil) {
+ stringsEqual = YES;
+ } else if (self.string != nil && other.string != nil) {
+ stringsEqual = [self.string isEqualToString:other.string];
+ }
+ return self.eventType == other.eventType && stringsEqual && [[self.firebase description] isEqualToString:[other.firebase description]];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/SenTest+FWaiter.h b/Example/Database/Tests/Helpers/SenTest+FWaiter.h
new file mode 100644
index 0000000..81556df
--- /dev/null
+++ b/Example/Database/Tests/Helpers/SenTest+FWaiter.h
@@ -0,0 +1,26 @@
+/*
+ * 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/XCTest.h>
+
+@interface XCTest (FWaiter)
+
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate;
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate description:(NSString*)desc;
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds;
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds description:(NSString*)desc;
+
+@end
diff --git a/Example/Database/Tests/Helpers/SenTest+FWaiter.m b/Example/Database/Tests/Helpers/SenTest+FWaiter.m
new file mode 100644
index 0000000..4c5c854
--- /dev/null
+++ b/Example/Database/Tests/Helpers/SenTest+FWaiter.m
@@ -0,0 +1,57 @@
+/*
+ * 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 "SenTest+FWaiter.h"
+#import "FTestContants.h"
+
+@implementation XCTestCase (FWaiter)
+
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate {
+ return [self waitUntil:predicate timeout:kFirebaseTestWaitUntilTimeout description:nil];
+}
+
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate description:(NSString*)desc {
+ return [self waitUntil:predicate timeout:kFirebaseTestWaitUntilTimeout description:desc];
+}
+
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds {
+ return [self waitUntil:predicate timeout:seconds description:nil];
+}
+
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds description:(NSString*)desc {
+ NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
+ NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:seconds];
+ NSTimeInterval timeoutTime = [timeoutDate timeIntervalSinceReferenceDate];
+ NSTimeInterval currentTime;
+
+ for (currentTime = [NSDate timeIntervalSinceReferenceDate];
+ !predicate() && currentTime < timeoutTime;
+ currentTime = [NSDate timeIntervalSinceReferenceDate]) {
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
+ }
+
+ NSTimeInterval finish = [NSDate timeIntervalSinceReferenceDate];
+ if (currentTime > timeoutTime) {
+ if (desc != nil) {
+ XCTFail("Timed out on: %@", desc);
+ } else {
+ XCTFail("Timed out");
+ }
+ }
+ return (finish - start);
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FConnectionTest.m b/Example/Database/Tests/Integration/FConnectionTest.m
new file mode 100644
index 0000000..e72f6e4
--- /dev/null
+++ b/Example/Database/Tests/Integration/FConnectionTest.m
@@ -0,0 +1,77 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+#import "FTestHelpers.h"
+#import "FConnection.h"
+#import "FTestBase.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@interface FConnectionTest : FTestBase
+
+@end
+
+@interface FTestConnectionDelegate : NSObject<FConnectionDelegate>
+
+@property (nonatomic, copy) void (^onReady)(NSString *);
+@property (nonatomic, copy) void (^onDisconnect)(FDisconnectReason);
+
+@end
+
+@implementation FTestConnectionDelegate
+
+- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID{
+ self.onReady(sessionID);
+}
+- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message {}
+- (void)onDisconnect:(FConnection *)fwebSocket withReason:(FDisconnectReason)reason {
+ self.onDisconnect(reason);
+}
+- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason {}
+
+@end
+@implementation FConnectionTest
+
+-(void) XXXtestObtainSessionId {
+ NSString* host = [NSString stringWithFormat:@"%@.firebaseio.com", [[FIRApp defaultApp] options].projectID];
+ FRepoInfo *info = [[FRepoInfo alloc] initWithHost:host isSecure:YES withNamespace:@"default"];
+ FConnection *conn = [[FConnection alloc] initWith:info andDispatchQueue:[FIRDatabaseQuery sharedQueue] lastSessionID:nil];
+ FTestConnectionDelegate *delegate = [[FTestConnectionDelegate alloc] init];
+
+ __block BOOL done = NO;
+
+ delegate.onDisconnect = ^(FDisconnectReason reason) {
+ if (reason == DISCONNECT_REASON_SERVER_RESET) {
+ // It is very likely that the first connection attempt sends us a redirect to the project's designated server.
+ // We need follow that redirect before 'onReady' is invoked.
+ [conn open];
+ }
+ };
+ delegate.onReady = ^(NSString *sessionID) {
+ NSAssert(sessionID, @"sessionID cannot be null");
+ NSAssert([sessionID length] != 0, @"sessionID must have length > 0");
+ done = YES;
+ };
+
+ conn.delegate = delegate;
+ [conn open];
+
+ WAIT_FOR(done);
+}
+@end
diff --git a/Example/Database/Tests/Integration/FData.h b/Example/Database/Tests/Integration/FData.h
new file mode 100644
index 0000000..ebb502e
--- /dev/null
+++ b/Example/Database/Tests/Integration/FData.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 <XCTest/XCTest.h>
+#import "FTestBase.h"
+
+@interface FData : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FData.m b/Example/Database/Tests/Integration/FData.m
new file mode 100644
index 0000000..390522c
--- /dev/null
+++ b/Example/Database/Tests/Integration/FData.m
@@ -0,0 +1,2687 @@
+/*
+ * 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 "FData.h"
+#import "FTestHelpers.h"
+#import "FEventTester.h"
+#import "FTupleEventTypeString.h"
+#import "FIRApp.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FIROptions.h"
+#import "FRepo_Private.h"
+#import <limits.h>
+
+@implementation FData
+
+- (void) testGetNode {
+ __unused FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ XCTAssertTrue(YES, @"Properly created node without throwing error");
+}
+
+- (void) testWriteData {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ [node setValue:@42];
+ XCTAssertTrue(YES, @"Properly write to node without throwing error");
+}
+
+- (void) testWriteDataWithDebugLogging {
+ [FIRDatabase setLoggingEnabled:YES];
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ [node setValue:@42];
+ [FIRDatabase setLoggingEnabled:NO];
+ XCTAssertTrue(YES, @"Properly write to node without throwing error");
+}
+
+- (void) testWriteAndReadData {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ [node setValue:@42];
+
+ [self snapWaiter:node withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw correct value");
+ }];
+}
+
+- (void) testProperParamChecking {
+ // ios doesn't have an equivalent of this test
+}
+
+- (void) testNamespaceCaseInsensitivityWithinARepo {
+ FIRDatabaseReference * ref1 = [[FIRDatabase database] referenceFromURL:[self.databaseURL uppercaseString]];
+ FIRDatabaseReference * ref2 = [[FIRDatabase database] referenceFromURL:[self.databaseURL lowercaseString]];
+
+ XCTAssertTrue([ref1.description isEqualToString:ref2.description], @"Descriptions should match");
+}
+
+- (void) testRootProperty {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ FIRDatabaseReference * root = node.root;
+ XCTAssertTrue(root != nil, @"Should get a root");
+ XCTAssertTrue([[root description] isEqualToString:self.databaseURL], @"Root is actually the root");
+}
+
+- (void) testValReturnsCompoundObjectWithChildren {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [node setValue:@{@"foo": @{@"bar": @5}}];
+
+ [self snapWaiter:node withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqualObjects([[[snapshot value] objectForKey:@"foo"] objectForKey:@"bar"], @5, @"Properly saw compound object");
+ }];
+}
+
+- (void) testWriteDataAndWaitForServerConfirmation {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [self waitForCompletionOf:node setValue:@42];
+}
+
+- (void) testWriteAValueAndRead {
+ // dupe of FEvent testWriteLeafExpectValueChanged
+}
+
+- (void) testWriteABunchOfDataAndRead {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writeNode = tuple.one;
+ FIRDatabaseReference * readNode = tuple.two;
+
+
+ __block BOOL done = NO;
+
+ [[[[writeNode child:@"a"] child:@"b"] child:@"c"] setValue:@1];
+ [[[[writeNode child:@"a"] child:@"d"] child:@"e"] setValue:@2];
+ [[[[writeNode child:@"a"] child:@"d"] child:@"f"] setValue:@3];
+ [[writeNode child:@"g"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { done = YES; }];
+
+ [self waitUntil:^BOOL{ return done; }];
+
+ [super snapWaiter:readNode withBlock:^(FIRDataSnapshot *s) {
+ XCTAssertEqualObjects([[[[s childSnapshotForPath:@"a"] childSnapshotForPath:@"b"] childSnapshotForPath:@"c"] value], @1, @"Proper child value");
+ XCTAssertEqualObjects([[[[s childSnapshotForPath:@"a"] childSnapshotForPath:@"d"] childSnapshotForPath:@"e"] value], @2, @"Proper child value");
+ XCTAssertEqualObjects([[[[s childSnapshotForPath:@"a"] childSnapshotForPath:@"d"] childSnapshotForPath:@"f"] value], @3, @"Proper child value");
+ XCTAssertEqualObjects([[s childSnapshotForPath:@"g"] value], @4, @"Proper child value");
+ }];
+}
+
+- (void) testWriteABunchOfDataWithLeadingZeroesAndRead {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writeNode = tuple.one;
+ FIRDatabaseReference * readNode = tuple.two;
+
+ [self waitForCompletionOf:[writeNode child:@"1"] setValue:@1];
+ [self waitForCompletionOf:[writeNode child:@"01"] setValue:@2];
+ [self waitForCompletionOf:[writeNode child:@"001"] setValue:@3];
+ [self waitForCompletionOf:[writeNode child:@"0001"] setValue:@4];
+
+ [super snapWaiter:readNode withBlock:^(FIRDataSnapshot *s) {
+ XCTAssertEqualObjects([[s childSnapshotForPath:@"1"] value], @1, @"Proper child value");
+ XCTAssertEqualObjects([[s childSnapshotForPath:@"01"] value], @2, @"Proper child value");
+ XCTAssertEqualObjects([[s childSnapshotForPath:@"001"] value], @3, @"Proper child value");
+ XCTAssertEqualObjects([[s childSnapshotForPath:@"0001"] value], @4, @"Proper child value");
+ }];
+}
+
+- (void) testLeadingZeroesTurnIntoDictionary {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ [self waitForCompletionOf:[ref child:@"1"] setValue:@1];
+ [self waitForCompletionOf:[ref child:@"01"] setValue:@2];
+
+ __block BOOL done = NO;
+ __block FIRDataSnapshot * snap = nil;
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ XCTAssertTrue([snap.value isKindOfClass:[NSDictionary class]], @"Should be dictionary");
+ XCTAssertEqualObjects([snap.value objectForKey:@"1"], @1, @"Proper child value");
+ XCTAssertEqualObjects([snap.value objectForKey:@"01"], @2, @"Proper child value");
+}
+
+- (void) testLeadingZerosDontCollapseLocally {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ __block FIRDataSnapshot * snap = nil;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ done = (snapshot.childrenCount == 2);
+ }];
+
+ [[ref child:@"3"] setValue:@YES];
+ [[ref child:@"03"] setValue:@NO];
+
+ WAIT_FOR(done);
+
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"3"] value], @YES, @"Proper child value");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"03"] value], @NO, @"Proper child value");
+}
+
+- (void) testSnapshotRef {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [snapshot.ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) testWriteLeafNodeOverwriteAtParentVerifyExpectedEvents {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ FIRDatabaseReference * connected = [[[FIRDatabase database] reference] child:@".info/connected"];
+ __block BOOL ready = NO;
+ [connected observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSNumber *val = [snapshot value];
+ ready = [val boolValue];
+ }];
+
+ WAIT_FOR(ready);
+
+ NSArray* lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], // 4
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"], // 0
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], // 4
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildChanged withString:@"aa"], // 2
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], // 4
+ ];
+
+ [[node repo] interrupt]; // Going offline ensures that local events get queued up before server events
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:lookingFor];
+
+ [[node child:@"a/aa"] setValue:@1];
+ [[node child:@"a"] setValue:@{@"aa": @2}];
+
+ [[node repo] resume];
+ [et wait];
+}
+
+- (void) testWriteLeafNodeOverwriteAtParentMultipleTimesVerifyExpectedEvents {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSArray* lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/bb"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildChanged withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildChanged withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+
+ [[node repo] interrupt]; // Going offline ensures that local events get queued up before server events
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:lookingFor];
+
+ [[node child:@"a/aa"] setValue:@1];
+ [[node child:@"a"] setValue:@{@"aa": @2}];
+ [[node child:@"a"] setValue:@{@"aa": @3}];
+ [[node child:@"a"] setValue:@{@"aa": @3}];
+
+ [[node repo] resume];
+ [et wait];
+}
+
+- (void) testWriteParentNodeOverwriteAtLeafVerifyExpectedEvents {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSArray* lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildChanged withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+
+ [[node repo] interrupt]; // Going offline ensures that local events get queued up before server events
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:lookingFor];
+
+ [[node child:@"a"] setValue:@{@"aa": @2}];
+ [[node child:@"a/aa"] setValue:@1];
+
+ [[node repo] resume];
+ [et wait];
+}
+
+- (void) testWriteLeafNodeRemoveParentNodeVerifyExpectedEvents {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ NSArray* lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeChildAdded withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:lookingFor];
+
+ [[writer child:@"a/aa"] setValue:@42];
+ // the local events
+ [et wait];
+
+ // the reader should get all of the events intermingled
+ FEventTester* readerEvents = [[FEventTester alloc] initFrom:self];
+ lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildAdded withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [readerEvents addLookingFor:lookingFor];
+
+ [readerEvents wait];
+
+ lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeChildRemoved withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildRemoved withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+ [readerEvents addLookingFor:lookingFor];
+
+ lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeChildRemoved withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeChildRemoved withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:lookingFor];
+
+ [[writer child:@"a"] removeValue];
+
+ [et wait];
+ [readerEvents wait];
+
+ [et unregister];
+ [readerEvents unregister];
+
+ // Ensure we can write a new value
+ __block NSNumber* readVal = @0.0;
+ __block NSNumber* writeVal = @0.0;
+
+ [[reader child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ if (val != [NSNull null]) {
+ readVal = val;
+ }
+ }];
+
+ [[writer child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ if (val != [NSNull null]) {
+ writeVal = val;
+ }
+ }];
+
+ [[writer child:@"a/aa"] setValue:@3.1415];
+
+ [self waitUntil:^BOOL{
+ return fabs([readVal doubleValue] - 3.1415) < 0.001 && fabs([writeVal doubleValue] - 3.1415) < 0.001;
+ //return [readVal isEqualToNumber:@3.1415] && [writeVal isEqualToNumber:@3.1415];
+ }];
+}
+
+- (void) testWriteLeafNodeRemoveLeafVerifyExpectedEvents {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ NSArray* lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeChildAdded withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:lookingFor];
+ [[writer child:@"a/aa"] setValue:@42];
+
+ // the local events
+ [et wait];
+
+ // the reader should get all of the events intermingled
+ FEventTester* readerEvents = [[FEventTester alloc] initFrom:self];
+ lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildAdded withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [readerEvents addLookingFor:lookingFor];
+
+ [readerEvents wait];
+
+ lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeChildRemoved withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildRemoved withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+ [readerEvents addLookingFor:lookingFor];
+
+ lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeChildRemoved withString:@"aa"],
+ [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeChildRemoved withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:lookingFor];
+
+ // remove just the leaf
+ [[writer child:@"a/aa"] removeValue];
+
+ [et wait];
+ [readerEvents wait];
+
+ [et unregister];
+ [readerEvents unregister];
+
+ // Ensure we can write a new value
+ __block NSNumber* readVal = @0.0;
+ __block NSNumber* writeVal = @0.0;
+
+ [[reader child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ if (val != [NSNull null]) {
+ readVal = val;
+ }
+ }];
+
+ [[writer child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ if (val != [NSNull null]) {
+ writeVal = val;
+ }
+ }];
+
+ [[writer child:@"a/aa"] setValue:@3.1415];
+
+ [self waitUntil:^BOOL{
+ //NSLog(@"readVal: %@, writeVal: %@, vs %@", readVal, writeVal, @3.1415);
+ //return [readVal isEqualToNumber:@3.1415] && [writeVal isEqualToNumber:@3.1415];
+ return fabs([readVal doubleValue] - 3.1415) < 0.001 && fabs([writeVal doubleValue] - 3.1415) < 0.001;
+ }];
+}
+
+- (void) testWriteMultipleLeafNodesRemoveOnlyOneVerifyExpectedEvents {
+ // XXX impl
+}
+
+- (void) testVerifyNodeNamesCantStartWithADot {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ XCTAssertThrows([ref child:@".foo"], @"not a valid .prefix");
+ XCTAssertThrows([ref child:@"foo/.foo"], @"not a valid path");
+ // Should not throw
+ [[ref parent] child:@".info"];
+}
+
+- (void) testVerifyWritingToDotLengthAndDotKeysThrows {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ XCTAssertThrows([[ref child:@".keys"] setValue:@42], @"not a valid .prefix");
+ XCTAssertThrows([[ref child:@".length"] setValue:@42], @"not a valid path");
+}
+
+- (void) testNumericKeysGetTurnedIntoArrays {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ [[ref child:@"0"] setValue:@"alpha"];
+ [[ref child:@"1"] setValue:@"bravo"];
+ [[ref child:@"2"] setValue:@"charlie"];
+ [[ref child:@"3"] setValue:@"delta"];
+ [[ref child:@"4"] setValue:@"echo"];
+
+ __block BOOL ready = NO;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ XCTAssertTrue([val isKindOfClass:[NSArray class]], @"Expected an array");
+ NSArray *expected = @[@"alpha", @"bravo", @"charlie", @"delta", @"echo"];
+ XCTAssertTrue([expected isEqualToArray:val], @"Did not get the correct array");
+ ready = YES;
+ }];
+
+ [self waitUntil:^{ return ready; }];
+}
+
+// This was an issue on 64-bit.
+- (void) testLargeNumericKeysDontGetTurnedIntoArrays {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ [[ref child:@"100003354884401"] setValue:@"alpha"];
+
+ __block BOOL ready = NO;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ XCTAssertTrue([val isKindOfClass:[NSDictionary class]], @"Expected a dictionary.");
+ ready = YES;
+ }];
+
+ [self waitUntil:^{ return ready; }];
+}
+
+- (void) testWriteCompoundObjectAndGetItBack {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSDictionary* data = @{
+ @"a": @{@"aa": @5,
+ @"ab": @3},
+ @"b": @{@"ba": @"hey there!",
+ @"bb": @{@"bba": @NO}},
+ @"c": @[@0,
+ @{@"c_1": @4},
+ @"hey",
+ @YES,
+ @NO,
+ @"dude"]
+ };
+
+ __block FIRDataSnapshot *snap = nil;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ __block BOOL done = NO;
+ [node setValue:data withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { done = YES; }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [self snapWaiter:node withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([[[[snapshot value] objectForKey:@"c"] objectAtIndex:3] boolValue], @"Got proper boolean");
+ }];
+}
+
+- (void) testCanPassValueToPush {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ FIRDatabaseReference * pushA = [node childByAutoId];
+ [pushA setValue:@5];
+
+ [self snapWaiter:pushA withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqualObjects(@5, [snapshot value], @"Got proper value");
+ }];
+
+ FIRDatabaseReference * pushB = [node childByAutoId];
+ [pushB setValue:@{@"a": @5, @"b": @6}];
+
+ [self snapWaiter:pushB withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqualObjects(@5, [[snapshot value] objectForKey:@"a"], @"Got proper value");
+ XCTAssertEqualObjects(@6, [[snapshot value] objectForKey:@"b"], @"Got proper value");
+ }];
+}
+
+// Dropped test that tested callbacks to push. Support was removed.
+
+- (void) testRemoveCallbackHit {
+
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ __block BOOL setDone = NO;
+ __block BOOL removeDone = NO;
+ __block BOOL readDone = NO;
+
+ [node setValue:@42 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ setDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return setDone;
+ }];
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ if (val == [NSNull null]) {
+ readDone = YES;
+ }
+ }];
+
+ [node removeValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ XCTAssertTrue(error == nil, @"Should not be an error removing");
+ removeDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return readDone && removeDone;
+ }];
+}
+
+- (void) testRemoveCallbackIsHitForNodesThatAreAlreadyRemoved {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block int removes = 0;
+
+ [node removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ removes = removes + 1;
+ }];
+
+ [node removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ removes = removes + 1;
+ }];
+
+ [self waitUntil:^BOOL{
+ return removes == 2;
+ }];
+}
+
+- (void) testUsingNumbersAsKeysDoesntCreateHugeSparseArrays {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ [[ref child:@"3024"] setValue:@5];
+
+ __block BOOL ready = NO;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ XCTAssertTrue(![val isKindOfClass:[NSArray class]], @"Should not be an array");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testOnceWithACallbackHitsServer {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodeTriple];
+ FIRDatabaseReference * writeNode = tuple.one;
+ FIRDatabaseReference * readNode = tuple.two;
+ FIRDatabaseReference * readNodeB = tuple.three;
+
+ __block BOOL initialReadDone = NO;
+
+ [readNode observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([[snapshot value] isEqual:[NSNull null]], @"First callback is null");
+ initialReadDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return initialReadDone;
+ }];
+
+ __block BOOL writeDone = NO;
+
+ [writeNode setValue:@42 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ writeDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return writeDone;
+ }];
+
+ __block BOOL readDone = NO;
+
+ [readNodeB observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqualObjects(@42, [snapshot value], @"Proper second read");
+ readDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return readDone;
+ }];
+}
+
+// Removed test of forEach aborting iteration. Support dropped, use for .. in syntax
+
+- (void) testSetAndThenListenForValueEventsAreCorrect {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL setDone = NO;
+
+ [node setValue:@"moo" withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ setDone = YES;
+ }];
+
+ __block int calls = 0;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ calls = calls + 1;
+ XCTAssertTrue(calls == 1, @"Only called once");
+ XCTAssertEqualObjects([snapshot value], @"moo", @"Proper snapshot value");
+ }];
+
+ [self waitUntil:^BOOL{
+ return setDone && calls == 1;
+ }];
+}
+
+- (void) testHasChildrenWorksCorrectly {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [node setValue:@{@"one" : @42, @"two": @{@"a": @5}, @"three": @{@"a": @5, @"b": @6}}];
+
+ __block BOOL removedTwo = NO;
+ __block BOOL done = NO;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (!removedTwo) {
+ XCTAssertFalse([[snapshot childSnapshotForPath:@"one"] hasChildren], @"nope");
+ XCTAssertTrue([[snapshot childSnapshotForPath:@"two"] hasChildren], @"nope");
+ XCTAssertTrue([[snapshot childSnapshotForPath:@"three"] hasChildren], @"nope");
+ XCTAssertFalse([[snapshot childSnapshotForPath:@"four"] hasChildren], @"nope");
+
+ removedTwo = YES;
+ [[node child:@"two"] removeValue];
+ }
+ else {
+ XCTAssertFalse([[snapshot childSnapshotForPath:@"two"] hasChildren], @"Second time around");
+ done = YES;
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testNumChildrenWorksCorrectly {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [node setValue:@{@"one" : @42, @"two": @{@"a": @5}, @"three": @{@"a": @5, @"b": @6}}];
+
+ __block BOOL removedTwo = NO;
+ __block BOOL done = NO;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (!removedTwo) {
+ XCTAssertTrue([snapshot childrenCount] == 3, @"Total children");
+ XCTAssertTrue([[snapshot childSnapshotForPath:@"one"] childrenCount] == 0, @"Two's children");
+ XCTAssertTrue([[snapshot childSnapshotForPath:@"two"] childrenCount] == 1, @"Two's children");
+ XCTAssertTrue([[snapshot childSnapshotForPath:@"three"] childrenCount] == 2, @"Two's children");
+ XCTAssertTrue([[snapshot childSnapshotForPath:@"four"] childrenCount] == 0, @"Two's children");
+
+ removedTwo = YES;
+ [[node child:@"two"] removeValue];
+ }
+ else {
+ XCTAssertTrue([snapshot childrenCount] == 2, @"Total children");
+ XCTAssertTrue([[snapshot childSnapshotForPath:@"two"] childrenCount] == 0, @"Two's children");
+ done = YES;
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testSettingANodeWithChildrenToAPrimitiveAndBack {
+ // Can't tolerate stale data; so disable persistence.
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePairWithoutPersistence];
+ FIRDatabaseReference * writeNode = tuple.one;
+ FIRDatabaseReference * readNode = tuple.two;
+
+ __block BOOL done = NO;
+
+ NSDictionary* compound = @{@"a": @5, @"b": @6};
+ NSNumber* number = @76;
+
+ [writeNode setValue:compound];
+
+ [self snapWaiter:writeNode withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([snapshot hasChildren], @"Has children");
+ XCTAssertEqualObjects(@5, [[snapshot childSnapshotForPath:@"a"] value], @"First child");
+ XCTAssertEqualObjects(@6, [[snapshot childSnapshotForPath:@"b"] value], @"First child");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ [self snapWaiter:readNode withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([snapshot hasChildren], @"has children");
+ XCTAssertEqualObjects(@5, [[snapshot childSnapshotForPath:@"a"] value], @"First child");
+ XCTAssertEqualObjects(@6, [[snapshot childSnapshotForPath:@"b"] value], @"First child");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+
+ [writeNode setValue:number withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ [self snapWaiter:readNode withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertFalse([snapshot hasChildren], @"No more children");
+ XCTAssertEqualObjects(number, [snapshot value], @"Proper non compound value");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ [writeNode setValue:compound withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ [self snapWaiter:readNode withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([snapshot hasChildren], @"Has children");
+ XCTAssertEqualObjects(@5, [[snapshot childSnapshotForPath:@"a"] value], @"First child");
+ XCTAssertEqualObjects(@6, [[snapshot childSnapshotForPath:@"b"] value], @"First child");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ XCTAssertTrue(done, @"Properly finished");
+}
+
+- (void) testWriteLeafRemoveLeafAddChildToRemovedNode {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL ready = NO;
+ [writer setValue:@5];
+ [writer removeValue];
+ [[writer child:@"abc"] setValue:@5 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ __block NSDictionary* readVal = nil;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ readVal = [snapshot value];
+ }];
+
+ [self waitUntil:^BOOL{
+ return readVal != nil;
+ }];
+
+ NSNumber* five = [readVal objectForKey:@"abc"];
+ XCTAssertTrue([five isEqualToNumber:@5], @"Should get 5");
+}
+
+- (void) testListenForValueAndThenWriteOnANodeWithExistingData {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ [self waitForCompletionOf:writer setValue:@{@"a": @5, @"b": @2}];
+
+ __block int calls = 0;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ calls++;
+ if (calls == 1) {
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"a" : @10, @"b" : @2};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got the correct value");
+ } else {
+ XCTFail(@"Should only be called once");
+ }
+ }];
+
+ [[reader child:@"a"] setValue:@10];
+ [self waitUntil:^BOOL{
+ return calls == 1;
+ }];
+}
+
+- (void) testSetPriorityOnNonexistentNodeFails {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setPriority:@5 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ XCTAssertTrue(error != nil, @"This should not succeed");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testSetPriorityOnExistentNodeSucceeds {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@"hello!"];
+ [ref setPriority:@5 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ XCTAssertTrue(error == nil, @"This should succeed");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testSetWithPrioritySetsValueAndPriority {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ [self waitForCompletionOf:writer setValue:@"hello" andPriority:@5];
+
+ __block FIRDataSnapshot * writeSnap = nil;
+ __block FIRDataSnapshot * readSnap = nil;
+ [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ writeSnap = snapshot;
+ }];
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ readSnap = snapshot;
+ }];
+
+ [self waitUntil:^BOOL{
+ return readSnap != nil && writeSnap != nil;
+ }];
+
+ XCTAssertTrue([@"hello" isEqualToString:[readSnap value]], @"Got the value on the reader");
+ XCTAssertTrue([@"hello" isEqualToString:[writeSnap value]], @"Got the value on the writer");
+ XCTAssertTrue([@5 isEqualToNumber:[readSnap priority]], @"Got the priority on the reader");
+ XCTAssertTrue([@5 isEqualToNumber:[writeSnap priority]], @"Got the priority on the writer");
+}
+
+- (void) testEffectsOfSetPriorityIsImmediatelyEvident {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* values = [[NSMutableArray alloc] init];
+ NSMutableArray* priorities = [[NSMutableArray alloc] init];
+
+ [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [values addObject:[snapshot value]];
+ [priorities addObject:[snapshot priority]];
+ }];
+ [ref setValue:@5];
+ [ref setPriority:@10];
+ __block BOOL ready = NO;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [values addObject:[snapshot value]];
+ [priorities addObject:[snapshot priority]];
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expectedValues = @[@5, @5];
+ NSArray* expectedPriorites = @[[NSNull null], @10];
+ XCTAssertTrue([values isEqualToArray:expectedValues], @"Expected both listeners to get 5, got %@ instead", values);
+ XCTAssertTrue([priorities isEqualToArray:expectedPriorites], @"The first listener should have missed the priority, got %@ instead", priorities);
+}
+
+- (void) testSetOverwritesPriorityOfTopLevelNodeAndSubnodes {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL ready = NO;
+ [writer setValue:@{@"a": @5}];
+ [writer setPriority:@10];
+ [[writer child:@"a"] setPriority:@18];
+ [writer setValue:@{@"a": @7} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id pri = [snapshot priority];
+ XCTAssertTrue([NSNull null] == pri, @"Expected null priority");
+ FIRDataSnapshot *child = [snapshot childSnapshotForPath:@"a"];
+ XCTAssertTrue([NSNull null] == [child priority], @"Child priority should be null too");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testSetPriorityOfLeafSavesCorrectly {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL ready = NO;
+ [writer setValue:@"testleaf" andPriority:@992 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id pri = [snapshot priority];
+ XCTAssertTrue([@992 isEqualToNumber:pri], @"Expected non-null priority");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testSetPriorityOfObjectSavesCorrectly {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL ready = NO;
+ [writer setValue:@{@"a": @5} andPriority:@991 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id pri = [snapshot priority];
+ XCTAssertTrue([@991 isEqualToNumber:pri], @"Expected non-null priority");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+
+- (void) testSetWithPriorityFollowedBySetClearsPriority {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL ready = NO;
+ [writer setValue:@{@"a": @5} andPriority:@991 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [reader setValue:@{@"a": @19} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id pri = [snapshot priority];
+ XCTAssertTrue([NSNull null] == pri, @"Expected null priority");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testGetPriorityReturnsCorrectType {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block FIRDataSnapshot * snap = nil;
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [ref setValue:@"a"];
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([snap priority] == [NSNull null], @"Expect null priority");
+ snap = nil;
+
+ [ref setValue:@"b" andPriority:@5];
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap priority] isEqualToNumber:@5], @"Expect priority");
+ snap = nil;
+
+ [ref setValue:@"c" andPriority:@"6"];
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap priority] isEqualToString:@"6"], @"Expect priority");
+ snap = nil;
+
+ [ref setValue:@"d" andPriority:@7];
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap priority] isEqualToNumber:@7], @"Expect priority");
+ snap = nil;
+
+ [ref setValue:@{@".value": @"e", @".priority": @8}];
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap priority] isEqualToNumber:@8], @"Expect priority");
+ snap = nil;
+
+ [ref setValue:@{@".value": @"f", @".priority": @"8"}];
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap priority] isEqualToString:@"8"], @"Expect priority");
+ snap = nil;
+
+ [ref setValue:@{@".value": @"e", @".priority": [NSNull null]}];
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([snap priority] == [NSNull null], @"Expect priority");
+ snap = nil;
+
+}
+
+- (void) testExportValIncludesPriorities {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSDictionary* contents = @{@"foo": @{@"bar": @{@".value": @5, @".priority": @7}, @".priority": @"hi"}};
+ __block FIRDataSnapshot * snap = nil;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+ [ref setValue:contents];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([contents isEqualToDictionary:[snap valueInExportFormat]], @"Expected priorities in snapshot");
+}
+
+- (void) testPriorityIsOverwrittenByServer {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ __block int event = 0;
+ __block BOOL done = NO;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSLog(@"%@ Snapshot", snapshot);
+ id pri = [snapshot priority];
+ if (event == 0) {
+ XCTAssertTrue([@100 isEqualToNumber:pri], @"Expect local priority. Got %@ instead.", pri);
+ } else if (event == 1) {
+ XCTAssertTrue(pri == [NSNull null], @"Expect remote priority. Got %@ instead.", pri);
+ } else {
+ XCTFail(@"Extra event");
+ }
+ event++;
+ if (event == 2) {
+ done = YES;
+ }
+ }];
+
+ [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id pri = [snapshot priority];
+ if ([[pri class] isSubclassOfClass:[NSNumber class]] && [@100 isEqualToNumber:pri]) {
+ [writer setValue:@"whatever"];
+ }
+ }];
+
+ [reader setValue:@"hi" andPriority:@100];
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testLargeNumericPrioritiesWork {
+ NSNumber* bigPriority = @1356721306842;
+ __block BOOL ready = NO;
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ [self waitForCompletionOf:writer setValue:@5 andPriority:bigPriority];
+
+ __block NSNumber* serverPriority = @0;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ serverPriority = [snapshot priority];
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([bigPriority isEqualToNumber:serverPriority], @"Expect big priority back");
+}
+
+- (void) testToString {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FIRDatabaseReference * parent = [ref parent];
+
+ XCTAssertTrue([[parent description] isEqualToString:self.databaseURL], @"Expect domain");
+ FIRDatabaseReference * child = [parent child:@"a/b/c"];
+ NSString* expected = [NSString stringWithFormat:@"%@/a/b/c", self.databaseURL];
+ XCTAssertTrue([[child description] isEqualToString:expected], @"Expected path");
+}
+
+- (void) testURLEncodingOfDescriptionAndURLDecodingOfNewFirebase {
+ __block BOOL ready = NO;
+ NSString* test1 = [NSString stringWithFormat:@"%@/a%%b&c@d/space: /non-ascii_character:ø", self.databaseURL];
+ NSString* expected1 = [NSString stringWithFormat:@"%@/a%%25b%%26c%%40d/space%%3A%%20/non-ascii_character%%3A%%C3%%B8", self.databaseURL];
+ FIRDatabaseReference * ref = [[FIRDatabase database] referenceFromURL:test1];
+ NSString* result = [ref description];
+ XCTAssertTrue([result isEqualToString:expected1], @"Encodes properly");
+
+ int rnd = arc4random_uniform(100000000);
+ NSString* path = [NSString stringWithFormat:@"%i", rnd];
+ [[ref child:path] setValue:@"testdata" withCompletionBlock:^(NSError* error, FIRDatabaseReference * childRef) {
+ FIRDatabaseReference * other = [[FIRDatabase database] referenceFromURL:[ref description]];
+ [[other child:path] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *val = snapshot.value;
+ XCTAssertTrue([val isEqualToString:@"testdata"], @"Expected to get testdata back");
+ ready = YES;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testNameAtRootAndNonRootLocations {
+ FIRDatabaseReference * ref = [[FIRDatabase database] referenceFromURL:self.databaseURL];
+ XCTAssertTrue(ref.key == nil, @"Root key should be nil");
+ FIRDatabaseReference * child = [ref child:@"a"];
+ XCTAssertTrue([child.key isEqualToString:@"a"], @"Should be 'a'");
+ FIRDatabaseReference * deeperChild = [child child:@"b/c"];
+ XCTAssertTrue([deeperChild.key isEqualToString:@"c"], @"Should be 'c'");
+}
+
+- (void) testNameAndRefOnSnapshotsForRootAndNonRootLocations {
+ FIRDatabaseReference * ref = [[FIRDatabase database] reference];
+
+ __block BOOL ready = NO;
+ [ref removeValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(snapshot.key == nil, @"Root snap should not have a key");
+ NSString *snapString = [snapshot.ref description];
+ XCTAssertTrue([snapString isEqualToString:snapString], @"Refs should be equivalent");
+ FIRDataSnapshot *childSnap = [snapshot childSnapshotForPath:@"a"];
+ XCTAssertTrue([childSnap.key isEqualToString:@"a"], @"Properly keys children");
+ FIRDatabaseReference *childRef = [ref child:@"a"];
+ NSString *refString = [childRef description];
+ snapString = [childSnap.ref description];
+ XCTAssertTrue([refString isEqualToString:snapString], @"Refs should be equivalent");
+ childSnap = [childSnap childSnapshotForPath:@"b/c"];
+ childRef = [childRef child:@"b/c"];
+ XCTAssertTrue([childSnap.key isEqualToString:@"c"], @"properly keys children");
+ refString = [childRef description];
+ snapString = [childSnap.ref description];
+ XCTAssertTrue([refString isEqualToString:snapString], @"Refs should be equivalent");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ // generate value event at root
+ [ref setValue:@"foo"];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testParentForRootAndNonRootLocations {
+ FIRDatabaseReference * ref = [[FIRDatabase database] reference];
+
+ XCTAssertTrue(ref.parent == nil, @"Parent of root should be nil");
+
+ FIRDatabaseReference * child = [ref child:@"a"];
+ XCTAssertTrue([[child.parent description] isEqualToString:[ref description]], @"Should be equivalent locations");
+ child = [ref child:@"a/b/c"];
+ XCTAssertTrue([[child.parent.parent.parent description] isEqualToString:[ref description]], @"Should be equivalent locations");
+}
+
+- (void) testSettingNumericKeysConvertsToStrings {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSDictionary* toSet = @{@4: @"hi", @5: @"test"};
+
+ XCTAssertThrows([ref setValue:toSet], @"Keys must be strings");
+}
+
+- (void) testSetChildAndListenAtRootRegressionTest {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL ready = NO;
+ [writer removeValue];
+ [[writer child:@"foo"] setValue:@"hi" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"foo" : @"hi"};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got child");
+ ready = YES;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+
+- (void) testAccessingInvalidPathsThrows {
+ NSArray* badPaths = @[
+ @".test",
+ @"test.",
+ @"fo$o",
+ @"[what",
+ @"ever]",
+ @"ha#sh"
+ ];
+
+ for (NSString* key in badPaths) {
+ NSString* url = [NSString stringWithFormat:@"%@/%@", self.databaseURL, key];
+ XCTAssertThrows(^{
+ FIRDatabaseReference * ref = [[FIRDatabase database] referenceFromURL:url];
+ XCTFail(@"Should not get here with ref: %@", ref);
+ }(), @"should throw");
+ url = [NSString stringWithFormat:@"%@/TESTS/%@", self.databaseURL, key];
+ XCTAssertThrows(^{
+ FIRDatabaseReference * ref = [[FIRDatabase database] referenceFromURL:url];
+ XCTFail(@"Should not get here with ref: %@", ref);
+ }(), @"should throw");
+ }
+
+ __block BOOL ready = NO;
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (NSString *key in badPaths) {
+ XCTAssertThrows([snapshot childSnapshotForPath:key], @"should throw");
+ XCTAssertThrows([snapshot hasChild:key], @"should throw");
+ }
+ ready = YES;
+ }];
+ [ref setValue:nil];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testSettingObjectsAtInvalidKeysThrow {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSArray* badPaths = @[
+ @".test",
+ @"test.",
+ @"fo$o",
+ @"[what",
+ @"ever]",
+ @"ha#sh",
+ @"/thing",
+ @"th/ing",
+ @"thing/"
+ ];
+ NSMutableArray* badObjs = [[NSMutableArray alloc] init];
+ for (NSString* key in badPaths) {
+ [badObjs addObject:@{key: @"test"}];
+ [badObjs addObject:@{@"deeper": @{key: @"test"}}];
+ }
+
+ for (NSDictionary* badObj in badObjs) {
+ XCTAssertThrows([ref setValue:badObj], @"Should throw");
+ XCTAssertThrows([ref setValue:badObj andPriority:@5], @"Should throw");
+ XCTAssertThrows([ref onDisconnectSetValue:badObj], @"Should throw");
+ XCTAssertThrows([ref onDisconnectSetValue:badObj andPriority:@5], @"Should throw");
+ // XXX transaction
+ }
+}
+
+- (void) testSettingInvalidObjectsThrow {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ XCTAssertThrows([ref setValue:[NSDate date]], @"Should throw");
+
+ NSDictionary *data = @{@"invalid":@"data", @".sv":@"timestamp"};
+ XCTAssertThrows([ref setValue:data], @"Should throw");
+
+ data = @{@".value": @{}};
+ XCTAssertThrows([ref setValue:data], @"Should throw");
+}
+
+- (void) testInvalidUpdateThrow {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSArray *badUpdates = @[
+ @{@"/":@"t", @"a":@"t"},
+ @{@"a":@"t", @"a/b":@"t"},
+ @{@"/a":@"t", @"a/b":@"t"},
+ @{@"/a/b":@"t", @"a":@"t"},
+ @{@"/a/b/.priority":@"t", @"/a/b":@"t"},
+ @{@"/a/b/.sv":@"timestamp"},
+ @{@"/a/b/.value":@"t"},
+ @{@"/a/b/.priority":@{@"x": @"y"}}];
+
+ for (NSDictionary* update in badUpdates) {
+ XCTAssertThrows([ref updateChildValues:update], @"Should throw");
+ XCTAssertThrows([ref onDisconnectUpdateChildValues:update], @"Should throw");
+ }
+}
+
+- (void) testSettingNull {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ XCTAssertNoThrow([ref setValue:nil], @"Should not throw");
+ XCTAssertNoThrow([ref setValue:[NSNull null]], @"Should not throw");
+}
+
+- (void) testSettingNaN {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ XCTAssertThrows([ref setValue:[NSDecimalNumber notANumber]], @"Should throw");
+}
+
+- (void) testSettingInvalidPriority {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ XCTAssertThrows([ref setValue:@"3" andPriority:[NSDecimalNumber notANumber]], @"Should throw");
+ XCTAssertThrows([ref setValue:@"4" andPriority:@{}], @"Should throw");
+ XCTAssertThrows([ref setValue:@"5" andPriority:@[]], @"Should throw");
+}
+
+- (void) testRemoveFromOnMobileGraffitiBugAtAngelHack {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+
+ [node observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [[node child:[snapshot key]] removeValueWithCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ }];
+
+ [[node childByAutoId] setValue:@"moo"];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ XCTAssertTrue(done, @"Properly finished");
+}
+
+- (void) testSetANodeWithAQuotedKey {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ __block FIRDataSnapshot * snap;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [node setValue:@{@"\"herp\"": @1234} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ done = YES;
+ XCTAssertEqualObjects(@1234, [[snap childSnapshotForPath:@"\"herp\""] value], @"Got it back");
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ XCTAssertTrue(done, @"Properly finished");
+}
+
+- (void) testSetANodeWithASingleQuoteKey {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ __block FIRDataSnapshot * snap;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [node setValue:@{@"\"": @1234} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ done = YES;
+ XCTAssertEqualObjects(@1234, [[snap childSnapshotForPath:@"\""] value], @"Got it back");
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ XCTAssertTrue(done, @"Properly finished");
+}
+
+- (void) testEmptyChildGetValueEventBeforeParent {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSArray* lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa/aaa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:lookingFor];
+
+ [node setValue:@{@"b": @5}];
+
+ [et wait];
+
+}
+
+// iOS behavior is different from what the recursive set test looks for. We don't raise events synchronously
+
+- (void) testOnAfterSetWaitsForLatestData {
+ // We test here that we don't cache sets, but they would be persisted so make sure we are running without
+ // persistence
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePairWithoutPersistence];
+ FIRDatabaseReference * node1 = refs.one;
+ FIRDatabaseReference * node2 = refs.two;
+
+ __block BOOL ready = NO;
+ [node1 setValue:@5 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ [node2 setValue:@42 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+
+ [node1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSNumber *val = [snapshot value];
+ XCTAssertTrue([val isEqualToNumber:@42], @"Should not have cached earlier set");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testOnceWaitsForLatestData {
+ // Can't tolerate stale data; so disable persistence.
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePairWithoutPersistence];
+ FIRDatabaseReference * node1 = refs.one;
+ FIRDatabaseReference * node2 = refs.two;
+
+ __block BOOL ready = NO;
+
+ [node1 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ XCTAssertTrue([NSNull null] == val, @"First value should be null");
+
+ [node2 setValue:@5 withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [node1 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSNumber *val = [snapshot value];
+ XCTAssertTrue([val isKindOfClass:[NSNumber class]] && [val isEqualToNumber:@5], @"Should get first value");
+ ready = YES;
+ }];
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [node2 setValue:@42 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ [node1 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSNumber *val = [snapshot value];
+ XCTAssertTrue([val isEqualToNumber:@42], @"Got second number");
+ ready = YES;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testMemoryFreeingOnUnlistenDoesNotCorruptData {
+ // Can't tolerate stale data; so disable persistence.
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePairWithoutPersistence];
+ FIRDatabaseReference * node2 = [[refs.one root] childByAutoId];
+
+ __block BOOL hasRun = NO;
+ __block BOOL ready = NO;
+ FIRDatabaseHandle handle1 = [refs.one observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (!hasRun) {
+ hasRun = YES;
+ id val = [snapshot value];
+ XCTAssertTrue([NSNull null] == val, @"First time should be null");
+ [refs.one setValue:@"test" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ ready = YES;
+ }];
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [refs.one removeObserverWithHandle:handle1];
+
+ ready = NO;
+ [node2 setValue:@"hello" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ [refs.one observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *val = [snapshot value];
+ XCTAssertTrue([val isEqualToString:@"test"], @"Get back the value we set above");
+ [refs.two observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *val = [snapshot value];
+ XCTAssertTrue([val isEqualToString:@"test"], @"Get back the value we set above");
+ ready = YES;
+ }];
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ //write {x: 1, y : {t: 2, u: 3}}
+ //Listen at /. Then listen at /x/t
+ //unlisten at /y/t. Off at /. Once at /. Ensure data is still all there.
+ //Once at /y. Ensure data is still all there.
+ refs = [FTestHelpers getRandomNodePairWithoutPersistence];
+
+ ready = NO;
+ __block FIRDatabaseHandle deeplisten = NSNotFound;
+ __block FIRDatabaseHandle slashlisten = NSNotFound;
+ __weak FIRDatabaseReference * refOne = refs.one;
+ [refs.one setValue:@{@"x": @1, @"y": @{@"t": @2, @"u": @3}} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ slashlisten = [refOne observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ deeplisten = [[refOne child:@"y/t"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [[refOne child:@"y/t"] removeObserverWithHandle:deeplisten];
+ [refOne removeObserverWithHandle:slashlisten];
+ ready = YES;
+ }];
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [[refs.one child:@"x"] setValue:@"test" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ [refs.one observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"x" : @"test", @"y" : @{@"t" : @2, @"u" : @3}};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got the final value");
+ ready = YES;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testUpdateRaisesCorrectLocalEvents {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * snap = nil;
+ [node observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ __block BOOL ready = NO;
+ [node setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ NSArray* expectations = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"d"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildChanged withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildChanged withString:@"d"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expectations];
+
+ [et waitForInitialization];
+
+ [node updateChildValues:@{@"a": @4, @"d": @1}];
+
+ [et wait];
+}
+
+- (void) testUpdateRaisesCorrectRemoteEvents {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ __block BOOL ready = NO;
+ [writer setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ NSArray* expectations = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"d"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildChanged withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildChanged withString:@"d"],
+ [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expectations];
+
+ [et waitForInitialization];
+
+ [writer updateChildValues:@{@"a": @4, @"d": @1}];
+
+ [et wait];
+
+ ready = NO;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *result = [snapshot value];
+ NSDictionary *expected = @{@"a" : @4, @"b" : @2, @"c" : @3, @"d" : @1};
+ XCTAssertTrue([result isEqualToDictionary:expected], @"Got expected results");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testUpdateChangesAreStoredCorrectlyByTheServer {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ [self waitForCompletionOf:writer setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+
+ [self waitForCompletionOf:writer updateChildValues:@{@"a": @42}];
+
+ [self snapWaiter:reader withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary* result = [snapshot value];
+ NSDictionary* expected = @{@"a": @42, @"b": @2, @"c": @3, @"d": @4};
+ XCTAssertTrue([result isEqualToDictionary:expected], @"Expected updated value");
+ }];
+}
+
+- (void) testUpdateDoesntAffectPriorityLocally {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * snap = nil;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} andPriority:@"testpri"];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap priority] isEqualToString:@"testpri"], @"Got initial priority");
+ snap = nil;
+
+ [ref updateChildValues:@{@"a": @4}];
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap priority] isEqualToString:@"testpri"], @"Got initial priority");
+}
+
+- (void) testUpdateDoesntAffectPriorityRemotely {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ __block BOOL ready = NO;
+ [writer setValue:@{@"a": @1, @"b": @2, @"c": @3} andPriority:@"testpri" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *result = [snapshot priority];
+ XCTAssertTrue([result isEqualToString:@"testpri"], @"Expected initial priority");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [writer updateChildValues:@{@"a": @4} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *result = [snapshot priority];
+ XCTAssertTrue([result isEqualToString:@"testpri"], @"Expected initial priority");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testUpdateReplacesChildrenAndIsNotRecursive {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ __block FIRDataSnapshot * localSnap = nil;
+ __block BOOL ready = NO;
+
+ [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ localSnap = snapshot;
+ }];
+
+ [writer setValue:@{@"a": @{@"aa": @1, @"ab": @2}}];
+ [writer updateChildValues:@{@"a": @{@"aa": @1}} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+
+ [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *result = [snapshot value];
+ NSDictionary *expected = @{@"a" : @{@"aa" : @1}};
+ XCTAssertTrue([result isEqualToDictionary:expected], @"Should get new value");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ NSDictionary* result = [localSnap value];
+ NSDictionary* expected = @{@"a": @{@"aa": @1}};
+ return ready && [result isEqualToDictionary:expected];
+ }];
+}
+
+- (void) testDeepUpdatesWork {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ __block FIRDataSnapshot * localSnap = nil;
+ __block BOOL ready = NO;
+
+ [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ localSnap = snapshot;
+ }];
+
+ [writer setValue:@{@"a": @{@"aa": @1, @"ab": @2}}];
+ [writer updateChildValues:@{@"a/aa": @10,
+ @".priority": @3.0,
+ @"a/ab": @{@".priority": @2.0,
+ @".value": @20}}
+ withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+ ready = NO;
+
+ [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *result = [snapshot value];
+ NSDictionary *expected = @{@"a" : @{@"aa" : @10, @"ab" : @20}};
+ XCTAssertTrue([result isEqualToDictionary:expected], @"Should get new value");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ NSDictionary* result = [localSnap value];
+ NSDictionary* expected = @{@"a": @{@"aa": @10, @"ab": @20}};
+ return ready && [result isEqualToDictionary:expected];
+ }];
+}
+
+// Type signature means we don't need a test for updating scalars. They wouldn't compile
+
+- (void) testEmptyUpdateWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref updateChildValues:@{} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ XCTAssertTrue(error == nil, @"Should not be an error");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+// XXX update stress test
+
+- (void) testUpdateFiresCorrectEventWhenAChildIsDeleted {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ __block FIRDataSnapshot * localSnap = nil;
+ __block FIRDataSnapshot * remoteSnap = nil;
+
+ [self waitForCompletionOf:writer setValue:@{@"a": @12, @"b": @6}];
+ [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ localSnap = snapshot;
+ }];
+
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ remoteSnap = snapshot;
+ }];
+
+ [self waitUntil:^BOOL{
+ return localSnap != nil && remoteSnap != nil;
+ }];
+
+ localSnap = nil;
+ remoteSnap = nil;
+
+ [writer updateChildValues:@{@"a": [NSNull null]}];
+
+ [self waitUntil:^BOOL{
+ return localSnap != nil && remoteSnap != nil;
+ }];
+
+ NSDictionary* expected = @{@"b": @6};
+ XCTAssertTrue([[remoteSnap value] isEqualToDictionary:expected], @"Removed child");
+ XCTAssertTrue([[localSnap value] isEqualToDictionary:expected], @"Removed child");
+}
+
+- (void) testUpdateFiresCorrectEventOnNewChildren {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ __block FIRDataSnapshot * localSnap = nil;
+ __block FIRDataSnapshot * remoteSnap = nil;
+
+ [[writer child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ localSnap = snapshot;
+ }];
+
+ [[reader child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ remoteSnap = snapshot;
+ }];
+
+ [self waitUntil:^BOOL{
+ return localSnap != nil && remoteSnap != nil;
+ }];
+
+ localSnap = nil;
+ remoteSnap = nil;
+
+ [writer updateChildValues:@{@"a": @42}];
+
+ [self waitUntil:^BOOL{
+ return localSnap != nil && remoteSnap != nil;
+ }];
+
+ XCTAssertTrue([[remoteSnap value] isEqualToNumber:@42], @"Added child");
+ XCTAssertTrue([[localSnap value] isEqualToNumber:@42], @"Added child");
+}
+
+- (void) testUpdateFiresCorrectEventOnDeletedChildren {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ __block FIRDataSnapshot * localSnap = nil;
+ __block FIRDataSnapshot * remoteSnap = nil;
+ [self waitForCompletionOf:writer setValue:@{@"a": @12}];
+ [[writer child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ localSnap = snapshot;
+ }];
+
+ [[reader child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ remoteSnap = snapshot;
+ }];
+
+ [self waitUntil:^BOOL{
+ return localSnap != nil && remoteSnap != nil;
+ }];
+
+ localSnap = nil;
+ remoteSnap = nil;
+
+ [writer updateChildValues:@{@"a": [NSNull null]}];
+
+ [self waitUntil:^BOOL{
+ return localSnap != nil && remoteSnap != nil;
+ }];
+
+ XCTAssertTrue([remoteSnap value] == [NSNull null], @"Removed child");
+ XCTAssertTrue([localSnap value] == [NSNull null], @"Removed child");
+}
+
+- (void) testUpdateFiresCorrectEventOnChangedChildren {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ [self waitForCompletionOf:writer setValue:@{@"a": @12}];
+
+ __block FIRDataSnapshot * localSnap = nil;
+ __block FIRDataSnapshot * remoteSnap = nil;
+
+ [[writer child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ localSnap = snapshot;
+ }];
+
+ [[reader child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ remoteSnap = snapshot;
+ }];
+
+ [self waitUntil:^BOOL{
+ return localSnap != nil && remoteSnap != nil;
+ }];
+
+ localSnap = nil;
+ remoteSnap = nil;
+
+ [self waitForCompletionOf:writer updateChildValues:@{@"a": @11}];
+
+ [self waitUntil:^BOOL{
+ return localSnap != nil && remoteSnap != nil;
+ }];
+
+ XCTAssertTrue([[remoteSnap value] isEqualToNumber:@11], @"Changed child");
+ XCTAssertTrue([[localSnap value] isEqualToNumber:@11], @"Changed child");
+}
+
+
+- (void) testUpdateOfPriorityWorks {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+ FIRDatabaseReference * writer = refs.two;
+
+ __block BOOL ready = NO;
+ [writer setValue:@{@"a": @5, @".priority": @"pri1"}];
+ [writer updateChildValues:@{@"a": @6, @".priority": @"pri2", @"b": @{ @".priority": @"pri3", @"c": @10 } } withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ NSLog(@"error? %@", error);
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqualObjects([[snapshot childSnapshotForPath:@"a"] value], @6, @"Should match write values");
+ XCTAssertTrue([[snapshot priority] isEqualToString:@"pri2"], @"Should get updated priority");
+ XCTAssertTrue([[[snapshot childSnapshotForPath:@"b"] priority] isEqualToString:@"pri3"], @"Should get updated priority");
+ XCTAssertEqualObjects([[snapshot childSnapshotForPath:@"b/c"] value], @10, @"Should match write values");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testSetWithCircularReferenceFails {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableDictionary* toSet = [[NSMutableDictionary alloc] init];
+ NSDictionary* lol = @{@"foo": @"bar", @"circular": toSet};
+ [toSet setObject:lol forKey:@"lol"];
+
+ XCTAssertThrows([ref setValue:toSet], @"Should not be able to set circular dictionary");
+}
+
+- (void) testLargeNumbers {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ long long jsMaxInt = 9007199254740992;
+ long jsMaxIntPlusOne = jsMaxInt + 1;
+ NSNumber* toSet = [NSNumber numberWithLong:jsMaxIntPlusOne];
+ [ref setValue:toSet];
+
+ __block FIRDataSnapshot * snap = nil;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ NSNumber* result = [snap value];
+ XCTAssertTrue([result isEqualToNumber:toSet], @"Should get back same number");
+
+ toSet = [NSNumber numberWithLong:LONG_MAX];
+ snap = nil;
+
+ [ref setValue:toSet];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ result = [snap value];
+ XCTAssertTrue([result isEqualToNumber:toSet], @"Should get back same number");
+
+ snap = nil;
+ toSet = [NSNumber numberWithDouble:DBL_MAX];
+ [ref setValue:toSet];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ result = [snap value];
+ XCTAssertTrue([result isEqualToNumber:toSet], @"Should get back same number");
+}
+
+- (void) testParentDeleteShadowsChildListeners {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * deleter = refs.two;
+
+ NSString* childName = [writer childByAutoId].key;
+
+ __block BOOL called = NO;
+ [[deleter child:childName] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(called, @"Should only be hit once");
+ called = YES;
+ XCTAssertTrue(snapshot.value == [NSNull null], @"Value should be null");
+ }];
+
+ WAIT_FOR(called);
+
+ __block BOOL done = NO;
+ [[writer child:childName] setValue:@"foo"];
+ [deleter removeValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void) testParentDeleteShadowsChildListenersWithNonDefaultQuery {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * deleter = refs.two;
+
+ NSString* childName = [writer childByAutoId].key;
+
+ __block BOOL queryCalled = NO;
+ __block BOOL deepChildCalled = NO;
+ [[[[deleter child:childName] queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(queryCalled, @"Should only be hit once");
+ queryCalled = YES;
+ XCTAssertTrue(snapshot.value == [NSNull null], @"Value should be null");
+ }];
+
+ [[[deleter child:childName] child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(deepChildCalled, @"Should only be hit once");
+ deepChildCalled = YES;
+ XCTAssertTrue(snapshot.value == [NSNull null], @"Value should be null");
+ }];
+
+ WAIT_FOR(deepChildCalled && queryCalled);
+
+ __block BOOL done = NO;
+ [[writer child:childName] setValue:@"foo"];
+ [deleter removeValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void) testLocalServerValuesEventuallyButNotImmediatelyMatchServer {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference* writer = refs.one;
+ FIRDatabaseReference* reader = refs.two;
+ __block int done = 0;
+
+ NSMutableArray* readSnaps = [[NSMutableArray alloc] init];
+ NSMutableArray* writeSnaps = [[NSMutableArray alloc] init];
+
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([snapshot value] != [NSNull null]) {
+ [readSnaps addObject:snapshot];
+ if (readSnaps.count == 1) {
+ done += 1;
+ }
+ }
+ }];
+
+ [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([snapshot value] != [NSNull null]) {
+ [writeSnaps addObject:snapshot];
+ if (writeSnaps.count == 2) {
+ done += 1;
+ }
+ }
+ }];
+
+ [writer setValue:[FIRServerValue timestamp] andPriority:[FIRServerValue timestamp]];
+
+ [self waitUntil:^BOOL{
+ return done == 2;
+ }];
+
+ XCTAssertEqual((unsigned long)[readSnaps count], (unsigned long)1, @"Should have received one snapshot on reader");
+ XCTAssertEqual((unsigned long)[writeSnaps count], (unsigned long)2, @"Should have received two snapshots on writer");
+
+ FIRDataSnapshot * firstReadSnap = [readSnaps objectAtIndex:0];
+ FIRDataSnapshot * firstWriteSnap = [writeSnaps objectAtIndex:0];
+ FIRDataSnapshot * secondWriteSnap = [writeSnaps objectAtIndex:1];
+
+ NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)];
+ XCTAssertTrue([now doubleValue] - [firstWriteSnap.value doubleValue] < 3000, @"Should have received a local event with a value close to timestamp");
+ XCTAssertTrue([now doubleValue] - [firstWriteSnap.priority doubleValue] < 3000, @"Should have received a local event with a priority close to timestamp");
+ XCTAssertTrue([now doubleValue] - [secondWriteSnap.value doubleValue] < 3000, @"Should have received a server event with a value close to timestamp");
+ XCTAssertTrue([now doubleValue] - [secondWriteSnap.priority doubleValue] < 3000, @"Should have received a server event with a priority close to timestamp");
+
+ XCTAssertFalse([firstWriteSnap value] == [secondWriteSnap value], @"Initial and future writer values should be different");
+ XCTAssertFalse([firstWriteSnap priority] == [secondWriteSnap priority], @"Initial and future writer priorities should be different");
+ XCTAssertEqualObjects(firstReadSnap.value, secondWriteSnap.value, @"Eventual reader and writer values should be equal");
+ XCTAssertEqualObjects(firstReadSnap.priority, secondWriteSnap.priority, @"Eventual reader and writer priorities should be equal");
+}
+
+- (void) testServerValuesSetWithPriorityRemoteEvents {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ NSDictionary* data = @{
+ @"a": [FIRServerValue timestamp],
+ @"b": @{
+ @".value": [FIRServerValue timestamp],
+ @".priority": [FIRServerValue timestamp]
+ }
+ };
+
+ __block BOOL done = NO;
+ [writer setValue:data andPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { done = YES; }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [self snapWaiter:reader withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary* value = [snapshot value];
+ NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)];
+ NSNumber* timestamp = [snapshot priority];
+ XCTAssertTrue([[snapshot priority] isKindOfClass:[NSNumber class]], @"Should get back number");
+ XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago");
+ XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"a"], @"Should get back matching ServerValue.TIMESTAMP");
+ XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"b"], @"Should get back matching ServerValue.TIMESTAMP");
+ XCTAssertEqualObjects([snapshot priority], [[snapshot childSnapshotForPath:@"b"] priority], @"Should get back matching ServerValue.TIMESTAMP");
+ }];
+}
+
+- (void) testServerValuesSetPriorityRemoteEvents {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block FIRDataSnapshot *snap = nil;
+ [reader observeEventType:FIRDataEventTypeChildMoved withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [self waitForCompletionOf:[writer child:@"a"] setValue:@1 andPriority:nil];
+ [self waitForCompletionOf:[writer child:@"b"] setValue:@1 andPriority:@1];
+ [self waitForValueOf:[reader child:@"a"] toBe:@1];
+
+ __block BOOL done = NO;
+ [[writer child:@"a"] setPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done && snap != nil;
+ }];
+
+ NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)];
+ NSNumber* timestamp = [snap priority];
+ XCTAssertTrue([[snap priority] isKindOfClass:[NSNumber class]], @"Should get back number");
+ XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago");
+}
+
+- (void) testServerValuesUpdateRemoteEvents {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block FIRDataSnapshot *snap = nil;
+ __block BOOL done = NO;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ if (snap && [[snap childSnapshotForPath:@"a/b/d"] value] != [NSNull null]) {
+ done = YES;
+ }
+ }];
+
+ [[writer child:@"a/b/c"] setValue:@1];
+ [[writer child:@"a"] updateChildValues:@{ @"b": @{ @"c": [FIRServerValue timestamp], @"d":@1 } }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)];
+ NSNumber* timestamp = [[snap childSnapshotForPath:@"a/b/c"] value];
+ XCTAssertTrue([[[snap childSnapshotForPath:@"a/b/c"] value] isKindOfClass:[NSNumber class]], @"Should get back number");
+ XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago");
+}
+
+- (void) testServerValuesSetWithPriorityLocalEvents {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSDictionary* data = @{
+ @"a": [FIRServerValue timestamp],
+ @"b": @{
+ @".value": [FIRServerValue timestamp],
+ @".priority": [FIRServerValue timestamp]
+ }
+ };
+
+ __block FIRDataSnapshot *snap = nil;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ __block BOOL done = NO;
+ [node setValue:data andPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { done = YES; }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [self snapWaiter:node withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary* value = [snapshot value];
+ NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)];
+ NSNumber* timestamp = [snapshot priority];
+ XCTAssertTrue([[snapshot priority] isKindOfClass:[NSNumber class]], @"Should get back number");
+ XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago");
+ XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"a"], @"Should get back matching ServerValue.TIMESTAMP");
+ XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"b"], @"Should get back matching ServerValue.TIMESTAMP");
+ XCTAssertEqualObjects([snapshot priority], [[snapshot childSnapshotForPath:@"b"] priority], @"Should get back matching ServerValue.TIMESTAMP");
+ }];
+}
+
+- (void) testServerValuesSetPriorityLocalEvents {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot *snap = nil;
+ [node observeEventType:FIRDataEventTypeChildMoved withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ __block BOOL done = NO;
+
+ [[node child:@"a"] setValue:@1 andPriority:nil];
+ [[node child:@"b"] setValue:@1 andPriority:@1];
+ [[node child:@"a"] setPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)];
+ NSNumber* timestamp = [snap priority];
+ XCTAssertTrue([[snap priority] isKindOfClass:[NSNumber class]], @"Should get back number");
+ XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago");
+}
+
+- (void) testServerValuesUpdateLocalEvents {
+ FIRDatabaseReference * node1 = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot *snap1 = nil;
+ [node1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap1 = snapshot;
+ }];
+
+ __block FIRDataSnapshot *snap2 = nil;
+ [node1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap2 = snapshot;
+ }];
+
+ [node1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:[FIRServerValue timestamp]];
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+
+ [self waitUntil:^BOOL{
+ return snap1 != nil && snap2 != nil && [snap1 value] != nil && [snap2 value] != nil;
+ }];
+
+ NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)];
+
+ NSNumber* timestamp1 = [snap1 value];
+ XCTAssertTrue([[snap1 value] isKindOfClass:[NSNumber class]], @"Should get back number");
+ XCTAssertTrue([now doubleValue] - [timestamp1 doubleValue] < 2000, @"Number should be no more than 2 seconds ago");
+
+ NSNumber* timestamp2 = [snap2 value];
+ XCTAssertTrue([[snap2 value] isKindOfClass:[NSNumber class]], @"Should get back number");
+ XCTAssertTrue([now doubleValue] - [timestamp2 doubleValue] < 2000, @"Number should be no more than 2 seconds ago");
+}
+
+- (void) testServerValuesTransactionLocalEvents {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot *snap = nil;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [[node child:@"a/b/c"] setValue:@1];
+ [[node child:@"a"] updateChildValues:@{ @"b": @{ @"c": [FIRServerValue timestamp], @"d":@1 } }];
+
+ [self waitUntil:^BOOL{
+ return snap != nil && [[snap childSnapshotForPath:@"a/b/d"] value] != nil;
+ }];
+
+ NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)];
+ NSNumber* timestamp = [[snap childSnapshotForPath:@"a/b/c"] value];
+ XCTAssertTrue([[[snap childSnapshotForPath:@"a/b/c"] value] isKindOfClass:[NSNumber class]], @"Should get back number");
+ XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago");
+}
+
+- (void) testUpdateAfterChildSet {
+ FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ __weak FIRDatabaseReference *weakRef = node;
+ [node setValue:@{@"a": @"a"} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [weakRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (snapshot.childrenCount == 3 && [snapshot hasChild:@"a"] && [snapshot hasChild:@"b"] && [snapshot hasChild:@"c"]) {
+ done = YES;
+ }
+ }];
+
+ [[weakRef child:@"b"] setValue:@"b"];
+
+ [weakRef updateChildValues:@{@"c" : @"c"}];
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testDeltaSyncNoDataUpdatesAfterReconnect {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"];
+ FIRDatabaseReference * ref2 = [[[FIRDatabaseReference alloc] initWithConfig:cfg] child:ref.key];
+ __block id data = @{ @"a": @1, @"b": @2, @"c": @{ @".priority": @3, @".value": @3}, @"d": @4 };
+ [self waitForCompletionOf:ref setValue:data];
+
+ __block BOOL gotData = NO;
+ [ref2 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(gotData, @"event triggered twice.");
+ gotData = YES;
+ XCTAssertEqualObjects(snapshot.valueInExportFormat, data, @"Got wrong data.");
+ }];
+
+ [self waitUntil:^BOOL{ return gotData; }];
+
+ __block BOOL done = NO;
+ XCTAssertEqual(ref2.repo.dataUpdateCount, 1L, @"Should have gotten one update.");
+
+ // Bounce connection
+ [FRepoManager interrupt:cfg];
+ [FRepoManager resume:cfg];
+
+ [[[ref2 root] child:@".info/connected"] observeEventType:FIRDataEventTypeValue
+ withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([snapshot.value boolValue]) {
+ // We're connected. Do one more round-trip to make sure all state restoration is done
+ [[[ref2 root] child:@"foobar/empty/blah"] setValue:nil withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(ref2.repo.dataUpdateCount, 1L, @"Should have gotten one update.");
+ done = YES;
+ }];
+ }
+ }
+ ];
+
+ [self waitUntil:^BOOL{ return done; }];
+
+ // cleanup
+ [FRepoManager interrupt:cfg];
+ [FRepoManager disposeRepos:cfg];
+}
+
+- (void) testServerValuesEventualConsistencyBetweenLocalAndRemote {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block FIRDataSnapshot *writerSnap = nil;
+ __block FIRDataSnapshot *readerSnap = nil;
+
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ readerSnap = snapshot;
+ }];
+
+ [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ writerSnap = snapshot;
+ }];
+
+ [writer setValue:[FIRServerValue timestamp] andPriority:[FIRServerValue timestamp]];
+
+ [self waitUntil:^BOOL{
+ if (readerSnap && writerSnap && [[readerSnap value] isKindOfClass:[NSNumber class]] && [[writerSnap value] isKindOfClass:[NSNumber class]]) {
+ if ([[readerSnap value] doubleValue] == [[writerSnap value] doubleValue]) {
+ return YES;
+ }
+ }
+ return NO;
+ }];
+}
+
+// Listens at a location and then creates a bunch of children, waiting for them all to complete.
+- (void) testChildAddedPerf1 {
+ if (!runPerfTests) return;
+
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ [ref observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ }];
+
+ NSDate *start = [NSDate date];
+ int COUNT = 1000;
+ __block BOOL done = NO;
+ __block NSDate *finished = nil;
+ for(int i = 0; i < COUNT; i++) {
+ [[ref childByAutoId] setValue:@"01234567890123456789012345678901234567890123456789" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ if (i == (COUNT - 1)) {
+ finished = [NSDate date];
+ done = YES;
+ }
+ }];
+ }
+ [self waitUntil:^BOOL {
+ return done;
+ } timeout:300];
+ NSTimeInterval elapsed = [finished timeIntervalSinceDate:start];
+ NSLog(@"Elapsed: %f", elapsed);
+}
+
+// Listens at a location, then adds a bunch of grandchildren under a single child.
+- (void) testDeepChildAddedPerf1 {
+ if (!runPerfTests) return;
+
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode],
+ *childRef = [ref child:@"child"];
+
+ [ref observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ }];
+
+ NSDate *start = [NSDate date];
+ int COUNT = 1000;
+ __block BOOL done = NO;
+ __block NSDate *finished = nil;
+ for(int i = 0; i < COUNT; i++) {
+ [[childRef childByAutoId] setValue:@"01234567890123456789012345678901234567890123456789" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ if (i == (COUNT - 1)) {
+ finished = [NSDate date];
+ done = YES;
+ }
+ }];
+ }
+ [self waitUntil:^BOOL {
+ return done;
+ } timeout:300];
+
+ NSTimeInterval elapsed = [finished timeIntervalSinceDate:start];
+ NSLog(@"Elapsed: %f", elapsed);
+}
+
+// Listens at a location, then adds a bunch of grandchildren under a single child, but does it with merges.
+// NOTE[2015-07-14]: This test is still pretty slow, because [FWriteTree removeWriteId] ends up rebuilding the tree after
+// every ack.
+- (void) testDeepChildAddedPerfViaMerge1 {
+ if (!runPerfTests) return;
+
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode],
+ *childRef = [ref child:@"child"];
+
+ [ref observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ }];
+
+ NSDate *start = [NSDate date];
+ int COUNT = 250;
+ __block BOOL done = NO;
+ __block NSDate *finished = nil;
+ for(int i = 0; i < COUNT; i++) {
+ NSString *childName = [childRef childByAutoId].key;
+ [childRef updateChildValues:@{
+ childName: @"01234567890123456789012345678901234567890123456789"
+ } withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ if (i == (COUNT - 1)) {
+ finished = [NSDate date];
+ done = YES;
+ }
+ }];
+ }
+ [self waitUntil:^BOOL {
+ return done;
+ } timeout:300];
+
+ NSTimeInterval elapsed = [finished timeIntervalSinceDate:start];
+ NSLog(@"Elapsed: %f", elapsed);
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FDotInfo.h b/Example/Database/Tests/Integration/FDotInfo.h
new file mode 100644
index 0000000..73bd4c7
--- /dev/null
+++ b/Example/Database/Tests/Integration/FDotInfo.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 "FTestBase.h"
+
+@interface FDotInfo : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FDotInfo.m b/Example/Database/Tests/Integration/FDotInfo.m
new file mode 100644
index 0000000..0245dc5
--- /dev/null
+++ b/Example/Database/Tests/Integration/FDotInfo.m
@@ -0,0 +1,173 @@
+/*
+ * 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 "FDotInfo.h"
+#import "FTestHelpers.h"
+#import "FIRDatabaseConfig_Private.h"
+
+@implementation FDotInfo
+
+- (void) testCanGetReferenceToInfoNodes {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ [ref.root child:@".info"];
+ [ref.root child:@".info/foo"];
+}
+
+- (void) testCantWriteToInfo {
+ FIRDatabaseReference * ref = [[FTestHelpers getRandomNode].root child:@".info"];
+ XCTAssertThrows([ref setValue:@"hi"], @"Cannot write to path at /.info");
+ XCTAssertThrows([ref setValue:@"hi" andPriority:@5], @"Cannot write to path at /.info");
+ XCTAssertThrows([ref setPriority:@"hi"], @"Cannot write to path at /.info");
+ XCTAssertThrows([ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ }], @"Cannot write to path at /.info");
+ XCTAssertThrows([ref removeValue], @"Cannot write to path at /.info");
+ XCTAssertThrows([[ref child:@"test"] setValue:@"hi"], @"Cannot write to path at /.info");
+}
+
+- (void) testCanWatchInfoConnected {
+ FIRDatabaseReference * rootRef = [FTestHelpers getRandomNode].root;
+ __block BOOL done = NO;
+ [[rootRef child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([[snapshot value] boolValue]) {
+ done = YES;
+ }
+ }];
+ [self waitUntil:^{ return done; }];
+}
+
+- (void) testInfoConnectedGoesToFalseOnDisconnect {
+ FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"];
+ FIRDatabaseReference * rootRef = [[FIRDatabaseReference alloc] initWithConfig:cfg];
+ __block BOOL everConnected = NO;
+ __block NSMutableString *connectedHistory = [[NSMutableString alloc] init];
+ [[rootRef child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([[snapshot value] boolValue]) {
+ everConnected = YES;
+ }
+
+ if (everConnected) {
+ [connectedHistory appendString:([[snapshot value] boolValue] ? @"YES," : @"NO,")];
+ }
+ }];
+ [self waitUntil:^{ return everConnected; }];
+
+ [FRepoManager interrupt:cfg];
+ [FRepoManager resume:cfg];
+
+ [self waitUntil:^BOOL{
+ return [connectedHistory isEqualToString:@"YES,NO,YES,"];
+ }];
+
+ [FRepoManager interrupt:cfg];
+ [FRepoManager disposeRepos:cfg];
+}
+
+- (void) testInfoServerTimeOffset {
+ FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"];
+ FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithConfig:cfg];
+
+ // make sure childByAutoId works
+ [ref childByAutoId];
+
+ NSMutableArray* offsets = [[NSMutableArray alloc] init];
+
+ [[ref child:@".info/serverTimeOffset"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSLog(@"got value: %@", snapshot.value);
+ [offsets addObject:snapshot.value];
+ }];
+
+ WAIT_FOR(offsets.count == 1);
+
+ XCTAssertTrue([[offsets objectAtIndex:0] isKindOfClass:[NSNumber class]], @"Second element should be a number, in milliseconds");
+
+ // make sure childByAutoId still works
+ [ref childByAutoId];
+
+ [FRepoManager interrupt:cfg];
+ [FRepoManager disposeRepos:cfg];
+}
+
+- (void) testManualConnectionManagement {
+ FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"];
+ FIRDatabaseConfig *altCfg = [FIRDatabaseConfig configForName:@"alt-config"];
+
+ FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithConfig:cfg];
+ FIRDatabaseReference * refAlt = [[FIRDatabaseReference alloc] initWithConfig:altCfg];
+
+ // Wait until we're connected to both Firebases
+ __block BOOL ready = NO;
+ [[ref child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = [[snapshot value] boolValue];
+ }];
+ [self waitUntil:^{ return ready; }];
+ [[ref child:@".info/connected"] removeAllObservers];
+
+ ready = NO;
+ [[refAlt child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = [[snapshot value] boolValue];
+ }];
+ [self waitUntil:^{ return ready; }];
+ [[refAlt child:@".info/connected"] removeAllObservers];
+
+ [FIRDatabaseReference goOffline];
+
+ // Ensure we're disconnected from both Firebases
+ ready = NO;
+
+ [[ref child:@".info/connected"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertFalse([[snapshot value] boolValue], @".info/connected should be false");
+ ready = YES;
+ }];
+ [self waitUntil:^{ return ready; }];
+ ready = NO;
+ [[refAlt child:@".info/connected"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertFalse([[snapshot value] boolValue], @".info/connected should be false");
+ ready = YES;
+ }];
+ [self waitUntil:^{ return ready; }];
+
+ // Ensure that we don't automatically reconnect upon new Firebase creation
+ FIRDatabaseReference * refDup = [[FIRDatabaseReference alloc] initWithConfig:altCfg];
+ [[refDup child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([[snapshot value] boolValue]) {
+ XCTFail(@".info/connected should remain false");
+ }
+ }];
+
+ // Wait for 1.5 seconds to make sure connected remains false
+ [NSThread sleepForTimeInterval:1.5];
+ [[refDup child:@".info/connected"] removeAllObservers];
+
+ [FIRDatabaseReference goOnline];
+
+ // Ensure we're reconnected to both Firebases
+ ready = NO;
+ [[ref child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = [[snapshot value] boolValue];
+ }];
+ [self waitUntil:^{ return ready; }];
+ [[ref child:@".info/connected"] removeAllObservers];
+
+ ready = NO;
+ [[refAlt child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = [[snapshot value] boolValue];
+ }];
+ [self waitUntil:^{ return ready; }];
+ [[refAlt child:@".info/connected"] removeAllObservers];
+}
+@end
diff --git a/Example/Database/Tests/Integration/FEventTests.h b/Example/Database/Tests/Integration/FEventTests.h
new file mode 100644
index 0000000..8ea5eef
--- /dev/null
+++ b/Example/Database/Tests/Integration/FEventTests.h
@@ -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 <XCTest/XCTest.h>
+#import "FTestBase.h"
+
+@interface FEventTests : FTestBase {
+ BOOL rl;
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FEventTests.m b/Example/Database/Tests/Integration/FEventTests.m
new file mode 100644
index 0000000..8b11e9d
--- /dev/null
+++ b/Example/Database/Tests/Integration/FEventTests.m
@@ -0,0 +1,506 @@
+/*
+ * 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 "FEventTests.h"
+#import "FTestHelpers.h"
+#import "FTupleEventTypeString.h"
+#import "FEventTester.h"
+
+@implementation FEventTests
+
+
+
+- (void) testInvalidEventType {
+ FIRDatabaseReference * f = [FTestHelpers getRandomNode];
+ XCTAssertThrows([f observeEventType:-4 withBlock:^(FIRDataSnapshot *s) {}], @"Invalid event type properly throws an error");
+}
+
+- (void) testWriteLeafExpectValueChanged {
+
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writeNode = tuple.one;
+ FIRDatabaseReference * readNode = tuple.two;
+
+ __block BOOL done = NO;
+ [writeNode setValue:@1234 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+
+ [super snapWaiter:readNode withBlock:^(FIRDataSnapshot *s) {
+ XCTAssertEqualObjects([s value], @1234, @"Proper value in snapshot");
+ }];
+}
+
+- (void) testWRiteLeafNodeThenExpectValueEvent {
+ FIRDatabaseReference * writeNode = [FTestHelpers getRandomNode];
+ [writeNode setValue:@42];
+
+ [super snapWaiter:writeNode withBlock:^(FIRDataSnapshot *s) {
+ XCTAssertEqualObjects([s value], @42, @"Proper value in snapshot");
+ }];
+
+}
+
+- (void) testWriteLeafNodeThenExpectChildAddedEventThenValueEvent {
+
+ FIRDatabaseReference * writeNode = [FTestHelpers getRandomNode];
+
+ [[writeNode child:@"foo"] setValue:@878787];
+
+ NSArray* lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeChildAdded withString:@"foo"],
+ [[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:lookingFor];
+ [et wait];
+
+ [super snapWaiter:writeNode withBlock:^(FIRDataSnapshot *s) {
+ XCTAssertEqualObjects([[s childSnapshotForPath:@"foo"] value], @878787, @"Got proper value");
+ }];
+
+}
+
+- (void) testWriteTwoNestedLeafNodesChange {
+
+}
+
+- (void) testSetMultipleEventListenersOnSameNode {
+
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writeNode = tuple.one;
+ FIRDatabaseReference * readNode = tuple.two;
+
+ [writeNode setValue:@42];
+
+ // two write nodes
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeValue withString:nil] ]];
+ [et wait];
+
+ et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeValue withString:nil] ]];
+ [et wait];
+
+ // two read nodes
+ et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:readNode withEvent:FIRDataEventTypeValue withString:nil] ]];
+ [et wait];
+
+ et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:readNode withEvent:FIRDataEventTypeValue withString:nil] ]];
+ [et wait];
+
+}
+
+- (void) testUnsubscribeEventsAndConfirmThatEventsNoLongerFire {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ __block int numValueCB = 0;
+
+ FIRDatabaseHandle handle = [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ numValueCB = numValueCB + 1;
+ }];
+
+ // Set
+ for(int i = 0; i < 3; i++) {
+ [node setValue:[NSNumber numberWithInt:i]];
+ }
+
+ // bye
+ [node removeObserverWithHandle:handle];
+
+ // set again
+ for(int i = 10; i < 15; i++) {
+ [node setValue:[NSNumber numberWithInt:i]];
+ }
+
+ for(int i = 20; i < 25; i++) {
+ [node setValue:[NSNumber numberWithInt:i]];
+ }
+
+ // Should just be 3
+ [self waitUntil:^BOOL{
+ return numValueCB == 3;
+ }];
+}
+
+- (void) testCanWriteACompoundObjectAndGetMoreGranularEventsForIndividualChanges {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writeNode = tuple.one;
+ FIRDatabaseReference * readNode = tuple.two;
+
+ __block BOOL done = NO;
+ [writeNode setValue:@{@"a": @10, @"b": @20} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{ return done; }];
+
+ NSArray* lookingForW = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[writeNode child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[writeNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+
+ NSArray* lookingForR = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[readNode child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[readNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+
+ FEventTester* etW = [[FEventTester alloc] initFrom:self];
+ [etW addLookingFor:lookingForW];
+ [etW wait];
+
+ FEventTester* etR = [[FEventTester alloc] initFrom:self];
+ [etR addLookingFor:lookingForR];
+ [etR wait];
+
+ // Modify compound but just change one of them
+
+ lookingForW = @[[[FTupleEventTypeString alloc] initWithFirebase:[writeNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil] ];
+ lookingForR = @[[[FTupleEventTypeString alloc] initWithFirebase:[readNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil] ];
+
+ [etW addLookingFor:lookingForW];
+ [etR addLookingFor:lookingForR];
+
+ [writeNode setValue:@{@"a": @10, @"b": @30}];
+
+ [etW wait];
+ [etR wait];
+}
+
+
+- (void) testValueEventIsFiredForEmptyNode {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL valueFired = NO;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ XCTAssertTrue([[s value] isEqual:[NSNull null]], @"Value is properly nil");
+ valueFired = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return valueFired;
+ }];
+}
+
+- (void) testCorrectEventsRaisedWhenLeafTurnsIntoInternalNode {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ NSMutableString* eventString = [[NSMutableString alloc] init];
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ if ([s hasChildren]) {
+ [eventString appendString:@", got children"];
+ }
+ else {
+ [eventString appendFormat:@", value %@", [s value]];
+ }
+ }];
+
+ [node observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *s) {
+ [eventString appendFormat:@", child_added %@", [s key]];
+ }];
+
+ [node setValue:@42];
+ [node setValue:@{@"a": @2}];
+ [node setValue:@84];
+ __block BOOL done = NO;
+ [node setValue:nil withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+
+ XCTAssertEqualObjects(@", value 42, child_added a, got children, value 84, value <null>", eventString, @"Proper order seen");
+}
+
+- (void) testRegisteringCallbackMultipleTimesAndUnregistering {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ __block int changes = 0;
+
+ fbt_void_datasnapshot cb = ^(FIRDataSnapshot *snapshot) { changes = changes + 1; };
+
+ FIRDatabaseHandle handle1 = [node observeEventType:FIRDataEventTypeValue withBlock:cb];
+ FIRDatabaseHandle handle2 = [node observeEventType:FIRDataEventTypeValue withBlock:cb];
+ FIRDatabaseHandle handle3 = [node observeEventType:FIRDataEventTypeValue withBlock:cb];
+
+ __block BOOL done = NO;
+
+ [node setValue:@42 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+ done = NO;
+
+ XCTAssertTrue(changes == 3, @"Saw 3 callback events %d", changes);
+
+ [node removeObserverWithHandle:handle1];
+ [node setValue:@84 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+ done = NO;
+
+ XCTAssertTrue(changes == 5, @"Saw 5 callback events %d", changes);
+
+ [node removeObserverWithHandle:handle2];
+ [node setValue:@168 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+ done = NO;
+
+ XCTAssertTrue(changes == 6, @"Saw 6 callback events %d", changes);
+
+ [node removeObserverWithHandle:handle3];
+ [node setValue:@376 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+ done = NO;
+
+ XCTAssertTrue(changes == 6, @"Saw 6 callback events %d", changes);
+
+ NSLog(@"callbacks: %d", changes);
+
+}
+
+- (void) testUnregisteringTheSameCallbackTooManyTimesDoesNothing {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ fbt_void_datasnapshot cb = ^(FIRDataSnapshot *snapshot) { };
+
+ FIRDatabaseHandle handle1 = [node observeEventType:FIRDataEventTypeValue withBlock:cb];
+ [node removeObserverWithHandle:handle1];
+ [node removeObserverWithHandle:handle1];
+
+ XCTAssertTrue(YES, @"Properly reached end of test without throwing errors.");
+}
+
+- (void) testOnceValueFiresExactlyOnce {
+ FIRDatabaseReference * path = [FTestHelpers getRandomNode];
+ __block BOOL firstCall = YES;
+
+ [path observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(firstCall, @"Properly saw first call");
+ firstCall = NO;
+ XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw node value");
+ }];
+
+ [path setValue:@42];
+ [path setValue:@84];
+
+ __block BOOL done = NO;
+
+ [path setValue:nil withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+}
+
+- (void) testOnceChildAddedFiresExaclyOnce {
+ __block int badCount = 0;
+
+ // for(int i = 0; i < 100; i++) {
+
+ FIRDatabaseReference * path = [FTestHelpers getRandomNode];
+ __block BOOL firstCall = YES;
+
+ __block BOOL done = NO;
+
+
+ [path observeSingleEventOfType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(firstCall, @"Properly saw first call");
+ firstCall = NO;
+ XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw node value");
+ XCTAssertEqualObjects(@"foo", [snapshot key], @"Properly saw the first node");
+ if (![[snapshot value] isEqual:@42]) {
+ exit(-1);
+ badCount = badCount + 1;
+ }
+
+ done = YES;
+
+
+ }];
+
+ [[path child:@"foo"] setValue:@42];
+ [[path child:@"bar"] setValue:@84]; // XXX FIXME sometimes this event fires first
+ [[path child:@"foo"] setValue:@168];
+
+
+// [path setValue:nil withCompletionBlock:^(BOOL status) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+
+
+ // }
+
+ NSLog(@"BADCOUNT: %d", badCount);
+}
+
+- (void) testOnceValueFiresExacltyOnceEvenIfThereIsASetInsideCallback {
+ FIRDatabaseReference * path = [FTestHelpers getRandomNode];
+ __block BOOL firstCall = YES;
+ __block BOOL done = NO;
+
+ [path observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(firstCall, @"Properly saw first call");
+ if (firstCall) {
+ firstCall = NO;
+ XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw node value");
+ [path setValue:@43];
+ done = YES;
+ }
+ else {
+ XCTFail(@"Callback got called more than once.");
+ }
+ }];
+
+ [path setValue:@42];
+ [path setValue:@84];
+
+ [self waitUntil:^BOOL{ return done; }];
+}
+
+- (void) testOnceChildAddedFiresOnceEvenWithCompoundObject {
+ FIRDatabaseReference * path = [FTestHelpers getRandomNode];
+ __block BOOL firstCall = YES;
+
+ [path observeSingleEventOfType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(firstCall, @"Properly saw first call");
+ firstCall = NO;
+ XCTAssertEqualObjects(@84, [snapshot value], @"Properly saw node value");
+ XCTAssertEqualObjects(@"bar", [snapshot key], @"Properly saw the first node");
+ }];
+
+ [path setValue:@{@"foo": @42, @"bar": @84}];
+
+ __block BOOL done = NO;
+
+ [path setValue:nil withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+}
+
+- (void) testOnEmptyChildFires {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ __block BOOL done = NO;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ [[node child:@"test"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([[snapshot value] isEqual:[NSNull null]], @"Properly saw nil child node");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{ return done; }];
+}
+
+
+- (void) testOnEmptyChildEvenAfterParentIsSynched {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ __block BOOL parentDone = NO;
+ __block BOOL done = NO;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ parentDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return parentDone;
+ }];
+
+ [[node child:@"test"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([[snapshot value] isEqual:[NSNull null]], @"Child is properly nil");
+ done = YES;
+ }];
+
+ // This test really isn't in the same spirit as the JS test; we can't currently make sure that the test fires right away since the ON and callback are async
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ XCTAssertTrue(done, @"Done fired.");
+}
+
+- (void) testEventsAreRaisedChildRemovedChildAddedChildMoved {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSMutableArray* events = [[NSMutableArray alloc] init];
+
+ [node observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snap) {
+ [events addObject:[NSString stringWithFormat:@"added %@", [snap key]]];
+ }];
+
+ [node observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snap) {
+ [events addObject:[NSString stringWithFormat:@"removed %@", [snap key]]];
+ }];
+
+ [node observeEventType:FIRDataEventTypeChildMoved withBlock:^(FIRDataSnapshot *snap) {
+ [events addObject:[NSString stringWithFormat:@"moved %@", [snap key]]];
+ }];
+
+ __block BOOL done = NO;
+
+ [node setValue:@{
+ @"a": @{@".value": @1, @".priority": @0 },
+ @"b": @{@".value": @1, @".priority": @1 },
+ @"c": @{@".value": @1, @".priority": @2 },
+ @"d": @{@".value": @1, @".priority": @3 },
+ @"e": @{@".value": @1, @".priority": @4 },
+ @"f": @{@".value": @1, @".priority": @5 },
+ } withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [events removeAllObjects];
+
+ done = NO;
+
+ [node setValue:@{
+ @"a": @{@".value": @1, @".priority": @5 },
+ @"aa": @{@".value": @1, @".priority": @0 },
+ @"b": @{@".value": @1, @".priority": @1 },
+ @"bb": @{@".value": @1, @".priority": @2 },
+ @"d": @{@".value": @1, @".priority": @3 },
+ @"e": @{@".value": @1, @".priority": @6 },
+ }
+ withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }
+ ];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ XCTAssertEqualObjects(@"removed c, removed f, added aa, added bb, moved a, moved e", [events componentsJoinedByString:@", "], @"Got expected results");
+}
+
+- (void) testIntegerToDoubleConversions {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSMutableArray<NSString *>* events = [[NSMutableArray alloc] init];
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snap) {
+ [events addObject:[NSString stringWithFormat:@"value %@", [snap value]]];
+ }];
+
+ for(NSNumber *number in @[@1, @1.0, @1, @1.1]) {
+ [self waitForCompletionOf:node setValue:number];
+ }
+
+ XCTAssertEqualObjects(@"value 1, value 1.1", [events componentsJoinedByString:@", "],
+ @"Got expected results");
+
+}
+
+- (void) testEventsAreRaisedProperlyWithOnQueryLimits {
+ // xxx impl query
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FIRAuthTests.m b/Example/Database/Tests/Integration/FIRAuthTests.m
new file mode 100644
index 0000000..2c44580
--- /dev/null
+++ b/Example/Database/Tests/Integration/FIRAuthTests.m
@@ -0,0 +1,67 @@
+/*
+ * 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/XCTest.h>
+#import "FIRApp.h"
+#import "FTestHelpers.h"
+#import "FTestAuthTokenGenerator.h"
+#import "FIRTestAuthTokenProvider.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FTestBase.h"
+
+@interface FIRAuthTests : FTestBase
+
+@end
+
+@implementation FIRAuthTests
+
+- (void)setUp {
+ [super setUp];
+}
+
+- (void)tearDown {
+ [super tearDown];
+}
+
+- (void)testListensAndAuthRaceCondition {
+ [FIRDatabase setLoggingEnabled:YES];
+ id<FAuthTokenProvider> tokenProvider = [FAuthTokenProvider authTokenProviderForApp:[FIRApp defaultApp]];
+
+ FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:@"testWritesRestoredAfterAuth"];
+ config.authTokenProvider = tokenProvider;
+
+ FIRDatabaseReference *ref = [[[FIRDatabaseReference alloc] initWithConfig:config] childByAutoId];
+
+ __block BOOL done = NO;
+
+ [[[ref root] child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^void(
+ FIRDataSnapshot *snapshot) {
+ if ([snapshot.value boolValue]) {
+ // Start a listen before auth credentials are restored.
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+
+ }];
+
+ // subsequent writes should complete successfully.
+ [ref setValue:@42 withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ }
+ }];
+
+ WAIT_FOR(done);
+}
+@end
diff --git a/Example/Database/Tests/Integration/FIRDatabaseQueryTests.h b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.h
new file mode 100644
index 0000000..d6074ac
--- /dev/null
+++ b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.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 <Foundation/Foundation.h>
+#import "FTestBase.h"
+
+@interface FIRDatabaseQueryTests : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FIRDatabaseQueryTests.m b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.m
new file mode 100644
index 0000000..a5bff5a
--- /dev/null
+++ b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.m
@@ -0,0 +1,2780 @@
+/*
+ * 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 "FIRDatabaseQueryTests.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQuerySpec.h"
+#import "FTestExpectations.h"
+
+@implementation FIRDatabaseQueryTests
+
+- (void) testCanCreateBasicQueries {
+ // Just make sure none of these throw anything
+
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ [ref queryLimitedToFirst:10];
+ [ref queryLimitedToLast:10];
+
+ [[ref queryOrderedByKey] queryStartingAtValue:@"foo"];
+ [[ref queryOrderedByKey] queryEndingAtValue:@"foo"];
+ [[ref queryOrderedByKey] queryEqualToValue:@"foo"];
+
+ [[ref queryOrderedByChild:@"index"] queryStartingAtValue:@YES];
+ [[ref queryOrderedByChild:@"index"] queryStartingAtValue:@1];
+ [[ref queryOrderedByChild:@"index"] queryStartingAtValue:@"foo"];
+ [[ref queryOrderedByChild:@"index"] queryStartingAtValue:nil];
+ [[ref queryOrderedByChild:@"index"] queryEndingAtValue:@YES];
+ [[ref queryOrderedByChild:@"index"] queryEndingAtValue:@1];
+ [[ref queryOrderedByChild:@"index"] queryEndingAtValue:@"foo"];
+ [[ref queryOrderedByChild:@"index"] queryEndingAtValue:nil];
+ [[ref queryOrderedByChild:@"index"] queryEqualToValue:@YES];
+ [[ref queryOrderedByChild:@"index"] queryEqualToValue:@1];
+ [[ref queryOrderedByChild:@"index"] queryEqualToValue:@"foo"];
+ [[ref queryOrderedByChild:@"index"] queryEqualToValue:nil];
+
+ [[ref queryOrderedByPriority] queryStartingAtValue:@1];
+ [[ref queryOrderedByPriority] queryStartingAtValue:@"foo"];
+ [[ref queryOrderedByPriority] queryStartingAtValue:nil];
+ [[ref queryOrderedByPriority] queryEndingAtValue:@1];
+ [[ref queryOrderedByPriority] queryEndingAtValue:@"foo"];
+ [[ref queryOrderedByPriority] queryEndingAtValue:nil];
+ [[ref queryOrderedByPriority] queryEqualToValue:@1];
+ [[ref queryOrderedByPriority] queryEqualToValue:@"foo"];
+ [[ref queryOrderedByPriority] queryEqualToValue:nil];
+}
+
+- (void) testInvalidQueryParams {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ XCTAssertThrows([[ref queryLimitedToFirst:100] queryLimitedToFirst:100]);
+ XCTAssertThrows([[ref queryLimitedToFirst:100] queryLimitedToLast:100]);
+ XCTAssertThrows([[ref queryLimitedToLast:100] queryLimitedToFirst:100]);
+ XCTAssertThrows([[ref queryLimitedToLast:100] queryLimitedToLast:100]);
+ XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByPriority]);
+ XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByChild:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByValue]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByPriority]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByChild:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByValue]);
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByPriority]);
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByChild:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByValue]);
+ XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByPriority]);
+ XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByChild:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByValue]);
+ XCTAssertThrows([[ref queryStartingAtValue:@"foo"] queryStartingAtValue:@"foo"]);
+ XCTAssertThrows([[ref queryStartingAtValue:@"foo"] queryEqualToValue:@"foo"]);
+ XCTAssertThrows([[ref queryEndingAtValue:@"foo"] queryEndingAtValue:@"foo"]);
+ XCTAssertThrows([[ref queryEndingAtValue:@"foo"] queryEqualToValue:@"foo"]);
+ XCTAssertThrows([[ref queryEqualToValue:@"foo"] queryStartingAtValue:@"foo"]);
+ XCTAssertThrows([[ref queryEqualToValue:@"foo"] queryEndingAtValue:@"foo"]);
+ XCTAssertThrows([[ref queryEqualToValue:@"foo"] queryEqualToValue:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@"foo" childKey:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@"foo" childKey:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEqualToValue:@"foo" childKey:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@1 childKey:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@YES]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@1]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@YES]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:nil]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:nil]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEqualToValue:nil]);
+ XCTAssertThrows([[ref queryStartingAtValue:@"foo" childKey:@"foo"] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryEndingAtValue:@"foo" childKey:@"foo"] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryEqualToValue:@"foo" childKey:@"foo"] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryStartingAtValue:@1] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryStartingAtValue:@YES] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryEndingAtValue:@1] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryEndingAtValue:@YES] queryOrderedByKey]);
+ XCTAssertThrows([ref queryStartingAtValue:@[]]);
+ XCTAssertThrows([ref queryStartingAtValue:@{}]);
+ XCTAssertThrows([ref queryEndingAtValue:@[]]);
+ XCTAssertThrows([ref queryEndingAtValue:@{}]);
+ XCTAssertThrows([ref queryEqualToValue:@[]]);
+ XCTAssertThrows([ref queryEqualToValue:@{}]);
+
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByPriority], @"Cannot call orderBy multiple times");
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByPriority], @"Cannot call orderBy multiple times");
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByKey], @"Cannot call orderBy multiple times");
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByKey], @"Cannot call orderBy multiple times");
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByChild:@"foo"], @"Cannot call orderBy multiple times");
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByChild:@"foo"], @"Cannot call orderBy multiple times");
+
+ XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@"a" childKey:@"b"], @"Cannot specify starting child name when ordering by key.");
+ XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@"a" childKey:@"b"], @"Cannot specify ending child name when ordering by key.");
+ XCTAssertThrows([[ref queryOrderedByKey] queryEqualToValue:@"a" childKey:@"b"], @"Cannot specify equalTo child name when ordering by key.");
+
+ XCTAssertThrows([[ref queryOrderedByPriority] queryStartingAtValue:@YES], @"Can't pass booleans as start/end when using priority index.");
+ XCTAssertThrows([[ref queryOrderedByPriority] queryEndingAtValue:@NO], @"Can't pass booleans as start/end when using priority index.");
+ XCTAssertThrows([[ref queryOrderedByPriority] queryEqualToValue:@YES], @"Can't pass booleans as start/end when using priority index.");
+}
+
+- (void) testLimitRanges
+{
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ XCTAssertThrows([ref queryLimitedToLast:0], @"Can't pass zero as limit");
+ XCTAssertThrows([ref queryLimitedToFirst:0], @"Can't pass zero as limit");
+ XCTAssertThrows([ref queryLimitedToLast:0], @"Can't pass zero as limit");
+ uint64_t MAX_ALLOWED_VALUE = (1l << 31) - 1;
+ [ref queryLimitedToFirst:MAX_ALLOWED_VALUE];
+ [ref queryLimitedToLast:MAX_ALLOWED_VALUE];
+ XCTAssertThrows([ref queryLimitedToFirst:(MAX_ALLOWED_VALUE+1)], @"Can't pass limits that don't fit into 32 bit signed integer range");
+ XCTAssertThrows([ref queryLimitedToLast:(MAX_ALLOWED_VALUE+1)], @"Can't pass limits that don't fit into 32 bit signed integer range");
+}
+
+- (void) testInvalidKeys {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSArray* badKeys = @[ @".test", @"test.", @"fo$o", @"[what", @"ever]", @"ha#sh", @"/thing", @"th/ing", @"thing/"];
+
+ for (NSString* badKey in badKeys) {
+ XCTAssertThrows([[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:badKey], @"Setting bad key");
+ XCTAssertThrows([[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:badKey], @"Setting bad key");
+ }
+}
+
+- (void) testOffCanBeCalledOnDefault {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL called = NO;
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (called) {
+ XCTFail(@"Should not be called twice");
+ } else {
+ called = YES;
+ }
+ }];
+
+ [ref setValue:@{@"a": @5, @"b": @6}];
+
+ [self waitUntil:^BOOL{
+ return called;
+ }];
+
+ called = NO;
+
+ [ref removeAllObservers];
+
+ __block BOOL complete = NO;
+ [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ complete = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return complete;
+ }];
+
+ XCTAssertFalse(called, @"Should not have been called again");
+}
+
+- (void) testOffCanBeCalledOnHandle {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL called = NO;
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ FIRDatabaseHandle handle = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (called) {
+ XCTFail(@"Should not be called twice");
+ } else {
+ called = YES;
+ }
+ }];
+
+ [ref setValue:@{@"a": @5, @"b": @6}];
+
+ [self waitUntil:^BOOL{
+ return called;
+ }];
+
+ called = NO;
+
+ [ref removeObserverWithHandle:handle];
+
+ __block BOOL complete = NO;
+ [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ complete = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return complete;
+ }];
+
+ XCTAssertFalse(called, @"Should not have been called again");
+}
+
+- (void) testOffCanBeCalledOnSpecificQuery {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL called = NO;
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ FIRDatabaseHandle handle = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (called) {
+ XCTFail(@"Should not be called twice");
+ } else {
+ called = YES;
+ }
+ }];
+
+ [ref setValue:@{@"a": @5, @"b": @6}];
+
+ [self waitUntil:^BOOL{
+ return called;
+ }];
+
+ called = NO;
+
+ [query removeObserverWithHandle:handle];
+
+ __block BOOL complete = NO;
+ [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ complete = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return complete;
+ }];
+
+ XCTAssertFalse(called, @"Should not have been called again");
+}
+
+- (void) testOffCanBeCalledOnMultipleQueries {
+ FIRDatabaseQuery *query = [[FTestHelpers getRandomNode] queryLimitedToFirst:10];
+ FIRDatabaseHandle handle1 = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ FIRDatabaseHandle handle2 = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ [query removeObserverWithHandle:handle1];
+ [query removeObserverWithHandle:handle2];
+}
+
+- (void) testOffCanBeCalledWithoutHandle {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL called1 = NO;
+ __block BOOL called2 = NO;
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ called1 = YES;
+ }];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ called2 = YES;
+ }];
+
+ [ref setValue:@{@"a": @5, @"b": @6}];
+
+ [self waitUntil:^BOOL{
+ return called1 && called2;
+ }];
+
+ called1 = NO;
+ called2 = NO;
+
+ [ref removeAllObservers];
+
+ __block BOOL complete = NO;
+ [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ complete = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return complete;
+ }];
+
+ XCTAssertFalse(called1 || called2, @"Should not have called either callback");
+}
+
+- (void) testEnsureOnly5ItemsAreKept {
+ __block FIRDataSnapshot * snap = nil;
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ __block int count = 0;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ count++;
+ }];
+
+ [ref setValue:nil];
+ for (int i = 0; i < 10; ++i) {
+ [[ref childByAutoId] setValue:[NSNumber numberWithInt:i]];
+ }
+
+ [self waitUntil:^BOOL{
+ // The initial set triggers the callback, so we need to wait for 11 events
+ return count == 11;
+ }];
+
+ count = 5;
+ for (FIRDataSnapshot * snapshot in snap.children) {
+ NSNumber* num = [snapshot value];
+ NSNumber* current = [NSNumber numberWithInt:count];
+ XCTAssertTrue([num isEqualToNumber:current], @"Expect children in order");
+ count++;
+ }
+
+ XCTAssertTrue(count == 10, @"Expected 5 children");
+}
+
+- (void) testOnlyLast5SentFromServer {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block int count = 0;
+
+ [ref setValue:nil];
+
+ for (int i = 0; i < 10; ++i) {
+ [[ref childByAutoId] setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ count++;
+ }];
+ }
+
+ [self waitUntil:^BOOL{
+ return count == 10;
+ }];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ count = 5;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ NSNumber *num = [child value];
+ NSNumber *current = [NSNumber numberWithInt:count];
+ XCTAssertTrue([num isEqualToNumber:current], @"Expect children to be in order");
+ count++;
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 10;
+ }];
+}
+
+- (void) testVariousLimits {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[ref queryLimitedToLast:1] withExpectation:@{@"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:1] withExpectation:@{@"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:2] withExpectation:@{@"b": @2, @"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:3] withExpectation:@{@"a": @1, @"b": @2, @"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:4] withExpectation:@{@"a": @1, @"b": @2, @"c": @3}];
+
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [expectations validate];
+}
+
+- (void) testSetLimitsWithStartAt {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1] withExpectation:@{@"a": @1}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"c"] queryLimitedToFirst:1] withExpectation:@{@"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] queryLimitedToFirst:1] withExpectation:@{@"b": @2}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] queryLimitedToFirst:2] withExpectation:@{@"b": @2, @"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] queryLimitedToFirst:3] withExpectation:@{@"b": @2, @"c": @3}];
+
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [expectations validate];
+}
+
+- (void) testLimitsAndStartAtWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1] withExpectation:@{@"a": @1}];
+
+ /*params = [[FQueryParams alloc] init];
+ params = [params setStartPriority:nil andName:@"c"];
+ params = [params limitTo:1];
+ [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"c": @3}];
+
+ params = [[FQueryParams alloc] init];
+ params = [params setStartPriority:nil andName:@"b"];
+ params = [params limitTo:1];
+ [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"b": @2}];
+
+ params = [[FQueryParams alloc] init];
+ params = [params setStartPriority:nil andName:@"b"];
+ params = [params limitTo:2];
+ [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"b": @2, @"c": @3}];
+
+ params = [[FQueryParams alloc] init];
+ params = [params setStartPriority:nil andName:@"b"];
+ params = [params limitTo:3];
+ [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"b": @2, @"c": @3}];*/
+
+ [self waitUntil:^BOOL{
+ return expectations.isReady;
+ }];
+ [expectations validate];
+ [ref removeAllObservers];
+}
+
+- (void) testChildEventsAreFiredWhenLimitIsHit {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"d"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ expected = @[@"d"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add d");
+ [ref removeAllObservers];
+}
+
+- (void) testChildEventsAreFiredWhenLimitIsHitWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:2];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return [added count] == 2;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"d"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ expected = @[@"d"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add d");
+ [ref removeAllObservers];
+}
+
+- (void) testChildEventsAreFiredWhenLimitIsHitWithStart {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"a", @"b"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"aa"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ expected = @[@"aa"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add aa");
+ [ref removeAllObservers];
+}
+
+- (void) testChildEventsAreFiredWhenLimitIsHitWithStartAndServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2];
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return [added count] == 2;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"a", @"b"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"aa"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ expected = @[@"aa"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add aa");
+ [ref removeAllObservers];
+}
+
+- (void) testStartAndLimitWithIncompleteWindow {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2];
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready && [added count] >= 1;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have one item");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Expected to remove nothing");
+ expected = @[@"b"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add b");
+ [ref removeAllObservers];
+}
+
+- (void) testStartAndLimitWithIncompleteWindowAndServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return [added count] == 1;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have one item");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Expected to remove nothing");
+ expected = @[@"b"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add b");
+ [ref removeAllObservers];
+}
+
+- (void) testChildEventsFiredWhenItemDeleted {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:2];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready && [added count] >= 1;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have one item");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ expected = @[@"a"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add a");
+ [ref removeAllObservers];
+}
+
+-(void) testChildEventsAreFiredWhenItemDeletedAtServer {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:2];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return [added count] == 2;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertEqualObjects(removed, (@[@"b"]), @"Expected to remove b");
+ XCTAssertEqualObjects(added, (@[@"a"]), @"Expected to add a");
+ [ref removeAllObservers];
+}
+
+- (void) testRemoveFiredWhenItemDeleted {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:2];
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready && [added count] >= 1;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have one item");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ XCTAssertTrue([added count] == 0, @"Expected to add nothing");
+ [ref removeAllObservers];
+}
+
+-(void) testRemoveFiredWhenItemDeletedAtServer {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:2];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return [added count] == 2;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ XCTAssertTrue([added count] == 0, @"Expected to add nothing");
+ [ref removeAllObservers];
+}
+
+- (void) testStartAtPriorityAndEndAtPriorityWork {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"y"] withExpectation:@{@"b": @2, @"c": @3, @"d": @4}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"w"] withExpectation:@{@"d": @4}];
+
+ __block id nullSnap = @"dummy";
+ [[[[ref queryOrderedByPriority] queryStartingAtValue:@"a"] queryEndingAtValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @"z"},
+ @"b": @{@".value": @2, @".priority": @"y"},
+ @"c": @{@".value": @3, @".priority": @"x"},
+ @"d": @{@".value": @4, @".priority": @"w"}
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testStartAtPriorityAndEndAtPriorityWorkWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @"z"},
+ @"b": @{@".value": @2, @".priority": @"y"},
+ @"c": @{@".value": @3, @".priority": @"x"},
+ @"d": @{@".value": @4, @".priority": @"w"}
+ } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"y"] withExpectation:@{@"b": @2, @"c": @3, @"d": @4}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"w"] withExpectation:@{@"d": @4}];
+
+ __block id nullSnap = @"dummy";
+ [[[[ref queryOrderedByPriority] queryStartingAtValue:@"a"] queryEndingAtValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testStartAtAndEndAtPriorityAndNameWork {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"a"] queryEndingAtValue:@2 childKey:@"d"];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"b"] queryEndingAtValue:@2 childKey:@"c"];
+ [expectations addQuery:query withExpectation:@{@"b": @2, @"c": @3}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2];
+ [expectations addQuery:query withExpectation:@{@"c": @3, @"d": @4}];
+
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @1},
+ @"b": @{@".value": @2, @".priority": @1},
+ @"c": @{@".value": @3, @".priority": @2},
+ @"d": @{@".value": @4, @".priority": @2}
+ }];
+
+ WAIT_FOR(expectations.isReady);
+
+ [expectations validate];
+}
+
+- (void) testStartAtAndEndAtPriorityAndNameWorkWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block BOOL ready = NO;
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @1},
+ @"b": @{@".value": @2, @".priority": @1},
+ @"c": @{@".value": @3, @".priority": @2},
+ @"d": @{@".value": @4, @".priority": @2}
+ } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"a"] queryEndingAtValue:@2 childKey:@"d"];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"b"] queryEndingAtValue:@2 childKey:@"c"];
+ [expectations addQuery:query withExpectation:@{@"b": @2, @"c": @3}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2];
+ [expectations addQuery:query withExpectation:@{@"c": @3, @"d": @4}];
+
+ WAIT_FOR(expectations.isReady);
+
+ [expectations validate];
+}
+
+- (void) testStartAtAndEndAtPriorityAndNameWork2 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2 childKey:@"b"];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"d"] queryEndingAtValue:@2 childKey:@"a"];
+ [expectations addQuery:query withExpectation:@{@"d": @4, @"a": @1}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"e"] queryEndingAtValue:@2];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2}];
+
+ [ref setValue:@{
+ @"c": @{@".value": @3, @".priority": @1},
+ @"d": @{@".value": @4, @".priority": @1},
+ @"a": @{@".value": @1, @".priority": @2},
+ @"b": @{@".value": @2, @".priority": @2}
+ }];
+
+ WAIT_FOR(expectations.isReady);
+
+ [expectations validate];
+}
+
+- (void) testStartAtAndEndAtPriorityAndNameWorkWithServerData2 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block BOOL ready = NO;
+ [ref setValue:@{
+ @"c": @{@".value": @3, @".priority": @1},
+ @"d": @{@".value": @4, @".priority": @1},
+ @"a": @{@".value": @1, @".priority": @2},
+ @"b": @{@".value": @2, @".priority": @2}
+ } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2 childKey:@"b"];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"d"] queryEndingAtValue:@2 childKey:@"a"];
+ [expectations addQuery:query withExpectation:@{@"d": @4, @"a": @1}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"e"] queryEndingAtValue:@2];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2}];
+
+ WAIT_FOR(expectations.isReady);
+
+ [expectations validate];
+}
+
+- (void) testEqualToPriorityWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[ref queryOrderedByPriority] queryEqualToValue:@"w"] withExpectation:@{@"d": @4}];
+
+ __block id nullSnap = @"dummy";
+ [[[ref queryOrderedByPriority] queryEqualToValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @"z"},
+ @"b": @{@".value": @2, @".priority": @"y"},
+ @"c": @{@".value": @3, @".priority": @"x"},
+ @"d": @{@".value": @4, @".priority": @"w"}
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testEqualToPriorityWorksWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @"z"},
+ @"b": @{@".value": @2, @".priority": @"y"},
+ @"c": @{@".value": @3, @".priority": @"x"},
+ @"d": @{@".value": @4, @".priority": @"w"}
+ } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[ref queryOrderedByPriority] queryEqualToValue:@"w"] withExpectation:@{@"d": @4}];
+
+ __block id nullSnap = @"dummy";
+ [[[ref queryOrderedByPriority] queryEqualToValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testEqualToPriorityAndNameWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[ref queryOrderedByPriority] queryEqualToValue:@1 childKey:@"a"];
+ [expectations addQuery:query withExpectation:@{@"a": @1}];
+
+ __block id nullSnap = @"dummy";
+ [[[ref queryOrderedByPriority] queryEqualToValue:@"1" childKey:@"z"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @1},
+ @"b": @{@".value": @2, @".priority": @1},
+ @"c": @{@".value": @3, @".priority": @2},
+ @"d": @{@".value": @4, @".priority": @2}
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testEqualToPriorityAndNameWorksWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block BOOL ready = NO;
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @1},
+ @"b": @{@".value": @2, @".priority": @1},
+ @"c": @{@".value": @3, @".priority": @2},
+ @"d": @{@".value": @4, @".priority": @2}
+ } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[ref queryOrderedByPriority] queryEqualToValue:@1 childKey:@"a"];
+ [expectations addQuery:query withExpectation:@{@"a": @1}];
+
+ __block id nullSnap = @"dummy";
+ [[[ref queryOrderedByPriority] queryEqualToValue:@"1" childKey:@"z"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testPrevNameWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [added addObject:snapshot.key];
+ if (prevName) {
+ [added addObject:prevName];
+ } else {
+ [added addObject:@"null"];
+ }
+
+ }];
+
+ [[ref child:@"a"] setValue:@1];
+ [self waitUntil:^BOOL{
+ NSArray* expected = @[@"a", @"null"];
+ return [added isEqualToArray:expected];
+ }];
+
+ [added removeAllObjects];
+
+ [[ref child:@"c"] setValue:@3];
+ [self waitUntil:^BOOL{
+ NSArray* expected = @[@"c", @"a"];
+ return [added isEqualToArray:expected];
+ }];
+
+ [added removeAllObjects];
+
+ [[ref child:@"b"] setValue:@2];
+ [self waitUntil:^BOOL{
+ NSArray* expected = @[@"b", @"null"];
+ return [added isEqualToArray:expected];
+ }];
+
+ [added removeAllObjects];
+
+ [[ref child:@"d"] setValue:@3];
+ [self waitUntil:^BOOL{
+ NSArray* expected = @[@"d", @"c"];
+ return [added isEqualToArray:expected];
+ }];
+}
+
+// Dropping some of the server data tests here, around prevName. They don't really test anything new, and mostly don't even test server data
+
+- (void) testPrevNameWorksWithMoves {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* moved = [[NSMutableArray alloc] init];
+
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [moved addObject:snapshot.key];
+ if (prevName) {
+ [moved addObject:prevName];
+ } else {
+ [moved addObject:@"null"];
+ }
+ }];
+
+ [ref setValue:@{
+ @"a": @{@".value": @"a", @".priority": @10},
+ @"b": @{@".value": @"b", @".priority": @20},
+ @"c": @{@".value": @"c", @".priority": @30},
+ @"d": @{@".value": @"d", @".priority": @40}
+ }];
+
+ __block BOOL ready = NO;
+ [[ref child:@"c"] setPriority:@50 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expected = @[@"c", @"d"];
+ XCTAssertTrue([moved isEqualToArray:expected], @"Expected changed node and prevChild");
+
+ [moved removeAllObjects];
+ ready = NO;
+ [[ref child:@"c"] setPriority:@35 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"c", @"null"];
+ XCTAssertTrue([moved isEqualToArray:expected], @"Expected changed node and prevChild");
+
+ [moved removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] setPriority:@33 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[];
+ XCTAssertTrue([moved isEqualToArray:expected], @"Expected changed node and prevChild to be empty");
+}
+
+- (void) testLocalEvents {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* events = [[NSMutableArray alloc] init];
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *eventString = [NSString stringWithFormat:@"%@ added", [snapshot value]];
+ [events addObject:eventString];
+ }];
+
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *eventString = [NSString stringWithFormat:@"%@ removed", [snapshot value]];
+ [events addObject:eventString];
+ }];
+
+ __block BOOL ready = NO;
+ for (int i = 0; i < 5; ++i) {
+ [[ref childByAutoId] setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ if (i == 4) {
+ ready = YES;
+ }
+ }];
+ }
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expected = @[@"0 added", @"1 added", @"0 removed", @"2 added", @"1 removed", @"3 added", @"2 removed", @"4 added"];
+ XCTAssertTrue([events isEqualToArray:expected], @"Expecting window to stay at two nodes");
+}
+
+- (void) testRemoteEvents {
+ FTupleFirebase* pair = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = pair.one;
+ FIRDatabaseReference * reader = pair.two;
+
+ NSMutableArray* events = [[NSMutableArray alloc] init];
+
+ [[reader queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *eventString = [NSString stringWithFormat:@"%@ added", [snapshot value]];
+ [events addObject:eventString];
+ }];
+
+ [[reader queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *oldEventString = [NSString stringWithFormat:@"%@ added", [snapshot value]];
+ [events removeObject:oldEventString];
+ }];
+
+ for (int i = 0; i < 5; ++i) {
+ [[writer childByAutoId] setValue:[NSNumber numberWithInt:i]];
+ }
+
+ NSArray* expected = @[@"3 added", @"4 added"];
+ [self waitUntil:^BOOL{
+ return [events isEqualToArray:expected];
+ }];
+}
+
+- (void) testLimitOnEmptyNodeFiresValue {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testFilteringToNullPriorities {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ // Note: cannot set nil in a dictionary, just leave out priority
+ [ref setValue:@{
+ @"a": @0,
+ @"b": @1,
+ @"c": @{@".priority": @2, @".value": @2},
+ @"d": @{@".priority": @3, @".value": @3},
+ @"e": @{@".priority": @"hi", @".value": @4}
+ }];
+
+ __block BOOL ready = NO;
+ [[[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryEndingAtValue:nil] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *expected = @{@"a" : @0, @"b" : @1};
+ NSDictionary *val = [snapshot value];
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Expected only null priority keys");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testNullPrioritiesIncludedInEndAt {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ // Note: cannot set nil in a dictionary, just leave out priority
+ [ref setValue:@{
+ @"a": @0,
+ @"b": @1,
+ @"c": @{@".priority": @2, @".value": @2},
+ @"d": @{@".priority": @3, @".value": @3},
+ @"e": @{@".priority": @"hi", @".value": @4}
+ }];
+
+ __block BOOL ready = NO;
+ [[[ref queryOrderedByPriority] queryEndingAtValue:@2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *expected = @{@"a" : @0, @"b" : @1, @"c" : @2};
+ NSDictionary *val = [snapshot value];
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Expected up to priority 2");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (NSSet *) dumpListensForRef:(FIRDatabaseReference *)ref {
+ NSMutableSet* dumpPieces = [[NSMutableSet alloc] init];
+ NSDictionary* listens = [ref.repo dumpListens];
+
+ FPath* nodePath = ref.path;
+ [listens enumerateKeysAndObjectsUsingBlock:^(FQuerySpec *spec, id obj, BOOL *stop) {
+ if ([nodePath contains:spec.path]) {
+ FPath *relative = [FPath relativePathFrom:nodePath to:spec.path];
+ [dumpPieces addObject:[[FQuerySpec alloc] initWithPath:relative params:spec.params]];
+ }
+ }];
+
+ return dumpPieces;
+}
+
+- (NSSet *) expectDefaultListenerAtPath:(FPath *)path {
+ return [self expectParams:[FQueryParams defaultInstance] atPath:path];
+}
+
+- (NSSet *) expectParamssetValue:(NSSet *)paramsSet atPath:(FPath *)path {
+ NSMutableSet *all = [NSMutableSet set];
+ [paramsSet enumerateObjectsUsingBlock:^(FQueryParams *params, BOOL *stop) {
+ [all addObject:[[FQuerySpec alloc] initWithPath:path params:params]];
+ }];
+ return all;
+}
+
+- (NSSet *) expectParams:(FQueryParams *)params atPath:(FPath *)path {
+ return [self expectParamssetValue:[NSSet setWithObject:params] atPath:path];
+}
+
+- (void) testDedupesListensOnChild {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block NSSet* listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No Listens yet");
+
+ [[ref child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ __block BOOL ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [NSSet setWithObject:[FQuerySpec defaultQueryAtPath:PATH(@"a")]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected child listener");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [NSSet setWithObject:[FQuerySpec defaultQueryAtPath:PATH(@"")]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected parent listener");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [NSSet setWithObject:[FQuerySpec defaultQueryAtPath:PATH(@"a")]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Child listener should be back");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [[ref child:@"a"] removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No more listeners");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testDedupeListensOnGrandchild {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block NSSet* listens;
+ __block BOOL ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No Listens yet");
+ ready = YES;
+ });
+ WAIT_FOR(ready);
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected one listener");
+ ready = YES;
+ });
+ WAIT_FOR(ready);
+
+ [[ref child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected parent listener to override");
+ ready = YES;
+ });
+ WAIT_FOR(ready);
+
+ [ref removeAllObservers];
+ [[ref child:@"a/aa"] removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No more listeners");
+ ready = YES;
+ });
+ WAIT_FOR(ready);
+}
+
+- (void) testListenOnGrandparentOfTwoChildren {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block NSSet* listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No Listens yet");
+
+ [[ref child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ __block BOOL ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/aa"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected grandchild");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [[ref child:@"a/bb"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expecteda = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/aa"]];
+ NSSet* expectedb = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/bb"]];
+ NSMutableSet* expected = [NSMutableSet setWithSet:expecteda];
+ [expected unionSet:expectedb];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected two grandchildren");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected parent listener to override");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expecteda = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/aa"]];
+ NSSet* expectedb = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/bb"]];
+ NSMutableSet* expected = [NSMutableSet setWithSet:expecteda];
+ [expected unionSet:expectedb];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected grandchild listeners to return");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [[ref child:@"a/aa"] removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/bb"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected one listener");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [[ref child:@"a/bb"] removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No more listeners");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testDedupingMultipleListenQueries {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block NSSet* listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No Listens yet");
+
+ __block BOOL ready = NO;
+ FIRDatabaseQuery * aLim1 = [[ref child:@"a"] queryLimitedToLast:1];
+ FIRDatabaseHandle handle1 = [aLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams = [[FQueryParams alloc] init];
+ expectedParams = [expectedParams limitTo:1];
+ NSSet* expected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Single query");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ FIRDatabaseQuery * rootLim1 = [ref queryLimitedToLast:1];
+ FIRDatabaseHandle handle2 = [rootLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams = [[FQueryParams alloc] init];
+ expectedParams = [expectedParams limitTo:1];
+ NSSet* rootExpected = [self expectParams:expectedParams atPath:[FPath empty]];
+ NSSet* childExpected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]];
+ NSMutableSet* expected = [NSMutableSet setWithSet:rootExpected];
+ [expected unionSet:childExpected];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Two queries");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ FIRDatabaseQuery * aLim5 = [[ref child:@"a"] queryLimitedToLast:5];
+ FIRDatabaseHandle handle3 = [aLim5 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams1 = [[FQueryParams alloc] init];
+ expectedParams1 = [expectedParams1 limitTo:1];
+ NSSet* rootExpected = [self expectParams:expectedParams1 atPath:[FPath empty]];
+
+ FQueryParams* expectedParams2 = [[FQueryParams alloc] init];
+ expectedParams2 = [expectedParams2 limitTo:5];
+ NSSet* childExpected = [self expectParamssetValue:[NSSet setWithObjects:expectedParams1, expectedParams2, nil] atPath:[FPath pathWithString:@"/a"]];
+ NSMutableSet* expected = [NSMutableSet setWithSet:childExpected];
+ [expected unionSet:rootExpected];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Three queries");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref removeObserverWithHandle:handle2];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams1 = [[FQueryParams alloc] init];
+ expectedParams1 = [expectedParams1 limitTo:1];
+ FQueryParams* expectedParams2 = [[FQueryParams alloc] init];
+ expectedParams2= [expectedParams2 limitTo:5];
+ NSSet* expected = [self expectParamssetValue:[NSSet setWithObjects:expectedParams1, expectedParams2, nil] atPath:[FPath pathWithString:@"/a"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Two queries");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [aLim1 removeObserverWithHandle:handle1];
+ [aLim5 removeObserverWithHandle:handle3];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No more listeners");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testListenOnParentOfQueriedChildren {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block NSSet* listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No Listens yet");
+
+ __block BOOL ready = NO;
+ FIRDatabaseQuery * aLim1 = [[ref child:@"a"] queryLimitedToLast:1];
+ FIRDatabaseHandle handle1 = [aLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams = [[FQueryParams alloc] init];
+ expectedParams = [expectedParams limitTo:1];
+ NSSet* expected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Single query");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ FIRDatabaseQuery * bLim1 = [[ref child:@"b"] queryLimitedToLast:1];
+ FIRDatabaseHandle handle2 = [bLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams = [[FQueryParams alloc] init];
+ expectedParams = [expectedParams limitTo:1];
+ NSSet* expecteda = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]];
+ NSSet* expectedb = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/b"]];
+ NSMutableSet* expected = [NSMutableSet setWithSet:expecteda];
+ [expected unionSet:expectedb];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Two queries");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ FIRDatabaseHandle handle3 = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Parent should override");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ // remove in slightly random order
+ [aLim1 removeObserverWithHandle:handle1];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Parent should override");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref removeObserverWithHandle:handle3];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams = [[FQueryParams alloc] init];
+ expectedParams = [expectedParams limitTo:1];
+ NSSet* expected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/b"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Single query");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [bLim1 removeObserverWithHandle:handle2];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No more listeners");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+-(void) testLimitWithMixOfNullAndNonNullPriorities {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* children = [[NSMutableArray alloc] init];
+
+ [[ref queryLimitedToLast:5] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [children addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ NSDictionary* toSet = @{
+ @"Vikrum": @{@".priority": @1000, @"score": @1000, @"name": @"Vikrum"},
+ @"Mike": @{@".priority": @500, @"score": @500, @"name": @"Mike"},
+ @"Andrew": @{@".priority": @50, @"score": @50, @"name": @"Andrew"},
+ @"James": @{@".priority": @7, @"score": @7, @"name": @"James"},
+ @"Sally": @{@".priority": @-7, @"score": @-7, @"name": @"Sally"},
+ @"Fred": @{@"score": @0, @"name": @"Fred"}
+ };
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expected = @[@"Sally", @"James", @"Andrew", @"Mike", @"Vikrum"];
+ XCTAssertTrue([children isEqualToArray:expected], @"Null priority should be left out");
+
+}
+
+-(void) testLimitWithMixOfNullAndNonNullPrioritiesOnServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ NSDictionary* toSet = @{
+ @"Vikrum": @{@".priority": @1000, @"score": @1000, @"name": @"Vikrum"},
+ @"Mike": @{@".priority": @500, @"score": @500, @"name": @"Mike"},
+ @"Andrew": @{@".priority": @50, @"score": @50, @"name": @"Andrew"},
+ @"James": @{@".priority": @7, @"score": @7, @"name": @"James"},
+ @"Sally": @{@".priority": @-7, @"score": @-7, @"name": @"Sally"},
+ @"Fred": @{@"score": @0, @"name": @"Fred"}
+ };
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ __block int count = 0;
+ NSMutableArray* children = [[NSMutableArray alloc] init];
+
+ [[ref queryLimitedToLast:5] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [children addObject:[snapshot key]];
+ count++;
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 5;
+ }];
+
+
+ NSArray* expected = @[@"Sally", @"James", @"Andrew", @"Mike", @"Vikrum"];
+ XCTAssertTrue([children isEqualToArray:expected], @"Null priority should be left out");
+
+}
+
+// Skipping context tests. Context is not implemented on iOS
+
+/* DISABLING for now, since I'm not 100% sure what the right behavior is.
+ Perhaps a merge at /foo should shadow server updates at /foo instead of
+ just the modified children? Not sure.
+- (void) testHandleUpdateThatDeletesEntireWindow {
+ Firebase* ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* snaps = [[NSMutableArray alloc] init];
+
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ if (val == nil) {
+ [snaps addObject:[NSNull null]];
+ } else {
+ [snaps addObject:val];
+ }
+ }];
+
+ NSDictionary* toSet = @{
+ @"a": @{@".priority": @1, @".value": @1},
+ @"b": @{@".priority": @2, @".value": @2},
+ @"c": @{@".priority": @3, @".value": @3}
+ };
+
+ [ref setValue:toSet];
+
+ __block BOOL ready = NO;
+ toSet = @{@"b": [NSNull null], @"c": [NSNull null]};
+ [ref updateChildValues:toSet withCompletionBlock:^(NSError* err, Firebase* ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expected = @[@{@"b": @2, @"c": @3}, [NSNull null], @{@"a": @1}];
+ STAssertTrue([snaps isEqualToArray:expected], @"Expected %@ to equal %@", snaps, expected);
+}
+*/
+
+- (void) testHandlesAnOutOfViewQueryOnAChild {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSDictionary* parent = nil;
+ [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ parent = [snapshot value];
+ }];
+
+ __block NSNumber* child = nil;
+ [[ref child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ child = [snapshot value];
+ }];
+
+ __block BOOL ready = NO;
+ NSDictionary* toSet = @{@"a": @1, @"b": @2};
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSDictionary* parentExpected = @{@"b": @2};
+ NSNumber* childExpected = [NSNumber numberWithInt:1];
+ XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element");
+ XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a");
+
+ ready = NO;
+ [ref updateChildValues:@{@"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ parentExpected = @{@"c": @3};
+ XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element");
+ XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a");
+}
+
+- (void) testHandlesAChildQueryGoingOutOfViewOfTheParent {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSDictionary* parent = nil;
+ [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ parent = [snapshot value];
+ }];
+
+ __block NSNumber* child = nil;
+ [[ref child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ child = [snapshot value];
+ }];
+
+ __block BOOL ready = NO;
+ NSDictionary* toSet = @{@"a": @1};
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ NSDictionary* parentExpected = @{@"a": @1};
+ NSNumber* childExpected = [NSNumber numberWithInt:1];
+ XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element");
+ XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a");
+
+ ready = NO;
+ [[ref child:@"b"] setValue:@2 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ parentExpected = @{@"b": @2};
+ XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element");
+ XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a");
+
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ parentExpected = @{@"a": @1};
+ XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element");
+ XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a");
+}
+
+- (void) testHandlesDivergingViews {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSDictionary* cVal = nil;
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"c"] queryLimitedToLast:1];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ cVal = [snapshot value];
+ }];
+
+ __block NSDictionary* dVal = nil;
+ query = [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"d"] queryLimitedToLast:1];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ dVal = [snapshot value];
+ }];
+
+ __block BOOL ready = NO;
+ NSDictionary* toSet = @{@"a": @1, @"b": @2, @"c": @3};
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSDictionary* expected = @{@"c": @3};
+ XCTAssertTrue([cVal isEqualToDictionary:expected], @"should be c");
+ XCTAssertTrue([dVal isEqualToDictionary:expected], @"should be c");
+
+ ready = NO;
+ [[ref child:@"d"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([cVal isEqualToDictionary:expected], @"should be c");
+ expected = @{@"d": @4};
+ XCTAssertTrue([dVal isEqualToDictionary:expected], @"should be d");
+}
+
+- (void) testHandlesRemovingAQueriedElement {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSNumber* val = nil;
+ [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ id newVal = [snapshot value];
+ if (newVal != nil) {
+ val = [snapshot value];
+ }
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([val isEqualToNumber:@2], @"Expected last element in window");
+
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([val isEqualToNumber:@1], @"Should now be the next element in the window");
+}
+
+- (void) testStartAtAndLimit1Works {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSNumber* val = nil;
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ id newVal = [snapshot value];
+ if (newVal != nil) {
+ val = [snapshot value];
+ }
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([val isEqualToNumber:@1], @"Expected first element in window");
+}
+
+// See case 1664
+- (void) testStartAtAndLimit1AndRemoveFirstChild {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSNumber* val = nil;
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ id newVal = [snapshot value];
+ if (newVal != nil) {
+ val = [snapshot value];
+ }
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([val isEqualToNumber:@1], @"Expected first element in window");
+
+ ready = NO;
+ [[ref child:@"a"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([val isEqualToNumber:@2], @"Expected next element in window");
+}
+
+// See case 1169
+- (void) testStartAtWithTwoArgumentsWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ NSMutableArray* children = [[NSMutableArray alloc] init];
+
+ NSDictionary* toSet = @{
+ @"Walker": @{@"name": @"Walker", @"score": @20, @".priority": @20},
+ @"Michael": @{@"name": @"Michael", @"score": @100, @".priority": @100}
+ };
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@20 childKey:@"Walker"] queryLimitedToFirst:2];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [children addObject:child.key];
+ }
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expected = @[@"Walker", @"Michael"];
+ XCTAssertTrue([children isEqualToArray:expected], @"Expected both children");
+}
+
+- (void) testHandlesMultipleQueriesOnSameNode {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+
+ NSDictionary* toSet = @{
+ @"a": @1, @"b": @2, @"c": @3, @"d": @4, @"e": @5, @"f": @6
+ };
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ __block BOOL called = NO;
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // we got the initial data
+ XCTAssertFalse(called, @"This should only get called once, we don't update data after this");
+ called = YES;
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ __block NSDictionary* snap = nil;
+ // now do nested once calls
+ [[ref queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [[ref queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+
+ snap = [snapshot value];
+ ready = YES;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSDictionary* expected = @{@"f": @6};
+ XCTAssertTrue([snap isEqualToDictionary:expected], @"Expected the correct data");
+}
+
+- (void) testHandlesOnceCalledOnNodeWithDefaultListener {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+
+ NSDictionary* toSet = @{
+ @"a": @1, @"b": @2, @"c": @3, @"d": @4, @"e": @5, @"f": @6
+ };
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // we got the initial data
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+
+ __block NSNumber* snap = nil;
+ [[ref queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = [snapshot value];
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([snap isEqualToNumber:@6], @"Got once response");
+}
+
+- (void) testHandlesOnceCalledOnNodeWithDefaultListenerAndNonCompleteLimit {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+
+ NSDictionary* toSet = @{@"a": @1, @"b": @2, @"c": @3};
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ // do first listen
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+
+ __block NSDictionary* snap = nil;
+ [[ref queryLimitedToLast:5] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = [snapshot value];
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSDictionary* expected = @{@"a": @1, @"b": @2, @"c": @3};
+ XCTAssertTrue([snap isEqualToDictionary:expected], @"Got once response");
+}
+
+- (void) testRemoveTriggersRemoteEvents {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = tuple.one;
+ FIRDatabaseReference * reader = tuple.two;
+
+ __block BOOL ready = NO;
+
+ NSDictionary* toSet = @{@"a": @"a", @"b": @"b", @"c": @"c", @"d": @"d", @"e": @"e"};
+
+ [writer setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ __block int count = 0;
+
+ [[reader queryLimitedToLast:5] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ count++;
+ if (count == 1) {
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"a" : @"a", @"b" : @"b", @"c" : @"c", @"d" : @"d", @"e" : @"e"};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"First callback, expect all the data");
+ [[writer child:@"c"] removeValue];
+ } else {
+ XCTAssertTrue(count == 2, @"Should only get called twice");
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"a" : @"a", @"b" : @"b", @"d" : @"d", @"e" : @"e"};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Second callback, expect all the remaining data");
+ ready = YES;
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testEndingAtNameReturnsCorrectChildren {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSDictionary* toSet = @{
+ @"a": @"a",
+ @"b": @"b",
+ @"c": @"c",
+ @"d": @"d",
+ @"e": @"e",
+ @"f": @"f",
+ @"g": @"g",
+ @"h": @"h"
+ };
+
+ [self waitForCompletionOf:ref setValue:toSet];
+
+ __block NSDictionary* snap = nil;
+ __block BOOL done = NO;
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"f"] queryLimitedToLast:5];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = [snapshot value];
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ NSDictionary* expected = @{
+ @"b": @"b",
+ @"c": @"c",
+ @"d": @"d",
+ @"e": @"e",
+ @"f": @"f"
+ };
+ XCTAssertTrue([snap isEqualToDictionary:expected], @"Expected 5 elements, ending at f");
+}
+
+- (void) testListenForChildAddedWithLimitEnsureEventsFireProperly {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL done = NO;
+
+ NSDictionary* toSet = @{@"a": @1, @"b": @"b", @"c": @{@"deep": @"path", @"of": @{@"stuff": @YES}}};
+ [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ __block int count = 0;
+ [[reader queryLimitedToLast:3] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ count++;
+ if (count == 1) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Got first child");
+ XCTAssertTrue([snapshot.value isEqualToNumber:@1], @"Got correct value");
+ } else if (count == 2) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"b"], @"Got second child");
+ XCTAssertTrue([snapshot.value isEqualToString:@"b"], @"got correct value");
+ } else if (count == 3) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"c"], @"Got third child");
+ NSDictionary *expected = @{@"deep" : @"path", @"of" : @{@"stuff" : @YES}};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got deep object");
+ } else {
+ XCTFail(@"wrong event count");
+ }
+ }];
+
+ WAIT_FOR(count == 3);
+}
+
+
+- (void) testListenForChildChangedWithLimitEnsureEventsFireProperly {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL done = NO;
+
+ NSDictionary* toSet = @{@"a": @"something", @"b": @"we'll", @"c": @"overwrite"};
+ [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ __block int count = 0;
+ [reader observeEventType:FIRDataEventTypeChildChanged withBlock:^(FIRDataSnapshot *snapshot) {
+ count++;
+ if (count == 1) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Got first child");
+ XCTAssertTrue([snapshot.value isEqualToNumber:@1], @"Got correct value");
+ } else if (count == 2) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"b"], @"Got second child");
+ XCTAssertTrue([snapshot.value isEqualToString:@"b"], @"got correct value");
+ } else if (count == 3) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"c"], @"Got third child");
+ NSDictionary *expected = @{@"deep" : @"path", @"of" : @{@"stuff" : @YES}};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got deep object");
+ } else {
+ XCTFail(@"wrong event count");
+ }
+ }];
+ toSet = @{@"a": @1, @"b": @"b", @"c": @{@"deep": @"path", @"of": @{@"stuff": @YES}}};
+ [writer setValue:toSet];
+
+ WAIT_FOR(count == 3);
+}
+
+- (void) testListenForChildRemovedWithLimitEnsureEventsFireProperly {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL done = NO;
+
+ NSDictionary* toSet = @{@"a": @1, @"b": @"b", @"c": @{@"deep": @"path", @"of": @{@"stuff": @YES}}};
+ [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ __block int count = 0;
+ [reader observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+ count++;
+ if (count == 1) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Got first child");
+ XCTAssertTrue([snapshot.value isEqualToNumber:@1], @"Got correct value");
+ } else if (count == 2) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"b"], @"Got second child");
+ XCTAssertTrue([snapshot.value isEqualToString:@"b"], @"got correct value");
+ } else if (count == 3) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"c"], @"Got third child");
+ NSDictionary *expected = @{@"deep" : @"path", @"of" : @{@"stuff" : @YES}};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got deep object");
+ } else {
+ XCTFail(@"wrong event count");
+ }
+ }];
+
+ done = NO;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // Load the data first
+ done = snapshot.value != [NSNull null] && [snapshot.value isEqualToDictionary:toSet];
+ }];
+
+ WAIT_FOR(done);
+
+ // Now do the removes
+ [[writer child:@"a"] removeValue];
+ [[writer child:@"b"] removeValue];
+ [[writer child:@"c"] removeValue];
+
+ WAIT_FOR(count == 3);
+}
+
+- (void) testQueriesBehaveProperlyAfterOnceCall {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL done = NO;
+ NSDictionary* toSet = @{@"a": @1, @"b": @2, @"c": @3, @"d": @4};
+ [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ done = NO;
+ [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ // Ok, now do some queries
+ __block int startCount = 0;
+ __block int defaultCount = 0;
+ [[[reader queryOrderedByPriority] queryStartingAtValue:nil childKey:@"d"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ startCount++;
+ }];
+
+ [reader observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ defaultCount++;
+ }];
+
+ [reader observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTFail(@"Should not remove any children");
+ }];
+
+ WAIT_FOR(startCount == 1 && defaultCount == 4);
+}
+
+- (void) testIntegerKeysBehaveNumerically1 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSDictionary* toSet = @{@"1": @YES, @"50": @YES, @"550": @YES, @"6": @YES, @"600": @YES, @"70": @YES, @"8": @YES, @"80": @YES };
+ __block BOOL done = NO;
+ [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"80"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *expected = @{@"80" : @YES, @"550" : @YES, @"600" : @YES};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got correct result.");
+ done = YES;
+ }];
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) testIntegerKeysBehaveNumerically2 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSDictionary* toSet = @{@"1": @YES, @"50": @YES, @"550": @YES, @"6": @YES, @"600": @YES, @"70": @YES, @"8": @YES, @"80": @YES };
+ __block BOOL done = NO;
+ [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"50"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *expected = @{@"1" : @YES, @"6" : @YES, @"8" : @YES, @"50" : @YES};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got correct result.");
+ done = YES;
+ }];
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) testIntegerKeysBehaveNumerically3 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSDictionary* toSet = @{@"1": @YES, @"50": @YES, @"550": @YES, @"6": @YES, @"600": @YES, @"70": @YES, @"8": @YES, @"80": @YES };
+ __block BOOL done = NO;
+ [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"50"] queryEndingAtValue:nil childKey:@"80"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *expected = @{@"50" : @YES, @"70" : @YES, @"80" : @YES};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got correct result.");
+ done = YES;
+ }];
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) testItemsPulledIntoLimitCorrectly {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* snaps = [[NSMutableArray alloc] init];
+
+ // Just so everything is cached locally.
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+
+ }];
+
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ [snaps addObject:val];
+ }];
+
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @1},
+ @"b": @{@".value": @2, @".priority": @2},
+ @"c": @{@".value": @3, @".priority": @3}
+ }];
+
+ __block BOOL ready = NO;
+ [[ref child:@"b"] setValue:[NSNull null] withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ NSArray* expected = @[@{@"b": @2, @"c": @3}, @{@"a": @1, @"c": @3}];
+ XCTAssertEqualObjects(snaps, expected, @"Incorrect snapshots.");
+}
+
+- (void)testChildChangedCausesChildRemovedEvent
+{
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ [[ref child:@"l/a"] setValue:@"1" andPriority:@"a"];
+ [[ref child:@"l/b"] setValue:@"2" andPriority:@"b"];
+ FIRDatabaseQuery *query = [[[[ref child:@"l"] queryOrderedByPriority] queryStartingAtValue:@"b"] queryEndingAtValue:@"d"];
+ __block BOOL removed = NO;
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqualObjects(snapshot.value, @"2", @"Incorrect snapshot");
+ removed = YES;
+ }];
+
+ [[ref child:@"l/b"] setValue:@"4" andPriority:@"a"];
+
+ WAIT_FOR(removed);
+}
+
+- (void) testQueryHasRef {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ FIRDatabaseQuery *query = [ref queryOrderedByKey];
+ XCTAssertEqualObjects([query.ref path], [ref path], @"Should have same path");
+}
+
+- (void) testQuerySnapshotChildrenRespectDefaultOrdering {
+ FTupleFirebase* pair = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = pair.one;
+ FIRDatabaseReference * reader = pair.two;
+ __block BOOL done = NO;
+
+ NSDictionary* list = @{
+ @"a": @{
+ @"thisvaluefirst": @{ @".value": @true, @".priority": @1 },
+ @"name": @{ @".value": @"Michael", @".priority": @2 },
+ @"thisvaluelast": @{ @".value": @true, @".priority": @3 },
+ },
+ @"b": @{
+ @"thisvaluefirst": @{ @".value": @true },
+ @"name": @{ @".value": @"Rob", @".priority": @2 },
+ @"thisvaluelast": @{ @".value": @true, @".priority": @3 },
+ },
+ @"c": @{
+ @"thisvaluefirst": @{ @".value": @true, @".priority": @1 },
+ @"name": @{ @".value": @"Jonny", @".priority": @2 },
+ @"thisvaluelast": @{ @".value": @true, @".priority": @"somestring" },
+ }
+ };
+
+ [writer setValue:list withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ done = NO;
+ [[reader queryOrderedByChild:@"name"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSArray *expectedKeys = @[@"thisvaluefirst", @"name", @"thisvaluelast"];
+ NSArray *expectedNames = @[@"Jonny", @"Michael", @"Rob"];
+
+ // Validate that snap.child() resets order to default for child snaps
+ NSMutableArray *orderedKeys = [[NSMutableArray alloc] init];
+ for (FIRDataSnapshot *childSnap in [snapshot childSnapshotForPath:@"b"].children) {
+ [orderedKeys addObject:childSnap.key];
+ }
+ XCTAssertEqualObjects(expectedKeys, orderedKeys, @"Should have matching ordered lists of keys");
+
+ // Validate that snap.forEach() resets ordering to default for child snaps
+ NSMutableArray *orderedNames = [[NSMutableArray alloc] init];
+ for (FIRDataSnapshot *childSnap in snapshot.children) {
+ [orderedNames addObject:[childSnap childSnapshotForPath:@"name"].value];
+ [orderedKeys removeAllObjects];
+ for (FIRDataSnapshot *grandchildSnap in childSnap.children) {
+ [orderedKeys addObject:grandchildSnap.key];
+ }
+ XCTAssertEqualObjects(expectedKeys, orderedKeys, @"Should have matching ordered lists of keys");
+ }
+ XCTAssertEqualObjects(expectedNames, orderedNames, @"Should have matching ordered lists of names");
+
+ done = YES;
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) testAddingListensForTheSamePathDoesNotCheckFail {
+ // This bug manifests itself if there's a hierarchy of query listener, default listener and one-time listener
+ // underneath.
+ // In Java implementation, during one-time listener registration, sync-tree traversal stopped as soon as it found
+ // a complete server cache (this is the case for not indexed query view). The problem is that the same traversal was
+ // looking for a ancestor default view, and the early exit prevented from finding the default listener above the
+ // one-time listener. Event removal code path wasn't removing the listener because it stopped as soon as it
+ // found the default view. This left the zombie one-time listener and check failed on the second attempt to
+ // create a listener for the same path (asana#61028598952586).
+
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+
+ [[ref child:@"child"] setValue:@{@"name": @"John"}];
+ [[[ref queryOrderedByChild:@"name"] queryEqualToValue:@"John"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ done = NO;
+ [[[ref child:@"child"] child:@"favoriteToy"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ done = NO;
+ [[[ref child:@"child"] child:@"favoriteToy"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FIRDatabaseTests.m b/Example/Database/Tests/Integration/FIRDatabaseTests.m
new file mode 100644
index 0000000..8a5742d
--- /dev/null
+++ b/Example/Database/Tests/Integration/FIRDatabaseTests.m
@@ -0,0 +1,375 @@
+/*
+ * 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/XCTest.h>
+#import "FIRApp.h"
+#import "FIRDatabaseReference.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FIRDatabase.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FIROptions.h"
+#import "FTestHelpers.h"
+#import "FMockStorageEngine.h"
+#import "FTestBase.h"
+#import "FTestHelpers.h"
+#import "FIRFakeApp.h"
+
+@interface FIRDatabaseTests : FTestBase
+
+@end
+
+static const NSInteger kFErrorCodeWriteCanceled = 3;
+
+@implementation FIRDatabaseTests
+
+- (void) testFIRDatabaseForNilApp {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+ XCTAssertThrowsSpecificNamed([FIRDatabase databaseForApp:nil], NSException, @"InvalidFIRApp");
+#pragma clang diagnostic pop
+}
+
+- (void) testDatabaseForApp {
+ FIRDatabase *database = [self databaseForURL:self.databaseURL];
+ XCTAssertEqualObjects(self.databaseURL, [database reference].URL);
+}
+
+- (void) testDatabaseForAppWithInvalidURLs {
+ XCTAssertThrows([self databaseForURL:nil]);
+ XCTAssertThrows([self databaseForURL:@"not-a-url"]);
+ XCTAssertThrows([self databaseForURL:@"http://x.example.com/paths/are/bad"]);
+}
+
+- (void) testReferenceWithPath {
+ FIRDatabase *db = [self defaultDatabase];
+ NSString *expectedURL = [NSString stringWithFormat:@"%@/foo", self.databaseURL];
+ XCTAssertEqualObjects(expectedURL, [db referenceWithPath:@"foo"].URL);
+}
+
+- (void) testReferenceFromURLWithEmptyPath {
+ FIRDatabaseReference *ref = [[self defaultDatabase] referenceFromURL:self.databaseURL];
+ XCTAssertEqualObjects(self.databaseURL, ref.URL);
+}
+
+- (void) testReferenceFromURLWithPath {
+ NSString *url = [NSString stringWithFormat:@"%@/foo/bar", self.databaseURL];
+ FIRDatabaseReference *ref = [[self defaultDatabase] referenceFromURL:url];
+ XCTAssertEqualObjects(url, ref.URL);
+}
+
+- (void) testReferenceFromURLWithWrongURL {
+ NSString *url = [NSString stringWithFormat:@"%@/foo/bar", @"https://foobar.firebaseio.com"];
+ XCTAssertThrows([[self defaultDatabase] referenceFromURL:url]);
+}
+
+- (void) testReferenceEqualityForFIRDatabase {
+ FIRDatabase *db1 = [self databaseForURL:self.databaseURL name:@"db1"];
+ FIRDatabase *db2 = [self databaseForURL:self.databaseURL name:@"db2"];
+ FIRDatabase *altDb = [self databaseForURL:self.databaseURL name:@"altDb"];
+ FIRDatabase *wrongHostDb = [self databaseForURL:@"http://tests.example.com"];
+
+ FIRDatabaseReference *testRef1 = [db1 reference];
+ FIRDatabaseReference *testRef2 = [db1 referenceWithPath:@"foo"];
+ FIRDatabaseReference *testRef3 = [altDb reference];
+ FIRDatabaseReference *testRef4 = [wrongHostDb reference];
+ FIRDatabaseReference *testRef5 = [db2 reference];
+ FIRDatabaseReference *testRef6 = [db2 reference];
+
+ // Referential equality
+ XCTAssertTrue(testRef1.database == testRef2.database);
+ XCTAssertFalse(testRef1.database == testRef3.database);
+ XCTAssertFalse(testRef1.database == testRef4.database);
+ XCTAssertFalse(testRef1.database == testRef5.database);
+ XCTAssertFalse(testRef1.database == testRef6.database);
+
+ // references from same FIRDatabase same identical .database references.
+ XCTAssertTrue(testRef5.database == testRef6.database);
+
+ [db1 goOffline];
+ [db2 goOffline];
+ [altDb goOffline];
+ [wrongHostDb goOffline];
+}
+
+- (FIRDatabaseReference *)rootRefWithEngine:(id<FStorageEngine>)engine name:(NSString *)name {
+ FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:name];
+ config.persistenceEnabled = YES;
+ config.forceStorageEngine = engine;
+ return [[FIRDatabaseReference alloc] initWithConfig:config];
+}
+
+- (void) testPurgeWritesPurgesAllWrites {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FIRDatabaseReference *ref = [self rootRefWithEngine:engine name:@"purgeWritesPurgesAllWrites"];
+ FIRDatabase *database = ref.database;
+
+ [database goOffline];
+
+ [[ref childByAutoId] setValue:@"test-value-1"];
+ [[ref childByAutoId] setValue:@"test-value-2"];
+ [[ref childByAutoId] setValue:@"test-value-3"];
+ [[ref childByAutoId] setValue:@"test-value-4"];
+
+ [self waitForEvents:ref];
+
+ XCTAssertEqual(engine.userWrites.count, (NSUInteger)4);
+
+ [database purgeOutstandingWrites];
+ [self waitForEvents:ref];
+ XCTAssertEqual(engine.userWrites.count, (NSUInteger)0);
+
+ [database goOnline];
+}
+
+- (void) testPurgeWritesAreCanceledInOrder {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FIRDatabaseReference *ref = [self rootRefWithEngine:engine name:@"purgeWritesAndCanceledInOrder"];
+ FIRDatabase *database = ref.database;
+
+ [database goOffline];
+
+ NSMutableArray *order = [NSMutableArray array];
+
+ [[ref childByAutoId] setValue:@"test-value-1" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [order addObject:@"1"];
+ }];
+ [[ref childByAutoId] setValue:@"test-value-2" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [order addObject:@"2"];
+ }];
+ [[ref childByAutoId] setValue:@"test-value-3" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [order addObject:@"3"];
+ }];
+ [[ref childByAutoId] setValue:@"test-value-4" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [order addObject:@"4"];
+ }];
+
+ [self waitForEvents:ref];
+
+ XCTAssertEqual(engine.userWrites.count, (NSUInteger)4);
+
+ [database purgeOutstandingWrites];
+ [self waitForEvents:ref];
+ XCTAssertEqual(engine.userWrites.count, (NSUInteger)0);
+ XCTAssertEqualObjects(order, (@[@"1", @"2", @"3", @"4"]));
+
+ [database goOnline];
+}
+
+- (void)testPurgeWritesCancelsOnDisconnects {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FIRDatabaseReference *ref = [self rootRefWithEngine:engine name:@"purgeWritesCancelsOnDisconnects"];
+ FIRDatabase *database = ref.database;
+
+ [database goOffline];
+
+ NSMutableArray *events = [NSMutableArray array];
+
+ [[ref childByAutoId] onDisconnectSetValue:@"test-value-1" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [events addObject:@"1"];
+ }];
+
+ [[ref childByAutoId] onDisconnectSetValue:@"test-value-2" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [events addObject:@"2"];
+ }];
+
+ [self waitForEvents:ref];
+
+ [database purgeOutstandingWrites];
+
+ [self waitForEvents:ref];
+
+ XCTAssertEqualObjects(events, (@[@"1", @"2"]));
+}
+
+- (void) testPurgeWritesReraisesEvents {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FIRDatabaseReference *ref = [[self rootRefWithEngine:engine name:@"purgeWritesReraiseEvents"] childByAutoId];
+ FIRDatabase *database = ref.database;
+
+ [self waitForCompletionOf:ref setValue:@{@"foo": @"foo-value", @"bar": @{@"qux": @"qux-value"}}];
+
+ NSMutableArray *fooValues = [NSMutableArray array];
+ NSMutableArray *barQuuValues = [NSMutableArray array];
+ NSMutableArray *barQuxValues = [NSMutableArray array];
+ NSMutableArray *cancelOrder = [NSMutableArray array];
+
+ [[ref child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [fooValues addObject:snapshot.value];
+ }];
+ [[ref child:@"bar/quu"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [barQuuValues addObject:snapshot.value];
+ }];
+ [[ref child:@"bar/qux"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [barQuxValues addObject:snapshot.value];
+ }];
+
+ [database goOffline];
+
+ [[ref child:@"foo"] setValue:@"new-foo-value" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ // This should be after we raised events
+ XCTAssertEqualObjects(fooValues.lastObject, @"foo-value");
+ [cancelOrder addObject:@"foo-1"];
+ }];
+
+ [[ref child:@"bar"] updateChildValues:@{@"quu": @"quu-value", @"qux": @"new-qux-value"}
+ withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ // This should be after we raised events
+ XCTAssertEqualObjects(barQuxValues.lastObject, @"qux-value");
+ XCTAssertEqualObjects(barQuuValues.lastObject, [NSNull null]);
+ [cancelOrder addObject:@"bar"];
+ }];
+
+ [[ref child:@"foo"] setValue:@"newest-foo-value" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ // This should be after we raised events
+ XCTAssertEqualObjects(fooValues.lastObject, @"foo-value");
+ [cancelOrder addObject:@"foo-2"];
+ }];
+
+ [database purgeOutstandingWrites];
+
+ [self waitForEvents:ref];
+
+ XCTAssertEqualObjects(cancelOrder, (@[@"foo-1", @"bar", @"foo-2"]));
+ XCTAssertEqualObjects(fooValues, (@[@"foo-value", @"new-foo-value", @"newest-foo-value", @"foo-value"]));
+ XCTAssertEqualObjects(barQuuValues, (@[[NSNull null], @"quu-value", [NSNull null]]));
+ XCTAssertEqualObjects(barQuxValues, (@[@"qux-value", @"new-qux-value", @"qux-value"]));
+
+ [database goOnline];
+ // Make sure we're back online and reconnected again
+ [self waitForRoundTrip:ref];
+
+ // No events should be reraised
+ XCTAssertEqualObjects(cancelOrder, (@[@"foo-1", @"bar", @"foo-2"]));
+ XCTAssertEqualObjects(fooValues, (@[@"foo-value", @"new-foo-value", @"newest-foo-value", @"foo-value"]));
+ XCTAssertEqualObjects(barQuuValues, (@[[NSNull null], @"quu-value", [NSNull null]]));
+ XCTAssertEqualObjects(barQuxValues, (@[@"qux-value", @"new-qux-value", @"qux-value"]));
+}
+
+- (void)testPurgeWritesCancelsTransactions {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FIRDatabaseReference *ref = [[self rootRefWithEngine:engine name:@"purgeWritesCancelsTransactions"] childByAutoId];
+ FIRDatabase *database = ref.database;
+
+ NSMutableArray *events = [NSMutableArray array];
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [events addObject:[NSString stringWithFormat:@"value-%@", snapshot.value]];
+ }];
+
+ // Make sure the first value event is fired
+ [self waitForRoundTrip:ref];
+
+ [database goOffline];
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"1"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [events addObject:@"cancel-1"];
+ }];
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"2"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [events addObject:@"cancel-2"];
+ }];
+
+ [database purgeOutstandingWrites];
+
+ [self waitForEvents:ref];
+
+ // The order should really be cancel-1 then cancel-2, but meh, to difficult to implement currently...
+ XCTAssertEqualObjects(events, (@[@"value-<null>", @"value-1", @"value-2", @"value-<null>", @"cancel-2", @"cancel-1"]));
+}
+
+- (void) testPersistenceEnabled {
+ id app = [[FIRFakeApp alloc] initWithName:@"testPersistenceEnabled" URL:self.databaseURL];
+ FIRDatabase *database = [FIRDatabase databaseForApp:app];
+ database.persistenceEnabled = YES;
+ XCTAssertTrue(database.persistenceEnabled);
+
+ // Just do a dummy observe that should get null added to the persistent cache.
+ FIRDatabaseReference *ref = [[database reference] childByAutoId];
+ [self waitForValueOf:ref toBe:[NSNull null]];
+
+ // Now go offline and since null is cached offline, our observer should still complete.
+ [database goOffline];
+ [self waitForValueOf:ref toBe:[NSNull null]];
+}
+
+- (void) testPersistenceCacheSizeBytes {
+ id app = [[FIRFakeApp alloc] initWithName:@"testPersistenceCacheSizeBytes" URL:self.databaseURL];
+ FIRDatabase *database = [FIRDatabase databaseForApp:app];
+ database.persistenceEnabled = YES;
+
+ int oneMegabyte = 1 * 1024 * 1024;
+
+ XCTAssertThrows([database setPersistenceCacheSizeBytes: 1], @"Cache must be a least 1 MB.");
+ XCTAssertThrows([database setPersistenceCacheSizeBytes: 101 * oneMegabyte],
+ @"Cache must be less than 100 MB.");
+ database.persistenceCacheSizeBytes = 2 * oneMegabyte;
+ XCTAssertEqual(2 * oneMegabyte, database.persistenceCacheSizeBytes);
+
+ [database reference]; // Initialize database.
+
+ XCTAssertThrows([database setPersistenceCacheSizeBytes: 3 * oneMegabyte],
+ @"Persistence can't be changed after initialization.");
+ XCTAssertEqual(2 * oneMegabyte, database.persistenceCacheSizeBytes);
+}
+
+- (void) testCallbackQueue {
+ id app = [[FIRFakeApp alloc] initWithName:@"testCallbackQueue" URL:self.databaseURL];
+ FIRDatabase *database = [FIRDatabase databaseForApp:app];
+ dispatch_queue_t callbackQueue = dispatch_queue_create("testCallbackQueue", NULL);
+ database.callbackQueue = callbackQueue;
+ XCTAssertEqual(callbackQueue, database.callbackQueue);
+
+ __block BOOL done = NO;
+ [database.reference.childByAutoId observeSingleEventOfType:FIRDataEventTypeValue
+ withBlock:^(FIRDataSnapshot *snapshot) {
+ dispatch_assert_queue(callbackQueue);
+ done = YES;
+ }];
+ WAIT_FOR(done);
+ [database goOffline];
+}
+
+- (FIRDatabase *) defaultDatabase {
+ return [self databaseForURL:self.databaseURL];
+}
+
+- (FIRDatabase *) databaseForURL:(NSString *)url {
+ NSString *name = [NSString stringWithFormat:@"url:%@", url];
+ return [self databaseForURL:url name:name];
+}
+
+- (FIRDatabase *) databaseForURL:(NSString *)url name:(NSString *)name {
+ id app = [[FIRFakeApp alloc] initWithName:name URL:url];
+ return [FIRDatabase databaseForApp:app];
+}
+@end
diff --git a/Example/Database/Tests/Integration/FKeepSyncedTest.m b/Example/Database/Tests/Integration/FKeepSyncedTest.m
new file mode 100644
index 0000000..96d5cf8
--- /dev/null
+++ b/Example/Database/Tests/Integration/FKeepSyncedTest.m
@@ -0,0 +1,230 @@
+/*
+ * 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/XCTest.h>
+
+#import "FTestHelpers.h"
+#import "FTestBase.h"
+
+@interface FKeepSyncedTest : FTestBase
+
+@end
+
+@implementation FKeepSyncedTest
+
+static NSUInteger fGlobalKeepSyncedTestCounter = 0;
+
+- (void)assertIsKeptSynced:(FIRDatabaseQuery *)query {
+ FIRDatabaseReference *ref = query.ref;
+
+ // First set a unique value to the value of child
+ fGlobalKeepSyncedTestCounter++;
+ NSNumber *currentValue = @(fGlobalKeepSyncedTestCounter);
+ __block BOOL done = NO;
+ [ref setValue:@{ @"child": currentValue} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertNil(error);
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ // Next go offline, if it's kept synced we should have kept the value, after going offline no way to get the value
+ // except from cache
+ [FIRDatabaseReference goOffline];
+
+ [query observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // We should receive an event
+ XCTAssertEqualObjects(snapshot.value, @{@"child" : currentValue});
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ // All good, go back online
+ [FIRDatabaseReference goOnline];
+}
+
+- (void)assertNotKeptSynced:(FIRDatabaseQuery *)query {
+ FIRDatabaseReference *ref = query.ref;
+
+ // First set a unique value to the value of child
+ fGlobalKeepSyncedTestCounter++;
+ NSNumber *currentValue = @(fGlobalKeepSyncedTestCounter);
+ fGlobalKeepSyncedTestCounter++;
+ NSNumber *newValue = @(fGlobalKeepSyncedTestCounter);
+ __block BOOL done = NO;
+ [ref setValue:@{ @"child": currentValue} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertNil(error);
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ // Next go offline, if it's kept synced we should have kept the value, after going offline no way to get the value
+ // except from cache
+ [FIRDatabaseReference goOffline];
+
+ [query observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // We should receive an event
+ XCTAssertEqualObjects(snapshot.value, @{@"child" : newValue});
+ done = YES;
+ }];
+
+ // By now, if we had it synced we should have gotten an event with the wrong value
+ // Write a new value so the value event listener will be triggered
+ [ref setValue:@{ @"child": newValue}];
+ WAIT_FOR(done);
+
+ // All good, go back online
+ [FIRDatabaseReference goOnline];
+}
+
+- (void)testKeepSynced {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ [ref keepSynced:NO];
+ [self assertNotKeptSynced:ref];
+}
+
+- (void)testManyKeepSyncedCallsDontAccumulate {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ [ref keepSynced:YES];
+ [ref keepSynced:YES];
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ // If it were balanced, this would not be enough
+ [ref keepSynced:NO];
+ [ref keepSynced:NO];
+ [self assertNotKeptSynced:ref];
+
+ // If it were balanced, this would not be enough
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ // cleanup
+ [ref keepSynced:NO];
+}
+
+- (void)testRemoveAllObserversDoesNotAffectKeepSynced {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ [ref removeAllObservers];
+ [self assertIsKeptSynced:ref];
+
+ // cleanup
+ [ref keepSynced:NO];
+}
+
+- (void)testRemoveSingleObserverDoesNotAffectKeepSynced {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ __block BOOL done = NO;
+ FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ [ref removeObserverWithHandle:handle];
+
+ [self assertIsKeptSynced:ref];
+
+ // cleanup
+ [ref keepSynced:NO];
+}
+
+- (void)testKeepSyncedNoDoesNotAffectExistingObserver {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ __block BOOL done = NO;
+ FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = [snapshot.value isEqual:@"done"];
+ }];
+
+ // cleanup
+ [ref keepSynced:NO];
+
+ [ref setValue:@"done"];
+
+ WAIT_FOR(done);
+ [ref removeObserverWithHandle:handle];
+}
+
+
+- (void)testDifferentQueriesAreIndependent {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+ FIRDatabaseQuery *query1 = [ref queryLimitedToFirst:1];
+ FIRDatabaseQuery *query2 = [ref queryLimitedToFirst:2];
+
+ [query1 keepSynced:YES];
+ [self assertIsKeptSynced:query1];
+ [self assertNotKeptSynced:query2];
+
+ [query2 keepSynced:YES];
+ [self assertIsKeptSynced:query1];
+ [self assertIsKeptSynced:query2];
+
+ [query1 keepSynced:NO];
+ [self assertIsKeptSynced:query2];
+ [self assertNotKeptSynced:query1];
+
+ [query2 keepSynced:NO];
+ [self assertNotKeptSynced:query1];
+ [self assertNotKeptSynced:query2];
+
+}
+
+- (void)testChildIsKeptSynced {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+ FIRDatabaseReference *child = [ref child:@"random-child"];
+
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:child];
+
+ // cleanup
+ [ref keepSynced:NO];
+}
+
+- (void)testRootIsKeptSynced {
+ FIRDatabaseReference *ref = [[FTestHelpers getRandomNodeWithoutPersistence] root];
+
+ [ref keepSynced:YES];
+ // Run on random child to make sure writes from this test doesn't interfere with any other tests.
+ [self assertIsKeptSynced:[ref childByAutoId]];
+
+ // cleanup
+ [ref keepSynced:NO];
+}
+
+// TODO[offline]: Cancel listens for keep synced....
+
+
+
+@end
diff --git a/Example/Database/Tests/Integration/FOrder.h b/Example/Database/Tests/Integration/FOrder.h
new file mode 100644
index 0000000..d39de2a
--- /dev/null
+++ b/Example/Database/Tests/Integration/FOrder.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 <XCTest/XCTest.h>
+#import "FTestBase.h"
+
+@interface FOrder : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FOrder.m b/Example/Database/Tests/Integration/FOrder.m
new file mode 100644
index 0000000..e8c628b
--- /dev/null
+++ b/Example/Database/Tests/Integration/FOrder.m
@@ -0,0 +1,646 @@
+/*
+ * 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 "FOrder.h"
+#import "FIRDatabaseReference.h"
+#import "FTypedefs_Private.h"
+#import "FTupleFirebase.h"
+#import "FTestHelpers.h"
+#import "FEventTester.h"
+#import "FTupleEventTypeString.h"
+
+@implementation FOrder
+
+- (void) testPushEnumerateAndCheckCorrectOrder {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ for(int i = 0; i < 10; i++) {
+ [[node childByAutoId] setValue:[NSNumber numberWithInt:i]];
+ }
+
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot * snapshot) {
+ int expected = 0;
+ for (FIRDataSnapshot * child in snapshot.children) {
+ XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expects values match.");
+ expected = expected + 1;
+ }
+ XCTAssertTrue(expected == 10, @"Should get all of the children");
+ XCTAssertTrue(expected == snapshot.childrenCount, @"Snapshot should report correct count");
+ }];
+}
+
+- (void) testPushEnumerateManyPathsWriteAndCheckOrder {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ NSMutableArray* paths = [[NSMutableArray alloc] init];
+
+ for(int i = 0; i < 20; i++) {
+ [paths addObject:[node childByAutoId]];
+ }
+
+ for(int i = 0; i < 20; i++) {
+ [(FIRDatabaseReference *)[paths objectAtIndex:i] setValue:[NSNumber numberWithInt:i]];
+ }
+
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) {
+ int expected = 0;
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expects values match.");
+ expected = expected + 1;
+ }
+ XCTAssertTrue(expected == 20, @"Should get all of the children");
+ XCTAssertTrue(expected == snap.childrenCount, @"Snapshot should report correct count");
+ }];
+}
+
+- (void) testPushDataReconnectReadBackAndVerifyOrder {
+
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+
+ __block int expected = 0;
+ __block int nodesSet = 0;
+ FIRDatabaseReference * node = tuple.one;
+ for(int i = 0; i < 10; i++) {
+ [[node childByAutoId] setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ nodesSet++;
+ }];
+ }
+
+ [self waitUntil:^BOOL{
+ return nodesSet == 10;
+ }];
+
+ __block BOOL done = NO;
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) {
+ expected = 0;
+ //[snap forEach:^BOOL(FIRDataSnapshot *child) {
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expected child value");
+ expected = expected + 1;
+ //return NO;
+ }
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ XCTAssertTrue(nodesSet == 10, @"All of the nodes have been set");
+
+ [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) {
+ expected = 0;
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expected child value");
+ expected = expected + 1;
+ }
+ done = YES;
+ XCTAssertTrue(expected == 10, @"Saw the expected number of children %d == 10", expected);
+ }];
+
+}
+
+- (void) testPushDataWithPrioritiesReconnectReadBackAndVerifyOrder {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+
+ __block int expected = 0;
+ __block int nodesSet = 0;
+ FIRDatabaseReference * node = tuple.one;
+ for(int i = 0; i < 10; i++) {
+ [[node childByAutoId] setValue:[NSNumber numberWithInt:i] andPriority:[NSNumber numberWithInt:(10 - i)] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ nodesSet = nodesSet + 1;
+ }];
+ }
+
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) {
+ expected = 9;
+
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority");
+ expected = expected - 1;
+ }
+ XCTAssertTrue(expected == -1, @"Saw the expected number of children");
+ }];
+
+ [self waitUntil:^BOOL{
+ return nodesSet == 10;
+ }];
+
+ XCTAssertTrue(nodesSet == 10, @"All of the nodes have been set");
+
+ [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) {
+ expected = 9;
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority");
+ expected = expected - 1;
+ }
+ XCTAssertTrue(expected == -1, @"Saw the expected number of children");
+ }];
+}
+
+- (void) testPushDataWithExponentialPrioritiesReconnectReadBackAndVerifyOrder {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+
+ __block int expected = 0;
+ __block int nodesSet = 0;
+ FIRDatabaseReference * node = tuple.one;
+ for(int i = 0; i < 10; i++) {
+ [[node childByAutoId] setValue:[NSNumber numberWithInt:i] andPriority:[NSNumber numberWithDouble:(111111111111111111111111111111.0 / pow(10, i))] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ nodesSet = nodesSet + 1;
+ }];
+ }
+
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) {
+ expected = 9;
+
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority");
+ expected = expected - 1;
+ }
+ XCTAssertTrue(expected == -1, @"Saw the expected number of children");
+ }];
+
+ WAIT_FOR(nodesSet == 10);
+
+ [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) {
+ expected = 9;
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority");
+ expected = expected - 1;
+ }
+ XCTAssertTrue(expected == -1, @"Saw the expected number of children");
+ }];
+}
+
+- (void) testThatNodesWithoutValuesAreNotEnumerated {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ [node child:@"foo"];
+ [[node child:@"bar"] setValue:@"test"];
+
+ __block int items = 0;
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) {
+
+ for (FIRDataSnapshot * child in snap.children) {
+ items = items + 1;
+ XCTAssertEqualObjects([child key], @"bar", @"Saw the child which had a value set and not the empty one");
+ }
+
+ XCTAssertTrue(items == 1, @"Saw only the one that was actually set.");
+ }];
+}
+
+- (void) testChildMovedEventWhenPriorityChanges {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+
+ NSArray* expect = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"b"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"c"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildMoved withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildChanged withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expect];
+
+ [et waitForInitialization];
+
+ [[node child:@"a"] setValue:@"first" andPriority:@1];
+ [[node child:@"b"] setValue:@"second" andPriority:@2];
+ [[node child:@"c"] setValue:@"third" andPriority:@3];
+
+ [[node child:@"a"] setPriority:@15];
+
+ [et wait];
+}
+
+
+- (void) testCanResetPriorityToNull {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [[node child:@"a"] setValue:@"a" andPriority:@1];
+ [[node child:@"b"] setValue:@"b" andPriority:@2];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ NSArray* expect = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"b"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expect];
+
+ [et wait];
+
+ expect = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildMoved withString:@"b"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildChanged withString:@"b"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expect];
+
+ [[node child:@"b"] setPriority:nil];
+
+ [et wait];
+
+ __block BOOL ready = NO;
+ [[node child:@"b"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([snapshot priority] == [NSNull null], @"Should be null");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testInsertingANodeUnderALeafPreservesItsPriority {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * snap;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ snap = s;
+ }];
+
+ [node setValue:@"a" andPriority:@10];
+ [[node child:@"deeper"] setValue:@"deeper"];
+
+ [self waitUntil:^BOOL{
+ id result = [snap value];
+ NSDictionary* expected = @{@"deeper": @"deeper"};
+ return snap != nil && [result isKindOfClass:[NSDictionary class]] && [result isEqualToDictionary:expected];
+ }];
+
+ XCTAssertEqualObjects([snap priority], @10, @"Proper value");
+}
+
+- (void) testVerifyOrderOfMixedNumbersStringNoPriorities {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+
+ NSArray* nodeAndPriorities = @[
+ @"alpha42", @"zed",
+ @"noPriorityC", [NSNull null],
+ @"num41", @500,
+ @"noPriorityB", [NSNull null],
+ @"num80", @4000.1,
+ @"num50", @4000,
+ @"num10", @24,
+ @"alpha41", @"zed",
+ @"alpha20", @"horse",
+ @"num20", @123,
+ @"num70", @4000.01,
+ @"noPriorityA", [NSNull null],
+ @"alpha30", @"tree",
+ @"num30", @300,
+ @"num60", @4000.001,
+ @"alpha10", @"0horse",
+ @"num42", @500,
+ @"alpha40", @"zed",
+ @"num40", @500
+ ];
+
+ __block int setsCompleted = 0;
+
+ for (int i = 0; i < [nodeAndPriorities count]; i++) {
+ FIRDatabaseReference * n = [tuple.one child:[nodeAndPriorities objectAtIndex:i++]];
+ [n setValue:@1 andPriority:[nodeAndPriorities objectAtIndex:i] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ setsCompleted = setsCompleted + 1;
+ }];
+ }
+
+ NSString* expected = @"noPriorityA, noPriorityB, noPriorityC, num10, num20, num30, num40, num41, num42, num50, num60, num70, num80, alpha10, alpha20, alpha30, alpha40, alpha41, alpha42, ";
+
+ [super snapWaiter:tuple.one withBlock:^(FIRDataSnapshot *snap) {
+ NSMutableString* output = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * n in snap.children) {
+ [output appendFormat:@"%@, ", [n key]];
+ }
+
+ XCTAssertTrue([expected isEqualToString:output], @"Proper order");
+ }];
+
+ WAIT_FOR(setsCompleted == [nodeAndPriorities count] / 2);
+
+ [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) {
+ NSMutableString* output = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * n in snap.children) {
+ [output appendFormat:@"%@, ", [n key]];
+ }
+
+ XCTAssertTrue([expected isEqualToString:output], @"Proper order");
+ }];
+}
+
+- (void) testVerifyOrderOfIntegerNames {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSArray* keys = @[
+ @"foo",
+ @"bar",
+ @"03",
+ @"0",
+ @"100",
+ @"20",
+ @"5",
+ @"3",
+ @"003",
+ @"9"
+ ];
+
+ __block int setsCompleted = 0;
+
+ for (int i = 0; i < [keys count]; i++) {
+ FIRDatabaseReference * n = [ref child:[keys objectAtIndex:i]];
+ [n setValue:@1 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ setsCompleted = setsCompleted + 1;
+ }];
+ }
+
+ NSString* expected = @"0, 3, 03, 003, 5, 9, 20, 100, bar, foo, ";
+
+ [super snapWaiter:ref withBlock:^(FIRDataSnapshot *snap) {
+ NSMutableString* output = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * n in snap.children) {
+ [output appendFormat:@"%@, ", [n key]];
+ }
+
+ XCTAssertTrue([expected isEqualToString:output], @"Proper order");
+ }];
+}
+
+- (void) testPrevNameIsCorrectOnChildAddedEvent {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [node setValue:@{@"a": @1, @"b": @2, @"c": @3}];
+
+
+ NSMutableString* added = [[NSMutableString alloc] init];
+
+ __block int count = 0;
+ [node observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snap, NSString *prevName) {
+ [added appendFormat:@"%@ %@, ", [snap key], prevName];
+ count++;
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 3;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"a (null), b a, c b, "], @"Proper order and prevname");
+
+}
+
+- (void) testPrevNameIsCorrectWhenAddingNewNodes {
+
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [node setValue:@{@"b": @2, @"c": @3, @"d": @4}];
+
+ NSMutableString* added = [[NSMutableString alloc] init];
+
+ __block int count = 0;
+ [node observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snap, NSString *prevName) {
+ [added appendFormat:@"%@ %@, ", [snap key], prevName];
+ count++;
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 3;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"b (null), c b, d c, "], @"Proper order and prevname");
+
+ [added setString:@""];
+ [[node child:@"a"] setValue:@1];
+ [self waitUntil:^BOOL{
+ return count == 4;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"a (null), "], @"Proper insertion of new node");
+
+ [added setString:@""];
+ [[node child:@"e"] setValue:@5];
+ [self waitUntil:^BOOL{
+ return count == 5;
+ }];
+ XCTAssertTrue([added isEqualToString:@"e d, "], @"Proper insertion of new node");
+}
+
+- (void) testPrevNameIsCorrectWhenAddingNewNodesWithJSON {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [node setValue:@{@"b": @2, @"c": @3, @"d": @4}];
+
+ NSMutableString* added = [[NSMutableString alloc] init];
+ __block int count = 0;
+ [node observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snap, NSString *prevName) {
+ [added appendFormat:@"%@ %@, ", [snap key], prevName];
+ count++;
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 3;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"b (null), c b, d c, "], @"Proper order and prevname");
+
+ [added setString:@""];
+ [node setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+ [self waitUntil:^BOOL{
+ return count == 4;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"a (null), "], @"Proper insertion of new node");
+
+ [added setString:@""];
+ [node setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4, @"e": @5}];
+ [self waitUntil:^BOOL{
+ return count == 5;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"e d, "], @"Proper insertion of new node");
+}
+
+- (void) testPrevNameIsCorrectWhenMovingNodes {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSMutableString* moved = [[NSMutableString alloc] init];
+
+ __block int count = 0;
+ [node observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [moved appendFormat:@"%@ %@, ", snapshot.key, prevName];
+ count++;
+ }];
+
+ [[node child:@"a"] setValue:@"a" andPriority:@1];
+ [[node child:@"b"] setValue:@"a" andPriority:@2];
+ [[node child:@"c"] setValue:@"a" andPriority:@3];
+ [[node child:@"d"] setValue:@"a" andPriority:@4];
+
+ [[node child:@"d"] setPriority:@0];
+ [self waitUntil:^BOOL{
+ return count == 1;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"d (null), "], @"Got first move");
+
+ [moved setString:@""];
+ [[node child:@"a"] setPriority:@4];
+ [self waitUntil:^BOOL{
+ return count == 2;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"a c, "], @"Got second move");
+
+ [moved setString:@""];
+ [[node child:@"c"] setPriority:@0.5];
+ [self waitUntil:^BOOL{
+ return count == 3;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"c d, "], @"Got third move");
+}
+
+
+- (void) testPrevNameIsCorrectWhenSettingWholeJsonDict {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSMutableString* moved = [[NSMutableString alloc] init];
+
+ __block int count = 0;
+ [node observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [moved appendFormat:@"%@ %@, ", snapshot.key, prevName];
+ count++;
+ }];
+
+ [node setValue:@{
+ @"a": @{@".value": @"a", @".priority": @1},
+ @"b": @{@".value": @"b", @".priority": @2},
+ @"c": @{@".value": @"c", @".priority": @3},
+ @"d": @{@".value": @"d", @".priority": @4}
+ }];
+
+ [node setValue:@{
+ @"d": @{@".value": @"d", @".priority": @0},
+ @"a": @{@".value": @"a", @".priority": @1},
+ @"b": @{@".value": @"b", @".priority": @2},
+ @"c": @{@".value": @"c", @".priority": @3}
+ }];
+ [self waitUntil:^BOOL{
+ return count == 1;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"d (null), "], @"Got move");
+
+ [moved setString:@""];
+
+ [node setValue:@{
+ @"d": @{@".value": @"d", @".priority": @0},
+ @"b": @{@".value": @"b", @".priority": @2},
+ @"c": @{@".value": @"c", @".priority": @3},
+ @"a": @{@".value": @"a", @".priority": @4}
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 2;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"a c, "], @"Got move");
+
+ [moved setString:@""];
+
+ [node setValue:@{
+ @"d": @{@".value": @"d", @".priority": @0},
+ @"c": @{@".value": @"c", @".priority": @0.5},
+ @"b": @{@".value": @"b", @".priority": @2},
+ @"a": @{@".value": @"a", @".priority": @4}
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 3;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"c d, "], @"Got move");
+}
+
+- (void) testCase595NoChildMovedEventWhenDeletingPrioritizedGrandchild {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block int moves = 0;
+ [node observeEventType:FIRDataEventTypeChildMoved withBlock:^(FIRDataSnapshot *snapshot) {
+ moves++;
+ }];
+
+ __block BOOL ready = NO;
+ [[node child:@"test/foo"] setValue:@42 andPriority:@"5"];
+ [[node child:@"test/foo2"] setValue:@42 andPriority:@"10"];
+ [[node child:@"test/foo"] removeValue];
+ [[node child:@"test/foo"] removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue(moves == 0, @"Nothing should have moved");
+
+}
+
+- (void) testCanSetAValueWithPriZero {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * snap = nil;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ snap = s;
+ }];
+
+ [node setValue:@"test" andPriority:@0];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertEqualObjects([snap value], @"test", @"Proper value");
+ XCTAssertEqualObjects([snap priority], @0, @"Proper value");
+}
+
+- (void) testCanSetObjectWithPriZero {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * snap = nil;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ snap = s;
+ }];
+
+ [node setValue:@{@"x": @"test", @"y": @7} andPriority:@0];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertEqualObjects([[snap value] objectForKey:@"x"], @"test", @"Proper value");
+ XCTAssertEqualObjects([[snap value] objectForKey:@"y"], @7, @"Proper value");
+ XCTAssertEqualObjects([snap priority], @0, @"Proper value");
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FOrderByTests.h b/Example/Database/Tests/Integration/FOrderByTests.h
new file mode 100644
index 0000000..ce7b6f6
--- /dev/null
+++ b/Example/Database/Tests/Integration/FOrderByTests.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 <Foundation/Foundation.h>
+#import "FTestBase.h"
+
+
+@interface FOrderByTests : FTestBase
+@end
diff --git a/Example/Database/Tests/Integration/FOrderByTests.m b/Example/Database/Tests/Integration/FOrderByTests.m
new file mode 100644
index 0000000..aea6b47
--- /dev/null
+++ b/Example/Database/Tests/Integration/FOrderByTests.m
@@ -0,0 +1,671 @@
+/*
+ * 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 "FOrderByTests.h"
+
+@interface FOrderByTests ()
+@end
+
+@implementation FOrderByTests
+
+
+- (void) testCanDefineAndUseAnIndex {
+ __block FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSArray *users = @[
+ @{@"name": @"Andrew", @"nuggets": @35},
+ @{@"name": @"Rob", @"nuggets": @40},
+ @{@"name": @"Greg", @"nuggets": @38}
+ ];
+
+ __block int setCount = 0;
+ [users enumerateObjectsUsingBlock:^(NSDictionary *user, NSUInteger idx, BOOL *stop) {
+ [[ref childByAutoId] setValue:user withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ setCount++;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return setCount == users.count;
+ }];
+
+ __block NSMutableArray *byNuggets = [[NSMutableArray alloc] init];
+ [[ref queryOrderedByChild:@"nuggets"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *user = snapshot.value;
+ [byNuggets addObject:user[@"name"]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return byNuggets.count == users.count;
+ }];
+
+ NSArray *expected = @[@"Andrew", @"Greg", @"Rob"];
+ XCTAssertEqualObjects(byNuggets, expected, @"Correct by-nugget ordering.");
+}
+
+- (void) testCanDefineAndUseDeepIndex {
+ __block FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSArray *users = @[
+ @{@"name": @"Andrew", @"deep": @{@"nuggets": @35}},
+ @{@"name": @"Rob", @"deep": @{@"nuggets": @40}},
+ @{@"name": @"Greg", @"deep": @{@"nuggets": @38}}
+ ];
+
+ __block int setCount = 0;
+ [users enumerateObjectsUsingBlock:^(NSDictionary *user, NSUInteger idx, BOOL *stop) {
+ [[ref childByAutoId] setValue:user withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ setCount++;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return setCount == users.count;
+ }];
+
+ __block NSMutableArray *byNuggets = [[NSMutableArray alloc] init];
+ [[ref queryOrderedByChild:@"deep/nuggets"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *user = snapshot.value;
+ [byNuggets addObject:user[@"name"]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return byNuggets.count == users.count;
+ }];
+
+ NSArray *expected = @[@"Andrew", @"Greg", @"Rob"];
+ XCTAssertEqualObjects(byNuggets, expected, @"Correct by-nugget ordering.");
+}
+
+- (void) testCanUsaAFallbackThenDefineTheSpecifiedIndex {
+ FTupleFirebase *tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *reader = tuple.one, *writer = tuple.two;
+
+ NSDictionary *foo1 = @{
+ @"a" : @{@"order" : @2, @"foo" : @1},
+ @"b" : @{@"order" : @0},
+ @"c" : @{@"order" : @1, @"foo" : @NO},
+ @"d" : @{@"order" : @3, @"foo" : @"hello"}
+ };
+
+ NSDictionary *foo_e = @{@"order": @1.5, @"foo": @YES};
+ NSDictionary *foo_f = @{@"order": @4, @"foo": @{@"bar": @"baz"}};
+
+ [self waitForCompletionOf:writer setValue:foo1];
+
+ NSMutableArray *snaps = [[NSMutableArray alloc] init];
+ [[[reader queryOrderedByChild:@"order"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [snaps addObject:snapshot.value];
+ }];
+ WAIT_FOR(snaps.count == 1);
+
+ NSDictionary *expected = @{
+ @"d": @{@"order": @3, @"foo": @"hello"},
+ @"a": @{@"order": @2, @"foo": @1}
+ };
+ XCTAssertEqualObjects(snaps[0], expected, @"Got correct result");
+
+
+ [self waitForCompletionOf:[writer child:@"e"] setValue:foo_e];
+
+ [self waitForRoundTrip:reader];
+ NSLog(@"snaps: %@", snaps);
+ NSLog(@"snaps.count: %ld", (unsigned long) snaps.count);
+ XCTAssertEqual(snaps.count, (NSUInteger)1, @"Should still have one event.");
+
+ [self waitForCompletionOf:[writer child:@"f"] setValue:foo_f];
+
+ [self waitForRoundTrip:reader];
+ XCTAssertEqual(snaps.count, (NSUInteger)2, @"Should have gotten another event.");
+ expected = @{
+ @"f": foo_f,
+ @"d": @{@"order": @3, @"foo": @"hello"}
+ };
+ XCTAssertEqualObjects(snaps[1], expected, @"Correct event.");
+}
+
+- (void) testSnapshotsAreIteratedInOrder {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initial = @{
+ @"alex": @{@"nuggets": @60},
+ @"rob": @{@"nuggets": @56},
+ @"vassili": @{@"nuggets": @55.5},
+ @"tony": @{@"nuggets": @52},
+ @"greg": @{@"nuggets": @52}
+ };
+
+ NSArray *expectedOrder = @[@"greg", @"tony", @"vassili", @"rob", @"alex"];
+ NSArray *expectedPrevNames = @[[NSNull null], @"greg", @"tony", @"vassili", @"rob"];
+
+ NSMutableArray *valueOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedPrevNames = [[NSMutableArray alloc] init];
+
+ FIRDatabaseQuery *orderedRef = [ref queryOrderedByChild:@"nuggets"];
+
+ [orderedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [valueOrder addObject:child.key];
+ }
+ }];
+
+ [orderedRef observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [addedOrder addObject:snapshot.key];
+ [addedPrevNames addObject:prevName ? prevName : [NSNull null]];
+ }];
+
+ [ref setValue:initial];
+ WAIT_FOR(addedOrder.count == expectedOrder.count && valueOrder.count == expectedOrder.count);
+
+ XCTAssertEqualObjects(addedOrder, expectedOrder, @"child_added events in correct order.");
+ XCTAssertEqualObjects(addedPrevNames, expectedPrevNames, @"Got correct prevnames for child_added events.");
+ XCTAssertEqualObjects(valueOrder, expectedOrder, @"enumerated snapshot children in correct order.");
+}
+
+- (void) testSnapshotsAreIteratedInOrderForValueIndex {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initial = @{
+ @"alex": @60,
+ @"rob": @56,
+ @"vassili": @55.5,
+ @"tony": @52,
+ @"greg": @52
+ };
+
+ NSArray *expectedOrder = @[@"greg", @"tony", @"vassili", @"rob", @"alex"];
+ NSArray *expectedPrevNames = @[[NSNull null], @"greg", @"tony", @"vassili", @"rob"];
+
+ NSMutableArray *valueOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedPrevNames = [[NSMutableArray alloc] init];
+
+ FIRDatabaseQuery *orderedRef = [ref queryOrderedByValue];
+
+ [orderedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [valueOrder addObject:child.key];
+ }
+ }];
+
+ [orderedRef observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [addedOrder addObject:snapshot.key];
+ [addedPrevNames addObject:prevName ? prevName : [NSNull null]];
+ }];
+
+ [ref setValue:initial];
+ WAIT_FOR(addedOrder.count == expectedOrder.count && valueOrder.count == expectedOrder.count);
+
+ XCTAssertEqualObjects(addedOrder, expectedOrder, @"child_added events in correct order.");
+ XCTAssertEqualObjects(addedPrevNames, expectedPrevNames, @"Got correct prevnames for child_added events.");
+ XCTAssertEqualObjects(valueOrder, expectedOrder, @"enumerated snapshot children in correct order.");
+}
+
+- (void) testFiresChildMovedEvents {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initial = @{
+ @"alex": @{@"nuggets": @60},
+ @"rob": @{@"nuggets": @56},
+ @"vassili": @{@"nuggets": @55.5},
+ @"tony": @{@"nuggets": @52},
+ @"greg": @{@"nuggets": @52}
+ };
+
+ FIRDatabaseQuery *orderedRef = [ref queryOrderedByChild:@"nuggets"];
+
+ __block BOOL moved = NO;
+ [orderedRef observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ moved = YES;
+ XCTAssertEqualObjects(snapshot.key, @"greg", @"");
+ XCTAssertEqualObjects(prevName, @"rob", @"");
+ XCTAssertEqualObjects(snapshot.value, @{@"nuggets" : @57}, @"");
+ }];
+
+ [ref setValue:initial];
+ [[ref child:@"greg/nuggets"] setValue:@57];
+ WAIT_FOR(moved);
+}
+
+- (void) testDefineMultipleIndexesAtALocation {
+ FTupleFirebase *tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *reader = tuple.one, *writer = tuple.two;
+
+ NSDictionary *foo1 = @{
+ @"a" : @{@"order" : @2, @"foo" : @2},
+ @"b" : @{@"order" : @0},
+ @"c" : @{@"order" : @1, @"foo" : @NO},
+ @"d" : @{@"order" : @3, @"foo" : @"hello"}
+ };
+
+
+ [self waitForCompletionOf:writer setValue:foo1];
+
+ FIRDatabaseQuery *fooOrder = [reader queryOrderedByChild:@"foo"];
+ FIRDatabaseQuery *orderOrder = [reader queryOrderedByChild:@"order"];
+ NSMutableArray *fooSnaps = [[NSMutableArray alloc] init];
+ NSMutableArray *orderSnaps = [[NSMutableArray alloc] init];
+
+ [[[fooOrder queryStartingAtValue:nil] queryEndingAtValue:@1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [fooSnaps addObject:snapshot.value];
+ }];
+
+ [[orderOrder queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [orderSnaps addObject:snapshot.value];
+ }];
+
+ WAIT_FOR(fooSnaps.count == 1 && orderSnaps.count == 1);
+
+ NSDictionary *expected = @{
+ @"b": @{@"order": @0},
+ @"c": @{@"order": @1, @"foo": @NO}
+ };
+ XCTAssertEqualObjects(fooSnaps[0], expected, @"");
+
+ expected = @{
+ @"d": @{@"order": @3, @"foo": @"hello"},
+ @"a": @{@"order": @2, @"foo": @2},
+ };
+ XCTAssertEqualObjects(orderSnaps[0], expected, @"");
+
+ [[writer child:@"a"] setValue:@{
+ @"order": @-1, @"foo": @1
+ }];
+
+ WAIT_FOR(fooSnaps.count == 2 && orderSnaps.count == 2);
+
+ expected = @{
+ @"a": @{@"order": @-1, @"foo": @1 },
+ @"b": @{@"order": @0},
+ @"c": @{@"order": @1, @"foo": @NO}
+ };
+ XCTAssertEqualObjects(fooSnaps[1], expected, @"");
+
+ expected = @{
+ @"d": @{@"order": @3, @"foo": @"hello"},
+ @"c": @{@"order": @1, @"foo": @NO}
+ };
+ XCTAssertEqualObjects(orderSnaps[1], expected, @"");
+}
+
+- (void) testCallbackRemovalWorks {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ __block int reads = 0;
+ FIRDatabaseHandle fooHandle, bazHandle;
+ fooHandle = [[ref queryOrderedByChild:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ reads++;
+ }];
+
+ [[ref queryOrderedByChild:@"bar"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ reads++;
+ }];
+
+ bazHandle = [[ref queryOrderedByChild:@"baz"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ reads++;
+ }];
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ reads++;
+ }];
+
+ [self waitForCompletionOf:ref setValue:@1];
+
+ XCTAssertEqual(reads, 4, @"");
+
+ [ref removeObserverWithHandle:fooHandle];
+ [self waitForCompletionOf:ref setValue:@2];
+ XCTAssertEqual(reads, 7, @"");
+
+ // should be a no-op, resulting in 3 more reads.
+ [[ref queryOrderedByChild:@"foo"] removeObserverWithHandle:bazHandle];
+ [self waitForCompletionOf:ref setValue:@3];
+ XCTAssertEqual(reads, 10, @"");
+
+ [[ref queryOrderedByChild:@"bar"] removeAllObservers];
+ [self waitForCompletionOf:ref setValue:@4];
+ XCTAssertEqual(reads, 12, @"");
+
+ // Now, remove everything.
+ [ref removeAllObservers];
+ [self waitForCompletionOf:ref setValue:@5];
+ XCTAssertEqual(reads, 12, @"");
+}
+
+- (void) testChildAddedEventsAreInTheCorrectOrder {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initial = @{
+ @"a": @{@"value": @5},
+ @"c": @{@"value": @3}
+ };
+
+ NSMutableArray *added = [[NSMutableArray alloc] init];
+ [[ref queryOrderedByChild:@"value"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [added addObject:snapshot.key];
+ }];
+ [ref setValue:initial];
+
+ WAIT_FOR(added.count == 2);
+ NSArray *expected = @[@"c", @"a"];
+ XCTAssertEqualObjects(added, expected, @"");
+
+ [ref updateChildValues:@{
+ @"b": @{@"value": @4},
+ @"d": @{@"value": @2}
+ }];
+
+ WAIT_FOR(added.count == 4);
+ expected = @[@"c", @"a", @"d", @"b"];
+ XCTAssertEqualObjects(added, expected, @"");
+}
+
+- (void) testCanUseKeyIndex {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *data = @{
+ @"a": @{ @".priority": @10, @".value": @"a" },
+ @"b": @{ @".priority": @5, @".value": @"b" },
+ @"c": @{ @".priority": @20, @".value": @"c" },
+ @"d": @{ @".priority": @7, @".value": @"d" },
+ @"e": @{ @".priority": @30, @".value": @"e" },
+ @"f": @{ @".priority": @8, @".value": @"f" }
+ };
+
+ [self waitForCompletionOf:ref setValue:data];
+
+ __block BOOL valueDone = NO;
+ [[[ref queryOrderedByKey] queryStartingAtValue:@"c"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSMutableArray *keys = [[NSMutableArray alloc] init];
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [keys addObject:child.key];
+ }
+ NSArray *expected = @[@"c", @"d", @"e", @"f"];
+ XCTAssertEqualObjects(keys, expected, @"");
+ valueDone = YES;
+ }];
+ WAIT_FOR(valueDone);
+
+ NSMutableArray *keys = [[NSMutableArray alloc] init];
+ [[[ref queryOrderedByKey] queryLimitedToLast:5] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [keys addObject:child.key];
+ }
+ }];
+
+ WAIT_FOR(keys.count == 5);
+ NSArray *expected = @[@"b", @"c", @"d", @"e", @"f"];
+ XCTAssertEqualObjects(keys, expected, @"");
+}
+
+- (void) testQueriesWorkOnLeafNodes {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ [self waitForCompletionOf:ref setValue:@"leaf-node"];
+
+ __block BOOL valueDone = NO;
+ [[[ref queryOrderedByChild:@"foo"] queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqual(snapshot.value, [NSNull null]);
+ valueDone = YES;
+ }];
+ WAIT_FOR(valueDone);
+}
+
+- (void) testUpdatesForUnindexedQuery {
+ FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *reader = refs.one;
+ FIRDatabaseReference *writer = refs.two;
+
+ __block BOOL done = NO;
+ NSDictionary *value = @{ @"one": @{ @"index": @1, @"value": @"one" },
+ @"two": @{ @"index": @2, @"value": @"two" },
+ @"three": @{ @"index": @3, @"value": @"three" } };
+ [writer setValue:value withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ done = NO;
+
+ NSMutableArray *snapshots = [NSMutableArray array];
+
+ [[[reader queryOrderedByChild:@"index"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [snapshots addObject:snapshot.value];
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ NSDictionary *expected = @{ @"two": @{ @"index": @2, @"value": @"two" },
+ @"three": @{ @"index": @3, @"value": @"three" } };
+
+ XCTAssertEqual(snapshots.count, (NSUInteger)1);
+ XCTAssertEqualObjects(snapshots[0], expected);
+
+ done = NO;
+ [[writer child:@"one/index"] setValue:@4];
+
+ WAIT_FOR(done);
+
+ expected = @{ @"one": @{ @"index": @4, @"value": @"one" },
+ @"three": @{ @"index": @3, @"value": @"three" } };
+ XCTAssertEqual(snapshots.count, (NSUInteger)2);
+ XCTAssertEqualObjects(snapshots[1], expected);
+}
+
+- (void) testServerRespectsKeyIndex {
+ FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *writer = refs.one;
+ FIRDatabaseReference *reader = refs.two;
+
+ NSDictionary *initial = @{
+ @"a": @1,
+ @"b": @2,
+ @"c": @3
+ };
+
+ // If the server doesn't respect the index, it will send down limited data, but with no offset, so the expected
+ // and actual data don't match
+ FIRDatabaseQuery *query = [[[reader queryOrderedByKey] queryStartingAtValue:@"b"] queryLimitedToFirst:2];
+
+ NSArray *expectedChildren = @[@"b", @"c"];
+
+ [self waitForCompletionOf:writer setValue:initial];
+
+ NSMutableArray *children = [[NSMutableArray alloc] init];
+
+ __block BOOL done = NO;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [children addObject:child.key];
+ }
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ XCTAssertEqualObjects(expectedChildren, children, @"Got correct children");
+}
+
+- (void) testServerRespectsValueIndex {
+ FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *writer = refs.one;
+ FIRDatabaseReference *reader = refs.two;
+
+ NSDictionary *initial = @{
+ @"a": @1,
+ @"c": @2,
+ @"b": @3
+ };
+
+ // If the server doesn't respect the index, it will send down limited data, but with no offset, so the expected
+ // and actual data don't match
+ FIRDatabaseQuery *query = [[[reader queryOrderedByValue] queryStartingAtValue:@2] queryLimitedToFirst:2];
+
+ NSArray *expectedChildren = @[@"c", @"b"];
+
+ [self waitForCompletionOf:writer setValue:initial];
+
+ NSMutableArray *children = [[NSMutableArray alloc] init];
+
+ __block BOOL done = NO;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [children addObject:child.key];
+ }
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ XCTAssertEqualObjects(expectedChildren, children, @"Got correct children");
+}
+
+- (void) testDeepUpdatesWorkWithQueries {
+ FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *writer = refs.one;
+ FIRDatabaseReference *reader = refs.two;
+
+
+ NSDictionary *initial = @{@"a": @{@"data": @"foo",
+ @"idx": @YES},
+ @"b": @{@"data": @"bar",
+ @"idx": @YES},
+ @"c": @{@"data": @"baz",
+ @"idx": @NO}};
+ [self waitForCompletionOf:writer setValue:initial];
+
+ FIRDatabaseQuery *query = [[reader queryOrderedByChild:@"idx"] queryEqualToValue:@YES];
+
+ NSDictionary* expected = @{@"a": @{@"data": @"foo",
+ @"idx": @YES},
+ @"b": @{@"data": @"bar",
+ @"idx": @YES}};
+
+ [self waitForExportValueOf:query toBe:expected];
+
+ NSDictionary *update = @{@"a/idx": @NO,
+ @"b/data": @"blah",
+ @"c/idx": @YES};
+ [self waitForCompletionOf:writer updateChildValues:update];
+
+ expected = @{@"b": @{@"data": @"blah",
+ @"idx": @YES},
+ @"c": @{@"data": @"baz",
+ @"idx": @YES}};
+ [self waitForExportValueOf:query toBe:expected];
+}
+
+- (void) testServerRespectsDeepIndex {
+ FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *writer = refs.one;
+ FIRDatabaseReference *reader = refs.two;
+
+
+ NSDictionary *initial = @{
+ @"a": @{@"deep":@{@"index":@1}},
+ @"c": @{@"deep":@{@"index":@2}},
+ @"b": @{@"deep":@{@"index":@3}}
+ };
+
+ // If the server doesn't respect the index, it will send down limited data, but with no offset, so the expected
+ // and actual data don't match
+ FIRDatabaseQuery *query = [[[reader queryOrderedByChild:@"deep/index"] queryStartingAtValue:@2] queryLimitedToFirst:2];
+
+ NSArray *expectedChildren = @[@"c", @"b"];
+
+ [self waitForCompletionOf:writer setValue:initial];
+
+ NSMutableArray *children = [[NSMutableArray alloc] init];
+
+ __block BOOL done = NO;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [children addObject:child.key];
+ }
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ XCTAssertEqualObjects(expectedChildren, children, @"Got correct children");
+}
+
+- (void) testStartAtEndAtWorksWithValueIndex {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initial = @{
+ @"alex": @60,
+ @"rob": @56,
+ @"vassili": @55.5,
+ @"tony": @52,
+ @"greg": @52
+ };
+
+ NSArray *expectedOrder = @[@"tony", @"vassili", @"rob"];
+ NSArray *expectedPrevNames = @[[NSNull null], @"tony", @"vassili"];
+
+ NSMutableArray *valueOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedPrevNames = [[NSMutableArray alloc] init];
+
+ FIRDatabaseQuery *orderedRef = [[[ref queryOrderedByValue] queryStartingAtValue:@52 childKey:@"tony"] queryEndingAtValue:@59];
+
+ [orderedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [valueOrder addObject:child.key];
+ }
+ }];
+
+ [orderedRef observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [addedOrder addObject:snapshot.key];
+ [addedPrevNames addObject:prevName ? prevName : [NSNull null]];
+ }];
+
+ [ref setValue:initial];
+ WAIT_FOR(addedOrder.count == expectedOrder.count && valueOrder.count == expectedOrder.count);
+
+ XCTAssertEqualObjects(addedOrder, expectedOrder, @"child_added events in correct order.");
+ XCTAssertEqualObjects(addedPrevNames, expectedPrevNames, @"Got correct prevnames for child_added events.");
+ XCTAssertEqualObjects(valueOrder, expectedOrder, @"enumerated snapshot children in correct order.");
+}
+
+- (void) testRemovingDefaultListenerRemovesNonDefaultListenWithLoadsAllData {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initialData = @{ @"key": @"value" };
+ [self waitForCompletionOf:ref setValue:initialData];
+
+ [[ref queryOrderedByKey] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+
+ // Should remove both listener and should remove the listen sent to the server
+ [ref removeAllObservers];
+
+ __block id result = nil;
+ // This used to crash because a listener for [ref queryOrderedByKey] existed already
+ [[ref queryOrderedByKey] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ result = snapshot.value;
+ }];
+
+ WAIT_FOR(result);
+ XCTAssertEqualObjects(result, initialData);
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FPersist.h b/Example/Database/Tests/Integration/FPersist.h
new file mode 100644
index 0000000..5bdfff5
--- /dev/null
+++ b/Example/Database/Tests/Integration/FPersist.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 <Foundation/Foundation.h>
+#import "FTestBase.h"
+
+@interface FPersist : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FPersist.m b/Example/Database/Tests/Integration/FPersist.m
new file mode 100644
index 0000000..2326e08
--- /dev/null
+++ b/Example/Database/Tests/Integration/FPersist.m
@@ -0,0 +1,489 @@
+/*
+ * 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/XCTest.h>
+#import <Foundation/Foundation.h>
+#import "FPersist.h"
+#import "FIRDatabaseReference.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FRepo_Private.h"
+#import "FTestHelpers.h"
+#import "FDevice.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@implementation FPersist
+
+- (void) setUp {
+ [super setUp];
+
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+
+ NSString *baseDir = [FPersist getFirebaseDir];
+ // HACK: We want to clean up old persistence files from previous test runs, but on OSX, baseDir is going to be something
+ // like /Users/michael/Documents/firebase, and we probably shouldn't blindly delete it, since somebody might have actual
+ // documents there. We should probably change the directory where we store persistence on OSX to .firebase or something
+ // to avoid colliding with real files, but for now, we'll leave it and just manually delete each of the /0, /1, /2, etc.
+ // directories that may exist from previous test runs. As of now (2014/09/07), these directories only go up to ~50, but
+ // if we add a ton more tests, we may need to increase the 100. But I'm guessing we'll rewrite persistence and move the
+ // persistence folder before then though.
+ for(int i = 0; i < 100; i++) {
+ // TODO: This hack is uneffective because the format now follows different rules. Persistence really needs a purge
+ // option
+ NSString *dir = [NSString stringWithFormat:@"%@/%d", baseDir, i];
+ if ([fileManager fileExistsAtPath:dir]) {
+ NSError *error;
+ [[NSFileManager defaultManager] removeItemAtPath:dir error:&error];
+ if (error) {
+ XCTFail(@"Failed to clear persisted data at %@: %@", dir, error);
+ }
+ }
+ }
+}
+
+- (void) testSetIsResentAfterRestart {
+ FIRDatabaseReference *readerRef = [FTestHelpers getRandomNode];
+ NSString *url = [readerRef description];
+ FDevice* device = [[FDevice alloc] initOfflineWithUrl:url];
+
+ // Monitor the data at this location.
+ __block FIRDataSnapshot *readSnapshot = nil;
+ [readerRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ readSnapshot = snapshot;
+ }];
+
+ // Do some sets while offline and then "kill" the app, so it doesn't get sent to Firebase.
+ [device do:^(FIRDatabaseReference *ref) {
+ [ref setValue:@{ @"a": @42, @"b": @3.1415, @"c": @"hello", @"d": @{ @"dd": @"dd-val", @".priority": @"d-pri"} }];
+ [[ref child:@"a"] setValue:@"a-val"];
+ [[ref child:@"c"] setPriority:@"c-pri"];
+ [ref updateChildValues:@{ @"b": @"b-val"}];
+ }];
+
+ // restart and wait for "idle" (so all pending puts should have been sent).
+ [device restartOnline];
+ [device waitForIdleUsingWaiter:self];
+
+ // Pending sets should have gone through.
+ id expected = @{
+ @"a": @"a-val",
+ @"b": @"b-val",
+ @"c": @{ @".value": @"hello", @".priority": @"c-pri" },
+ @"d": @{ @"dd": @"dd-val", @".priority": @"d-pri" }
+ };
+ [self waitForExportValueOf:readerRef toBe:expected];
+
+ // Set the value to something else (12).
+ [readerRef setValue:@12];
+
+ // "restart" the app again and make sure it doesn't set it to 42 again.
+ [device restartOnline];
+ [device waitForIdleUsingWaiter:self];
+
+ // Make sure data is still 12.
+ [self waitForRoundTrip:readerRef];
+ XCTAssertEqual(readSnapshot.value, @12, @"Read data should still be 12.");
+ [device dispose];
+}
+
+- (void) testSetIsReappliedAfterRestart {
+ FDevice* device = [[FDevice alloc] initOffline];
+
+ // Do some sets while offline and then "kill" the app, so it doesn't get sent to Firebase.
+ [device do:^(FIRDatabaseReference *ref) {
+ [ref setValue:@{ @"a": @42, @"b": @3.1415, @"c": @"hello" }];
+ [[ref child:@"a"] setValue:@"a-val"];
+ [[ref child:@"c"] setPriority:@"c-pri"];
+ [ref updateChildValues:@{ @"b": @"b-val"}];
+ }];
+
+ // restart the app offline and observe the data.
+ [device restartOffline];
+
+ // Pending sets should be visible
+ id expected = @{
+ @"a": @"a-val",
+ @"b": @"b-val",
+ @"c": @{ @".value": @"hello", @".priority": @"c-pri" }
+ };
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForExportValueOf:ref toBe:expected];
+ }];
+ [device dispose];
+}
+
+- (void) testServerDataCachedOffline1 {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+ __block BOOL done = NO;
+ id data = @{@"a": @1, @"b": @2};
+ [writerRef setValue:data withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ // Wait for the data to get it cached.
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:ref toBe:data];
+ }];
+
+ // Should still be there after restart, offline.
+ [device restartOffline];
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:ref toBe:data];
+ }];
+
+ // Children should be there too.
+ [device restartOffline];
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:[ref child:@"a"] toBe:@1];
+ }];
+ [device dispose];
+}
+
+- (void) testServerDataCompleteness1 {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+ id data = @{@"child": @{@"a": @1, @"b": @2 }, @"other": @"blah"};
+ [self waitForCompletionOf:writerRef setValue:data];
+
+ // Wait for each child to get it cached (but not the parent).
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:[ref child:@"child/a"] toBe:@1];
+ [self waitForValueOf:[ref child:@"child/b"] toBe:@2];
+ [self waitForValueOf:[ref child:@"other"] toBe:@"blah"];
+ }];
+
+ // Restart, offline, should get child_added events, but not value.
+ [device restartOffline];
+ __block BOOL gotA, gotB;
+ [device do:^(FIRDatabaseReference *ref) {
+ FIRDatabaseReference *childRef = [ref child:@"child"];
+ [childRef observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([snapshot.key isEqualToString:@"a"]) {
+ XCTAssertEqualObjects(snapshot.value, @1, @"Got a");
+ gotA = YES;
+ } else if ([snapshot.key isEqualToString:@"b"]) {
+ XCTAssertEqualObjects(snapshot.value, @2, @"Got a");
+ gotB = YES;
+ } else {
+ XCTFail(@"Unexpected child event.");
+ }
+ }];
+
+ // Listen for value events (which we should *not* get).
+ [childRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTFail(@"Got a value event with incomplete data!");
+ }];
+
+ // Wait for another location just to make sure we wait long enough that we /would/ get a value event if it
+ // was coming.
+ [self waitForValueOf:[ref child:@"other"] toBe:@"blah"];
+ }];
+
+ XCTAssertTrue(gotA && gotB, @"Got a and b.");
+ [device dispose];
+}
+
+- (void) testServerDataCompleteness2 {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+ id data = @{@"a": @1, @"b": @2};
+ [self waitForCompletionOf:writerRef setValue:data];
+
+ // Wait for the children individually.
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:[ref child:@"a"] toBe:@1];
+ [self waitForValueOf:[ref child:@"b"] toBe:@2];
+ }];
+
+ // Should still be there after restart, offline.
+ [device restartOffline];
+ [device do:^(FIRDatabaseReference *ref) {
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // No-op. Just triggering a listen at this location.
+ }];
+ [self waitForValueOf:[ref child:@"a"] toBe:@1];
+ [self waitForValueOf:[ref child:@"b"] toBe:@2];
+ }];
+ [device dispose];
+}
+
+- (void)testServerDataLimit {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+ [self waitForCompletionOf:writerRef setValue:@{@"a": @1, @"b": @2, @"c": @3}];
+
+ // Cache limit(2) of the data.
+ [device do:^(FIRDatabaseReference *ref) {
+ FIRDatabaseQuery *limitRef = [ref queryLimitedToLast:2];
+ [self waitForValueOf:limitRef toBe:@{@"b": @2, @"c": @3 }];
+ }];
+
+ // We should be able to get limit(2) data offline, but not the whole node.
+ [device restartOffline];
+ [device do:^(FIRDatabaseReference *ref) {
+ [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTFail(@"Got value event for whole node!");
+ }];
+
+ FIRDatabaseQuery *limitRef = [ref queryLimitedToLast:2];
+ [self waitForValueOf:limitRef toBe:@{@"b": @2, @"c": @3 }];
+ }];
+ [device dispose];
+}
+
+- (void)testRemoveWhileOfflineAndRestart {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+
+ [[writerRef child:@"test"] setValue:@"test"];
+ [device do:^(FIRDatabaseReference *ref) {
+ // Cache this location.
+ __block id val = nil;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ val = snapshot.value;
+ }];
+ [self waitUntil:^BOOL {
+ return [val isEqual:@{@"test": @"test"}];
+ }];
+ }];
+ [device restartOffline];
+
+ __block BOOL done = NO;
+ [writerRef removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ [device goOnline];
+ [device waitForIdleUsingWaiter:self];
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:ref toBe:[NSNull null]];
+ }];
+ [device dispose];
+}
+
+
+- (void)testDeltaSyncAfterRestart {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+
+ [writerRef setValue:@"test"];
+
+ [device do:^(FIRDatabaseReference *ref) {
+ // Cache this location.
+ __block id val = nil;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ val = snapshot.value;
+ }];
+ [self waitUntil:^BOOL {
+ return [val isEqual:@"test"];
+ }];
+ XCTAssertEqual(ref.repo.dataUpdateCount, 1L, @"Should have gotten one update.");
+ }];
+ [device restartOnline];
+
+ [device waitForIdleUsingWaiter:self];
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:ref toBe:@"test"];
+ XCTAssertEqual(ref.repo.dataUpdateCount, 0L, @"Should have gotten no updates.");
+ }];
+ [device dispose];
+}
+
+- (void)testDeltaSyncWorksWithUnfilteredQuery {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+
+ // List must be large enough to trigger delta sync.
+ NSMutableDictionary *longList = [[NSMutableDictionary alloc] init];
+ for(NSInteger i = 0; i < 50; i++) {
+ NSString *key = [[writerRef childByAutoId] key];
+ longList[key] = @{ @"order": @1, @"text": @"This is an awesome message!" };
+ }
+
+ [writerRef setValue:longList];
+
+ [device do:^(FIRDatabaseReference *ref) {
+ // Cache this location.
+ [self waitForValueOf:[ref queryOrderedByChild:@"order"] toBe:longList];
+ XCTAssertEqual(ref.repo.dataUpdateCount, 1L, @"Should have gotten one update.");
+ }];
+ [device restartOffline];
+
+ // Add a new child while the device is offline.
+ FIRDatabaseReference *newChildRef = [writerRef childByAutoId];
+ NSDictionary *newChild = @{ @"order": @50, @"text": @"This is a new appended child!" };
+
+ [self waitForCompletionOf:newChildRef setValue:newChild];
+ longList[[newChildRef key]] = newChild;
+
+ [device goOnline];
+ [device do:^(FIRDatabaseReference *ref) {
+ // Wait for updated value with new child.
+ [self waitForValueOf:[ref queryOrderedByChild:@"order"] toBe:longList];
+ XCTAssertEqual(ref.repo.rangeMergeUpdateCount, 1L, @"Should have gotten a range merge update.");
+ }];
+ [device dispose];
+}
+
+- (void) testPutsAreRestoredInOrder {
+ FDevice *device = [[FDevice alloc] initOffline];
+
+ // Store puts which should have a putId with 10 which is lexiographical small than 9
+ [device do:^(FIRDatabaseReference *ref) {
+ for (int i = 0; i < 11; i++) {
+ [ref setValue:[NSNumber numberWithInt:i]];
+ }
+ }];
+
+ // restart the app offline and observe the data.
+ [device restartOffline];
+
+ // Make sure that the write with putId 10 wins, not 9
+ id expected = @10;
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForExportValueOf:ref toBe:expected];
+ }];
+ [device dispose];
+}
+
+- (void) testStoreSetsPerf1 {
+ if (!runPerfTests) return;
+ // Disable persistence in FDevice for comparison without persistence
+ FDevice *device = [[FDevice alloc] initOnline];
+
+ __block BOOL done = NO;
+ [device do:^(FIRDatabaseReference *ref) {
+ NSDate *start = [NSDate date];
+ [self writeChildren:ref count:1000 size:100 waitForComplete:NO];
+
+ [self waitForQueue:ref];
+
+ NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]);
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ [device dispose];
+}
+
+- (void) testStoreListenPerf1 {
+ if (!runPerfTests) return;
+ // Disable persistence in FDevice for comparison without persistence
+
+ // Write 1000 x 100-byte children, to read back.
+ unsigned int count = 1000;
+ FIRDatabaseReference *writer = [FTestHelpers getRandomNode];
+ [self writeChildren:writer count:count size:100];
+
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writer description]];
+
+ __block BOOL done = NO;
+ [device do:^(FIRDatabaseReference *ref) {
+ NSDate *start = [NSDate date];
+ [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // Wait to make sure we're done persisting everything.
+ [self waitForQueue:ref];
+ XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data.");
+ NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]);
+ done = YES;
+ }];
+ }];
+
+ WAIT_FOR(done);
+ [device dispose];
+}
+
+- (void) testRestoreListenPerf1 {
+ if (!runPerfTests) return;
+
+ // NOTE: Since this is testing restoration of data from cache after restarting, it only works with persistence on.
+
+ // Write 1000 * 100-byte children, to read back.
+ unsigned int count = 1000;
+ FIRDatabaseReference *writer = [FTestHelpers getRandomNode];
+ [self writeChildren:writer count:count size:100];
+
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writer description]];
+
+ // Get the data cached.
+ __block BOOL done = NO;
+ [device do:^(FIRDatabaseReference *ref) {
+ [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data.");
+ done = YES;
+ }];
+ }];
+ WAIT_FOR(done);
+
+ // Restart offline and see how long it takes to restore the data from cache.
+ [device restartOffline];
+ done = NO;
+ [device do:^(FIRDatabaseReference *ref) {
+ NSDate *start = [NSDate date];
+ [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // Wait to make sure we're done persisting everything.
+ XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data.");
+ [self waitForQueue:ref];
+ NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]);
+ done = YES;
+ }];
+ }];
+
+ WAIT_FOR(done);
+ [device dispose];
+}
+
+- (void)writeChildren:(FIRDatabaseReference *)writer count:(unsigned int)count size:(unsigned int)size {
+ [self writeChildren:writer count:count size:size waitForComplete:YES];
+}
+
+- (void)writeChildren:(FIRDatabaseReference *)writer count:(unsigned int)count size:(unsigned int)size waitForComplete:(BOOL)waitForComplete {
+ __block BOOL done = NO;
+
+ NSString *data = [self randomStringOfLength:size];
+ for(int i = 0; i < count; i++) {
+ [[writer childByAutoId] setValue:data withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ if (i == (count - 1)) {
+ done = YES;
+ }
+ }];
+ }
+ if (waitForComplete) {
+ WAIT_FOR(done);
+ }
+}
+
+NSString *letters = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+- (NSString*) randomStringOfLength:(unsigned int)len {
+ NSMutableString *randomString = [NSMutableString stringWithCapacity: len];
+
+ for (int i=0; i<len; i++) {
+ [randomString appendFormat: @"%C", [letters characterAtIndex: arc4random() % [letters length]]];
+ }
+ return randomString;
+}
+
++ (NSString *) getFirebaseDir {
+ NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDir = [dirPaths objectAtIndex:0];
+ NSString *firebaseDir = [documentsDir stringByAppendingPathComponent:@"firebase"];
+
+ return firebaseDir;
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FRealtime.h b/Example/Database/Tests/Integration/FRealtime.h
new file mode 100644
index 0000000..903ef49
--- /dev/null
+++ b/Example/Database/Tests/Integration/FRealtime.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 <XCTest/XCTest.h>
+#import "FTestBase.h"
+
+@interface FRealtime : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FRealtime.m b/Example/Database/Tests/Integration/FRealtime.m
new file mode 100644
index 0000000..e554bfe
--- /dev/null
+++ b/Example/Database/Tests/Integration/FRealtime.m
@@ -0,0 +1,605 @@
+/*
+ * 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 "FRealtime.h"
+#import "FTupleFirebase.h"
+#import "FRepoManager.h"
+#import "FUtilities.h"
+#import "FParsedUrl.h"
+#import "FIRDatabaseConfig_Private.h"
+
+@implementation FRealtime
+
+- (void) testUrlParsing {
+ FParsedUrl* parsed = [FUtilities parseUrl:@"http://www.example.com:9000"];
+ XCTAssertTrue([[parsed.path description] isEqualToString:@"/"], @"Got correct path");
+ XCTAssertTrue([parsed.repoInfo.host isEqualToString:@"www.example.com:9000"], @"Got correct host");
+ XCTAssertTrue([parsed.repoInfo.internalHost isEqualToString:@"www.example.com:9000"], @"Got correct host");
+ XCTAssertFalse(parsed.repoInfo.secure, @"Should not be secure, there's a port");
+
+ parsed = [FUtilities parseUrl:@"http://www.firebaseio.com/foo/bar"];
+ XCTAssertTrue([[parsed.path description] isEqualToString:@"/foo/bar"], @"Got correct path");
+ XCTAssertTrue([parsed.repoInfo.host isEqualToString:@"www.firebaseio.com"], @"Got correct host");
+ XCTAssertTrue([parsed.repoInfo.internalHost isEqualToString:@"www.firebaseio.com"], @"Got correct host");
+ XCTAssertTrue(parsed.repoInfo.secure, @"Should be secure, there's no port");
+}
+
+- (void) testCachingRedirects {
+ NSString* host = @"host.example.com";
+ NSString* host2 = @"host2.example.com";
+ NSString* internalHost = @"internal.example.com";
+ NSString* internalHost2 = @"internal2.example.com";
+
+ // Set host on first repo info
+ FRepoInfo* repoInfo = [[FRepoInfo alloc] initWithHost:host isSecure:YES withNamespace:host];
+ XCTAssertTrue([repoInfo.host isEqualToString:host], @"Got correct host");
+ XCTAssertTrue([repoInfo.internalHost isEqualToString:host], @"Got correct host");
+
+ // Set internal host on first repo info
+ repoInfo.internalHost = internalHost;
+ XCTAssertTrue([repoInfo.host isEqualToString:host], @"Got correct host");
+ XCTAssertTrue([repoInfo.internalHost isEqualToString:internalHost], @"Got correct host");
+
+ // Set up a second unrelated repo info to make sure caching is keyspaced properly
+ FRepoInfo* repoInfo2 = [[FRepoInfo alloc] initWithHost:host2 isSecure:YES withNamespace:host2];
+ XCTAssertTrue([repoInfo2.host isEqualToString:host2], @"Got correct host");
+ XCTAssertTrue([repoInfo2.internalHost isEqualToString:host2], @"Got correct host");
+
+ repoInfo2.internalHost = internalHost2;
+ XCTAssertTrue([repoInfo2.internalHost isEqualToString:internalHost2], @"Got correct host");
+
+ // Setting host on this repo info should also set the right internal host
+ FRepoInfo* repoInfoCached = [[FRepoInfo alloc] initWithHost:host isSecure:YES withNamespace:host];
+ XCTAssertTrue([repoInfoCached.host isEqualToString:host], @"Got correct host");
+ XCTAssertTrue([repoInfoCached.internalHost isEqualToString:internalHost], @"Got correct host");
+
+ [repoInfo clearInternalHostCache];
+ [repoInfo2 clearInternalHostCache];
+ [repoInfoCached clearInternalHostCache];
+
+ XCTAssertTrue([repoInfo.internalHost isEqualToString:host], @"Got correct host");
+ XCTAssertTrue([repoInfo2.internalHost isEqualToString:host2], @"Got correct host");
+ XCTAssertTrue([repoInfoCached.internalHost isEqualToString:host], @"Got correct host");
+}
+
+- (void) testOnDisconnectSetWorks {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"];
+
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+ FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key];
+
+ __block NSNumber* readValue = @0;
+ __block NSNumber* writeValue = @0;
+ [[reader child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSNumber *val = [snapshot value];
+ if (![val isEqual:[NSNull null]]) {
+ readValue = val;
+ }
+ }];
+
+ [[writer child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ if (val != [NSNull null]) {
+ writeValue = val;
+ }
+ }];
+
+ [writer child:@"hello"];
+
+ __block BOOL ready = NO;
+ [[writer child:@"disconnected"] onDisconnectSetValue:@1 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref){
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [writer child:@"s"];
+
+ ready = NO;
+ [[writer child:@"disconnected"] onDisconnectSetValue:@2 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref){
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return [@2 isEqualToNumber:readValue] && [@2 isEqualToNumber:writeValue];
+ }];
+
+ [FRepoManager interrupt:readerCfg];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+ [FRepoManager disposeRepos:readerCfg];
+}
+
+- (void) testOnDisconnectSetWithPriorityWorks {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"];
+
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+ FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key];
+
+ __block BOOL sawNewValue = NO;
+ __block BOOL writerSawNewValue = NO;
+ [[reader child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = snapshot.value;
+ id pri = snapshot.priority;
+ if (val != [NSNull null] && pri != [NSNull null]) {
+ sawNewValue = [(NSNumber *) val boolValue] && [pri isEqualToString:@"abcd"];
+ }
+ }];
+
+ [[writer child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ id pri = snapshot.priority;
+ if (val != [NSNull null] && pri != [NSNull null]) {
+ writerSawNewValue = [(NSNumber *) val boolValue] && [pri isEqualToString:@"abcd"];
+ }
+ }];
+
+ __block BOOL ready = NO;
+ [[writer child:@"disconnected"] onDisconnectSetValue:@YES andPriority:@"abcd" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return sawNewValue && writerSawNewValue;
+ }];
+
+ [FRepoManager interrupt:readerCfg];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+ [FRepoManager disposeRepos:readerCfg];
+}
+
+- (void) testOnDisconnectRemoveWorks {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"];
+
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+ FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key];
+
+ __block BOOL ready = NO;
+ [[writer child:@"foo"] setValue:@"bar" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ __block BOOL sawRemove = NO;
+ __block BOOL writerSawRemove = NO;
+ [[reader child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ sawRemove = [[NSNull null] isEqual:snapshot.value];
+ }];
+
+ [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ writerSawRemove = [[NSNull null] isEqual:snapshot.value];
+ }];
+
+ ready = NO;
+ [[writer child:@"foo"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return sawRemove && writerSawRemove;
+ }];
+
+ [FRepoManager interrupt:readerCfg];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+ [FRepoManager disposeRepos:readerCfg];
+}
+
+- (void) testOnDisconnectUpdateWorks {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"];
+
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+ FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key];
+
+ [self waitForCompletionOf:[writer child:@"foo"] setValue:@{@"bar": @"a", @"baz": @"b"}];
+
+ __block BOOL sawNewValue = NO;
+ __block BOOL writerSawNewValue = NO;
+ [[reader child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *val = [snapshot value];
+ if (val) {
+ sawNewValue = [@{@"bar" : @"a", @"baz" : @"c", @"bat" : @"d"} isEqualToDictionary:val];
+ }
+ }];
+
+ [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *val = [snapshot value];
+ if (val) {
+ writerSawNewValue = [@{@"bar" : @"a", @"baz" : @"c", @"bat" : @"d"} isEqualToDictionary:val];
+ }
+ }];
+
+ __block BOOL ready = NO;
+ [[writer child:@"foo"] onDisconnectUpdateChildValues:@{@"baz": @"c", @"bat": @"d"} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return sawNewValue && writerSawNewValue;
+ }];
+
+ [FRepoManager interrupt:readerCfg];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+ [FRepoManager disposeRepos:readerCfg];
+}
+
+- (void) testOnDisconnectTriggersSingleLocalValueEventForWriter {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+
+ __block int calls = 0;
+ [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ calls++;
+ if (calls == 2) {
+ // second call, verify the data
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"foo" : @{@"bar" : @"a", @"bam" : @"c"}};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one");
+ } else if (calls > 2) {
+ XCTFail(@"Extra calls");
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return calls == 1;
+ }];
+
+ __block BOOL done = NO;
+ FIRDatabaseReference * child = [writer child:@"foo"];
+ [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}];
+ [child onDisconnectUpdateChildValues:@{@"bam": @"c"}];
+ [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return calls == 2;
+ }];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+- (void) testOnDisconnectTriggersSingleLocalValueEventForReader {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * reader = [FTestHelpers getRandomNode];
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] child:reader.key];
+
+ __block int calls = 0;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ calls++;
+ if (calls == 2) {
+ // second call, verify the data
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"foo" : @{@"bar" : @"a", @"bam" : @"c"}};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one");
+ } else if (calls > 2) {
+ XCTFail(@"Extra calls");
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return calls == 1;
+ }];
+
+ __block BOOL done = NO;
+ FIRDatabaseReference * child = [writer child:@"foo"];
+ [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}];
+ [child onDisconnectUpdateChildValues:@{@"bam": @"c"}];
+ [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return calls == 2;
+ }];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+- (void) testOnDisconnectTriggersSingleLocalValueEventForWriterWithQuery {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+
+ __block int calls = 0;
+ [[[writer child:@"foo"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ calls++;
+ if (calls == 2) {
+ // second call, verify the data
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"bar" : @"a", @"bam" : @"c"};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one");
+ } else if (calls > 2) {
+ XCTFail(@"Extra calls");
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return calls == 1;
+ }];
+
+ __block BOOL done = NO;
+ FIRDatabaseReference * child = [writer child:@"foo"];
+ [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}];
+ [child onDisconnectUpdateChildValues:@{@"bam": @"c"}];
+ [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return calls == 2;
+ }];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+- (void) testOnDisconnectTriggersSingleLocalValueEventForReaderWithQuery {
+ FIRDatabaseReference * reader = [FTestHelpers getRandomNode];
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] child:reader.key];
+
+ __block int calls = 0;
+ [[[reader child:@"foo"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ calls++;
+ XCTAssertTrue([snapshot.key isEqualToString:@"foo"], @"Got the right snapshot");
+ if (calls == 2) {
+ // second call, verify the data
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"bar" : @"a", @"bam" : @"c"};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one");
+ } else if (calls > 2) {
+ XCTFail(@"Extra calls");
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return calls == 1;
+ }];
+
+ __block BOOL done = NO;
+ FIRDatabaseReference * child = [writer child:@"foo"];
+ [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}];
+ [child onDisconnectUpdateChildValues:@{@"bam": @"c"}];
+ [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return calls == 2;
+ }];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+- (void) testOnDisconnectDeepMergeTriggersOnlyOneValueEventForReaderWithQuery {
+ FIRDatabaseReference * reader = [FTestHelpers getRandomNode];
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+
+ __block BOOL done = NO;
+ NSDictionary* toSet = @{@"a": @1, @"b": @{@"c": @YES, @"d": @"scalar", @"e": @{@"f": @"hooray"}}};
+ [writer setValue:toSet];
+ [[writer child:@"a"] onDisconnectSetValue:@2];
+ [[writer child:@"b/d"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ __block int count = 2;
+ [[reader queryLimitedToLast:3] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ count++;
+ if (count == 1) {
+ // Loaded the data, kill the writer connection
+ [FRepoManager interrupt:writerCfg];
+ } else if (count == 2) {
+ NSDictionary *expected = @{@"a" : @2, @"b" : @{@"c" : @YES, @"e" : @{@"f" : @"hooray"}}};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Should see complete new snapshot");
+ } else {
+ XCTFail(@"Too many calls");
+ }
+ }];
+
+ WAIT_FOR(count == 2);
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+
+- (void) testOnDisconnectCancelWorks {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"];
+
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+ FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key];
+
+ __block BOOL ready = NO;
+ [[writer child:@"foo"] setValue:@{@"bar": @"a", @"baz": @"b"} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ __block BOOL sawNewValue = NO;
+ __block BOOL writerSawNewValue = NO;
+ [[reader child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *val = [snapshot value];
+ if (val) {
+ sawNewValue = [@{@"bar" : @"a", @"baz" : @"b", @"bat" : @"d"} isEqualToDictionary:val];
+ }
+ }];
+
+ [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *val = [snapshot value];
+ if (val) {
+ writerSawNewValue = [@{@"bar" : @"a", @"baz" : @"b", @"bat" : @"d"} isEqualToDictionary:val];
+ }
+ }];
+
+ ready = NO;
+ [[writer child:@"foo"] onDisconnectUpdateChildValues:@{@"baz": @"c", @"bat": @"d"}];
+ [[writer child:@"foo/baz"] cancelDisconnectOperationsWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return sawNewValue && writerSawNewValue;
+ }];
+
+ [FRepoManager interrupt:readerCfg];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+ [FRepoManager disposeRepos:readerCfg];
+}
+
+- (void) testOnDisconnectWithServerValuesWithLocalEvents {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * node = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+
+ __block FIRDataSnapshot *snap = nil;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ NSDictionary* data = @{
+ @"a": @1,
+ @"b": @{
+ @".value": [FIRServerValue timestamp],
+ @".priority": [FIRServerValue timestamp]
+ }
+ };
+
+ __block BOOL done = NO;
+ [node onDisconnectSetValue:data andPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ [node onDisconnectUpdateChildValues:@{ @"a": [FIRServerValue timestamp], @"c": [FIRServerValue timestamp] } withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ if ([snap value] != [NSNull null]) {
+ NSDictionary* val = [snap value];
+ done = (val[@"a"] && val[@"b"] && val[@"c"]);
+ }
+ return done;
+ }];
+
+ NSDictionary* value = [snap value];
+ NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)];
+ NSNumber* timestamp = [snap priority];
+ XCTAssertTrue([[snap priority] isKindOfClass:[NSNumber class]], @"Should get back number");
+ XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago");
+ XCTAssertEqualObjects([snap priority], [value objectForKey:@"a"], @"Should get back matching ServerValue.TIMESTAMP");
+ XCTAssertEqualObjects([snap priority], [value objectForKey:@"b"], @"Should get back matching ServerValue.TIMESTAMP");
+ XCTAssertEqualObjects([snap priority], [[snap childSnapshotForPath:@"b"] priority], @"Should get back matching ServerValue.TIMESTAMP");
+ XCTAssertEqualObjects([NSNull null], [[snap childSnapshotForPath:@"d"] value], @"Should get null for cancelled child");
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FTransactionTest.h b/Example/Database/Tests/Integration/FTransactionTest.h
new file mode 100644
index 0000000..6bb7d4d
--- /dev/null
+++ b/Example/Database/Tests/Integration/FTransactionTest.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 "FTestBase.h"
+
+@interface FTransactionTest : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FTransactionTest.m b/Example/Database/Tests/Integration/FTransactionTest.m
new file mode 100644
index 0000000..b78615b
--- /dev/null
+++ b/Example/Database/Tests/Integration/FTransactionTest.m
@@ -0,0 +1,1382 @@
+/*
+ * 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 "FTransactionTest.h"
+#import "FTestHelpers.h"
+#import "FEventTester.h"
+#import "FTupleEventTypeString.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FIRDatabaseConfig_Private.h"
+
+
+// HACK used by testUnsentTransactionsAreNotCancelledOnDisconnect to return one bad token and then a nil token.
+@interface FIROneBadTokenProvider : NSObject <FAuthTokenProvider> {
+ BOOL firstFetch;
+}
+@end
+
+@implementation FIROneBadTokenProvider
+- (instancetype) init {
+ self = [super init];
+ if (self) {
+ firstFetch = YES;
+ }
+ return self;
+}
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback {
+ // Simulate delay
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_MSEC)), [FIRDatabaseQuery sharedQueue], ^{
+ if (firstFetch) {
+ firstFetch = NO;
+ callback(@"bad-token", nil);
+ } else {
+ callback(nil, nil);
+ }
+ });
+}
+
+- (void) listenForTokenChanges:(fbt_void_nsstring)listener {
+}
+
+@end
+@implementation FTransactionTest
+
+- (void) testNewValueIsImmediatelyVisible {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL runOnce = NO;
+ [[node child:@"foo"] runTransactionBlock:^(FIRMutableData * currentValue){
+ runOnce = YES;
+ [currentValue setValue:@42];
+ return [FIRTransactionResult successWithValue:currentValue];
+ }];
+
+ [self waitUntil:^BOOL{
+ return runOnce;
+ }];
+
+ __block BOOL ready = NO;
+ [[node child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (!ready) {
+ NSNumber *val = [snapshot value];
+ XCTAssertTrue([val isEqualToNumber:@42], @"Got value set in transaction");
+ ready = YES;
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testNonAbortedTransactionSetsCommittedToTrueInCallback {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ [[node child:@"foo"] runTransactionBlock:^(FIRMutableData * currentValue){
+ [currentValue setValue:@42];
+ return [FIRTransactionResult successWithValue:currentValue];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should not have aborted");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testAbortedTransactionSetsCommittedToFalseInCallback {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ [[node child:@"foo"] runTransactionBlock:^(FIRMutableData * currentValue){
+ return [FIRTransactionResult abort];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"Should have aborted");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testBugTestSetDataReconnectDoTransactionThatAbortsOnceDataArrivesVerifyCorrectEvents {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+
+ __block BOOL dataWritten = NO;
+ [[reader child:@"foo"] setValue:@42 withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ dataWritten = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return dataWritten;
+ }];
+
+ FIRDatabaseReference * writer = refs.two;
+ __block int eventsReceived = 0;
+ [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (eventsReceived == 0) {
+ NSString *val = [snapshot value];
+ XCTAssertTrue([val isEqualToString:@"temp value"], @"Got initial transaction value");
+ } else if (eventsReceived == 1) {
+ NSNumber *val = [snapshot value];
+ XCTAssertTrue([val isEqualToNumber:@42], @"Got hidden original value");
+ } else {
+ XCTFail(@"Too many events");
+ }
+ eventsReceived++;
+ }];
+
+ [[writer child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id current = [currentData value];
+ if (current == [NSNull null]) {
+ [currentData setValue:@"temp value"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ return [FIRTransactionResult abort];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"This transaction should never commit");
+ XCTAssertTrue(error == nil, @"This transaction should not have an error");
+ }];
+
+ [self waitUntil:^BOOL{
+ return eventsReceived == 2;
+ }];
+
+}
+
+- (void) testUseTransactionToCreateANodeMakeSureExactlyOneEventIsReceived {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block int events = 0;
+ __block BOOL done = NO;
+
+ [[node child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ events++;
+ if (events > 1) {
+ XCTFail(@"Too many events");
+ }
+ }];
+
+ [[node child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@42];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done && events == 1;
+ }];
+}
+
+- (void) testUseTransactionToUpdateTwoExistingChildNodesMakeSureEventsAreOnlyRaisedForChangedNode {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * node1 = [refs.one child:@"foo"];
+ FIRDatabaseReference * node2 = [refs.two child:@"foo"];
+
+ __block BOOL ready = NO;
+ [[node1 child:@"a"] setValue:@42];
+ [[node1 child:@"b"] setValue:@42 withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ NSArray* expect = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[node2 child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node2 child:@"b"] withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expect];
+ [et wait];
+
+ expect = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[node2 child:@"b"] withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expect];
+
+ ready = NO;
+ [node2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ NSDictionary* toSet = @{@"a": @42, @"b": @87};
+ [currentData setValue:toSet];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [et wait];
+}
+
+- (void) testTransactionOnlyCalledOnceWhenInitializingAnEmptyNode {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL updateCalled = NO;
+ [node runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ XCTAssertTrue(val == [NSNull null], @"Should be no value here to start with");
+ if (updateCalled) {
+ XCTFail(@"Should not be called again");
+ }
+ updateCalled = YES;
+ [currentData setValue:@{@"a": @5, @"b": @6}];
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+
+ [self waitUntil:^BOOL{
+ return updateCalled;
+ }];
+}
+
+- (void) testSecondTransactionGetsRunImmediatelyOnPreviousOutputAndOnlyRunsOnce {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+
+ __block BOOL firstRun = NO;
+ __block BOOL firstDone = NO;
+ __block BOOL secondRun = NO;
+ __block BOOL secondDone = NO;
+
+ [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertFalse(firstRun, @"Should not be run twice");
+ firstRun = YES;
+ [currentData setValue:@42];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should not fail");
+ firstDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return firstRun;
+ }];
+
+ [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertFalse(secondRun, @"Should only run once");
+ secondRun = YES;
+ NSNumber* val = [currentData value];
+ XCTAssertTrue([val isEqualToNumber:@42], @"Should see result of last transaction");
+ [currentData setValue:@84];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should not fail");
+ secondDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return secondRun;
+ }];
+
+ __block FIRDataSnapshot * snap = nil;
+ [ref1 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap value] isEqualToNumber:@84], @"Should get updated value");
+
+ [self waitUntil:^BOOL{
+ return firstDone && secondDone;
+ }];
+
+ snap = nil;
+ [ref2 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap value] isEqualToNumber:@84], @"Should get updated value");
+}
+
+// The js test, "Set() cancels pending transactions and re-runs affected transactions.", does not cleanly port to ios
+// due to everything being asynchronous. Rather than attempt to mitigate the various race conditions inherent in a port,
+// I'm adding tests to cover the specific behaviors wrapped up in that one test.
+
+- (void) testSetCancelsPendingTransaction {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * nodeSnap = nil;
+ __block FIRDataSnapshot * nodeFooSnap = nil;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nodeSnap = snapshot;
+ }];
+
+ [[node child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nodeFooSnap = snapshot;
+ }];
+
+ __block BOOL firstDone = NO;
+ __block BOOL secondDone = NO;
+ __block BOOL firstRun = NO;
+
+ [[node child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertFalse(firstRun, @"Should only run once");
+ firstRun = YES;
+ [currentData setValue:@42];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should not fail");
+ firstDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return nodeFooSnap != nil;
+ }];
+
+ XCTAssertTrue([[nodeFooSnap value] isEqualToNumber:@42], @"Got first value");
+
+ [node runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@{@"foo": @84, @"bar": @1}];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"This should not ever be committed");
+ secondDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return nodeSnap != nil;
+ }];
+
+ [[node child:@"foo"] setValue:@0];
+}
+
+// It's difficult to force a transaction re-run on ios, since everything is async. There is also an outstanding case that prevents
+// this test from being before a connection is established (#1981)
+/*
+- (void) testSetRerunsAffectedTransactions {
+
+ Firebase* node = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [[node.parent child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = [[snapshot value] boolValue];
+ }];
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ __block FIRDataSnapshot* nodeSnap = nil;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nodeSnap = snapshot;
+ NSLog(@"SNAP value: %@", [snapshot value]);
+ }];
+
+ __block BOOL firstDone = NO;
+ __block BOOL secondDone = NO;
+ __block BOOL firstRun = NO;
+ __block int secondCount = 0;
+ __block BOOL setDone = NO;
+
+ [node runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ STAssertFalse(firstRun, @"Should only run once");
+ firstRun = YES;
+ [currentData setValue:@42];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ STAssertTrue(committed, @"Should not fail");
+ firstDone = YES;
+ }];
+
+ [[node child:@"bar"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ NSLog(@"RUNNING TRANSACTION");
+ secondCount++;
+ id val = [currentData value];
+ if (secondCount == 1) {
+ STAssertTrue(val == [NSNull null], @"Should not have a value");
+ [currentData setValue:@"first"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else if (secondCount == 2) {
+ NSLog(@"val: %@", val);
+ STAssertTrue(val == [NSNull null], @"Should not have a value");
+ [currentData setValue:@"second"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ STFail(@"Called too many times");
+ return [FIRTransactionResult abort];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ STAssertTrue(committed, @"Should eventually be committed");
+ secondDone = YES;
+ }];
+
+ [[node child:@"foo"] setValue:@0 andCompletionBlock:^(NSError *error) {
+ setDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return setDone;
+ }];
+
+ NSDictionary* expected = @{@"bar": @"second", @"foo": @0};
+ STAssertTrue([[nodeSnap value] isEqualToDictionary:expected], @"Got last value");
+
+ STAssertTrue(secondCount == 2, @"Should have re-run second transaction");
+}*/
+
+- (void) testTransactionSetSetWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ XCTAssertTrue(val == [NSNull null], @"Initial data should be null");
+ [currentData setValue:@"hi!"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error == nil, @"Should not be an error");
+ XCTAssertTrue(committed, @"Should commit");
+ done = YES;
+ }];
+
+ [ref setValue:@"foo"];
+ [ref setValue:@"bar"];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testPriorityIsNotPreservedWhenSettingData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * snap = nil;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [ref setValue:@"test" andPriority:@5];
+
+ __block BOOL ready = NO;
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"new value"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ id val = [snap value];
+ id pri = [snap priority];
+ XCTAssertTrue(pri == [NSNull null], @"Got priority");
+ XCTAssertTrue([val isEqualToString:@"new value"], @"Get new value");
+}
+
+// Skipping test with nested transactions. Everything is async on ios, so new transactions just get placed in a queue
+
+- (void) testResultSnapshotIsPassedToOnComplete {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+
+ __block BOOL done = NO;
+ [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val == [NSNull null]) {
+ [currentData setValue:@"hello!"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ return [FIRTransactionResult abort];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should commit");
+ XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"Got correct snapshot");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+ // do it again for the aborted case
+
+ done = NO;
+ [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val == [NSNull null]) {
+ [currentData setValue:@"hello!"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ return [FIRTransactionResult abort];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"Should not commit");
+ XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"Got correct snapshot");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ // do it again on a fresh connection, for the aborted case
+ done = NO;
+ [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val == [NSNull null]) {
+ [currentData setValue:@"hello!"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ return [FIRTransactionResult abort];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"Should not commit");
+ XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"Got correct snapshot");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testTransactionAbortsAfter25Retries {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ [ref.repo setHijackHash:YES];
+
+ __block int tries = 0;
+ __block BOOL done = NO;
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertTrue(tries < 25, @"Should not be more than 25 tries");
+ tries++;
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error != nil, @"Should fail, too many retries");
+ XCTAssertFalse(committed, @"Should not commit");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [ref.repo setHijackHash:NO];
+}
+
+- (void) testSetShouldCancelSentTransactionsThatComeBackAsDatastale {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+
+ __block BOOL ready = NO;
+ [ref1 setValue:@5 withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ XCTAssertTrue(val == [NSNull null], @"No current value");
+ [currentData setValue:@72];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error != nil, @"Should abort");
+ XCTAssertFalse(committed, @"Should not commit");
+ ready = YES;
+ }];
+
+ [ref2 setValue:@32];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testUpdateShouldNotCancelUnrelatedTransactions {
+ FIRDatabaseReference* ref = [FTestHelpers getRandomNode];
+
+ __block BOOL fooTransactionDone = NO;
+ __block BOOL barTransactionDone = NO;
+
+ [self waitForCompletionOf:[ref child:@"foo"] setValue:@"oldValue"];
+
+ [ref.repo setHijackHash:YES];
+
+ // This transaction should get cancelled as we update "foo" later on.
+ [[ref child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@72];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error != nil, @"Should abort");
+ XCTAssertFalse(committed, @"Should not commit");
+ fooTransactionDone = YES;
+ }];
+
+ // This transaction should not get cancelled since we don't update "bar".
+ [[ref child:@"bar"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@72];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ // Note: In rare cases, this might get aborted since failed transactions (forced by setHijackHash) are only
+ // retried 25 times. If we hit this limit before we stop hijacking the hash below, this test will flake.
+ XCTAssertTrue(error == nil, @"Should not abort");
+ XCTAssertTrue(committed, @"Should commit");
+ barTransactionDone = YES;
+ }];
+
+ NSDictionary *udpateData = @{ @"foo": @"newValue",
+ @"boo": @"newValue",
+ @"doo/foo": @"newValue",
+ @"loo" : @{ @"doo": @{ @"boo":@"newValue"}}} ;
+
+ [self waitForCompletionOf:ref updateChildValues:udpateData];
+ XCTAssertTrue(fooTransactionDone, "Should have gotten cancelled before the update");
+ XCTAssertFalse(barTransactionDone, "Should run after the update");
+ [ref.repo setHijackHash:NO];
+
+ WAIT_FOR(barTransactionDone);
+}
+
+- (void) testTransactionOnWackyUnicode {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+
+ __block BOOL ready = NO;
+ [ref1 setValue:@"♜♞♝♛♚♝♞♜" withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val != [NSNull null]) {
+ XCTAssertTrue([val isEqualToString:@"♜♞♝♛♚♝♞♜"], @"Got crazy unicode");
+ }
+ [currentData setValue:@"♖♘♗♕♔♗♘♖"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error == nil, @"Should not abort");
+ XCTAssertTrue(committed, @"Should commit");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testImmediatelyAbortedTransactions {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult abort];
+ }];
+
+ __block BOOL ready = NO;
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult abort];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error == nil, @"No error occurred, we just aborted");
+ XCTAssertFalse(committed, @"Should not commit");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testAddingToAnArrayWithATransaction {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ [ref setValue:@[@"cat", @"horse"] withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val != [NSNull null]) {
+ NSArray* arr = val;
+ NSMutableArray* toSet = [arr mutableCopy];
+ [toSet addObject:@"dog"];
+ [currentData setValue:toSet];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ [currentData setValue:@[@"dog"]];
+ return [FIRTransactionResult successWithValue:currentData];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should commit");
+ NSArray* val = [snapshot value];
+ NSArray* expected = @[@"cat", @"horse", @"dog"];
+ XCTAssertTrue([val isEqualToArray:expected], @"Got whole array");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testMergedTransactionsHaveCorrectSnapshotInOnComplete {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * node1 = refs.one;
+ FIRDatabaseReference * node2 = refs.two;
+
+ __block BOOL done = NO;
+ [node1 setValue:@{@"a": @0} withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ __block BOOL transaction1Done = NO;
+ __block BOOL transaction2Done = NO;
+
+ [node2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val != [NSNull null]) {
+ XCTAssertTrue([@{@"a": @0} isEqualToDictionary:val], @"Got initial data");
+ }
+ [currentData setValue:@{@"a": @1}];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should commit");
+ XCTAssertTrue([snapshot.key isEqualToString:node2.key], @"Correct snapshot name");
+ NSDictionary* val = [snapshot value];
+ // Per new behavior, will include the accepted value of the transaction, if it was successful.
+ NSDictionary* expected = @{@"a": @1};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got final result");
+ transaction1Done = YES;
+ }];
+
+ [[node2 child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val != [NSNull null]) {
+ XCTAssertTrue([@1 isEqualToNumber:val], @"Got initial data");
+ }
+ [currentData setValue:@2];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should commit");
+ XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Correct snapshot name");
+ NSNumber* val = [snapshot value];
+ NSNumber* expected = @2;
+ XCTAssertTrue([val isEqualToNumber:expected], @"Got final result");
+ transaction2Done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return transaction1Done && transaction2Done;
+ }];
+}
+
+// Skipping two tests on nested calls. Since iOS uses a work queue, nested calls don't actually happen synchronously, so they aren't problematic
+
+- (void) testPendingTransactionsAreCancelledOnDisconnect {
+ FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"pending-transactions"];
+ FIRDatabaseReference * ref = [[[FIRDatabaseReference alloc] initWithConfig:cfg] childByAutoId];
+
+ __block BOOL done = NO;
+ [[ref child:@"a"] setValue:@"initial" withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+ [[ref child:@"b"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"new"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"Should not commit");
+ XCTAssertTrue(error != nil, @"Should be an error");
+ done = YES;
+ }];
+
+ [FRepoManager interrupt:cfg];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ // cleanup
+ [FRepoManager interrupt:cfg];
+ [FRepoManager disposeRepos:cfg];
+}
+
+- (void) testTransactionWithoutLocalEvents1 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSMutableArray* values = [[NSMutableArray alloc] init];
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [values addObject:[snapshot value]];
+ }];
+
+ [self waitUntil:^BOOL{
+ // get initial data
+ return values.count > 0;
+ }];
+
+ __block BOOL done = NO;
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"hello!"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error == nil, @"Should not be an error");
+ XCTAssertTrue(committed, @"Committed");
+ XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"got correct snapshot");
+ done = YES;
+ } withLocalEvents:NO];
+
+ NSArray* expected = @[[NSNull null]];
+ XCTAssertTrue([values isEqualToArray:expected], @"Should not have gotten any values yet");
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ expected = @[[NSNull null], @"hello!"];
+ XCTAssertTrue([values isEqualToArray:expected], @"Should have the new value now");
+}
+
+- (void) testTransactionWithoutLocalEvents2 {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+ int SETS = 4;
+
+ [ref1.repo setHijackHash:YES];
+
+ NSMutableArray* events = [[NSMutableArray alloc] init];
+ [ref1 setValue:@0];
+ [ref1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [events addObject:[snapshot value]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return events.count > 0;
+ }];
+
+ NSArray* expected = @[@0];
+ XCTAssertTrue([events isEqualToArray:expected], @"Got initial set");
+
+ __block int retries = 0;
+ __block BOOL done = NO;
+ [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ retries++;
+ id val = [currentData value];
+ NSNumber* num = @0;
+ if (val != [NSNull null]) {
+ num = val;
+ }
+ int eventCount = [num intValue];
+ if (eventCount == SETS - 1) {
+ [ref1.repo setHijackHash:NO];
+ }
+
+ [currentData setValue:@"txn result"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error == nil, @"Should not be an error");
+ XCTAssertTrue(committed, @"Committed");
+ XCTAssertTrue([[snapshot value] isEqualToString:@"txn result"], @"got correct snapshot");
+ done = YES;
+ } withLocalEvents:NO];
+
+ // Meanwhile, do sets from the second connection
+ for (int i = 0; i < SETS; ++i) {
+ __block BOOL setDone = NO;
+ [ref2 setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ setDone = YES;
+ }];
+ [self waitUntil:^BOOL{
+ return setDone;
+ }];
+ }
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ XCTAssertTrue(retries > 0, @"Transaction should have retried");
+ XCTAssertEqualObjects([events lastObject], @"txn result", @"Final value matches expected value from txn");
+}
+
+// Skipping test of calling transaction from value callback. Since all api calls are async on iOS, nested calls are not a problem.
+
+- (void) testTransactionRevertsDataWhenAddADeeperListen {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+
+ __block BOOL done = NO;
+ [[ref1 child:@"y"] setValue:@"test" withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ if (currentData.value == [NSNull null]) {
+ [[currentData childDataByAppendingPath:@"x"] setValue:@5];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ return [FIRTransactionResult abort];
+ }
+ }];
+
+ [[ref2 child:@"y"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([snapshot.value isEqual:@"test"]) {
+ done = YES;
+ }
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testTransactionWithIntegerKeys {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block BOOL done = NO;
+ NSDictionary* toSet = @{@"1": @1, @"5": @5, @"10": @10, @"20": @20};
+ [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@42];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"Error should be nil.");
+ XCTAssertTrue(committed, @"Transaction should have committed.");
+ done = YES;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+// https://app.asana.com/0/5673976843758/9259161251948
+- (void) testBubbleAppTransactionBug {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block BOOL done = NO;
+ [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@1];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { }];
+
+ [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ NSNumber* val = currentData.value;
+ NSNumber *new = [NSNumber numberWithInt:(val.intValue + 42)];
+ [currentData setValue:new];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { }];
+
+ [[ref child:@"b"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@7];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { }];
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ NSNumber* a = [currentData childDataByAppendingPath:@"a"].value;
+ NSNumber* b = [currentData childDataByAppendingPath:@"b"].value;
+ NSNumber *new = [NSNumber numberWithInt:a.intValue + b.intValue];
+ [currentData setValue:new];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"Error should be nil.");
+ XCTAssertTrue(committed, @"Committed should be true.");
+ XCTAssertEqualObjects(@50, snapshot.value, @"Result should be 50.");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+// If we have cached data, transactions shouldn't run on null.
+- (void) testTransactionsAreRunInitiallyOnCurrentlyCachedData {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ id initialData = @{
+ @"a": @"a-val",
+ @"b": @"b-val"
+ };
+ __block BOOL done = NO;
+ __weak FIRDatabaseReference *weakRef = ref;
+ [ref setValue:initialData withCompletionBlock:^(NSError *error, FIRDatabaseReference *r) {
+ [weakRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [weakRef runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertEqualObjects(currentData.value, initialData, @"Should be initial data.");
+ done = YES;
+ return [FIRTransactionResult abort];
+ }];
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testMultipleLevels {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block BOOL done = NO;
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+
+ [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+
+ [[ref child:@"b"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void) testLocalServerValuesEventuallyButNotImmediatelyMatchServerWithTxns {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+ __block int done = 0;
+
+ NSMutableArray* readSnaps = [[NSMutableArray alloc] init];
+ NSMutableArray* writeSnaps = [[NSMutableArray alloc] init];
+
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([snapshot value] != [NSNull null]) {
+ [readSnaps addObject:snapshot];
+ if (readSnaps.count == 1) {
+ done += 1;
+ }
+ }
+ }];
+
+ [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([snapshot value] != [NSNull null]) {
+ [writeSnaps addObject:snapshot];
+ if (writeSnaps.count == 2) {
+ done += 1;
+ }
+ }
+ }];
+
+ [writer runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:[FIRServerValue timestamp]];
+ [currentData setPriority:[FIRServerValue timestamp]];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {}];
+
+ [self waitUntil:^BOOL{
+ return done == 2;
+ }];
+
+ XCTAssertEqual((unsigned long)[readSnaps count], (unsigned long)1, @"Should have received one snapshot on reader");
+ XCTAssertEqual((unsigned long)[writeSnaps count], (unsigned long)2, @"Should have received two snapshots on writer");
+
+ FIRDataSnapshot * firstReadSnap = [readSnaps objectAtIndex:0];
+ FIRDataSnapshot * firstWriteSnap = [writeSnaps objectAtIndex:0];
+ FIRDataSnapshot * secondWriteSnap = [writeSnaps objectAtIndex:1];
+
+ NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)];
+ XCTAssertTrue([now doubleValue] - [firstWriteSnap.value doubleValue] < 2000, @"Should have received a local event with a value close to timestamp");
+ XCTAssertTrue([now doubleValue] - [firstWriteSnap.priority doubleValue] < 2000, @"Should have received a local event with a priority close to timestamp");
+ XCTAssertTrue([now doubleValue] - [secondWriteSnap.value doubleValue] < 2000, @"Should have received a server event with a value close to timestamp");
+ XCTAssertTrue([now doubleValue] - [secondWriteSnap.priority doubleValue] < 2000, @"Should have received a server event with a priority close to timestamp");
+
+ XCTAssertFalse([firstWriteSnap value] == [secondWriteSnap value], @"Initial and future writer values should be different");
+ XCTAssertFalse([firstWriteSnap priority] == [secondWriteSnap priority], @"Initial and future writer priorities should be different");
+ XCTAssertEqualObjects(firstReadSnap.value, secondWriteSnap.value, @"Eventual reader and writer values should be equal");
+ XCTAssertEqualObjects(firstReadSnap.priority, secondWriteSnap.priority, @"Eventual reader and writer priorities should be equal");
+}
+
+- (void) testTransactionWithQueryListen {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block BOOL done = NO;
+
+ [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [[ref queryLimitedToFirst:1] observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ } withCancelBlock:^(NSError *error) {
+ }];
+
+ [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"This transaction should not have an error");
+ XCTAssertTrue(committed, @"Should not have aborted");
+ XCTAssertEqualObjects([snapshot value], @1, @"Transaction value should match initial set");
+ done = YES;
+ }];
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void) testTransactionDoesNotPickUpCachedDataFromPreviousOnce {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * me = refs.one;
+ FIRDatabaseReference * other = refs.two;
+ __block BOOL done = NO;
+
+ [me setValue:@"not null" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ [me observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ [other setValue:[NSNull null] withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ [me runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id current = [currentData value];
+ if (current == [NSNull null]) {
+ [currentData setValue:@"it was null!"];
+ } else {
+ [currentData setValue:@"it was not null!"];
+ }
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"This transaction should not have an error");
+ XCTAssertTrue(committed, @"Should not have aborted");
+ XCTAssertEqualObjects([snapshot value], @"it was null!", @"Transaction value should match remote null set");
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void) testTransactionDoesNotPickUpCachedDataFromPreviousTransaction {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * me = refs.one;
+ FIRDatabaseReference * other = refs.two;
+ __block BOOL done = NO;
+
+ [me runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"not null"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"This transaction should not have an error");
+ XCTAssertTrue(committed, @"Should not have aborted");
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ [other setValue:[NSNull null] withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ [me runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id current = [currentData value];
+ if (current == [NSNull null]) {
+ [currentData setValue:@"it was null!"];
+ } else {
+ [currentData setValue:@"it was not null!"];
+ }
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"This transaction should not have an error");
+ XCTAssertTrue(committed, @"Should not have aborted");
+ XCTAssertEqualObjects([snapshot value], @"it was null!", @"Transaction value should match remote null set");
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void) testTransactionOnQueriedLocationDoesntRunInitiallyOnNull {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block BOOL txnDone = NO;
+
+ [self waitForCompletionOf:[ref childByAutoId] setValue:@{ @"a": @1, @"b": @2 }];
+
+ [[ref queryLimitedToFirst:1] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [snapshot.ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id expected = @{@"a" : @1, @"b" : @2};
+ XCTAssertEqualObjects(currentData.value, expected, @"");
+ [currentData setValue:[NSNull null]];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"");
+ XCTAssertTrue(committed, @"");
+ XCTAssertEqualObjects(snapshot.value, [NSNull null], @"");
+ txnDone = YES;
+ }];
+ }];
+
+ WAIT_FOR(txnDone);
+}
+
+- (void) testTransactionsRaiseCorrectChildChangedEventsOnQueries {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block BOOL txnDone = NO;
+ NSMutableArray *snapshots = [[NSMutableArray alloc] init];
+
+ [self waitForCompletionOf:ref setValue:@{ @"foo": @{ @"value": @1 }}];
+
+ FIRDatabaseQuery *query = [ref queryEndingAtValue:@(DBL_MIN)];
+
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [snapshots addObject:snapshot];
+ }];
+
+ [query observeEventType:FIRDataEventTypeChildChanged withBlock:^(FIRDataSnapshot *snapshot) {
+ [snapshots addObject:snapshot];
+ }];
+
+ [[ref child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [[currentData childDataByAppendingPath:@"value"] setValue:@2];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"");
+ XCTAssertTrue(committed, @"");
+ txnDone = YES;
+ } withLocalEvents:NO];
+
+ WAIT_FOR(txnDone);
+
+ XCTAssertTrue(snapshots.count == 2, @"");
+ FIRDataSnapshot *addedSnapshot = snapshots[0];
+ XCTAssertEqualObjects(addedSnapshot.key, @"foo", @"");
+ XCTAssertEqualObjects(addedSnapshot.value, @{ @"value": @1 }, @"");
+
+ FIRDataSnapshot *changedSnapshot = snapshots[1];
+ XCTAssertEqualObjects(changedSnapshot.key, @"foo", @"");
+ XCTAssertEqualObjects(changedSnapshot.value, @{ @"value": @2 }, @"");
+}
+
+- (void) testTransactionsUseLocalMerges {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block BOOL txnDone = NO;
+ [ref updateChildValues:@{ @"foo": @"bar"}];
+
+ [[ref child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertEqualObjects(currentData.value, @"bar", @"Transaction value matches local updates");
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"");
+ XCTAssertTrue(committed, @"");
+ txnDone = YES;
+ }];
+
+ WAIT_FOR(txnDone);
+}
+
+//See https://app.asana.com/0/15566422264127/23303789496881
+- (void)testOutOfOrderRemoveWritesAreHandledCorrectly
+{
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ [ref setValue:@{@"foo": @"bar"}];
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"transaction-1"];
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"transaction-2"];
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+ __block BOOL done = NO;
+ // This will trigger an abort of the transaction which should not cause the client to crash
+ [ref updateChildValues:@{@"qux": @"quu"} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertNil(error);
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void)testUnsentTransactionsAreNotCancelledOnDisconnect {
+ // Hack: To trigger us to disconnect before restoring state, we inject a bad auth token.
+ // In real-world usage the much more common case is that we get redirected to a different
+ // server, but that's harder to manufacture from a test.
+ NSString *configName = @"testUnsentTransactionsAreNotCancelledOnDisconnect";
+ FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:configName];
+ config.authTokenProvider = [[FIROneBadTokenProvider alloc] init];
+
+ // Queue a transaction offline.
+ FIRDatabaseReference *root = [[FIRDatabaseReference alloc] initWithConfig:config];
+ [root.database goOffline];
+ __block BOOL done = NO;
+ [[root childByAutoId] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@0];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error);
+ XCTAssertTrue(committed);
+ done = YES;
+ }];
+
+ [root.database goOnline];
+ WAIT_FOR(done);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FArraySortedDictionaryTest.m b/Example/Database/Tests/Unit/FArraySortedDictionaryTest.m
new file mode 100644
index 0000000..cdc9e1c
--- /dev/null
+++ b/Example/Database/Tests/Unit/FArraySortedDictionaryTest.m
@@ -0,0 +1,485 @@
+/*
+ * 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/XCTest.h>
+
+#import "FArraySortedDictionary.h"
+#import "FTreeSortedDictionary.h"
+
+@interface FArraySortedDictionaryTests : XCTestCase
+
+@end
+
+@implementation FArraySortedDictionaryTests
+
+- (NSComparator) defaultComparator {
+ return ^(id obj1, id obj2) {
+ if([obj1 respondsToSelector:@selector(compare:)] && [obj2 respondsToSelector:@selector(compare:)]) {
+ return [obj1 compare:obj2];
+ }
+ else {
+ if(obj1 < obj2) {
+ return (NSComparisonResult)NSOrderedAscending;
+ }
+ else if (obj1 > obj2) {
+ return (NSComparisonResult)NSOrderedDescending;
+ }
+ else {
+ return (NSComparisonResult)NSOrderedSame;
+ }
+ }
+ };
+}
+
+
+
+- (void)testCreateNode
+{
+ FImmutableSortedDictionary *map = [[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertEqual(map.count, 1, @"Contains one element");
+}
+
+- (void)testGetNilReturnsNil {
+ FImmutableSortedDictionary *map1 = [[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertNil([map1 get:nil]);
+
+ FImmutableSortedDictionary *map2 = [[[FArraySortedDictionary alloc] initWithComparator:^NSComparisonResult(id obj1, id obj2) {
+ return [obj1 compare:obj2];
+ }]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertNil([map2 get:nil]);
+}
+
+- (void)testSearchForSpecificKey {
+ FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2];
+
+ XCTAssertEqualObjects([map get:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found second object");
+ XCTAssertNil([map get:@3], @"Properly not found object");
+}
+
+- (void)testRemoveKeyValuePair {
+ FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2];
+
+ FImmutableSortedDictionary* newMap = [map removeKey:@1];
+ XCTAssertEqualObjects([newMap get:@2], @2, @"Found second object");
+ XCTAssertNil([newMap get:@1], @"Properly not found object");
+
+ // Make sure the original one is not mutated
+ XCTAssertEqualObjects([map get:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found second object");
+}
+
+- (void)testMoreRemovals {
+ FImmutableSortedDictionary *map = [[[[[[[[[[[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9]
+ insertKey:@20 withValue:@20]
+ insertKey:@18 withValue:@18]
+ insertKey:@2 withValue:@2]
+ insertKey:@71 withValue:@71]
+ insertKey:@42 withValue:@42]
+ insertKey:@88 withValue:@88];
+ XCTAssertNotNil([map get:@7], @"Found object");
+ XCTAssertNotNil([map get:@3], @"Found object");
+ XCTAssertNotNil([map get:@1], @"Found object");
+
+
+ FImmutableSortedDictionary* m1 = [map removeKey:@7];
+ FImmutableSortedDictionary* m2 = [map removeKey:@3];
+ FImmutableSortedDictionary* m3 = [map removeKey:@1];
+
+ XCTAssertNil([m1 get:@7], @"Removed object");
+ XCTAssertNotNil([m1 get:@3], @"Found object");
+ XCTAssertNotNil([m1 get:@1], @"Found object");
+
+ XCTAssertNil([m2 get:@3], @"Removed object");
+ XCTAssertNotNil([m2 get:@7], @"Found object");
+ XCTAssertNotNil([m2 get:@1], @"Found object");
+
+
+ XCTAssertNil([m3 get:@1], @"Removed object");
+ XCTAssertNotNil([m3 get:@7], @"Found object");
+ XCTAssertNotNil([m3 get:@3], @"Found object");
+}
+
+- (void) testRemovalBug {
+ FImmutableSortedDictionary *map = [[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2]
+ insertKey:@3 withValue:@3];
+
+ XCTAssertEqualObjects([map get:@1], @1, @"Found object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found object");
+ XCTAssertEqualObjects([map get:@3], @3, @"Found object");
+
+ FImmutableSortedDictionary* m1 = [map removeKey:@2];
+ XCTAssertEqualObjects([m1 get:@1], @1, @"Found object");
+ XCTAssertEqualObjects([m1 get:@3], @3, @"Found object");
+ XCTAssertNil([m1 get:@2], @"Removed object");
+}
+
+- (void) testIncreasing {
+ int total = 20;
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ for(int i = 0; i < total; i++) {
+ NSNumber* item = [NSNumber numberWithInt:i];
+ map = [map insertKey:item withValue:item];
+ }
+
+ XCTAssertTrue([map count] == 20, @"Check if all 100 objects are in the map");
+
+ for(int i = 0; i < total; i++) {
+ NSNumber* item = [NSNumber numberWithInt:i];
+ map = [map removeKey:item];
+ }
+
+ XCTAssertTrue([map count] == 0, @"Check if all 100 objects were removed");
+}
+
+- (void) testOverride {
+ FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@10 withValue:@10]
+ insertKey:@10 withValue:@8];
+
+ XCTAssertEqualObjects([map get:@10], @8, @"Found first object");
+}
+- (void) testEmpty {
+ FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@10 withValue:@10]
+ removeKey:@10];
+
+ XCTAssertTrue([map isEmpty], @"Properly empty");
+
+}
+
+- (void) testEmptyGet {
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertNil([map get:@"something"], @"Properly nil");
+}
+
+- (void) testEmptyCount {
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertTrue([map count] == 0, @"Properly zero count");
+}
+
+- (void) testEmptyRemoval {
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertTrue([[map removeKey:@"sometjhing"] count] == 0, @"Properly zero count");
+}
+
+- (void) testReverseTraversal {
+ FImmutableSortedDictionary *map = [[[[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@5 withValue:@5]
+ insertKey:@3 withValue:@3]
+ insertKey:@2 withValue:@2]
+ insertKey:@4 withValue:@4];
+
+ __block int next = 5;
+ [map enumerateKeysAndObjectsReverse:YES usingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Properly equal");
+ next = next - 1;
+ }];
+}
+
+
+- (void) testInsertionAndRemovalOfAHundredItems {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+ NSMutableArray* toRemove = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ [toRemove addObject:[NSNumber numberWithInt:i]];
+ }
+
+
+ [self shuffleArray:toInsert];
+ [self shuffleArray:toRemove];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block int next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Correct key");
+ XCTAssertEqualObjects(value, [NSNumber numberWithInt:next], @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual(next, N, @"Check we traversed all of the items");
+
+ // remove them
+
+ for(int i = 0; i < N; i++) {
+ map = [map removeKey:[toRemove objectAtIndex:i]];
+ }
+
+
+ XCTAssertEqual([map count], 0, @"Check we removed all of the items");
+}
+
+- (void) shuffleArray:(NSMutableArray *)array {
+ NSUInteger count = [array count];
+ for(NSUInteger i = 0; i < count; i++) {
+ NSInteger nElements = count - i;
+ NSInteger n = (arc4random() % nElements) + i;
+ [array exchangeObjectAtIndex:i withObjectAtIndex:n];
+ }
+}
+
+- (void) testOrderIsCorrect {
+
+ NSArray* toInsert = [[NSArray alloc] initWithObjects:@1,@7,@8,@5,@2,@6,@4,@0,@3, nil];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < [toInsert count]; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == [toInsert count], @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block NSUInteger next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInteger:next], @"Correct key");
+ XCTAssertEqualObjects(value, [NSNumber numberWithInteger:next], @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual(next, [toInsert count], @"Check we traversed all of the items");
+}
+
+- (void) testPredecessorKey {
+ FImmutableSortedDictionary *map = [[[[[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9];
+
+ XCTAssertNil([map getPredecessorKey:@1], @"First object doesn't have a predecessor");
+ XCTAssertEqualObjects([map getPredecessorKey:@3], @1, @"@1");
+ XCTAssertEqualObjects([map getPredecessorKey:@4], @3, @"@3");
+ XCTAssertEqualObjects([map getPredecessorKey:@7], @4, @"@4");
+ XCTAssertEqualObjects([map getPredecessorKey:@9], @7, @"@7");
+ XCTAssertEqualObjects([map getPredecessorKey:@50], @9, @"@9");
+ XCTAssertThrows([map getPredecessorKey:@777], @"Expect exception about nonexistant key");
+}
+
+- (void) testEnumerator {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ NSEnumerator* enumerator = [map keyEnumerator];
+ id next = [enumerator nextObject];
+ int correctValue = 0;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 1;
+ }
+}
+
+- (void) testReverseEnumerator {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ NSEnumerator* enumerator = [map reverseKeyEnumerator];
+ id next = [enumerator nextObject];
+ int correctValue = N - 1;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue--;
+ }
+}
+
+- (void) testEnumeratorFrom {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i*2]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ // Test from inbetween keys
+ {
+ NSEnumerator* enumerator = [map keyEnumeratorFrom:@11];
+ id next = [enumerator nextObject];
+ int correctValue = 12;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 2;
+ }
+ }
+
+ // Test from key in map
+ {
+ NSEnumerator* enumerator = [map keyEnumeratorFrom:@10];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 2;
+ }
+ }
+}
+
+- (void) testReverseEnumeratorFrom {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i*2]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ // Test from inbetween keys
+ {
+ NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@11];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue - 2;
+ }
+ }
+
+ // Test from key in map
+ {
+ NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@10];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue - 2;
+ }
+ }
+}
+
+- (void)testConversionToTreeMap {
+ int N = SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD + 5;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *dict = [FImmutableSortedDictionary dictionaryWithComparator:[self defaultComparator]];
+
+ for(int i = 0; i < N; i++) {
+ dict = [dict insertKey:toInsert[i] withValue:toInsert[i]];
+ if (i < SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD) {
+ XCTAssertTrue([dict isKindOfClass:[FArraySortedDictionary class]],
+ @"We're below the threshold we should be an array backed implementation");
+ XCTAssertEqual(dict.count, i + 1, @"Size doesn't match");
+ } else {
+ XCTAssertTrue([dict isKindOfClass:[FTreeSortedDictionary class]],
+ @"We're above the threshold we should be a tree backed implementation");
+ XCTAssertEqual(dict.count, i + 1, @"Size doesn't match");
+ }
+ }
+
+ // check the order is correct
+ __block NSUInteger next = 0;
+ [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInteger:next], @"Correct key");
+ XCTAssertEqualObjects(value, [NSNumber numberWithInteger:next], @"Correct value");
+ next = next + 1;
+ }];
+}
+
+
+
+@end
+
diff --git a/Example/Database/Tests/Unit/FCompoundHashTest.m b/Example/Database/Tests/Unit/FCompoundHashTest.m
new file mode 100644
index 0000000..15e6d10
--- /dev/null
+++ b/Example/Database/Tests/Unit/FCompoundHashTest.m
@@ -0,0 +1,141 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FCompoundHash.h"
+#import "FTestHelpers.h"
+#import "FEmptyNode.h"
+#import "FStringUtilities.h"
+#import "FEmptyNode.h"
+
+@interface FCompoundHashTest : XCTestCase
+
+@end
+
+@implementation FCompoundHashTest
+
+static FCompoundHashSplitStrategy NEVER_SPLIT_STRATEGY = ^BOOL(FCompoundHashBuilder *builder) {
+ return NO;
+};
+
+- (FCompoundHashSplitStrategy)splitAtPaths:(NSArray *)paths {
+ return ^BOOL(FCompoundHashBuilder *builder) {
+ return [paths containsObject:builder.currentPath];
+ };
+}
+
+- (void)testEmptyNodeYieldsEmptyHash {
+ FCompoundHash *hash = [FCompoundHash fromNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(hash.posts, @[]);
+ XCTAssertEqualObjects(hash.hashes, @[@""]);
+}
+
+- (void)testCompoundHashIsAlwaysFollowedByEmptyHash {
+ id<FNode> node = NODE(@{@"foo": @"bar"});
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY];
+ NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(string:\"bar\"))"];
+
+ XCTAssertEqualObjects(hash.posts, @[PATH(@"foo")]);
+ XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""]));
+}
+
+- (void)testCompoundHashCanSplitAtPriority {
+ id<FNode> node = NODE((@{@"foo": @{@"!beforePriority": @"before", @".priority": @"prio", @"afterPriority": @"after"}, @"qux": @"qux"}));
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:[self splitAtPaths:@[PATH(@"foo/.priority")]]];
+ NSString *firstHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(\"!beforePriority\":(string:\"before\"),\".priority\":(string:\"prio\")))"];
+ NSString *secondHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(\"afterPriority\":(string:\"after\")),\"qux\":(string:\"qux\"))"];
+ XCTAssertEqualObjects(hash.posts, (@[PATH(@"foo/.priority"), PATH(@"qux")]));
+ XCTAssertEqualObjects(hash.hashes, (@[firstHash, secondHash, @""]));
+}
+
+- (void)testHashesPriorityLeafNodes {
+ id<FNode> node = NODE((@{@"foo": @{@".value": @"bar", @".priority": @"baz"}}));
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY];
+ NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(priority:string:\"baz\":string:\"bar\"))"];
+
+ XCTAssertEqualObjects(hash.posts, @[PATH(@"foo")]);
+ XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""]));
+}
+
+- (void)testHashingFollowsFirebaseKeySemantics {
+ id<FNode> node = NODE((@{@"1": @"one", @"2": @"two", @"10": @"ten"}));
+ // 10 is after 2 in Firebase key semantics, but would be before 2 in string semantics
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:[self splitAtPaths:@[PATH(@"2")]]];
+ NSString *firstHash = [FStringUtilities base64EncodedSha1:@"(\"1\":(string:\"one\"),\"2\":(string:\"two\"))"];
+ NSString *secondHash = [FStringUtilities base64EncodedSha1:@"(\"10\":(string:\"ten\"))"];
+ XCTAssertEqualObjects(hash.posts, (@[PATH(@"2"), PATH(@"10")]));
+ XCTAssertEqualObjects(hash.hashes, (@[firstHash, secondHash, @""]));
+}
+
+- (void)testHashingOnChildBoundariesWorks {
+ id<FNode> node = NODE((@{@"bar": @{@"deep": @"value"}, @"foo": @{@"other-deep": @"value"}}));
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:[self splitAtPaths:@[PATH(@"bar/deep")]]];
+ NSString *firstHash = [FStringUtilities base64EncodedSha1:@"(\"bar\":(\"deep\":(string:\"value\")))"];
+ NSString *secondHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(\"other-deep\":(string:\"value\")))"];
+ XCTAssertEqualObjects(hash.posts, (@[PATH(@"bar/deep"), PATH(@"foo/other-deep")]));
+ XCTAssertEqualObjects(hash.hashes, (@[firstHash, secondHash, @""]));
+}
+
+- (void)testCommasAreSetForNestedChildren {
+ id<FNode> node = NODE((@{@"bar": @{@"deep": @"value"}, @"foo": @{@"other-deep": @"value"}}));
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY];
+ NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"bar\":(\"deep\":(string:\"value\")),\"foo\":(\"other-deep\":(string:\"value\")))"];
+
+ XCTAssertEqualObjects(hash.posts, @[PATH(@"foo/other-deep")]);
+ XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""]));
+}
+
+- (void)testQuotedStringsAndKeys {
+ id<FNode> node = NODE((@{@"\"": @"\\", @"\"\\\"\\": @"\"\\\"\\"}));
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY];
+ NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"\\\"\":(string:\"\\\\\"),\"\\\"\\\\\\\"\\\\\":(string:\"\\\"\\\\\\\"\\\\\"))"];
+
+ XCTAssertEqualObjects(hash.posts, @[PATH(@"\"\\\"\\")]);
+ XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""]));
+}
+
+- (void)testDefaultSplitHasSensibleAmountOfHashes {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ for (int i = 0; i < 500; i++) {
+ // roughly 15-20 bytes serialized per node, 10k total
+ dict[[NSString stringWithFormat:@"%d", i]] = @"value";
+ }
+ id<FNode> node10k = NODE(dict);
+
+ dict = [NSMutableDictionary dictionary];
+ for (int i = 0; i < 5000; i++) {
+ // roughly 15-20 bytes serialized per node, 100k total
+ dict[[NSString stringWithFormat:@"%d", i]] = @"value";
+ }
+ id<FNode> node100k = NODE(dict);
+
+ dict = [NSMutableDictionary dictionary];
+ for (int i = 0; i < 50000; i++) {
+ // roughly 15-20 bytes serialized per node, 1M total
+ dict[[NSString stringWithFormat:@"%d", i]] = @"value";
+ }
+ id<FNode> node1M = NODE(dict);
+
+ FCompoundHash *hash10k = [FCompoundHash fromNode:node10k];
+ FCompoundHash *hash100k = [FCompoundHash fromNode:node100k];
+ FCompoundHash *hash1M = [FCompoundHash fromNode:node1M];
+ XCTAssertEqualWithAccuracy(hash10k.hashes.count, 15, 3);
+ XCTAssertEqualWithAccuracy(hash100k.hashes.count, 50, 5);
+ XCTAssertEqualWithAccuracy(hash1M.hashes.count, 150, 10);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FCompoundWriteTest.m b/Example/Database/Tests/Unit/FCompoundWriteTest.m
new file mode 100644
index 0000000..1e0a85e
--- /dev/null
+++ b/Example/Database/Tests/Unit/FCompoundWriteTest.m
@@ -0,0 +1,526 @@
+/*
+ * 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/XCTest.h>
+#import "FNode.h"
+#import "FSnapshotUtilities.h"
+#import "FCompoundWrite.h"
+#import "FEmptyNode.h"
+#import "FLeafNode.h"
+#import "FNamedNode.h"
+
+@interface FCompoundWriteTest : XCTestCase
+
+@end
+
+@implementation FCompoundWriteTest
+
+- (id<FNode>) leafNode {
+ static id<FNode> node = nil;
+ if (!node) {
+ node = [FSnapshotUtilities nodeFrom:@"leaf-node"];
+ }
+ return node;
+}
+
+- (id<FNode>) priorityNode {
+ static id<FNode> node = nil;
+ if (!node) {
+ node = [FSnapshotUtilities nodeFrom:@"prio"];
+ }
+ return node;
+}
+
+- (id<FNode>) baseNode {
+ static id<FNode> node = nil;
+ if (!node) {
+ NSDictionary *base = @{@"child-1" : @"value-1", @"child-2" : @"value-2"};
+ node = [FSnapshotUtilities nodeFrom:base];
+ }
+ return node;
+}
+
+- (void) assertAppliedCompoundWrite:(FCompoundWrite *)compoundWrite equalsNode:(id<FNode>)node withPriority:(id<FNode>)priority {
+ id<FNode> updatedNode = [compoundWrite applyToNode:node];
+ if (node.isEmpty) {
+ XCTAssertEqualObjects([FEmptyNode emptyNode], updatedNode,
+ @"Applied compound write should be empty. %@", updatedNode);
+ } else {
+ XCTAssertEqualObjects([node updatePriority:priority], updatedNode,
+ @"Applied compound write should equal node with priority. %@", updatedNode);
+ }
+}
+
+- (void) testEmptyMergeIsEmpty {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ XCTAssertTrue(compoundWrite.isEmpty, @"Empty write should be empty %@", compoundWrite);
+}
+
+- (void) testCompoundWriteWithPriorityUpdateIsNotEmpty {
+ FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.priorityNode atKey:@".priority"];
+ XCTAssertFalse(compoundWrite.isEmpty, @"Priority update should not be empty %@", compoundWrite);
+}
+
+- (void) testCompoundWriteWithUpdateIsNotEmpty {
+ FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.leafNode
+ atPath:[[FPath alloc] initWith:@"foo/bar"]];
+ XCTAssertFalse(compoundWrite.isEmpty, @"Update should not be empty %@", compoundWrite);
+}
+
+- (void) testCompoundWriteWithRootUpdateIsNotEmpty {
+ FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.leafNode
+ atPath:[FPath empty]];
+ XCTAssertFalse(compoundWrite.isEmpty, @"Update at root should not be empty %@", compoundWrite);
+}
+
+- (void) testCompoundWriteWithEmptyRootUpdateIsNotEmpty {
+ FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:[FEmptyNode emptyNode]
+ atPath:[FPath empty]];
+ XCTAssertFalse(compoundWrite.isEmpty, @"Empty root update should not be empty %@", compoundWrite);
+}
+
+- (void) testCompoundWriteWithRootPriorityUpdateAndChildMergeIsNotEmpty {
+ FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.priorityNode atKey:@".priority"];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@".priority"]];
+ XCTAssertFalse(compoundWrite.isEmpty, @"Compound write with root priority update and child merge should not be empty.");
+}
+
+- (void) testAppliesLeafOverwrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, self.leafNode, @"Should get leaf node once applied %@", updatedNode);
+}
+
+- (void) testAppliesChildrenOverwrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> childNode = [[FEmptyNode emptyNode] updateImmediateChild:@"child" withNewChild:self.leafNode];
+ compoundWrite = [compoundWrite addWrite:childNode atPath:[FPath empty]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, childNode, @"Child overwrite should work");
+}
+
+- (void) testAddsChildNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> expectedNode = [[FEmptyNode emptyNode] updateImmediateChild:@"child" withNewChild:self.leafNode];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atKey:@"child"];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Adding child node should work %@", updatedNode);
+}
+
+- (void) testAddsDeepChildNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ FPath *path = [[FPath alloc] initWith:@"deep/deep/node"];
+ id<FNode> expectedNode = [[FEmptyNode emptyNode] updateChild:path withNewChild:self.leafNode];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:path];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Should add deep child node correctly");
+}
+
+- (void) testOverwritesExistingChild {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ FPath *path = [[FPath alloc] initWith:@"child-1"];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:path];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:[path getFront] withNewChild:self.leafNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Overwriting existing child should work.");
+}
+
+- (void) testUpdatesExistingChild {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ FPath *path = [[FPath alloc] initWith:@"child-1/foo"];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:path];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [self.baseNode updateChild:path withNewChild:self.leafNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Updating existing child should work");
+}
+
+- (void) testDoesntUpdatePriorityOnEmptyNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atKey:@".priority"];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:[FEmptyNode emptyNode] withPriority:[FEmptyNode emptyNode]];
+}
+
+- (void) testUpdatesPriorityOnNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atKey:@".priority"];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@"value"];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:self.priorityNode];
+}
+
+- (void) testUpdatesPriorityOfChild {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ FPath *path = [[FPath alloc] initWith:@"child-1/.priority"];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:path];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [self.baseNode updateChild:path withNewChild:self.priorityNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Updating priority of child should work.");
+}
+
+- (void) testDoesntUpdatePriorityOfNonExistentChild {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ FPath *path = [[FPath alloc] initWith:@"child-3/.priority"];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:path];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, self.baseNode, @"Should not update priority of nonexistent child");
+}
+
+- (void) testDeepUpdateExistingUpdates {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update1 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"];
+ id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"];
+ compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1"]];
+ compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]];
+ compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ NSDictionary *expectedChild1 = @{@"foo":@"new-foo-value", @"bar":@"bar-value", @"baz":@"baz-value"};
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expectedChild1]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Deep update with existing updates should work.");
+}
+
+- (void) testShallowUpdateRemovesDeepUpdate {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update1 = [FSnapshotUtilities nodeFrom:@"new-foo-value"];
+ id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"];
+ id<FNode> update3 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]];
+ compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1"]];
+ NSDictionary *expectedChild1 = @{@"foo":@"foo-value", @"bar":@"bar-value"};
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expectedChild1]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Shallow update should remove deep udpates.");
+}
+
+- (void) testChildPriorityDoesntUpdateEmptyNodePriorityOnChildMerge {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"child-1/.priority"]];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"child-1"]];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:[FEmptyNode emptyNode] withPriority:[FEmptyNode emptyNode]];
+}
+
+- (void) testChildPriorityUpdatesPriorityOnChildMerge {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"child-1/.priority"]];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@"value"];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"child-1"]];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:self.priorityNode];
+}
+
+- (void) testChildPriorityUpdatesEmptyPriorityOnChildMerge {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child-1/.priority"]];
+ id<FNode> node = [[FLeafNode alloc] initWithValue:@"foo" withPriority:self.priorityNode];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"child-1"]];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:[FEmptyNode emptyNode]];
+}
+
+- (void) testDeepPrioritySetWorksOnEmptyNodeWhenOtherSetIsAvailable {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"foo/.priority"]];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"foo/child"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ id<FNode> updatedPriority = [updatedNode getChild:[[FPath alloc] initWith:@"foo"]].getPriority;
+ XCTAssertEqualObjects(updatedPriority, self.priorityNode, @"Should get priority");
+}
+
+- (void) testChildMergeLooksIntoUpdateNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"foo"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:@"foo-value"];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Child merge should get updates.");
+}
+
+- (void) testChildMergeRemovesNodeOnDeeperPaths {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"foo/not/existing"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.leafNode];
+ id<FNode> expectedNode = [FEmptyNode emptyNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Should not have node.");
+}
+
+- (void) testChildMergeWithEmptyPathIsSameMerge {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]];
+ XCTAssertEqualObjects([compoundWrite childCompoundWriteAtPath:[FPath empty]], compoundWrite,
+ @"Child merge with empty path should be the same merge.");
+}
+
+- (void) testRootUpdateRemovesRootPriority {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@"foo"];
+ compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, update, @"Root update should remove root priority");
+}
+
+- (void) testDeepUpdateRemovesPriorityThere {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"foo/.priority"]];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@"bar"];
+ compoundWrite = [compoundWrite addWrite:update atPath:[[FPath alloc] initWith:@"foo"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:@{@"foo":@"bar"}];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Deep update should remove priority there");
+}
+
+- (void) testAddingUpdatesAtPathWorks {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init];
+ [updateDictionary setObject:@"foo-value" forKey:@"foo"];
+ [updateDictionary setObject:@"bar-value" forKey:@"bar"];
+ FCompoundWrite *updates = [FCompoundWrite compoundWriteWithValueDictionary:updateDictionary];
+ compoundWrite = [compoundWrite addCompoundWrite:updates atPath:[[FPath alloc] initWith:@"child-1"]];
+
+ NSDictionary *expectedChild1 = @{@"foo":@"foo-value", @"bar":@"bar-value"};
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expectedChild1]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Adding updates at a path should work.");
+}
+
+- (void) testAddingUpdatesAtRootWorks {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init];
+ [updateDictionary setObject:@"new-value-1" forKey:@"child-1"];
+ [updateDictionary setObject:[NSNull null] forKey:@"child-2"];
+ [updateDictionary setObject:@"value-3" forKey:@"child-3"];
+ FCompoundWrite *updates = [FCompoundWrite compoundWriteWithValueDictionary:updateDictionary];
+ compoundWrite = [compoundWrite addCompoundWrite:updates atPath:[FPath empty]];
+
+ NSDictionary *expected = @{@"child-1":@"new-value-1", @"child-3":@"value-3"};
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:expected];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Adding updates at root should work.");
+}
+
+- (void) testChildMergeOfRootPriorityWorks {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@".priority"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, self.priorityNode, @"Child merge of root priority should work.");
+}
+
+- (void) testCompleteChildrenOnlyReturnsCompleteOverwrites {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-1"]];
+ NSArray *expectedChildren = @[[[FNamedNode alloc] initWithName:@"child-1" andNode:self.leafNode]];
+ NSArray *completeChildren = [compoundWrite completeChildren];
+ XCTAssertEqualObjects(completeChildren, expectedChildren, @"Complete children should only return on complete overwrites.");
+}
+
+- (void) testCompleteChildrenOnlyReturnsEmptyOverwrites {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child-1"]];
+ NSArray *expectedChildren = @[[[FNamedNode alloc] initWithName:@"child-1" andNode:[FEmptyNode emptyNode]]];
+ NSArray *completeChildren = [compoundWrite completeChildren];
+ XCTAssertEqualObjects(completeChildren, expectedChildren, @"Complete children should return list with empty on empty overwrites.");
+}
+
+- (void) testCompleteChildrenDoesntReturnDeepOverwrites {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-1/deep/path"]];
+ NSArray *expectedChildren = @[];
+ NSArray *completeChildren = [compoundWrite completeChildren];
+ XCTAssertEqualObjects(completeChildren, expectedChildren, @"Should not get complete children on deep overwrites.");
+}
+
+- (void) testCompleteChildrenReturnAllCompleteChildrenButNoIncomplete {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-1/deep/path"]];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-2"]];
+ compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child-3"]];
+ NSDictionary *expected = @{
+ @"child-2":self.leafNode,
+ @"child-3":[FEmptyNode emptyNode]
+ };
+ NSMutableDictionary *actual = [[NSMutableDictionary alloc] init];
+ for (FNamedNode *node in compoundWrite.completeChildren) {
+ [actual setObject:node.node forKey:node.name];
+ }
+ XCTAssertEqualObjects(actual, expected, @"Complete children should get returned, but not incomplete ones.");
+}
+
+- (void) testCompleteChildrenReturnAllChildrenForRootSet {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.baseNode atPath:[FPath empty]];
+
+ NSDictionary *expected = @{
+ @"child-1": [FSnapshotUtilities nodeFrom:@"value-1"],
+ @"child-2": [FSnapshotUtilities nodeFrom:@"value-2"]
+ };
+
+ NSMutableDictionary *actual = [[NSMutableDictionary alloc] init];
+ for (FNamedNode *node in compoundWrite.completeChildren) {
+ [actual setObject:node.node forKey:node.name];
+ }
+ XCTAssertEqualObjects(actual, expected, @"Complete children should return all children on root set.");
+}
+
+- (void) testEmptyMergeHasNoShadowingWrite {
+ XCTAssertFalse([[FCompoundWrite emptyWrite] hasCompleteWriteAtPath:[FPath empty]], @"Empty merge has no shadowing write.");
+}
+
+- (void) testCompoundWriteWithEmptyRootHasShadowingWrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[FPath empty]];
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Empty write should have shadowing write at root.");
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"child"]], @"Empty write should have complete write at child.");
+}
+
+- (void) testCompoundWriteWithRootHasShadowingWrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]];
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Root write should have shadowing write at root.");
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"child"]], @"Root write should have complete write at child.");
+}
+
+- (void) testCompoundWriteWithDeepUpdateHasShadowingWrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"deep/update"]];
+ XCTAssertFalse([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Deep write should not have complete write at root.");
+ XCTAssertFalse([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"deep"]], @"Deep write should not have should have complete write at child.");
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"deep/update"]], @"Deep write should have complete write at deep child.");
+}
+
+- (void) testCompoundWriteWithPriorityUpdateHasShadowingWrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]];
+ XCTAssertFalse([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Write with priority at root should not have complete write at root.");
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@".priority"]], @"Write with priority at root should have complete priority.");
+}
+
+- (void) testUpdatesCanBeRemoved {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ compoundWrite = [compoundWrite addWrite:update atPath:[[FPath alloc] initWith:@"child-1"]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-1"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, self.baseNode, @"Updates should be removed.");
+}
+
+- (void) testDeepRemovesHasNoEffectOnOverlayingSet {
+ // TODO I don't get this one.
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update1 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"];
+ id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"];
+ compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1"]];
+ compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]];
+ compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ NSDictionary *expected = @{
+ @"foo":@"new-foo-value",
+ @"bar":@"bar-value",
+ @"baz":@"baz-value"
+ };
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expected]];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Deep removes should have no effect on overlaying set.");
+}
+
+- (void) testRemoveAtPathWithoutSetIsWithoutEffect {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update1 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"];
+ id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"];
+ compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1"]];
+ compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]];
+ compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-2"]];
+ NSDictionary *expected = @{
+ @"foo":@"new-foo-value",
+ @"bar":@"bar-value",
+ @"baz":@"baz-value"
+ };
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expected]];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Removing at path without a set should have no effect.");
+}
+
+- (void) testCanRemovePriority {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@".priority"]];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:self.leafNode withPriority:[FEmptyNode emptyNode]];
+}
+
+- (void) testRemovingOnlyAffectsRemovedPath {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ NSDictionary *updateDictionary = @{
+ @"child-1": @"new-value-1",
+ @"child-2": [NSNull null],
+ @"child-3": @"value-3"
+ };
+ FCompoundWrite *updates = [FCompoundWrite compoundWriteWithValueDictionary:updateDictionary];
+ compoundWrite = [compoundWrite addCompoundWrite:updates atPath:[FPath empty]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-2"]];
+
+ NSDictionary *expected = @{
+ @"child-1": @"new-value-1",
+ @"child-2": @"value-2",
+ @"child-3": @"value-3"
+ };
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:expected];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Removing should only affected removed paths");
+}
+
+- (void) testRemoveRemovesAllDeeperSets {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"];
+ id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"];
+ compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]];
+ compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-1"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, self.baseNode, @"Remove should remove deeper sets.");
+}
+
+- (void) testRemoveAtRootAlsoRemovesPriority {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:[[FLeafNode alloc] initWithValue:@"foo" withPriority:self.priorityNode] atPath:[FPath empty]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[FPath empty]];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@"value"];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:[FEmptyNode emptyNode]];
+}
+
+- (void) testUpdatingPriorityDoesntOverwriteLeafNode {
+ // TODO I don't get this one.
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"child/.priority"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, self.leafNode, @"Updating priority should not overwrite leaf node.");
+}
+
+- (void) testUpdatingEmptyChildNodeDoesntOverwriteLeafNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]];
+ compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, self.leafNode, @"Updating empty node should not overwrite leaf node.");
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FIRDataSnapshotTests.h b/Example/Database/Tests/Unit/FIRDataSnapshotTests.h
new file mode 100644
index 0000000..b69e7f2
--- /dev/null
+++ b/Example/Database/Tests/Unit/FIRDataSnapshotTests.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 <XCTest/XCTest.h>
+
+@interface FIRDataSnapshotTests : XCTestCase
+
+@end
diff --git a/Example/Database/Tests/Unit/FIRDataSnapshotTests.m b/Example/Database/Tests/Unit/FIRDataSnapshotTests.m
new file mode 100644
index 0000000..2a442df
--- /dev/null
+++ b/Example/Database/Tests/Unit/FIRDataSnapshotTests.m
@@ -0,0 +1,449 @@
+/*
+ * 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 "FIRDataSnapshotTests.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FTestHelpers.h"
+#import "FLeafNode.h"
+#import "FChildrenNode.h"
+#import "FEmptyNode.h"
+#import "FImmutableSortedDictionary.h"
+#import "FUtilities.h"
+#import "FSnapshotUtilities.h"
+#import "FIRDatabaseReference.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FPathIndex.h"
+#import "FLeafNode.h"
+#import "FValueIndex.h"
+
+@implementation FIRDataSnapshotTests
+
+- (void)setUp
+{
+ [super setUp];
+
+ // Set-up code here.
+}
+
+- (void)tearDown
+{
+ // Tear-down code here.
+
+ [super tearDown];
+}
+
+- (FIRDataSnapshot *)snapshotFor:(id)jsonDict {
+ FIRDatabaseConfig *config = [FIRDatabaseConfig defaultConfig];
+ FRepoInfo* repoInfo = [[FRepoInfo alloc] initWithHost:@"example.com" isSecure:NO withNamespace:@"default"];
+ FIRDatabaseReference * dummyRef = [[FIRDatabaseReference alloc] initWithRepo:[FRepoManager getRepo:repoInfo config:config] path:[FPath empty]];
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:[FSnapshotUtilities nodeFrom:jsonDict]];
+ FIRDataSnapshot * snapshot = [[FIRDataSnapshot alloc] initWithRef:dummyRef indexedNode:indexed];
+ return snapshot;
+}
+
+- (void) testCreationLeafNodesVariousTypes {
+
+ id<FNode> fortyTwo = [FSnapshotUtilities nodeFrom:@42];
+ FLeafNode* x = [[FLeafNode alloc] initWithValue:@5 withPriority:fortyTwo];
+
+ XCTAssertEqualObjects(x.val, @5, @"Values are the same");
+ XCTAssertEqualObjects(x.getPriority, [FSnapshotUtilities nodeFrom:@42], @"Priority is the same");
+ XCTAssertTrue([x isLeafNode], @"Node is a leaf");
+
+ x = [[FLeafNode alloc] initWithValue:@"test"];
+ XCTAssertEqualObjects(x.value, @"test", @"Check if leaf node is holding onto a string value");
+
+ x = [[FLeafNode alloc] initWithValue:[NSNumber numberWithBool:YES]];
+ XCTAssertTrue([x.value boolValue], @"Check if leaf node is holding onto a YES boolean");
+
+ x = [[FLeafNode alloc] initWithValue:[NSNumber numberWithBool:NO]];
+ XCTAssertFalse([x.value boolValue], @"Check if leaf node is holding onto a NO boolean");
+}
+
+- (void) testUpdatingPriorityWithoutChangingOld {
+ FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test" withPriority:[FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]]];
+ FLeafNode* y = [x updatePriority:[FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:187]]];
+
+ // old node is the same
+ XCTAssertEqualObjects(x.value, @"test", @"Values of old node are the same");
+ XCTAssertEqualObjects(x.getPriority, [FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]], @"Priority of old node is the same.");
+
+ // new node has the new priority but the old value
+ XCTAssertEqualObjects(y.value, @"test", @"Values of old node are the same");
+ XCTAssertEqualObjects(y.getPriority, [FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:187]], @"Priority of new node is update");
+}
+
+- (void) testUpdateImmediateChildReturnsANewChildrenNode {
+ FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test" withPriority:[FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]]];
+ FChildrenNode* y = [x updateImmediateChild:@"test" withNewChild:[[FLeafNode alloc] initWithValue:@"foo"]];
+
+ XCTAssertFalse([y isLeafNode], @"New node is no longer a leaf");
+ XCTAssertEqualObjects(y.getPriority, [FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]], @"Priority of new node is update");
+
+ XCTAssertEqualObjects([[y getImmediateChild:@"test"] val], @"foo", @"Child node has the correct value");
+}
+
+- (void) testGetImmediateChildOnLeafNode {
+ FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test"];
+ XCTAssertEqualObjects([x getImmediateChild:@"foo"], [FEmptyNode emptyNode], @"Get immediate child on leaf node returns empty node");
+}
+
+- (void) testGetChildReturnsEmptyNode {
+ FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test"];
+ XCTAssertEqualObjects([x getChild:[[FPath alloc] initWith:@"foo/bar"]], [FEmptyNode emptyNode], @"Get child returns an empty node.");
+}
+
+- (NSComparator) defaultComparator {
+ return ^(id obj1, id obj2) {
+ if([obj1 respondsToSelector:@selector(compare:)] && [obj2 respondsToSelector:@selector(compare:)]) {
+ return [obj1 compare:obj2];
+ }
+ else {
+ if(obj1 < obj2) {
+ return (NSComparisonResult)NSOrderedAscending;
+ }
+ else if (obj1 > obj2) {
+ return (NSComparisonResult)NSOrderedDescending;
+ }
+ else {
+ return (NSComparisonResult)NSOrderedSame;
+ }
+ }
+ };
+}
+
+- (void) testUpdateImmediateChildWithNewNode {
+ FImmutableSortedDictionary* children = [FImmutableSortedDictionary dictionaryWithComparator:[self defaultComparator]];
+ FChildrenNode* x = [[FChildrenNode alloc] initWithChildren:children];
+ FLeafNode* newValue = [[FLeafNode alloc] initWithValue:@"new value"];
+ FChildrenNode* y = [x updateImmediateChild:@"test" withNewChild:newValue];
+
+ XCTAssertEqualObjects(x.children, children, @"Original object stays the same");
+ XCTAssertEqualObjects([y.children objectForKey:@"test"], newValue, @"New internal node with the proper new value");
+ XCTAssertEqualObjects([[y.children objectForKey:@"test"] val], @"new value", @"Check the payload");
+}
+
+- (void) testUpdatechildWithNewNode {
+ FImmutableSortedDictionary* children = [FImmutableSortedDictionary dictionaryWithComparator:[self defaultComparator]];
+ FChildrenNode* x = [[FChildrenNode alloc] initWithChildren:children];
+ FLeafNode* newValue = [[FLeafNode alloc] initWithValue:@"new value"];
+ FChildrenNode* y = [x updateChild:[[FPath alloc] initWith:@"test/foo"] withNewChild:newValue];
+ XCTAssertEqualObjects(x.children, children, @"Original object stays the same");
+ XCTAssertEqualObjects([y getChild:[[FPath alloc] initWith:@"test/foo"]], newValue, @"Check if the updateChild held");
+ XCTAssertEqualObjects([[y getChild:[[FPath alloc] initWith:@"test/foo"]] val], @"new value", @"Check the payload");
+}
+
+- (void) testObjectTypes {
+ XCTAssertEqualObjects(@"string", [FUtilities getJavascriptType:@""], @"Check string type");
+ XCTAssertEqualObjects(@"string", [FUtilities getJavascriptType:@"moo"], @"Check string type");
+
+ XCTAssertEqualObjects(@"boolean", [FUtilities getJavascriptType:@YES], @"Check boolean type");
+ XCTAssertEqualObjects(@"boolean", [FUtilities getJavascriptType:@NO], @"Check boolean type");
+
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@5], @"Check number type");
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@5.5], @"Check number type");
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@0], @"Check number type");
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@8273482734], @"Check number type");
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@-2], @"Check number type");
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@-2.11], @"Check number type");
+}
+
+- (void) testNodeHashWorksCorrectly {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@{ @"intNode" : @4,
+ @"doubleNode" : @4.5623,
+ @"stringNode" : @"hey guys",
+ @"boolNode" : @YES }];
+
+ XCTAssertEqualObjects(@"eVih19a6ZDz3NL32uVBtg9KSgQY=", [[node getImmediateChild:@"intNode"] dataHash], @"Check integer node");
+ XCTAssertEqualObjects(@"vf1CL0tIRwXXunHcG/irRECk3lY=", [[node getImmediateChild:@"doubleNode"] dataHash], @"Check double node");
+ XCTAssertEqualObjects(@"CUNLXWpCVoJE6z7z1vE57lGaKAU=", [[node getImmediateChild:@"stringNode"] dataHash], @"Check string node");
+ XCTAssertEqualObjects(@"E5z61QM0lN/U2WsOnusszCTkR8M=", [[node getImmediateChild:@"boolNode"] dataHash], @"Check boolean node");
+ XCTAssertEqualObjects(@"6Mc4jFmNdrLVIlJJjz2/MakTK9I=", [node dataHash], @"Check compound node");
+}
+
+- (void) testNodeHashWorksCorrectlyWithPriorities {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@{
+ @"root": @{ @"c": @{@".value": @99, @".priority": @"abc"}, @".priority" : @"def" }
+ }];
+
+ XCTAssertEqualObjects(@"Fm6tzN4CVEu5WxFDZUdTtqbTVaA=", [node dataHash], @"Check compound node");
+}
+
+- (void) testGetPredecessorChild {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@{@"d": @YES, @"a": @YES, @"g": @YES, @"c": @YES, @"e": @YES}];
+
+ XCTAssertNil([node predecessorChildKey:@"a"],
+ @"Check the first one sorted properly");
+ XCTAssertEqualObjects([node predecessorChildKey:@"c"],
+ @"a", @"Check a comes before c");
+ XCTAssertEqualObjects([node predecessorChildKey:@"d"],
+ @"c", @"Check c comes before d");
+ XCTAssertEqualObjects([node predecessorChildKey:@"e"],
+ @"d", @"Check d comes before e");
+ XCTAssertEqualObjects([node predecessorChildKey:@"g"],
+ @"e", @"Check e comes before g");
+}
+
+- (void) testSortedChildrenGetPredecessorChildWorksCorrectly {
+ // XXX impl SortedChildren
+}
+
+- (void) testSortedChildrenUpdateImmediateChildrenWorksCorrectly {
+ // XXX imple SortedChildren
+}
+
+- (void) testDataSnapshotHasChildrenWorks {
+
+ FIRDataSnapshot * snap = [self snapshotFor:@{}];
+ XCTAssertFalse([snap hasChildren], @"Empty dict has no children");
+
+ snap = [self snapshotFor:@5];
+ XCTAssertFalse([snap hasChildren], @"Leaf node has no children");
+
+ snap = [self snapshotFor:@{@"x": @5}];
+ XCTAssertTrue([snap hasChildren], @"Properly has children");
+}
+
+- (void) testDataSnapshotValWorks {
+ FIRDataSnapshot * snap = [self snapshotFor:@5];
+ XCTAssertEqualObjects([snap value], @5, @"Leaf node values are correct");
+
+ snap = [self snapshotFor:@{}];
+ XCTAssertTrue([snap value] == [NSNull null], @"Snapshot value is properly null");
+
+ NSDictionary* dict = @{
+ @"x": @5,
+ @"y": @{
+ @"ya": @1,
+ @"yb": @2,
+ @"yc": @{ @"yca" : @3}
+ }
+ };
+
+ snap = [self snapshotFor:dict];
+ XCTAssertTrue([dict isEqualToDictionary:[snap value]], @"Check if the dictionaries are the same");
+}
+
+- (void) testDataSnapshotChildWorks {
+ FIRDataSnapshot * snap = [self snapshotFor:@{@"x": @5, @"y": @{@"yy": @3, @"yz": @4}}];
+
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"x"] value], @5, @"Check x");
+ NSDictionary* dict = @{@"yy": @3, @"yz": @4};
+ XCTAssertTrue([[[snap childSnapshotForPath:@"y"] value] isEqualToDictionary:dict], @"Check y");
+
+ XCTAssertEqualObjects([[[snap childSnapshotForPath:@"y"] childSnapshotForPath:@"yy"] value], @3, @"Check y/yy");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"y/yz"] value], @4, @"Check y/yz");
+ XCTAssertTrue([[snap childSnapshotForPath:@"z"] value] == [NSNull null], @"Check nonexistent z");
+ XCTAssertTrue([[snap childSnapshotForPath:@"x/y"] value] == [NSNull null], @"Check value of existent internal node");
+ XCTAssertTrue([[[snap childSnapshotForPath:@"x"] childSnapshotForPath:@"y"] value] == [NSNull null], @"Check value of existent internal node");
+}
+
+- (void) testDataSnapshotHasChildWorks {
+ FIRDataSnapshot * snap = [self snapshotFor:@{@"x": @5, @"y": @{@"yy": @3, @"yz": @4}}];
+
+ XCTAssertTrue([snap hasChild:@"x"], @"Has child");
+ XCTAssertTrue([snap hasChild:@"y/yy"], @"Has child");
+
+ XCTAssertFalse([snap hasChild:@"dinosaur dinosaucer"], @"No child");
+ XCTAssertFalse([[snap childSnapshotForPath:@"x"] hasChild:@"anything"], @"No child");
+ XCTAssertFalse([snap hasChild:@"x/anything/at/all"], @"No child");
+}
+
+- (void) testDataSnapshotNameWorks {
+ FIRDataSnapshot * snap = [self snapshotFor:@{@"a": @{@"b": @{@"c": @5}}}];
+
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"a"] key], @"a", @"Check child key");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"a/b/c"] key], @"c", @"Check child key");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"/a/b/c"] key], @"c", @"Check child key");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"/a/b/c/"] key], @"c", @"Check child key");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"////a///b////c///"] key], @"c", @"Check child key");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"////"] key], [snap key], @"Check root key");
+
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"/z/q/r/v////m"] key], @"m", @"Should also work for nonexistent paths");
+}
+
+- (void) testDataSnapshotForEachWithNoPriorities {
+ FIRDataSnapshot * snap = [self snapshotFor:@{@"a": @1, @"z": @26, @"m": @13, @"n": @14, @"c": @3, @"b": @2, @"e": @5}];
+
+ NSMutableString* out = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * child in snap.children) {
+ [out appendFormat:@"%@:%@:", [child key], [child value] ];
+ }
+
+ XCTAssertTrue([out isEqualToString:@"a:1:b:2:c:3:e:5:m:13:n:14:z:26:"], @"Proper order");
+}
+
+- (void) testDataSnapshotForEachWorksWithNumericPriorities {
+ FIRDataSnapshot * snap = [self snapshotFor:@{
+ @"a": @{@".value" : @1, @".priority": @26},
+ @"z": @{@".value" : @26, @".priority": @1},
+ @"m": @{@".value" : @13, @".priority": @14},
+ @"n": @{@".value" : @14, @".priority": @12},
+ @"c": @{@".value" : @3, @".priority": @24},
+ @"b": @{@".value" : @2, @".priority": @25},
+ @"e": @{@".value" : @5, @".priority": @22},
+ }];
+
+ NSMutableString* out = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * child in snap.children) {
+ [out appendFormat:@"%@:%@:", [child key], [child value] ];
+ }
+
+ XCTAssertTrue([out isEqualToString:@"z:26:n:14:m:13:e:5:c:3:b:2:a:1:"], @"Proper order");
+}
+
+- (void) testDataSnapshotForEachWorksWithNumericPrioritiesAsStrings {
+ FIRDataSnapshot * snap = [self snapshotFor:@{
+ @"a": @{@".value" : @1, @".priority": @"26"},
+ @"z": @{@".value" : @26, @".priority": @"1"},
+ @"m": @{@".value" : @13, @".priority": @"14"},
+ @"n": @{@".value" : @14, @".priority": @"12"},
+ @"c": @{@".value" : @3, @".priority": @"24"},
+ @"b": @{@".value" : @2, @".priority": @"25"},
+ @"e": @{@".value" : @5, @".priority": @"22"},
+ }];
+
+ NSMutableString* out = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * child in snap.children) {
+ [out appendFormat:@"%@:%@:", [child key], [child value] ];
+ }
+
+ XCTAssertTrue([out isEqualToString:@"z:26:n:14:m:13:e:5:c:3:b:2:a:1:"], @"Proper order");
+}
+
+- (void) testDataSnapshotForEachWorksAlphaPriorities {
+ FIRDataSnapshot * snap = [self snapshotFor:@{
+ @"a": @{@".value" : @1, @".priority": @"first"},
+ @"z": @{@".value" : @26, @".priority": @"second"},
+ @"m": @{@".value" : @13, @".priority": @"third"},
+ @"n": @{@".value" : @14, @".priority": @"fourth"},
+ @"c": @{@".value" : @3, @".priority": @"fifth"},
+ @"b": @{@".value" : @2, @".priority": @"sixth"},
+ @"e": @{@".value" : @5, @".priority": @"seventh"},
+ }];
+
+ NSMutableString* output = [[NSMutableString alloc] init];
+ NSMutableArray* priorities = [[NSMutableArray alloc] init];
+ for (FIRDataSnapshot * child in snap.children) {
+ [output appendFormat:@"%@:%@:", child.key, child.value];
+ [priorities addObject:child.priority];
+ }
+
+ XCTAssertTrue([output isEqualToString:@"c:3:a:1:n:14:z:26:e:5:b:2:m:13:"], @"Proper order");
+ NSArray* expected = @[@"fifth", @"first", @"fourth", @"second", @"seventh", @"sixth", @"third"];
+ XCTAssertTrue([priorities isEqualToArray:expected], @"Correct priorities");
+ XCTAssertTrue(snap.childrenCount == 7, @"Got correct children count");
+}
+
+
+- (void) testDataSnapshotForEachWorksWithMixedPriorities {
+ FIRDataSnapshot * snap = [self snapshotFor:@{
+ @"alpha42": @{@".value": @1, @".priority": @"zed" },
+ @"noPriorityC": @{@".value": @1, @".priority": [NSNull null] },
+ @"alpha14": @{@".value": @1, @".priority": @"500" },
+ @"noPriorityB": @{@".value": @1, @".priority": [NSNull null] },
+ @"num80": @{@".value": @1, @".priority": @4000.1 },
+ @"alpha13": @{@".value": @1, @".priority": @"4000" },
+ @"alpha11": @{@".value": @1, @".priority": @"24" },
+ @"alpha41": @{@".value": @1, @".priority": @"zed" },
+ @"alpha20": @{@".value": @1, @".priority": @"horse" },
+ @"num20": @{@".value": @1, @".priority": @123 },
+ @"num70": @{@".value": @1, @".priority": @4000.01 },
+ @"noPriorityA": @{@".value": @1, @".priority": [NSNull null] },
+ @"alpha30": @{@".value": @1, @".priority": @"tree" },
+ @"alpha12": @{@".value": @1, @".priority": @"300" },
+ @"num60": @{@".value": @1, @".priority": @4000.001 },
+ @"alpha10": @{@".value": @1, @".priority": @"0horse" },
+ @"num42": @{@".value": @1, @".priority": @500 },
+ @"alpha40": @{@".value": @1, @".priority": @"zed" },
+ @"num40": @{@".value": @1, @".priority": @500 }
+ }];
+
+ NSMutableString* out = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * child in snap.children) {
+ [out appendFormat:@"%@, ", [child key]];
+ }
+
+ NSString* expected = @"noPriorityA, noPriorityB, noPriorityC, num20, num40, num42, num60, num70, num80, alpha10, alpha11, alpha12, alpha13, alpha14, alpha20, alpha30, alpha40, alpha41, alpha42, ";
+
+ XCTAssertTrue([expected isEqualToString:out], @"Proper ordering seen");
+
+}
+
+- (void) testIgnoresNullValues {
+ FIRDataSnapshot * snap = [self snapshotFor:@{@"a": @1, @"b": [NSNull null]}];
+ XCTAssertFalse([snap hasChild:@"b"], @"Should not have b, it was null");
+}
+
+- (void)testNameComparator {
+ NSComparator keyComparator = [FUtilities keyComparator];
+ XCTAssertEqual(keyComparator(@"1234", @"1234"), NSOrderedSame, @"NameComparator compares ints");
+ XCTAssertEqual(keyComparator(@"1234", @"12345"), NSOrderedAscending, @"NameComparator compares ints");
+ XCTAssertEqual(keyComparator(@"4321", @"1234"), NSOrderedDescending, @"NameComparator compares ints");
+ XCTAssertEqual(keyComparator(@"1234", @"zzzz"), NSOrderedAscending, @"NameComparator priorities ints");
+ XCTAssertEqual(keyComparator(@"4321", @"12a"), NSOrderedAscending, @"NameComparator priorities ints");
+ XCTAssertEqual(keyComparator(@"abc", @"abcd"), NSOrderedAscending, @"NameComparator uses lexiographical sorting for strings.");
+ XCTAssertEqual(keyComparator(@"zzzz", @"aaaa"), NSOrderedDescending, @"NameComparator compares strings");
+ XCTAssertEqual(keyComparator(@"-1234", @"0"), NSOrderedAscending, @"NameComparator compares negative values");
+ XCTAssertEqual(keyComparator(@"-1234", @"-1234"), NSOrderedSame, @"NameComparator compares negative values");
+ XCTAssertEqual(keyComparator(@"-1234", @"-4321"), NSOrderedDescending, @"NameComparator compares negative values");
+ XCTAssertEqual(keyComparator(@"-1234", @"-"), NSOrderedAscending, @"NameComparator does not parse - as integer");
+ XCTAssertEqual(keyComparator(@"-", @"1234"), NSOrderedDescending, @"NameComparator does not parse - as integer");
+}
+
+- (void) testExistsWorks {
+ FIRDataSnapshot * snap;
+
+ snap = [self snapshotFor:@{}];
+ XCTAssertFalse([snap exists], @"Should not exist");
+
+ snap = [self snapshotFor:@{ @".priority": @"1" }];
+ XCTAssertFalse([snap exists], @"Should not exist");
+
+ snap = [self snapshotFor:[NSNull null]];
+ XCTAssertFalse([snap exists], @"Should not exist");
+
+ snap = [self snapshotFor:[NSNumber numberWithBool:YES]];
+ XCTAssertTrue([snap exists], @"Should exist");
+
+ snap = [self snapshotFor:@5];
+ XCTAssertTrue([snap exists], @"Should exist");
+
+ snap = [self snapshotFor:@{ @"x": @5 }];
+ XCTAssertTrue([snap exists], @"Should exist");
+}
+
+- (void) testUpdatingEmptyChildDoesntOverwriteLeafNode {
+ FLeafNode *node = [[FLeafNode alloc] initWithValue:@"value"];
+ XCTAssertEqualObjects(node, [node updateChild:[[FPath alloc] initWith:@".priority"] withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node.");
+ XCTAssertEqualObjects(node, [node updateChild:[[FPath alloc] initWith:@"child"] withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node.");
+ XCTAssertEqualObjects(node, [node updateChild:[[FPath alloc] initWith:@"child/.priority"] withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node.");
+ XCTAssertEqualObjects(node, [node updateImmediateChild:@"child" withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node.");
+ XCTAssertEqualObjects(node, [node updateImmediateChild:@".priority" withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node.");
+}
+
+/* This was reported by a customer, which broke because 유주연 > 윤규완오빠 but also 윤규완오빠 > 유주연 with the default
+ * string comparison... */
+- (void)testUnicodeEquality {
+ FNamedNode *node1 = [[FNamedNode alloc] initWithName:@"a" andNode:[[FLeafNode alloc] initWithValue:@"유주연"]];
+ FNamedNode *node2 = [[FNamedNode alloc] initWithName:@"a" andNode:[[FLeafNode alloc] initWithValue:@"윤규완오빠"]];
+ id<FIndex> index = [FValueIndex valueIndex];
+
+ // x < y should imply y > x
+ XCTAssertEqual([index compareNamedNode:node1 toNamedNode:node2], -[index compareNamedNode:node2 toNamedNode:node1]);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FIRMutableDataTests.h b/Example/Database/Tests/Unit/FIRMutableDataTests.h
new file mode 100644
index 0000000..cd0cec7
--- /dev/null
+++ b/Example/Database/Tests/Unit/FIRMutableDataTests.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 "FTestBase.h"
+
+@interface FIRMutableDataTests : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Unit/FIRMutableDataTests.m b/Example/Database/Tests/Unit/FIRMutableDataTests.m
new file mode 100644
index 0000000..d36f139
--- /dev/null
+++ b/Example/Database/Tests/Unit/FIRMutableDataTests.m
@@ -0,0 +1,113 @@
+/*
+ * 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 "FIRMutableDataTests.h"
+#import "FSnapshotUtilities.h"
+#import "FIRMutableData_Private.h"
+
+@implementation FIRMutableDataTests
+
+- (FIRMutableData *)dataFor:(id)input {
+
+ id<FNode> node = [FSnapshotUtilities nodeFrom:input];
+ return [[FIRMutableData alloc] initWithNode:node];
+}
+
+- (void) testDataForInWorksAlphaPriorities {
+ FIRMutableData * data = [self dataFor:@{
+ @"a": @{@".value" : @1, @".priority": @"first"},
+ @"z": @{@".value" : @26, @".priority": @"second"},
+ @"m": @{@".value" : @13, @".priority": @"third"},
+ @"n": @{@".value" : @14, @".priority": @"fourth"},
+ @"c": @{@".value" : @3, @".priority": @"fifth"},
+ @"b": @{@".value" : @2, @".priority": @"sixth"},
+ @"e": @{@".value" : @5, @".priority": @"seventh"},
+ }];
+
+ NSMutableString* output = [[NSMutableString alloc] init];
+ NSMutableArray* priorities = [[NSMutableArray alloc] init];
+ for (FIRMutableData * child in data.children) {
+ [output appendFormat:@"%@:%@:", child.key, child.value];
+ [priorities addObject:child.priority];
+ }
+
+ XCTAssertTrue([output isEqualToString:@"c:3:a:1:n:14:z:26:e:5:b:2:m:13:"], @"Proper order");
+ NSArray* expected = @[@"fifth", @"first", @"fourth", @"second", @"seventh", @"sixth", @"third"];
+ XCTAssertTrue([priorities isEqualToArray:expected], @"Correct priorities");
+ XCTAssertTrue(data.childrenCount == 7, @"Got correct children count");
+}
+
+- (void) testWritingMutableData {
+ FIRMutableData * data = [self dataFor:@{}];
+
+ data.value = @{@"a": @1, @"b": @2};
+ XCTAssertTrue([data hasChildren], @"Should have children node");
+ XCTAssertTrue(data.childrenCount == 2, @"Counts both children");
+ XCTAssertTrue([data hasChildAtPath:@"a"], @"Can see the children individually");
+
+ FIRMutableData * childData = [data childDataByAppendingPath:@"b"];
+ XCTAssertTrue([childData.value isEqualToNumber:@2], @"Get the correct child data");
+ childData.value = @3;
+
+ NSDictionary* expected = @{@"a": @1, @"b": @3};
+ XCTAssertTrue([data.value isEqualToDictionary:expected], @"Updates the parent");
+
+ int count = 0;
+ for (FIRDataSnapshot * __unused child in data.children) {
+ count++;
+ if (count == 1) {
+ [data childDataByAppendingPath:@"c"].value = @4;
+ }
+ }
+ XCTAssertTrue(count == 2, @"Should not iterate nodes added while iterating");
+ XCTAssertTrue(data.childrenCount == 3, @"Got the new node we added while iterating");
+ XCTAssertTrue([[data childDataByAppendingPath:@"c"].value isEqualToNumber:@4], @"Can see the value of the new node");
+}
+
+- (void) testMutableDataNavigation {
+ FIRMutableData * data = [self dataFor:@{@"a": @1, @"b": @2}];
+
+ XCTAssertNil(data.key, @"Root data has no key");
+
+ // Can get a child
+ FIRMutableData * childData = [data childDataByAppendingPath:@"b"];
+ XCTAssertTrue([childData.key isEqualToString:@"b"], @"Child has correct key");
+
+ // Can get a non-existent child
+ childData = [data childDataByAppendingPath:@"c"];
+ XCTAssertTrue(childData != nil, @"Wrapper should not be nil");
+ XCTAssertTrue([childData.key isEqualToString:@"c"], @"Child should have correct key");
+ XCTAssertTrue(childData.value == [NSNull null], @"Non-existent data has no value");
+ childData.value = @{@"d": @4};
+
+ NSDictionary* expected = @{@"a": @1, @"b": @2, @"c": @{@"d": @4}};
+ XCTAssertTrue([data.value isEqualToDictionary:expected], @"Setting non-existent child updates parent");
+}
+
+- (void) testPriorities {
+ FIRMutableData * data = [self dataFor:@{@"a": @1, @"b": @2}];
+
+ XCTAssertTrue(data.priority == [NSNull null], @"Should not be a priority");
+ data.priority = @"foo";
+ XCTAssertTrue([data.priority isEqualToString:@"foo"], @"Should now have a priority");
+ data.value = @3;
+ XCTAssertTrue(data.priority == [NSNull null], @"Setting a value overrides a priority");
+ data.priority = @4;
+ data.value = nil;
+ XCTAssertTrue(data.priority == [NSNull null], @"Removing the value does remove the priority");
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m b/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m
new file mode 100644
index 0000000..658a894
--- /dev/null
+++ b/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m
@@ -0,0 +1,583 @@
+/*
+ * 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/XCTest.h>
+
+#import "FLevelDBStorageEngine.h"
+#import "FSnapshotUtilities.h"
+#import "FQueryParams.h"
+#import "FPathIndex.h"
+#import "FTrackedQuery.h"
+#import "FWriteRecord.h"
+#import "FTestHelpers.h"
+#import "FEmptyNode.h"
+
+@interface FLevelDBStorageEngineTests : XCTestCase
+
+@end
+
+@implementation FLevelDBStorageEngineTests
+
+- (FLevelDBStorageEngine *)cleanStorageEngine {
+ NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"test-db"];
+ FLevelDBStorageEngine *db = [[FLevelDBStorageEngine alloc] initWithPath:path];
+ [db purgeEverything];
+ return db;
+}
+
+#define SAMPLE_NODE ([FSnapshotUtilities nodeFrom:@{ @"foo": @{ @"bar": @YES, @"baz": @"string" }, @"qux": @2, @"quu": @1.2 }])
+
+#define ONE_MEG_NODE ([FTestHelpers leafNodeOfSize:1024*1024])
+#define FIVE_MEG_NODE ([FTestHelpers leafNodeOfSize:5*1024*1024])
+#define TEN_MEG_NODE ([FTestHelpers leafNodeOfSize:10*1024*1024])
+#define TEN_MEG_MINUS_ONE_NODE ([FTestHelpers leafNodeOfSize:10*1024*1024 - 1])
+
+#define SAMPLE_PARAMS \
+ ([[[[[FQueryParams defaultInstance] orderBy:[[FPathIndex alloc] initWithPath:PATH(@"child")]] \
+ startAt:[FSnapshotUtilities nodeFrom:@"startVal"] childKey:@"startKey"] \
+ endAt:[FSnapshotUtilities nodeFrom:@"endVal"] childKey:@"endKey"] \
+ limitToLast:5])
+
+#define SAMPLE_QUERY \
+ ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:SAMPLE_PARAMS])
+
+#define DEFAULT_FOO_QUERY \
+ ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:[FQueryParams defaultInstance]])
+
+#define SAMPLE_TRACKED_QUERY \
+ ([[FTrackedQuery alloc] initWithId:1 \
+ query:SAMPLE_QUERY \
+ isPinned:NO \
+ lastUse:100 \
+ Active:NO \
+ isComplete:NO])
+#define OVERWRITE_RECORD(__path, __node, __writeId) \
+ ([[FWriteRecord alloc] initWithPath:[FPath pathWithString:__path] overwrite:__node writeId:__writeId visible:YES])
+
+#define MERGE_RECORD(__path, __merge, __writeId) \
+ ([[FWriteRecord alloc] initWithPath:[FPath pathWithString:__path] merge:__merge writeId:__writeId])
+
+- (void)testUserWriteIsPersisted {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserOverwrite:SAMPLE_NODE atPath:[FPath pathWithString:@"foo/bar"] writeId:1];
+ XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"foo/bar", SAMPLE_NODE, 1)]);
+}
+
+- (void)testUserMergeIsPersisted {
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @{@"bar": @1, @"baz": @"string"}, @"quu": @YES}];
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:1];
+ XCTAssertEqualObjects(engine.userWrites, @[MERGE_RECORD(@"foo/bar", merge, 1)]);
+}
+
+- (void)testDeepUserMergeIsPersisted {
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo/bar": @1, @"foo/baz": @"string", @"quu/qux": @YES, @"shallow": @2}];
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:1];
+ XCTAssertEqualObjects(engine.userWrites, @[MERGE_RECORD(@"foo/bar", merge, 1)]);
+}
+
+- (void)testSameWriteIdOverwritesOldWrite {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserOverwrite:NODE(@"first") atPath:PATH(@"foo/bar") writeId:1];
+ [engine saveUserOverwrite:NODE(@"second") atPath:PATH(@"other/path") writeId:1];
+ XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"other/path", NODE(@"second"), 1)]);
+}
+
+- (void)testHugeWriteWorks {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1];
+ FCompoundWrite *merge = [[FCompoundWrite emptyWrite] addWrite:TEN_MEG_NODE atKey:@"update"];
+ [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:2];
+ NSArray *expected = @[OVERWRITE_RECORD(@"foo/bar", TEN_MEG_NODE, 1), MERGE_RECORD(@"foo/bar", merge, 2)];
+ XCTAssertEqualObjects(engine.userWrites, expected);
+}
+
+- (void)testHugeWritesCanBeDeleted {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1];
+ [engine removeUserWrite:1];
+ XCTAssertTrue(engine.userWrites.count == 0);
+}
+
+- (void)testHugeWritesCanBeInterleavedWithSmallWrites {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine saveUserOverwrite:NODE(@"node-1") atPath:PATH(@"foo/1") writeId:1];
+ [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/2") writeId:2];
+ [engine saveUserOverwrite:NODE(@"node-3") atPath:PATH(@"foo/3") writeId:3];
+ [engine saveUserOverwrite:FIVE_MEG_NODE atPath:PATH(@"foo/4") writeId:4];
+
+ NSArray *expected = @[OVERWRITE_RECORD(@"foo/1", NODE(@"node-1"), 1),
+ OVERWRITE_RECORD(@"foo/2", TEN_MEG_NODE, 2),
+ OVERWRITE_RECORD(@"foo/3", NODE(@"node-3"), 3),
+ OVERWRITE_RECORD(@"foo/4", FIVE_MEG_NODE, 4)];
+ XCTAssertEqualObjects(engine.userWrites, expected);
+}
+
+// This is ported from the Android client and doesn't really make sense since we don't have multi part writes, but
+// It's always good to have tests, so what the heck...
+- (void)testSameWriteIdOverwritesOldMultiPartWrite {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1];
+ [engine saveUserOverwrite:NODE(@"second") atPath:PATH(@"other/path") writeId:1];
+
+ XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"other/path", NODE(@"second"), 1)]);
+}
+
+- (void)testWritesAreReturnedInOrder {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ NSUInteger count = 20;
+ for (NSUInteger i = count - 1; i > 0; i--) {
+ NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)i];
+ [engine saveUserOverwrite:NODE(@(i)) atPath:PATH(path) writeId:i];
+ }
+ NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)count];
+ [engine saveUserOverwrite:NODE(@(count)) atPath:PATH(path) writeId:count];
+ NSArray *userWrites = engine.userWrites;
+ XCTAssertEqual(userWrites.count, count);
+ for (NSUInteger i = 1; i <= count; i++) {
+ NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)i];
+ XCTAssertEqualObjects(userWrites[i-1], OVERWRITE_RECORD(path, NODE(@(i)), i));
+ }
+}
+
+- (void)testRemoveAllUserWrites {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine saveUserOverwrite:NODE(@"node-1") atPath:PATH(@"foo/1") writeId:1];
+ [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/2") writeId:2];
+ FCompoundWrite *merge = [[FCompoundWrite emptyWrite] addWrite:TEN_MEG_NODE atKey:@"update"];
+ [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:3];
+ [engine removeAllUserWrites];
+ XCTAssertEqualObjects(engine.userWrites, @[]);
+}
+
+
+- (void)testCacheSavedIsReturned {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], SAMPLE_NODE);
+}
+
+- (void)testCacheSavedIsReturnedAtRoot {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"") merge:NO];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], SAMPLE_NODE);
+}
+
+- (void)testLaterCacheWritesOverwriteOlderWrites {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:NODE(@"later-bar") atPath:PATH(@"foo/bar") merge:NO];
+ // this does not affect the node
+ [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO];
+ [engine updateServerCache:NODE(@"later-qux") atPath:PATH(@"foo/later-qux") merge:NO];
+ [engine updateServerCache:NODE(@"latest-bar") atPath:PATH(@"foo/bar") merge:NO];
+
+ id<FNode> expected = [[SAMPLE_NODE updateImmediateChild:@"bar" withNewChild:NODE(@"latest-bar")]
+ updateImmediateChild:@"later-qux" withNewChild:NODE(@"later-qux")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected);
+}
+
+- (void)testLaterCacheWritesOverwriteOlderDeeperWrites {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:NODE(@"later-bar") atPath:PATH(@"foo/bar") merge:NO];
+ // this does not affect the node
+ [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO];
+ [engine updateServerCache:NODE(@"later-qux") atPath:PATH(@"foo/later-qux") merge:NO];
+ [engine updateServerCache:NODE(@"latest-bar") atPath:PATH(@"foo/bar") merge:NO];
+ [engine updateServerCache:NODE(@"latest-foo") atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@"latest-foo"));
+}
+
+- (void)testLaterCacheWritesDontAffectEarlierWritesAtUnaffectedPath {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO];
+ // this does not affect the node
+ [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO];
+ [engine updateServerCache:NODE(@"latest-foo") atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"unaffected")], NODE(@"unaffected"));
+}
+
+- (void)testMergeOnEmptyCacheGivesResults {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ NSDictionary *mergeData = @{@"foo": @"foo-value", @"bar": @"bar-value"};
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:mergeData];
+ [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(mergeData));
+}
+
+- (void)testMergePartlyOverwritingPreviousWrite {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> existingNode = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"}));
+ [engine updateServerCache:existingNode atPath:PATH(@"foo") merge:NO];
+
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}];
+ [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")];
+
+ id<FNode> expected = NODE((@{@"foo": @"new-foo-value", @"bar": @"bar-value", @"baz": @"baz-value"}));
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected);
+}
+
+- (void)testDeepMergePartlyOverwritingPreviousWrite {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> existingNode = NODE((@{@"foo": @{ @"bar": @"bar-value", @"baz": @"baz-value"}, @"qux": @"qux-value"}));
+ [engine updateServerCache:existingNode atPath:PATH(@"foo") merge:NO];
+
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo/bar": @"new-bar-value", @"quu": @"quu-value"}];
+ [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")];
+
+ id<FNode> expected = NODE((@{@"foo": @{ @"bar": @"new-bar-value", @"baz": @"baz-value"}, @"qux": @"qux-value", @"quu": @"quu-value"}));
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected);
+}
+
+- (void)testMergePartlyOverwritingPreviousMerge {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ FCompoundWrite *merge1 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"foo-value", @"bar": @"bar-value"}];
+ [engine updateServerCacheWithMerge:merge1 atPath:PATH(@"foo")];
+
+ FCompoundWrite *merge2 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}];
+ [engine updateServerCacheWithMerge:merge2 atPath:PATH(@"foo")];
+
+ id<FNode> expected = NODE((@{@"foo": @"new-foo-value", @"bar": @"bar-value", @"baz": @"baz-value"}));
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected);
+}
+
+- (void)testOverwriteRemovesPreviousMerge {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"}));
+ [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO];
+
+ FCompoundWrite *merge2 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}];
+ [engine updateServerCacheWithMerge:merge2 atPath:PATH(@"foo")];
+
+ id<FNode> replacingNode = NODE((@{@"qux": @"qux-value", @"quu": @"quu-value"}));
+ [engine updateServerCache:replacingNode atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], replacingNode);
+}
+
+- (void)testEmptyOverwriteDeletesNodeFromHigherWrite {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"}));
+ [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO];
+
+ // delete bar
+ [engine updateServerCache:NODE(nil) atPath:PATH(@"foo/bar") merge:NO];
+
+ id<FNode> expected = NODE((@{@"foo": @"foo-value"}));
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected);
+}
+
+- (void)testDeeperReadFromHigherSet {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"}));
+ [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/bar")], NODE(@"bar-value"));
+}
+
+- (void)testDeeperLeafNodeSetRemovesHigherLeafNodes {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:NODE(@"level-0") atPath:PATH(@"") merge:NO];
+ [engine updateServerCache:NODE(@"level-1") atPath:PATH(@"lvl1") merge:NO];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], NODE((@{@"lvl1": @"level-1"})));
+
+ [engine updateServerCache:NODE(@"level-2") atPath:PATH(@"lvl1/lvl2") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"lvl1")], NODE((@{@"lvl2": @"level-2"})));
+
+ [engine updateServerCache:NODE(@"level-4") atPath:PATH(@"lvl1/lvl2/lvl3/lvl4") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"lvl1")], NODE((@{@"lvl2": @{@"lvl3": @{@"lvl4": @"level-4"}}})));
+}
+
+
+// This test causes a split on Android so it doesn't really make sense here, but why not test anyways...
+- (void)testHugeNodeWithSplit {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ id<FNode> outer = [FEmptyNode emptyNode];
+ // This structure ensures splits at various depths
+ for (NSUInteger i = 0; i < 100; i++) { // Outer
+ id<FNode> inner = [FEmptyNode emptyNode];
+ for (NSUInteger j = 0; j < i; j++) { // Inner
+ id<FNode> innerMost = [FEmptyNode emptyNode];
+ for (NSUInteger k = 0; k < j; k++) {
+ NSString *key = [NSString stringWithFormat:@"key-%lu", (unsigned long)k];
+ id<FNode> node = NODE(([NSString stringWithFormat:@"leaf-%lu", (unsigned long)k]));
+ innerMost = [innerMost updateImmediateChild:key withNewChild:node];
+ }
+ NSString *innerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)j];
+ inner = [inner updateImmediateChild:innerKey withNewChild:innerMost];
+ }
+ NSString *outerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)i];
+ outer = [outer updateImmediateChild:outerKey withNewChild:inner];
+ }
+ [engine updateServerCache:outer atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], outer);
+}
+
+- (void)testManyLargeLeafNodes {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> outer = [FEmptyNode emptyNode];
+ for (NSUInteger i = 0; i < 30; i++) {
+ NSString *outerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)i];
+ outer = [outer updateImmediateChild:outerKey withNewChild:ONE_MEG_NODE];
+ }
+
+ [engine updateServerCache:outer atPath:PATH(@"foo") merge:NO];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], outer);
+}
+
+- (void)testPriorityWorks {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine updateServerCache:NODE(@"bar-value") atPath:PATH(@"foo/bar") merge:NO];
+ [engine updateServerCache:NODE(@"prio-value") atPath:PATH(@"foo/.priority") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE((@{ @".priority": @"prio-value", @"bar": @"bar-value"})));
+}
+
+- (void)testSimilarSiblingsAreNotLoaded {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine updateServerCache:NODE(@"value") atPath:PATH(@"foo/123") merge:NO];
+ [engine updateServerCache:NODE(@"sibling-value") atPath:PATH(@"foo/1230") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/123")], NODE(@"value"));
+}
+
+// TODO: this test fails, but it is a rare edge case around priorities which would require a bunch of code
+// Fix whenever we have too much time on our hands
+- (void)priorityIsCleared {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine updateServerCache:NODE((@{@"bar": @"bar-value"})) atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:NODE(@"prio-value") atPath:PATH(@"foo/.priority") merge:NO];
+ [engine updateServerCache:NODE(nil) atPath:PATH(@"foo/bar") merge:NO];
+ [engine updateServerCache:NODE(@"baz-value") atPath:PATH(@"foo/baz") merge:NO];
+
+ // Priority should have been cleaned out
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@{@"baz": @"baz-value"}));
+}
+
+- (void)testHugeLeafNode {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], TEN_MEG_NODE);
+}
+
+- (void)testHugeLeafNodeSiblings {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo/one") merge:NO];
+ [engine updateServerCache:TEN_MEG_MINUS_ONE_NODE atPath:PATH(@"foo/two") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/one")], TEN_MEG_NODE);
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/two")], TEN_MEG_MINUS_ONE_NODE);
+}
+
+- (void)testHugeLeafNodeThenTinyLeafNode {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:NODE(@"tiny") atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@"tiny"));
+}
+
+- (void)testHugeLeafNodeThenSmallerLeafNode {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:FIVE_MEG_NODE atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], FIVE_MEG_NODE);
+}
+
+- (void)testHugeLeafNodeThenDeeperSet {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:NODE(@"deep-value") atPath:PATH(@"foo/deep") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE((@{@"deep": @"deep-value"})));
+}
+
+// Well this is awkward, but NSJSONSerialization fails to deserialize JSON with tiny/huge doubles
+// It is kind of bad we raise "invalid" data, but at least we don't crash *trollface*
+- (void)testExtremeDoublesAsServerCache {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:NODE((@{@"works": @"value", @"fails": @(2.225073858507201e-308)})) atPath:PATH(@"foo") merge:NO];
+
+ // Will drop the tiny double
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@{@"works": @"value"}));
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/fails")], [FEmptyNode emptyNode]);
+}
+
+- (void)testExtremeDoublesAsTrackedQuery {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> tinyDouble = NODE(@(2.225073858507201e-308));
+
+ FQueryParams *params = [[[FQueryParams defaultInstance] startAt:tinyDouble] endAt:tinyDouble];
+ FTrackedQuery *doesNotWork = [[FTrackedQuery alloc] initWithId:0
+ query:[[FQuerySpec alloc] initWithPath:PATH(@"foo") params:params]
+ lastUse:0
+ isActive:NO];
+ FTrackedQuery *doesWork = [[FTrackedQuery alloc] initWithId:1
+ query:[FQuerySpec defaultQueryAtPath:PATH(@"bar")]
+ lastUse:0
+ isActive:NO];
+ [engine saveTrackedQuery:doesNotWork];
+ [engine saveTrackedQuery:doesWork];
+ // One will be dropped, the other should still be there
+ XCTAssertEqualObjects([engine loadTrackedQueries], @[doesWork]);
+}
+
+- (void)testExtremeDoublesAsUserWrites {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> tinyDouble = NODE(@(2.225073858507201e-308));
+
+ [engine saveUserOverwrite:tinyDouble atPath:PATH(@"foo") writeId:1];
+ [engine saveUserMerge:[[FCompoundWrite emptyWrite] addWrite:tinyDouble atPath:PATH(@"bar")] atPath:PATH(@"foo") writeId:2];
+ [engine saveUserOverwrite:NODE(@"should-work") atPath:PATH(@"other") writeId:3];
+
+ // The other two should be dropped and only the valid should remain
+ XCTAssertEqualObjects([engine userWrites], @[[[FWriteRecord alloc] initWithPath:PATH(@"other")
+ overwrite:NODE(@"should-work")
+ writeId:3
+ visible:YES]]);
+}
+
+- (void)testLongValuesDontLosePrecision {
+ id longValue = @1542405709418655810;
+ id floatValue = @2.47;
+ id<FNode> expectedData = NODE((@{@"long": longValue, @"float": floatValue}));
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:expectedData atPath:PATH(@"foo") merge:NO];
+ id<FNode> actualData = [engine serverCacheAtPath:PATH(@"foo")];
+ NSDictionary* value = [actualData val];
+ XCTAssertEqualObjects([value[@"long"] stringValue], [longValue stringValue]);
+ XCTAssertEqualObjects([value[@"float"] stringValue], [floatValue stringValue]);
+}
+
+// NSJSONSerialization has a bug in which it rounds doubles wrongly so hashes end up not matching on the server for
+// some doubles (including 2.47). Make sure LevelDB has the correct hash for that
+- (void)testDoublesAreRoundedProperly {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:NODE(@(2.47)) atPath:PATH(@"foo") merge:NO];
+
+ // Expected hash for 2.47 parsed correctly
+ NSString *hashFor247 = @"EsibHXKcBp2/b/bn/a0C5WffcUU=";
+ XCTAssertEqualObjects([[engine serverCacheAtPath:PATH(@"foo")] dataHash], hashFor247);
+}
+
+// TODO[offline]: Somehow test estimated server size?
+// TODO[offline]: Test pruning!
+
+- (void)testSaveAndLoadTrackedQueries {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ NSArray *queries = @[[[FTrackedQuery alloc] initWithId:1 query:SAMPLE_QUERY lastUse:100 isActive:NO isComplete:NO],
+ [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:200 isActive:NO isComplete:NO],
+ [[FTrackedQuery alloc] initWithId:3 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:300 isActive:YES isComplete:NO],
+ [[FTrackedQuery alloc] initWithId:4 query:[FQuerySpec defaultQueryAtPath:PATH(@"c")] lastUse:400 isActive:NO isComplete:YES],
+ [[FTrackedQuery alloc] initWithId:5 query:[FQuerySpec defaultQueryAtPath:PATH(@"foo")] lastUse:500 isActive:NO isComplete:NO]];
+
+ [queries enumerateObjectsUsingBlock:^(FTrackedQuery *query, NSUInteger idx, BOOL *stop) {
+ [engine saveTrackedQuery:query];
+ }];
+
+ XCTAssertEqualObjects([engine loadTrackedQueries], queries);
+}
+
+- (void)testOverwriteTrackedQueryById {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ FTrackedQuery *first = [[FTrackedQuery alloc] initWithId:1 query:SAMPLE_QUERY lastUse:100 isActive:NO isComplete:NO];
+ FTrackedQuery *second = [[FTrackedQuery alloc] initWithId:1 query:DEFAULT_FOO_QUERY lastUse:200 isActive:YES isComplete:YES];
+ [engine saveTrackedQuery:first];
+ [engine saveTrackedQuery:second];
+
+ XCTAssertEqualObjects([engine loadTrackedQueries], @[second]);
+}
+
+- (void)testDeleteTrackedQuery {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ FTrackedQuery *query1 = [[FTrackedQuery alloc] initWithId:1 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:100 isActive:NO isComplete:NO];
+ FTrackedQuery *query2 = [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:200 isActive:YES isComplete:NO];
+ FTrackedQuery *query3 = [[FTrackedQuery alloc] initWithId:3 query:[FQuerySpec defaultQueryAtPath:PATH(@"c")] lastUse:300 isActive:NO isComplete:YES];
+ [engine saveTrackedQuery:query1];
+ [engine saveTrackedQuery:query2];
+ [engine saveTrackedQuery:query3];
+
+ [engine removeTrackedQuery:2];
+ XCTAssertEqualObjects([engine loadTrackedQueries], (@[query1, query3]));
+}
+
+- (void)testSaveAndLoadTrackedQueryKeys {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ NSSet *keys = [NSSet setWithArray:@[@"foo", @"☁", @"10", @"٩(͡๏̯͡๏)۶"]];
+ [engine setTrackedQueryKeys:keys forQueryId:1];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"not", @"included"]] forQueryId:2];
+
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], keys);
+}
+
+- (void)testSaveOverwritesTrackedQueryKeys {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b", @"c"]] forQueryId:1];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"c", @"d", @"e"]] forQueryId:1];
+
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"c", @"d", @"e"]]));
+}
+
+- (void)testUpdateTrackedQueryKeys {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b", @"c"]] forQueryId:1];
+ [engine updateTrackedQueryKeysWithAddedKeys:[NSSet setWithArray:@[@"c", @"d", @"e"]]
+ removedKeys:[NSSet setWithArray:@[@"a", @"b"]]
+ forQueryId:1];
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"c", @"d", @"e"]]));
+}
+
+- (void)testRemoveTrackedQueryRemovesTrackedQueryKeys {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ FTrackedQuery *query1 = [[FTrackedQuery alloc] initWithId:1 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:100 isActive:NO isComplete:NO];
+ FTrackedQuery *query2 = [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:200 isActive:NO isComplete:NO];
+ [engine saveTrackedQuery:query1];
+ [engine saveTrackedQuery:query2];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b"]] forQueryId:1];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"b", @"c"]] forQueryId:2];
+
+ XCTAssertEqualObjects([engine loadTrackedQueries], (@[query1, query2]));
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"a", @"b"]]));
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:2], ([NSSet setWithArray:@[@"b", @"c"]]));
+
+ [engine removeTrackedQuery:1];
+
+ XCTAssertEqualObjects([engine loadTrackedQueries], (@[query2]));
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], [NSSet set]);
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:2], ([NSSet setWithArray:@[@"b", @"c"]]));
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FNodeTests.m b/Example/Database/Tests/Unit/FNodeTests.m
new file mode 100644
index 0000000..372b84f
--- /dev/null
+++ b/Example/Database/Tests/Unit/FNodeTests.m
@@ -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.
+ */
+
+#import <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+#import "FSnapshotUtilities.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FLeafNode.h"
+
+@interface FNodeTests : XCTestCase
+
+@end
+
+@implementation FNodeTests
+
+- (void) testLeafNodeEqualsHashCode {
+ id<FNode> falseNode = [FSnapshotUtilities nodeFrom:@NO];
+ id<FNode> trueNode = [FSnapshotUtilities nodeFrom:@YES];
+ id<FNode> stringOneNode = [FSnapshotUtilities nodeFrom:@"one"];
+ id<FNode> stringTwoNode = [FSnapshotUtilities nodeFrom:@"two"];
+ id<FNode> zeroNode = [FSnapshotUtilities nodeFrom:@0];
+ id<FNode> oneNode = [FSnapshotUtilities nodeFrom:@1];
+ id<FNode> emptyNode1 = [FSnapshotUtilities nodeFrom:nil];
+ id<FNode> emptyNode2 = [FSnapshotUtilities nodeFrom:[NSNull null]];
+
+ XCTAssertEqualObjects(falseNode, [FSnapshotUtilities nodeFrom:@NO]);
+ XCTAssertEqual(falseNode.hash, [FSnapshotUtilities nodeFrom:@NO].hash);
+ XCTAssertEqualObjects(trueNode, [FSnapshotUtilities nodeFrom:@YES]);
+ XCTAssertEqual(trueNode.hash, [FSnapshotUtilities nodeFrom:@YES].hash);
+ XCTAssertFalse([falseNode isEqual:trueNode]);
+ XCTAssertFalse([falseNode isEqual:oneNode]);
+ XCTAssertFalse([falseNode isEqual:stringOneNode]);
+ XCTAssertFalse([falseNode isEqual:emptyNode1]);
+
+ XCTAssertEqualObjects(stringOneNode, [FSnapshotUtilities nodeFrom:@"one"]);
+ XCTAssertEqual(stringOneNode.hash, [FSnapshotUtilities nodeFrom:@"one"].hash);
+ XCTAssertFalse([stringOneNode isEqual:stringTwoNode]);
+ XCTAssertFalse([stringOneNode isEqual:emptyNode1]);
+ XCTAssertFalse([stringOneNode isEqual:oneNode]);
+ XCTAssertFalse([stringOneNode isEqual:trueNode]);
+
+ XCTAssertEqualObjects(zeroNode, [FSnapshotUtilities nodeFrom:@0]);
+ XCTAssertEqual(zeroNode.hash, [FSnapshotUtilities nodeFrom:@0].hash);
+ XCTAssertFalse([zeroNode isEqual:oneNode]);
+ XCTAssertFalse([zeroNode isEqual:emptyNode1]);
+ XCTAssertFalse([zeroNode isEqual:falseNode]);
+
+ XCTAssertEqualObjects(emptyNode1, emptyNode2);
+ XCTAssertEqual(emptyNode1.hash, emptyNode2.hash);
+}
+
+- (void) testLeafNodePrioritiesEqualsHashCode {
+ id<FNode> oneOne = [FSnapshotUtilities nodeFrom:@1 priority:@1];
+ id<FNode> stringOne = [FSnapshotUtilities nodeFrom:@"value" priority:@1];
+ id<FNode> oneString = [FSnapshotUtilities nodeFrom:@1 priority:@"value"];
+ id<FNode> stringString = [FSnapshotUtilities nodeFrom:@"value" priority:@"value"];
+
+ XCTAssertEqualObjects(oneOne, [FSnapshotUtilities nodeFrom:@1 priority:@1]);
+ XCTAssertEqual(oneOne.hash, [FSnapshotUtilities nodeFrom:@1 priority:@1].hash);
+ XCTAssertFalse([oneOne isEqual:stringOne]);
+ XCTAssertFalse([oneOne isEqual:oneString]);
+ XCTAssertFalse([oneOne isEqual:stringString]);
+
+ XCTAssertEqualObjects(stringOne, [FSnapshotUtilities nodeFrom:@"value" priority:@1]);
+ XCTAssertEqual(stringOne.hash, [FSnapshotUtilities nodeFrom:@"value" priority:@1].hash);
+ XCTAssertFalse([stringOne isEqual:oneOne]);
+ XCTAssertFalse([stringOne isEqual:oneString]);
+ XCTAssertFalse([stringOne isEqual:stringString]);
+
+ XCTAssertEqualObjects(oneString, [FSnapshotUtilities nodeFrom:@1 priority:@"value"]);
+ XCTAssertEqual(oneString.hash, [FSnapshotUtilities nodeFrom:@1 priority:@"value"].hash);
+ XCTAssertFalse([oneString isEqual:stringOne]);
+ XCTAssertFalse([oneString isEqual:oneOne]);
+ XCTAssertFalse([oneString isEqual:stringString]);
+
+ XCTAssertEqualObjects(stringString, [FSnapshotUtilities nodeFrom:@"value" priority:@"value"]);
+ XCTAssertEqual(stringString.hash, [FSnapshotUtilities nodeFrom:@"value" priority:@"value"].hash);
+ XCTAssertFalse([stringString isEqual:stringOne]);
+ XCTAssertFalse([stringString isEqual:oneString]);
+ XCTAssertFalse([stringString isEqual:oneOne]);
+}
+
+- (void)testChildrenNodeEqualsHashCode {
+ id<FNode> nodeOne = [FSnapshotUtilities nodeFrom:@{ @"one": @1, @"two": @2, @".priority": @"prio"}];
+ id<FNode> nodeTwo = [[FEmptyNode emptyNode] updateImmediateChild:@"one" withNewChild:[FSnapshotUtilities nodeFrom:@1]];
+ nodeTwo = [nodeTwo updateImmediateChild:@"two" withNewChild:[FSnapshotUtilities nodeFrom:@2]];
+ nodeTwo = [nodeTwo updatePriority:[FSnapshotUtilities nodeFrom:@"prio"]];
+
+ XCTAssertEqualObjects(nodeOne, nodeTwo);
+ XCTAssertEqual(nodeOne.hash, nodeTwo.hash);
+ XCTAssertFalse([[nodeOne updatePriority:[FEmptyNode emptyNode]] isEqual:nodeOne]);
+ XCTAssertFalse([[nodeOne updateImmediateChild:@"one" withNewChild:[FEmptyNode emptyNode]] isEqual:nodeOne]);
+ XCTAssertFalse([[nodeOne updateImmediateChild:@"one" withNewChild:[FSnapshotUtilities nodeFrom:@2]] isEqual:nodeOne]);
+}
+
+- (void)testLeadingZerosWorkCorrectly {
+ NSDictionary *data = @{ @"1": @1, @"01": @2, @"001": @3, @"0001": @4 };
+
+ id<FNode> node = [FSnapshotUtilities nodeFrom:data];
+ XCTAssertEqualObjects([node getImmediateChild:@"1"].val, @1);
+ XCTAssertEqualObjects([node getImmediateChild:@"01"].val, @2);
+ XCTAssertEqualObjects([node getImmediateChild:@"001"].val, @3);
+ XCTAssertEqualObjects([node getImmediateChild:@"0001"].val, @4);
+}
+
+- (void)testLeadindZerosArePreservedInValue {
+ NSDictionary *data = @{ @"1": @1, @"01": @2, @"001": @3, @"0001": @4 };
+
+ XCTAssertEqualObjects([FSnapshotUtilities nodeFrom:data].val, data);
+}
+
+- (void)testEmptyNodeEqualsEmptyChildrenNode {
+ XCTAssertEqualObjects([FEmptyNode emptyNode], [[FChildrenNode alloc] init]);
+ XCTAssertEqualObjects([[FChildrenNode alloc] init], [FEmptyNode emptyNode]);
+ XCTAssertEqual([[FChildrenNode alloc] init].hash, [FEmptyNode emptyNode].hash);
+}
+
+- (void)testUpdatingEmptyChildrenDoesntOverwriteLeafNode {
+ FLeafNode *node = [[FLeafNode alloc] initWithValue:@"value"];
+ XCTAssertEqualObjects(node, [node updateChild:[FPath pathWithString:@".priority"] withNewChild:[FEmptyNode emptyNode]]);
+ XCTAssertEqualObjects(node, [node updateChild:[FPath pathWithString:@"child"] withNewChild:[FEmptyNode emptyNode]]);
+ XCTAssertEqualObjects(node, [node updateChild:[FPath pathWithString:@"child/.priority"] withNewChild:[FEmptyNode emptyNode]]);
+ XCTAssertEqualObjects(node, [node updateImmediateChild:@"child" withNewChild:[FEmptyNode emptyNode]]);
+ XCTAssertEqualObjects(node, [node updateImmediateChild:@".priority" withNewChild:[FEmptyNode emptyNode]]);
+}
+
+- (void)testUpdatingPrioritiesOnEmptyNodesIsANoOp {
+ id<FNode> priority = [FSnapshotUtilities nodeFrom:@"prio"];
+ XCTAssertTrue([[[[FEmptyNode emptyNode] updatePriority:priority] getPriority] isEmpty]);
+ XCTAssertTrue([[[[FEmptyNode emptyNode] updateChild:[FPath pathWithString:@".priority"] withNewChild:priority] getPriority] isEmpty]);
+ XCTAssertTrue([[[[FEmptyNode emptyNode] updateImmediateChild:@".priority" withNewChild:priority] getPriority] isEmpty]);
+
+ id<FNode> valueNode = [FSnapshotUtilities nodeFrom:@"value"];
+ FPath *childPath = [FPath pathWithString:@"child"];
+ id<FNode> reemptiedChildren = [[[FEmptyNode emptyNode] updateChild:childPath withNewChild:valueNode] updateChild:childPath withNewChild:[FEmptyNode emptyNode]];
+ XCTAssertTrue([[[reemptiedChildren updatePriority:priority] getPriority] isEmpty]);
+ XCTAssertTrue([[[reemptiedChildren updateChild:[FPath pathWithString:@".priority"] withNewChild:priority] getPriority] isEmpty]);
+ XCTAssertTrue([[[reemptiedChildren updateImmediateChild:@".priority" withNewChild:priority] getPriority] isEmpty]);
+}
+
+- (void)testDeletingLastChildFromChildrenNodeRemovesPriority {
+ id<FNode> priority = [FSnapshotUtilities nodeFrom:@"prio"];
+ id<FNode> valueNode = [FSnapshotUtilities nodeFrom:@"value"];
+ FPath *childPath = [FPath pathWithString:@"child"];
+ id<FNode> withPriority = [[[FEmptyNode emptyNode] updateChild:childPath withNewChild:valueNode] updatePriority:priority];
+ XCTAssertEqualObjects(priority, [withPriority getPriority]);
+ id<FNode> deletedChild = [withPriority updateChild:childPath withNewChild:[FEmptyNode emptyNode]];
+ XCTAssertTrue([[deletedChild getPriority] isEmpty]);
+}
+
+- (void)testFromNodeReturnsEmptyNodesWithoutPriority {
+ id<FNode> empty1 = [FSnapshotUtilities nodeFrom:@{ @".priority": @"prio" }];
+ XCTAssertTrue([[empty1 getPriority] isEmpty]);
+
+ id<FNode> empty2 = [FSnapshotUtilities nodeFrom:@{ @"dummy": [NSNull null], @".priority": @"prio" }];
+ XCTAssertTrue([[empty2 getPriority] isEmpty]);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FPathTests.h b/Example/Database/Tests/Unit/FPathTests.h
new file mode 100644
index 0000000..edd8330
--- /dev/null
+++ b/Example/Database/Tests/Unit/FPathTests.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 "FTestBase.h"
+
+@interface FPathTests : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Unit/FPathTests.m b/Example/Database/Tests/Unit/FPathTests.m
new file mode 100644
index 0000000..9b26a85
--- /dev/null
+++ b/Example/Database/Tests/Unit/FPathTests.m
@@ -0,0 +1,84 @@
+/*
+ * 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 "FPathTests.h"
+#import "FPath.h"
+
+@implementation FPathTests
+
+- (void)testContains
+{
+ XCTAssertTrue([[[FPath alloc] initWith:@"/"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWith:@"/a"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWith:@"/a/b"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct");
+
+ XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a/b"]], @"contains should be correct");
+ XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a"]], @"contains should be correct");
+ XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/"]], @"contains should be correct");
+
+ NSArray *pathPieces = @[@"a",@"b",@"c"];
+
+ XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c/d"]], @"contains should be correct");
+
+ XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/b/c"]], @"contains should be correct");
+ XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a/c/b"]], @"contains should be correct");
+
+ XCTAssertFalse([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1]contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c/d"]], @"contains should be correct");
+}
+
+- (void)testPopFront
+{
+ XCTAssertEqualObjects([[[FPath alloc] initWith:@"/a/b/c"] popFront], [[FPath alloc] initWith:@"/b/c"], @"should be correct");
+ XCTAssertEqualObjects([[[[FPath alloc] initWith:@"/a/b/c"] popFront] popFront], [[FPath alloc] initWith:@"/c"], @"should be correct");
+ XCTAssertEqualObjects([[[[[FPath alloc] initWith:@"/a/b/c"] popFront] popFront] popFront], [[FPath alloc] initWith:@"/"], @"should be correct");
+ XCTAssertEqualObjects([[[[[[FPath alloc] initWith:@"/a/b/c"] popFront] popFront] popFront] popFront], [[FPath alloc] initWith:@"/"], @"should be correct");
+}
+
+- (void)testParent
+{
+ XCTAssertEqualObjects([[[FPath alloc] initWith:@"/a/b/c"] parent], [[FPath alloc] initWith:@"/a/b/"], @"should be correct");
+ XCTAssertEqualObjects([[[[FPath alloc] initWith:@"/a/b/c"] parent] parent], [[FPath alloc] initWith:@"/a/"], @"should be correct");
+ XCTAssertEqualObjects([[[[[FPath alloc] initWith:@"/a/b/c"] parent] parent] parent], [[FPath alloc] initWith:@"/"], @"should be correct");
+ XCTAssertNil([[[[[[FPath alloc] initWith:@"/a/b/c"] parent] parent] parent] parent], @"should be correct");
+}
+
+- (void)testWireFormat
+{
+ XCTAssertEqualObjects(@"/", [[FPath empty] wireFormat]);
+ XCTAssertEqualObjects(@"a/b/c", [[[FPath alloc] initWith:@"/a/b//c/"] wireFormat]);
+ XCTAssertEqualObjects(@"b/c", [[[[FPath alloc] initWith:@"/a/b//c/"] popFront] wireFormat]);
+}
+
+- (void)testComparison
+{
+ NSArray *pathsInOrder = @[@"1", @"2", @"10", @"a", @"a/1", @"a/2", @"a/10", @"a/a", @"a/aa", @"a/b", @"a/b/c",
+ @"b", @"b/a"];
+ for (NSInteger i = 0; i < pathsInOrder.count; i++) {
+ FPath *path1 = PATH(pathsInOrder[i]);
+ for (NSInteger j = i + 1; j < pathsInOrder.count; j++) {
+ FPath *path2 = PATH(pathsInOrder[j]);
+ XCTAssertEqual([path1 compare:path2], NSOrderedAscending);
+ XCTAssertEqual([path2 compare:path1], NSOrderedDescending);
+ }
+ XCTAssertEqual([path1 compare:path1], NSOrderedSame);
+ }
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FPersistenceManagerTest.m b/Example/Database/Tests/Unit/FPersistenceManagerTest.m
new file mode 100644
index 0000000..c00d11f
--- /dev/null
+++ b/Example/Database/Tests/Unit/FPersistenceManagerTest.m
@@ -0,0 +1,106 @@
+/*
+ * 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/XCTest.h>
+#import "FPersistenceManager.h"
+#import "FTestCachePolicy.h"
+#import "FMockStorageEngine.h"
+#import "FTestHelpers.h"
+#import "FQuerySpec.h"
+#import "FSnapshotUtilities.h"
+#import "FPathIndex.h"
+#import "FIndexedNode.h"
+#import "FEmptyNode.h"
+
+@interface FPersistenceManagerTest : XCTestCase
+
+@end
+
+@implementation FPersistenceManagerTest
+
+- (FPersistenceManager *)newTestPersistenceManager {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FPersistenceManager *manager = [[FPersistenceManager alloc] initWithStorageEngine:engine
+ cachePolicy:[FNoCachePolicy noCachePolicy]];
+ return manager;
+}
+
+- (void)testServerCacheFiltersResults1 {
+ FPersistenceManager *manager = [self newTestPersistenceManager];
+
+ [manager updateServerCacheWithNode:NODE(@"1") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/bar")]];
+ [manager updateServerCacheWithNode:NODE(@"2") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/baz")]];
+ [manager updateServerCacheWithNode:NODE(@"3") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/quu/1")]];
+ [manager updateServerCacheWithNode:NODE(@"4") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/quu/2")]];
+
+ FCacheNode *cache = [manager serverCacheForQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]];
+ XCTAssertFalse(cache.isFullyInitialized);
+ XCTAssertEqualObjects(cache.node, [FEmptyNode emptyNode]);
+}
+
+- (void)testServerCacheFiltersResults2 {
+ FPersistenceManager *manager = [self newTestPersistenceManager];
+
+ FQuerySpec *limit2FooQuery = [[FQuerySpec alloc] initWithPath:PATH(@"foo") params:[[FQueryParams defaultInstance] limitToFirst:2]];
+ FQuerySpec *limit3FooQuery = [[FQuerySpec alloc] initWithPath:PATH(@"foo") params:[[FQueryParams defaultInstance] limitToFirst:3]];
+
+ [manager setQueryActive:limit2FooQuery];
+ [manager updateServerCacheWithNode:NODE((@{@"a": @1, @"b": @2, @"c": @3, @"d": @4})) forQuery:limit2FooQuery];
+ [manager setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b"]] forQuery:limit2FooQuery];
+
+ FCacheNode *cache = [manager serverCacheForQuery:limit3FooQuery];
+ XCTAssertFalse(cache.isFullyInitialized);
+ XCTAssertEqualObjects(cache.node, NODE((@{@"a": @1, @"b": @2})));
+}
+
+- (void)testNoLimitNonDefaultQueryIsTreatedAsDefaultQuery {
+ FPersistenceManager *manager = [self newTestPersistenceManager];
+
+ FQuerySpec *defaultQuery = [FQuerySpec defaultQueryAtPath:PATH(@"foo")];
+ id<FIndex> index = [[FPathIndex alloc] initWithPath:PATH(@"index-key")];
+ FQuerySpec *orderByQuery = [[FQuerySpec alloc] initWithPath:PATH(@"foo")
+ params:[[FQueryParams defaultInstance] orderBy:index]];
+ [manager setQueryActive:defaultQuery];
+ [manager updateServerCacheWithNode:NODE((@{@"foo": @1, @"bar": @2}))
+ forQuery:defaultQuery];
+ [manager setQueryComplete:defaultQuery];
+
+ FCacheNode *node = [manager serverCacheForQuery:orderByQuery];
+
+ XCTAssertEqualObjects(node.node, NODE((@{@"foo": @1, @"bar": @2})));
+ XCTAssertTrue(node.isFullyInitialized);
+ XCTAssertFalse(node.isFiltered);
+ XCTAssertTrue([node.indexedNode hasIndex:orderByQuery.index]);
+}
+
+- (void)testApplyUserMergeUsesRelativePath {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+
+ id<FNode> initialData = NODE((@{@"foo": @{ @"bar": @"bar-value", @"baz": @"baz-value"}}));
+ [engine updateServerCache:initialData atPath:PATH(@"") merge:NO];
+
+ FPersistenceManager *manager = [[FPersistenceManager alloc] initWithStorageEngine:engine
+ cachePolicy:[FNoCachePolicy noCachePolicy]];
+
+ FCompoundWrite *update = [FCompoundWrite compoundWriteWithValueDictionary:@{@"baz": @"new-baz", @"qux": @"qux"}];
+ [manager applyUserMerge:update toServerCacheAtPath:PATH(@"foo")];
+
+ id<FNode> expected = NODE((@{@"foo": @{ @"bar": @"bar-value", @"baz": @"new-baz", @"qux": @"qux"}}));
+ id<FNode> actual = [engine serverCacheAtPath:PATH(@"")];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FPruneForestTest.m b/Example/Database/Tests/Unit/FPruneForestTest.m
new file mode 100644
index 0000000..0694ba7
--- /dev/null
+++ b/Example/Database/Tests/Unit/FPruneForestTest.m
@@ -0,0 +1,98 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import <XCTest/XCTest.h>
+
+#import "FPruneForest.h"
+#import "FPath.h"
+
+@interface FPruneForestTest : XCTestCase
+
+@end
+
+@implementation FPruneForestTest
+
+- (void) testEmptyDoesNotAffectAnyPaths {
+ FPruneForest *forest = [FPruneForest empty];
+ XCTAssertFalse([forest affectsPath:[FPath empty]]);
+ XCTAssertFalse([forest affectsPath:[FPath pathWithString:@"foo"]]);
+}
+
+- (void) testPruneAffectsPath {
+ FPruneForest *forest = [FPruneForest empty];
+ forest = [forest prunePath:[FPath pathWithString:@"foo/bar"]];
+ forest = [forest keepPath:[FPath pathWithString:@"foo/bar/baz"]];
+ XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo"]]);
+ XCTAssertFalse([forest affectsPath:[FPath pathWithString:@"baz"]]);
+ XCTAssertFalse([forest affectsPath:[FPath pathWithString:@"baz/bar"]]);
+ XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo/bar"]]);
+ XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo/bar/baz"]]);
+ XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo/bar/qux"]]);
+}
+
+- (void) testPruneAnythingWorks {
+ FPruneForest *empty = [FPruneForest empty];
+ XCTAssertFalse([empty prunesAnything]);
+ XCTAssertTrue([[empty prunePath:[FPath pathWithString:@"foo"]] prunesAnything]);
+ XCTAssertFalse([[[empty prunePath:[FPath pathWithString:@"foo/bar"]] keepPath:[FPath pathWithString:@"foo"]] prunesAnything]);
+ XCTAssertTrue([[[empty prunePath:[FPath pathWithString:@"foo"]] keepPath:[FPath pathWithString:@"foo/bar"]] prunesAnything]);
+}
+
+- (void) testKeepUnderPruneWorks {
+ FPruneForest *forest = [FPruneForest empty];
+ forest = [forest prunePath:[FPath pathWithString:@"foo/bar"]];
+ forest = [forest keepPath:[FPath pathWithString:@"foo/bar/baz"]];
+ forest = [forest keepAll:[NSSet setWithArray:@[@"qux", @"quu"]] atPath:[FPath pathWithString:@"foo/bar"]];
+}
+
+- (void) testPruneUnderKeepThrows {
+ FPruneForest *forest = [FPruneForest empty];
+ forest = [forest prunePath:[FPath pathWithString:@"foo"]];
+ forest = [forest keepPath:[FPath pathWithString:@"foo/bar"]];
+ XCTAssertThrows([forest prunePath:[FPath pathWithString:@"foo/bar/baz"]]);
+ NSSet *children = [NSSet setWithArray:@[@"qux", @"quu"]];
+ XCTAssertThrows([forest pruneAll:children atPath:[FPath pathWithString:@"foo/bar"]]);
+}
+
+- (void) testChildKeepsPruneInfo {
+ FPruneForest *forest = [FPruneForest empty];
+ forest = [forest keepPath:[FPath pathWithString:@"foo/bar"]];
+ XCTAssertTrue([[forest child:@"foo"] affectsPath:[FPath pathWithString:@"bar"]]);
+ XCTAssertTrue([[[forest child:@"foo"] child:@"bar"] affectsPath:[FPath pathWithString:@""]]);
+ XCTAssertTrue([[[[forest child:@"foo"] child:@"bar"] child:@"baz"] affectsPath:[FPath pathWithString:@""]]);
+
+ forest = [[FPruneForest empty] prunePath:[FPath pathWithString:@"foo/bar"]];
+ XCTAssertTrue([[forest child:@"foo"] affectsPath:[FPath pathWithString:@"bar"]]);
+ XCTAssertTrue([[[forest child:@"foo"] child:@"bar"] affectsPath:[FPath pathWithString:@""]]);
+ XCTAssertTrue([[[[forest child:@"foo"] child:@"bar"] child:@"baz"] affectsPath:[FPath pathWithString:@""]]);
+
+ XCTAssertFalse([[forest child:@"non-existent"] affectsPath:[FPath pathWithString:@""]]);
+}
+
+- (void) testShouldPruneWorks {
+ FPruneForest *forest = [FPruneForest empty];
+ forest = [forest prunePath:[FPath pathWithString:@"foo"]];
+ forest = [forest keepPath:[FPath pathWithString:@"foo/bar/baz"]];
+ XCTAssertTrue([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"foo"]]);
+ XCTAssertTrue([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"foo/bar"]]);
+ XCTAssertFalse([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"foo/bar/baz"]]);
+ XCTAssertFalse([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"qux"]]);
+}
+
+
+@end
diff --git a/Example/Database/Tests/Unit/FPruningTest.m b/Example/Database/Tests/Unit/FPruningTest.m
new file mode 100644
index 0000000..d1e7354
--- /dev/null
+++ b/Example/Database/Tests/Unit/FPruningTest.m
@@ -0,0 +1,293 @@
+/*
+ * 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/XCTest.h>
+
+#import "FLevelDBStorageEngine.h"
+#import "FTestHelpers.h"
+#import "FPruneForest.h"
+#import "FEmptyNode.h"
+#import "FMockStorageEngine.h"
+
+@interface FPruningTest : XCTestCase
+
+@end
+
+static id<FNode> ABC_NODE = nil;
+static id<FNode> DEF_NODE = nil;
+static id<FNode> A_NODE = nil;
+static id<FNode> D_NODE = nil;
+static id<FNode> BC_NODE = nil;
+static id<FNode> LARGE_NODE = nil;
+
+@implementation FPruningTest
+
++ (void)initStatics {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ ABC_NODE = NODE((@{@"a": @{@"aa": @1.1, @"ab": @1.2}, @"b": @2, @"c": @3}));
+ DEF_NODE = NODE((@{@"d": @4, @"e": @5, @"f": @6}));
+ A_NODE = NODE((@{@"a": @{@"aa": @1.1, @"ab": @1.2}}));
+ D_NODE = NODE(@{@"d": @4});
+ LARGE_NODE = [FTestHelpers leafNodeOfSize:5*1024*1024];
+ BC_NODE = [ABC_NODE updateImmediateChild:@"a" withNewChild:[FEmptyNode emptyNode]];
+ });
+}
+
+- (void)runWithDb:(void (^)(id<FStorageEngine>engine))block {
+ [FPruningTest initStatics];
+ {
+ // Run with level DB implementation
+ FLevelDBStorageEngine *engine = [[FLevelDBStorageEngine alloc] initWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"purge-tests"]];
+ block(engine);
+ [engine purgeEverything];
+ [engine close];
+ }
+ {
+ // Run with mock implementation
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ block(engine);
+ [engine close];
+
+ }
+}
+
+- (FPruneForest *)prune:(NSString *)pathStr {
+ return [[FPruneForest empty] prunePath:PATH(pathStr)];
+}
+
+- (FPruneForest *)prune:(NSString *)path exceptRelative:(NSArray *)except {
+ __block FPruneForest *pruneForest = [FPruneForest empty];
+ pruneForest = [pruneForest prunePath:PATH(path)];
+ [except enumerateObjectsUsingBlock:^(NSString *keepPath, NSUInteger idx, BOOL *stop) {
+ pruneForest = [pruneForest keepPath:[PATH(path) childFromString:keepPath]];
+ }];
+ return pruneForest;
+}
+
+// Write document at root, prune it.
+- (void)test010 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"") merge:NO];
+ [engine pruneCache:[self prune:@""] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write document at /x, prune it via PruneForest for /x, at root.
+- (void)test020 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine pruneCache:[self prune:@"x"] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write document at /x, prune it via PruneForest for root, at /x.
+- (void)test030 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine pruneCache:[self prune:@""] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write document at /x, prune it via PruneForest for root, at root
+- (void)test040 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine pruneCache:[self prune:@""] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write document at /x/y, prune it via PruneForest for /y, at /x
+- (void)test050 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO];
+ [engine pruneCache:[self prune:@"y"] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write abc at /x/y, prune /x/y except b,c via PruneForest for /x/y -b,c, at root
+- (void)test060 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO];
+ [engine pruneCache:[self prune:@"x/y" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], BC_NODE);
+ }];
+}
+
+// Write abc at /x/y, prune /x/y except b,c via PruneForest for /y -b,c, at /x
+- (void)test070 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO];
+ [engine pruneCache:[self prune:@"y" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], BC_NODE);
+ }];
+}
+
+// Write abc at /x/y, prune /x/y except not-there via PruneForest for /x/y -d, at root
+- (void)test080 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO];
+ [engine pruneCache:[self prune:@"x/y" exceptRelative:@[@"not-there"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write abc at / and def at /a, prune all via PruneForest for / at root
+- (void)test090 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"a") merge:NO];
+ [engine pruneCache:[self prune:@""] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write abc at / and def at /a, prune all except b,c via PruneForest for root -b,c, at root
+- (void)test100 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"a") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], BC_NODE);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except b,c via PruneForest for /x -b,c, at root
+- (void)test110 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"x" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], BC_NODE);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except b,c via PruneForest for root -b,c, at /x
+- (void)test120 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], BC_NODE);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except a via PruneForest for /x -a, at root
+- (void)test130 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [ABC_NODE updateImmediateChild:@"a" withNewChild:DEF_NODE]);
+ [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except a via PruneForest for root -a, at /x
+- (void)test140 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"a"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except a/d via PruneForest for /x -a/d, at root
+- (void)test150 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a/d"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:D_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except a/d via PruneForest for / -a/d, at /x
+- (void)test160 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"a/d"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:D_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a/aa, prune /x except a via PruneForest for /x -a, at root
+- (void)test170 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO];
+ [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [A_NODE updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a/aa, prune /x except a via PruneForest for / -a, at /x
+- (void)test180 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"a/aa"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a/aa, prune /x except a/aa via PruneForest for /x -a/aa, at root
+- (void)test190 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO];
+ [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a/aa"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a/aa, prune /x except a/aa via PruneForest for / -a/aa, at /x
+- (void)test200 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"a/aa"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write large node at /x, prune x via PruneForest for x at root
+- (void)test210 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:LARGE_NODE atPath:PATH(@"x") merge:NO];
+ [engine pruneCache:[self prune:@"x"] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write abc at x and large node at /x/a, prune x except a via PruneForest for / -a, at x
+- (void)test220 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:LARGE_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"a"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:LARGE_NODE]);
+ }];
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FQueryParamsTest.m b/Example/Database/Tests/Unit/FQueryParamsTest.m
new file mode 100644
index 0000000..8c98ff9
--- /dev/null
+++ b/Example/Database/Tests/Unit/FQueryParamsTest.m
@@ -0,0 +1,162 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+#import "FQueryParams.h"
+#import "FIndex.h"
+#import "FPriorityIndex.h"
+#import "FValueIndex.h"
+#import "FLeafNode.h"
+#import "FPathIndex.h"
+#import "FSnapshotUtilities.h"
+#import "FKeyIndex.h"
+#import "FEmptyNode.h"
+
+@interface FQueryParamsTest : XCTestCase
+
+@end
+
+@implementation FQueryParamsTest
+
+- (void)testQueryParamsEquals {
+ { // Limit equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] limitToLast:10];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] limitTo:10];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] limitToFirst:10];
+ FQueryParams *params4 = [[FQueryParams defaultInstance] limitToLast:11];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ XCTAssertFalse([params1 isEqual:params4]);
+ }
+
+ { // Index equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] orderBy:[FPriorityIndex priorityIndex]];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] orderBy:[FPriorityIndex priorityIndex]];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] orderBy:[FKeyIndex keyIndex]];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+
+ { // startAt equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] startAt:[FSnapshotUtilities nodeFrom:@"value"]];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] startAt:[FSnapshotUtilities nodeFrom:@"value"] childKey:nil];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] startAt:[FSnapshotUtilities nodeFrom:@"value-2"]];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+
+ { // startAt with childkey equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] startAt:[FEmptyNode emptyNode] childKey:@"key"];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] startAt:[FEmptyNode emptyNode] childKey:@"key"];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] startAt:[FEmptyNode emptyNode] childKey:@"other-key"];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+
+ { // endAt equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] endAt:[FSnapshotUtilities nodeFrom:@"value"]];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] endAt:[FSnapshotUtilities nodeFrom:@"value"] childKey:nil];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] endAt:[FSnapshotUtilities nodeFrom:@"value-2"]];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+
+ { // endAt with childkey equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] endAt:[FEmptyNode emptyNode] childKey:@"key"];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] endAt:[FEmptyNode emptyNode] childKey:@"key"];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] endAt:[FEmptyNode emptyNode] childKey:@"other-key"];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+
+ { // Limit/startAt equals
+ FQueryParams *params1 = [[[FQueryParams defaultInstance] limitToFirst:10] startAt:[FSnapshotUtilities nodeFrom:@"value"]];
+ FQueryParams *params2 = [[[FQueryParams defaultInstance] limitTo:10] startAt:[FSnapshotUtilities nodeFrom:@"value"]];
+ FQueryParams *params3 = [[[FQueryParams defaultInstance] limitTo:10] startAt:[FSnapshotUtilities nodeFrom:@"value-2"]];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+}
+
+- (void)testFromDictionaryEquals {
+ FQueryParams *params1 = [[[[[FQueryParams defaultInstance] limitToLast:10]
+ startAt:[FSnapshotUtilities nodeFrom:@"start-value"] childKey:@"child-key-2"]
+ endAt:[FSnapshotUtilities nodeFrom:@"end-value"] childKey:@"child-key-2"]
+ orderBy:[FKeyIndex keyIndex]];
+ XCTAssertEqualObjects(params1, [FQueryParams fromQueryObject:params1.wireProtocolParams]);
+ XCTAssertEqual(params1.hash, [FQueryParams fromQueryObject:params1.wireProtocolParams].hash);
+}
+
+- (void)testCanCreateAllIndexes {
+ FQueryParams *params1 = [[FQueryParams defaultInstance] orderBy:[FKeyIndex keyIndex]];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] orderBy:[FValueIndex valueIndex]];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] orderBy:[FPriorityIndex priorityIndex]];
+ FQueryParams *params4 = [[FQueryParams defaultInstance] orderBy:[[FPathIndex alloc] initWithPath:[[FPath alloc] initWith:@"subkey"]]];
+ XCTAssertEqualObjects(params1, [FQueryParams fromQueryObject:params1.wireProtocolParams]);
+ XCTAssertEqualObjects(params2, [FQueryParams fromQueryObject:params2.wireProtocolParams]);
+ XCTAssertEqualObjects(params3, [FQueryParams fromQueryObject:params3.wireProtocolParams]);
+ XCTAssertEqualObjects(params4, [FQueryParams fromQueryObject:params4.wireProtocolParams]);
+ XCTAssertEqual(params1.hash, [FQueryParams fromQueryObject:params1.wireProtocolParams].hash);
+ XCTAssertEqual(params2.hash, [FQueryParams fromQueryObject:params2.wireProtocolParams].hash);
+ XCTAssertEqual(params3.hash, [FQueryParams fromQueryObject:params3.wireProtocolParams].hash);
+ XCTAssertEqual(params4.hash, [FQueryParams fromQueryObject:params4.wireProtocolParams].hash);
+}
+
+- (void)testDifferentLimits {
+ FQueryParams *params1 = [[FQueryParams defaultInstance] limitToFirst:10];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] limitToLast:10];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] limitTo:10];
+ XCTAssertEqualObjects(params1, [FQueryParams fromQueryObject:params1.wireProtocolParams]);
+ XCTAssertEqualObjects(params2, [FQueryParams fromQueryObject:params2.wireProtocolParams]);
+ XCTAssertEqualObjects(params3, [FQueryParams fromQueryObject:params3.wireProtocolParams]);
+ // 2 and 3 are equivalent
+ XCTAssertEqualObjects(params2, [FQueryParams fromQueryObject:params3.wireProtocolParams]);
+
+ XCTAssertEqual(params1.hash, [FQueryParams fromQueryObject:params1.wireProtocolParams].hash);
+ XCTAssertEqual(params2.hash, [FQueryParams fromQueryObject:params2.wireProtocolParams].hash);
+ XCTAssertEqual(params3.hash, [FQueryParams fromQueryObject:params3.wireProtocolParams].hash);
+ // 2 and 3 are equivalent
+ XCTAssertEqual(params2.hash, [FQueryParams fromQueryObject:params3.wireProtocolParams].hash);
+}
+
+- (void)testStartAtNullIsSerializable {
+ FQueryParams *params = [FQueryParams defaultInstance];
+ params = [params startAt:[FEmptyNode emptyNode] childKey:@"key"];
+ NSDictionary *dict = [params wireProtocolParams];
+ FQueryParams *parsed = [FQueryParams fromQueryObject:dict];
+ XCTAssertEqualObjects(parsed, params);
+ XCTAssertTrue([parsed hasStart]);
+}
+
+- (void)testEndAtNullIsSerializable {
+ FQueryParams *params = [FQueryParams defaultInstance];
+ params = [params endAt:[FEmptyNode emptyNode] childKey:@"key"];
+ NSDictionary *dict = [params wireProtocolParams];
+ FQueryParams *parsed = [FQueryParams fromQueryObject:dict];
+ XCTAssertEqualObjects(parsed, params);
+ XCTAssertTrue([parsed hasEnd]);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FRangeMergeTest.m b/Example/Database/Tests/Unit/FRangeMergeTest.m
new file mode 100644
index 0000000..32ea6ad
--- /dev/null
+++ b/Example/Database/Tests/Unit/FRangeMergeTest.m
@@ -0,0 +1,271 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import <XCTest/XCTest.h>
+
+#import "FRangeMerge.h"
+#import "FNode.h"
+#import "FTestHelpers.h"
+#import "FEmptyNode.h"
+
+@interface FRangeMergeTest : XCTestCase
+
+@end
+
+@implementation FRangeMergeTest
+
+- (void)testSmokeTest {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @{@"a": @{@"deep-a-1": @1, @"deep-a-2": @2}, @"b": @"b", @"c": @"c", @"d": @"d"}, @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@"a": @{@"deep-a-2": @"new-a-2", @"deep-a-3": @3}, @"b-2": @"new-b", @"c": @"new-c" }}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo/a/deep-a-1") end:PATH(@"foo/c") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @{@"a": @{@"deep-a-1": @1, @"deep-a-2": @"new-a-2", @"deep-a-3": @3}, @"b-2": @"new-b", @"c": @"new-c", @"d": @"d"}, @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testStartIsExclusive {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"foo": @"new-foo-value"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @"new-foo-value", @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testStartIsExclusiveButIncludesChildren {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"bar": @{@"bar-child": @"bar-child-value"}, @"foo": @"new-foo-value"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @{@"bar-child": @"bar-child-value"}, @"foo": @"new-foo-value", @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testEndIsInclusive {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"baz": @"baz-value"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates]; // foo should be deleted
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"baz": @"baz-value", @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testEndIsInclusiveButExcludesChildren {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @{@"foo-child": @"foo-child-value"}, @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"baz": @"baz-value"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates]; // foo should be deleted
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"baz": @"baz-value", @"foo": @{@"foo-child": @"foo-child-value"}, @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testCanUpdateLeafNode {
+ id<FNode> node = NODE(@"leaf-value");
+
+ id<FNode> updates = NODE((@{@"bar": @"bar-value"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"foo") updates:updates];
+ id<FNode> expected = NODE((@{@"bar": @"bar-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testCanReplaceLeafNodeWithLeafNode{
+ id<FNode> node = NODE(@"leaf-value");
+
+ id<FNode> updates = NODE(@"new-leaf-value");
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"") updates:updates];
+ id<FNode> expected = NODE(@"new-leaf-value");
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testLeafsAreUpdatedWhenRangesIncludeDeeperPath {
+ id<FNode> node = NODE((@{@"foo": @{@"bar": @"bar-value"}}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@"bar": @"new-bar-value"}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo") end:PATH(@"foo/bar/deep") updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @{@"bar": @"new-bar-value"}}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testLeafsAreNotUpdatedWhenRangesIncludeDeeperPaths {
+ id<FNode> node = NODE((@{@"foo": @{@"bar": @"bar-value"}}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@"bar": @"new-bar-value"}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo/bar") end:PATH(@"foo/bar/deep") updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @{@"bar": @"bar-value"}}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingEntireRangeUpdatesEverything {
+ id<FNode> node = [FEmptyNode emptyNode];
+
+ id<FNode> updates = NODE((@{@"foo": @"foo-value", @"bar": @{@"child": @"bar-child-value"}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:nil updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @"foo-value", @"bar": @{@"child": @"bar-child-value"}}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingRangeWithUnboundedLeftPostWorks {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value"}));
+
+ id<FNode> updates = NODE((@{@"bar": @"new-bar"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"bar") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"new-bar", @"foo": @"foo-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingRangeWithRightPostChildOfLeftPostWorks {
+ id<FNode> node = NODE((@{@"foo": @{@"a": @"a", @"b": @{@"1": @"1", @"2": @"2"}, @"c": @"c"}}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1"}}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo") end:PATH(@"foo/b/1") updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1", @"2": @"2"}, @"c": @"c"}}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingRangeWithRightPostChildOfLeftPostWorksWithIntegerKeys {
+ id<FNode> node = NODE((@{@"foo": @{@"a": @"a", @"b": @{@"1": @"1", @"2": @"2", @"10": @"10"}, @"c": @"c"}}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1"}}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo") end:PATH(@"foo/b/2") updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1", @"10": @"10"}, @"c": @"c"}}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingLeafIncludesPriority {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@".value": @"new-foo", @".priority": @"prio"}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @{@".value": @"new-foo", @".priority": @"prio" }, @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingPriorityInChildrenNodeWorks {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value"}));
+
+ id<FNode> updates = NODE((@{@"bar": @"new-bar", @".priority": @"prio"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"bar") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"new-bar", @"foo": @"foo-value", @".priority": @"prio"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+// TODO: this test should actuall;y work, but priorities on empty nodes are ignored :(
+- (void)updatingPriorityInChildrenNodeWorksAlone {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value"}));
+
+ id<FNode> updates = NODE((@{@".priority": @"prio" }));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@".priority") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @".priority": @"prio"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingPriorityOnInitiallyEmptyNodeDoesNotBreak {
+ id<FNode> node = NODE((@{}));
+
+ id<FNode> updates = NODE((@{@".priority": @"prio", @"foo": @"foo-value" }));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"foo") updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @"foo-value", @".priority": @"prio"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testPriorityIsDeletedWhenIncludedInChildrenRange {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @".priority": @"prio"}));
+
+ id<FNode> updates = NODE((@{@"bar": @"new-bar"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"bar") updates:updates]; // deletes priority
+
+ id<FNode> expected = NODE((@{@"bar": @"new-bar", @"foo": @"foo-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testPriorityIsIncludedInOpenStart {
+ id<FNode> node = NODE((@{@"foo": @{@"bar": @"bar-value"}}));
+
+ id<FNode> updates = NODE((@{@".priority": @"prio", @"baz": @"baz"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"foo/bar") updates:updates];
+
+ id<FNode> expected = NODE((@{@"baz": @"baz", @".priority": @"prio"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testPriorityIsIncludedInOpenEnd {
+ id<FNode> node = NODE(@"leaf-node");
+
+ id<FNode> updates = NODE((@{@".priority": @"prio", @"foo": @"bar"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"/") end:nil updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @"bar", @".priority": @"prio"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FRepoInfoTest.m b/Example/Database/Tests/Unit/FRepoInfoTest.m
new file mode 100644
index 0000000..94e6a70
--- /dev/null
+++ b/Example/Database/Tests/Unit/FRepoInfoTest.m
@@ -0,0 +1,44 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+#import "FRepoInfo.h"
+#import "FTestHelpers.h"
+@interface FRepoInfoTest : XCTestCase
+
+@end
+
+@implementation FRepoInfoTest
+
+- (void) testGetConnectionUrl {
+ FRepoInfo *info = [[FRepoInfo alloc] initWithHost:@"test-namespace.example.com"
+ isSecure:NO
+ withNamespace:@"tests"];
+ XCTAssertEqualObjects(info.connectionURL, @"ws://test-namespace.example.com/.ws?v=5&ns=tests",
+ @"getConnection works");
+}
+
+- (void) testGetConnectionUrlWithLastSession {
+ FRepoInfo *info = [[FRepoInfo alloc] initWithHost:@"tests-namespace.example.com"
+ isSecure:NO
+ withNamespace:@"tests"];
+ XCTAssertEqualObjects([info connectionURLWithLastSessionID:@"testsession"],
+ @"ws://tests-namespace.example.com/.ws?v=5&ns=tests&ls=testsession",
+ @"getConnectionWithLastSession works");
+}
+@end
diff --git a/Example/Database/Tests/Unit/FSparseSnapshotTests.h b/Example/Database/Tests/Unit/FSparseSnapshotTests.h
new file mode 100644
index 0000000..1f0acb2
--- /dev/null
+++ b/Example/Database/Tests/Unit/FSparseSnapshotTests.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 "FTestBase.h"
+
+@interface FSparseSnapshotTests : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Unit/FSparseSnapshotTests.m b/Example/Database/Tests/Unit/FSparseSnapshotTests.m
new file mode 100644
index 0000000..ab22c0d
--- /dev/null
+++ b/Example/Database/Tests/Unit/FSparseSnapshotTests.m
@@ -0,0 +1,207 @@
+/*
+ * 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 "FSparseSnapshotTests.h"
+#import "FSparseSnapshotTree.h"
+#import "FSnapshotUtilities.h"
+#import "FEmptyNode.h"
+
+@implementation FSparseSnapshotTests
+
+- (void) testBasicRememberAndFind {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ FPath* path = [[FPath alloc] initWith:@"a/b"];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@"sdfsd"];
+
+ [st rememberData:node onPath:path];
+ id<FNode> found = [st findPath:path];
+ XCTAssertFalse([found isEmpty], @"Should find node");
+ found = [st findPath:path.parent];
+ XCTAssertTrue(found == nil, @"Should not find a node");
+}
+
+- (void) testFindInsideAnExistingSnapshot {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ FPath* path = [[FPath alloc] initWith:@"t/tt"];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@{@"a": @"sdfsd", @"x": @5, @"999i": @YES}];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"goats": @88}];
+ node = [node updateImmediateChild:@"apples" withNewChild:update];
+ [st rememberData:node onPath:path];
+
+ id<FNode> found = [st findPath:path];
+ XCTAssertFalse([found isEmpty], @"Should find the node we set");
+ found = [st findPath:[path childFromString:@"a"]];
+ XCTAssertTrue([[found val] isEqualToString:@"sdfsd"], @"Find works inside data snapshot");
+ found = [st findPath:[path childFromString:@"999i"]];
+ XCTAssertTrue([[found val] isEqualToNumber:@YES], @"Find works inside data snapshot");
+ found = [st findPath:[path childFromString:@"apples"]];
+ XCTAssertFalse([found isEmpty], @"Should find the node we set");
+ found = [st findPath:[path childFromString:@"apples/goats"]];
+ XCTAssertTrue([[found val] isEqualToNumber:@88], @"Find works inside data snapshot");
+}
+
+- (void) testWriteASnapshotInsideASnapshot {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@19] onPath:[[FPath alloc] initWith:@"t/a/rr"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t/a/b"]];
+ XCTAssertTrue([[found val] isEqualToString:@"v"], @"Find inside snap");
+ found = [st findPath:[[FPath alloc] initWith:@"t/a/rr"]];
+ XCTAssertTrue([[found val] isEqualToNumber:@19], @"Find inside snap");
+}
+
+- (void) testWriteANullValueAndConfirmItIsRemembered {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:[NSNull null]] onPath:[[FPath alloc] initWith:@"awq/fff"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"awq/fff"]];
+ XCTAssertTrue([found isEmpty], @"Empty node");
+ found = [st findPath:[[FPath alloc] initWith:@"awq/sdf"]];
+ XCTAssertTrue(found == nil, @"No node here");
+ found = [st findPath:[[FPath alloc] initWith:@"awq/fff/jjj"]];
+ XCTAssertTrue([found isEmpty], @"Empty node");
+ found = [st findPath:[[FPath alloc] initWith:@"awq/sdf/sdj/q"]];
+ XCTAssertTrue(found == nil, @"No node here");
+}
+
+- (void) testOverwriteWithNullAndConfirmItIsRemembered {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertFalse([found isEmpty], @"non-empty node");
+ [st rememberData:[FSnapshotUtilities nodeFrom:[NSNull null]] onPath:[[FPath alloc] initWith:@"t"]];
+ found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertTrue([found isEmpty], @"Empty node");
+}
+
+- (void) testSimpleRememberAndForget {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertFalse([found isEmpty], @"non-empty node");
+ [st forgetPath:[[FPath alloc] initWith:@"t"]];
+ found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertTrue(found == nil, @"node is gone");
+}
+
+- (void) testForgetTheRoot {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertFalse([found isEmpty], @"non-empty node");
+ found = [st findPath:[[FPath alloc] initWith:@""]];
+ XCTAssertTrue(found == nil, @"node is gone");
+}
+
+- (void) testForgetSnapshotInsideSnapshot {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v", @"c": @9, @"art": @NO}}] onPath:[[FPath alloc] initWith:@"t"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t/a/c"]];
+ XCTAssertFalse([found isEmpty], @"non-empty node");
+ found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertFalse([found isEmpty], @"non-empty node");
+ [st forgetPath:PATH(@"t/a/c")];
+ XCTAssertTrue([st findPath:PATH(@"t")] == nil, @"no more node here");
+ XCTAssertTrue([st findPath:PATH(@"t/a")] == nil, @"no more node here");
+ XCTAssertTrue([[[st findPath:PATH(@"t/a/b")] val] isEqualToString:@"v"], @"child still exists");
+ XCTAssertTrue([st findPath:PATH(@"t/a/c")] == nil, @"no more node here");
+ XCTAssertTrue([[[st findPath:PATH(@"t/a/art")] val] isEqualToNumber:@NO], @"child still exists");
+}
+
+- (void) testPathShallowerThanSnapshots {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:NODE(@NO) onPath:PATH(@"t/x1")];
+ [st rememberData:NODE(@YES) onPath:PATH(@"t/x2")];
+
+ [st forgetPath:PATH(@"t")];
+ XCTAssertTrue([st findPath:PATH(@"t")] == nil, @"No more node here");
+}
+
+- (void) testIterateChildren {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@{@"b": @"v", @"c": @9, @"art": @NO}];
+ [st rememberData:node onPath:PATH(@"t")];
+ [st rememberData:[FEmptyNode emptyNode] onPath:PATH(@"q")];
+
+ __block int num = 0;
+ __block BOOL gotT = NO;
+ __block BOOL gotQ = NO;
+ [st forEachChild:^(NSString* key, FSparseSnapshotTree* tree) {
+ num++;
+ if ([key isEqualToString:@"t"]) {
+ gotT = YES;
+ } else if ([key isEqualToString:@"q"]) {
+ gotQ = YES;
+ } else {
+ XCTFail(@"Unknown child");
+ }
+ }];
+
+ XCTAssertTrue(gotT, @"Saw t");
+ XCTAssertTrue(gotQ, @"Saw q");
+ XCTAssertTrue(num == 2, @"Saw two children");
+}
+
+- (void) testIterateTrees {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ __block int count = 0;
+ [st forEachTreeAtPath:PATH(@"") do:^(FPath *path, id<FNode> data) {
+ count++;
+ }];
+ XCTAssertTrue(count == 0, @"No trees to iterate through");
+
+ [st rememberData:NODE(@1) onPath:PATH(@"t")];
+ [st rememberData:NODE(@2) onPath:PATH(@"a/b")];
+ [st rememberData:NODE(@3) onPath:PATH(@"a/x/g")];
+ [st rememberData:NODE([NSNull null]) onPath:PATH(@"a/x/null")];
+
+ __block int num = 0;
+ __block BOOL got1 = NO;
+ __block BOOL got2 = NO;
+ __block BOOL got3 = NO;
+ __block BOOL gotNull = NO;
+
+ [st forEachTreeAtPath:PATH(@"q") do:^(FPath *path, id<FNode> data) {
+ num++;
+ NSString* pathString = [path description];
+ if ([pathString isEqualToString:@"/q/t"]) {
+ got1 = YES;
+ XCTAssertTrue([[data val] isEqualToNumber:@1], @"got 1");
+ } else if ([pathString isEqualToString:@"/q/a/b"]) {
+ got2 = YES;
+ XCTAssertTrue([[data val] isEqualToNumber:@2], @"got 2");
+ } else if ([pathString isEqualToString:@"/q/a/x/g"]) {
+ got3 = YES;
+ XCTAssertTrue([[data val] isEqualToNumber:@3], @"got 3");
+ } else if ([pathString isEqualToString:@"/q/a/x/null"]) {
+ gotNull = YES;
+ XCTAssertTrue([data val] == [NSNull null], @"got null");
+ } else {
+ XCTFail(@"unknown tree");
+ }
+ }];
+
+ XCTAssertTrue(got1 && got2 && got3 && gotNull, @"saw all the children");
+ XCTAssertTrue(num == 4, @"Saw the right number of children");
+}
+
+- (void) testSetLeafAndForgetDeeperPath {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:NODE(@"bar") onPath:PATH(@"foo")];
+ BOOL safeToRemove = [st forgetPath:PATH(@"foo/baz")];
+ XCTAssertFalse(safeToRemove, @"Should not have deleted anything, nothing to remove");
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FSyncPointTests.h b/Example/Database/Tests/Unit/FSyncPointTests.h
new file mode 100644
index 0000000..bc010ae
--- /dev/null
+++ b/Example/Database/Tests/Unit/FSyncPointTests.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 "FTestBase.h"
+
+@interface FSyncPointTests : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Unit/FSyncPointTests.m b/Example/Database/Tests/Unit/FSyncPointTests.m
new file mode 100644
index 0000000..d36b48a
--- /dev/null
+++ b/Example/Database/Tests/Unit/FSyncPointTests.m
@@ -0,0 +1,905 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FSyncPointTests.h"
+#import "FListenProvider.h"
+#import "FQuerySpec.h"
+#import "FQueryParams.h"
+#import "FPathIndex.h"
+#import "FKeyIndex.h"
+#import "FPriorityIndex.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FSyncTree.h"
+#import "FChange.h"
+#import "FDataEvent.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FCancelEvent.h"
+#import "FSnapshotUtilities.h"
+#import "FEventRegistration.h"
+#import "FCompoundWrite.h"
+#import "FEmptyNode.h"
+#import "FTestClock.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FSnapshotUtilities.h"
+
+typedef NSDictionary* (^fbt_nsdictionary_void)(void);
+
+@interface FTestEventRegistration : NSObject<FEventRegistration>
+@property (nonatomic, strong) NSDictionary *spec;
+@property (nonatomic, strong) FQuerySpec *query;
+@end
+
+@implementation FTestEventRegistration
+- (id) initWithSpec:(NSDictionary *)eventSpec query:(FQuerySpec *)query {
+ self = [super init];
+ if (self) {
+ self.spec = eventSpec;
+ self.query = query;
+ }
+ return self;
+}
+
+- (BOOL) responseTo:(FIRDataEventType)eventType {
+ return YES;
+}
+- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query {
+ FIRDataSnapshot *snap = nil;
+ FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:nil path:query.path];
+ if (change.type == FIRDataEventTypeValue) {
+ snap = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:change.indexedNode];
+ } else {
+ snap = [[FIRDataSnapshot alloc] initWithRef:[ref child:change.childKey]
+ indexedNode:change.indexedNode];
+ }
+ return [[FDataEvent alloc] initWithEventType:change.type eventRegistration:self dataSnapshot:snap prevName:change.prevKey];
+}
+
+- (BOOL) matches:(id<FEventRegistration>)other {
+ if (![other isKindOfClass:[FTestEventRegistration class]]) {
+ return NO;
+ } else {
+ FTestEventRegistration *otherRegistration = other;
+ if (self.spec[@"callbackId"] && otherRegistration.spec[@"callbackId"] &&
+ [self.spec[@"callbackId"] isEqualToNumber:otherRegistration.spec[@"callbackId"]]) {
+ return YES;
+ } else {
+ return NO;
+ }
+ }
+}
+
+- (void) fireEvent:(id<FEvent>)event queue:(dispatch_queue_t)queue {
+ [NSException raise:@"NotImplementedError" format:@"Method not implemneted."];
+}
+- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path {
+ [NSException raise:@"NotImplementedError" format:@"Method not implemneted."];
+ return nil;
+}
+
+- (FIRDatabaseHandle) handle {
+ [NSException raise:@"NotImplementedError" format:@"Method not implemneted."];
+ return 0;
+}
+@end
+
+@implementation FSyncPointTests
+
+- (NSString *) queryKeyForQuery:(FQuerySpec *)query tagId:(NSNumber *)tagId {
+ return [NSString stringWithFormat:@"%@|%@|%@", query.path, query.params, tagId];
+}
+
+- (void) actualEvent:(FDataEvent *)actual equalsExpected:(NSDictionary *)expected {
+ XCTAssertEqual(actual.eventType, [self stringToEventType:expected[@"type"]], @"Event type should be equal");
+ if (actual.eventType != FIRDataEventTypeValue) {
+ NSString *childName = actual.snapshot.key;
+ XCTAssertEqualObjects(childName, expected[@"name"], @"Snapshot name should be equal");
+ if (expected[@"prevName"] == [NSNull null]) {
+ XCTAssertNil(actual.prevName, @"prevName should be nil");
+ } else {
+ XCTAssertEqualObjects(actual.prevName, expected[@"prevName"], @"prevName should be equal");
+ }
+ }
+ NSString *actualHash = [actual.snapshot.node.node dataHash];
+ NSString *expectedHash = [[FSnapshotUtilities nodeFrom:expected[@"data"]] dataHash];
+ XCTAssertEqualObjects(actualHash, expectedHash, @"Data hash should be equal");
+}
+
+/**
+* @param actual is an array of id<FEvent>
+* @param expected is an array of dictionaries?
+*/
+- (void) actualEvents:(NSArray *)actual exactMatchesExpected:(NSArray *)expected {
+ if ([expected count] < [actual count]) {
+ XCTFail(@"Got extra events: %@", actual);
+ } else if ([expected count] > [actual count]) {
+ XCTFail(@"Missing events: %@", actual);
+ } else {
+ NSUInteger i = 0;
+ for (i = 0; i < [expected count]; i++) {
+ FDataEvent *actualEvent = actual[i];
+ NSDictionary *expectedEvent = expected[i];
+ [self actualEvent:actualEvent equalsExpected:expectedEvent];
+ }
+ }
+}
+
+- (void)assertOrderedFirstEvent:(FIRDataEventType)e1 secondEvent:(FIRDataEventType)e2 {
+ static NSArray *eventOrdering = nil;
+ if (!eventOrdering) {
+ eventOrdering = @[
+ [NSNumber numberWithInteger:FIRDataEventTypeChildRemoved],
+ [NSNumber numberWithInteger:FIRDataEventTypeChildAdded],
+ [NSNumber numberWithInteger:FIRDataEventTypeChildMoved],
+ [NSNumber numberWithInteger:FIRDataEventTypeChildChanged],
+ [NSNumber numberWithInteger:FIRDataEventTypeValue]
+ ];
+ }
+ NSUInteger idx1 = [eventOrdering indexOfObject:[NSNumber numberWithInteger:e1]];
+ NSUInteger idx2 = [eventOrdering indexOfObject:[NSNumber numberWithInteger:e2]];
+ if (idx1 > idx2) {
+ XCTFail(@"Received %d after %d", (int)e2, (int)e1);
+ }
+}
+
+- (FIRDataEventType)stringToEventType:(NSString *)stringType {
+ if ([stringType isEqualToString:@"child_added"]) {
+ return FIRDataEventTypeChildAdded;
+ } else if ([stringType isEqualToString:@"child_removed"]) {
+ return FIRDataEventTypeChildRemoved;
+ } else if ([stringType isEqualToString:@"child_changed"]) {
+ return FIRDataEventTypeChildChanged;
+ } else if ([stringType isEqualToString:@"child_moved"]) {
+ return FIRDataEventTypeChildMoved;
+ } else if ([stringType isEqualToString:@"value"]) {
+ return FIRDataEventTypeValue;
+ } else {
+ XCTFail(@"Unknown event type %@", stringType);
+ return FIRDataEventTypeValue;
+ }
+}
+
+- (void) actualEventSet:(id)actual matchesExpected:(id)expected atBasePath:(NSString *)basePathStr {
+ // don't worry about order for now
+ XCTAssertEqual([expected count], [actual count], @"Mismatched lengths.\nExpected: %@\nActual: %@", expected, actual);
+
+ NSArray *currentExpected = expected;
+ NSArray *currentActual = actual;
+ FPath *basePath = basePathStr != nil ? [[FPath alloc] initWith:basePathStr] : [FPath empty];
+ while ([currentExpected count] > 0) {
+ // Step 1: find location range in expected
+ // we expect all events for a particular path to be in a group
+ FPath *currentPath = [basePath childFromString:currentExpected[0][@"path"]];
+ NSUInteger i = 1;
+ while (i < [currentExpected count]) {
+ FPath *otherPath = [basePath childFromString:currentExpected[i][@"path"]];
+ if ([currentPath isEqual:otherPath]) {
+ i++;
+ } else {
+ break;
+ }
+ }
+
+ // Step 2: foreach in actual, asserting location
+ NSUInteger j = 0;
+ for (j = 0; j < i; j++) {
+ FDataEvent *actualEventData = currentActual[j];
+ FTestEventRegistration *eventRegistration = actualEventData.eventRegistration;
+ NSDictionary *specStep = eventRegistration.spec;
+ FPath *actualPath = [basePath childFromString:specStep[@"path"]];
+ if (![currentPath isEqual:actualPath]) {
+ XCTFail(@"Expected path %@ to equal %@", actualPath, currentPath);
+ }
+ }
+
+ // Step 3: slice each array
+ NSMutableArray *expectedSlice = [[currentExpected subarrayWithRange:NSMakeRange(0, i)] mutableCopy];
+ NSArray *actualSlice = [currentActual subarrayWithRange:NSMakeRange(0, i)];
+
+ // foreach in actual, stack up to enforce ordering, find in expected
+ NSMutableDictionary *actualMap = [[NSMutableDictionary alloc] init];
+ for (FDataEvent *actualEvent in actualSlice) {
+ FTestEventRegistration *eventRegistration = actualEvent.eventRegistration;
+ FQuerySpec *query = eventRegistration.query;
+ NSDictionary *spec = eventRegistration.spec;
+ NSString *listenId = [NSString stringWithFormat:@"%@|%@", [basePath childFromString:spec[@"path"]], query];
+ if (actualMap[listenId]) {
+ // stack this event up, and make sure it obeys ordering constraints
+ NSMutableArray *eventStack = actualMap[listenId];
+ FDataEvent *prevEvent = eventStack[[eventStack count] - 1];
+ [self assertOrderedFirstEvent:prevEvent.eventType secondEvent:actualEvent.eventType];
+ [eventStack addObject:actualEvent];
+ } else {
+ // this is the first event for this listen, just initialize it
+ actualMap[listenId] = [[NSMutableArray alloc] initWithObjects:actualEvent, nil];
+ }
+ // Ordering has been enforced, make sure we can find this in the expected events
+ __block NSUInteger indexToRemove = NSNotFound;
+ [expectedSlice enumerateObjectsUsingBlock:^(NSDictionary *expectedEvent, NSUInteger idx, BOOL *stop) {
+ if ([self stringToEventType:expectedEvent[@"type"]] == actualEvent.eventType) {
+ if ([self stringToEventType:expectedEvent[@"type"]] != FIRDataEventTypeValue) {
+ if (![expectedEvent[@"name"] isEqualToString:actualEvent.snapshot.key]) {
+ return; // short circuit, not a match
+ }
+ if ([self stringToEventType:expectedEvent[@"type"]] != FIRDataEventTypeChildRemoved &&
+ !(expectedEvent[@"prevName"] == [NSNull null] && actualEvent.prevName == nil) &&
+ !(expectedEvent[@"prevName"] != [NSNull null] && [expectedEvent[@"prevName"] isEqualToString:actualEvent.prevName])) {
+ return; // short circuit, not a match
+ }
+ }
+ // make sure the snapshots match
+ NSString *snapHash = [actualEvent.snapshot.node.node dataHash];
+ NSString *expectedHash = [[FSnapshotUtilities nodeFrom:expectedEvent[@"data"]] dataHash];
+ if ([snapHash isEqualToString:expectedHash]) {
+ indexToRemove = idx;
+ *stop = YES;
+ }
+ }
+ }];
+ XCTAssertFalse(indexToRemove == NSNotFound, @"Could not find matching expected event for %@", actualEvent);
+ [expectedSlice removeObjectAtIndex:indexToRemove];
+ }
+ currentExpected = [currentExpected subarrayWithRange:NSMakeRange(i, [currentExpected count] - i)];
+ currentActual = [currentActual subarrayWithRange:NSMakeRange(i, [currentActual count] - i)];
+ }
+}
+
+- (FQuerySpec *)parseParams:(NSDictionary *)specParams forPath:(FPath *)path {
+ FQueryParams *query = [[FQueryParams alloc] init];
+ NSMutableDictionary *params;
+
+ if (specParams) {
+ params = [specParams mutableCopy];
+ if (!params[@"tag"]) {
+ XCTFail(@"Error: Non-default queries must have tag");
+ }
+ } else {
+ params = [NSMutableDictionary dictionary];
+ }
+
+ if (params[@"orderBy"]) {
+ FPath *indexPath = [FPath pathWithString:params[@"orderBy"]];
+ id<FIndex> index = [[FPathIndex alloc] initWithPath:indexPath];
+ query = [query orderBy:index];
+ [params removeObjectForKey:@"orderBy"];
+ }
+ if (params[@"orderByKey"]) {
+ query = [query orderBy:[FKeyIndex keyIndex]];
+ [params removeObjectForKey:@"orderByKey"];
+ }
+ if (params[@"orderByPriority"]) {
+ query = [query orderBy:[FPriorityIndex priorityIndex]];
+ [params removeObjectForKey:@"orderByPriority"];
+ }
+
+ if (params[@"startAt"]) {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:params[@"startAt"][@"index"]];
+ if (params[@"startAt"][@"name"]) {
+ query = [query startAt:node childKey:params[@"startAt"][@"name"]];
+ } else {
+ query = [query startAt:node];
+ }
+ [params removeObjectForKey:@"startAt"];
+ }
+ if (params[@"endAt"]) {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:params[@"endAt"][@"index"]];
+ if (params[@"endAt"][@"name"]) {
+ query = [query endAt:node childKey:params[@"endAt"][@"name"]];
+ } else {
+ query = [query endAt:node];
+ }
+ [params removeObjectForKey:@"endAt"];
+ }
+ if (params[@"equalTo"]) {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:params[@"equalTo"][@"index"]];
+ if (params[@"equalTo"][@"name"]) {
+ NSString *name = params[@"equalTo"][@"name"];
+ query = [[query startAt:node childKey:name] endAt:node childKey:name];
+ } else {
+ query = [[query startAt:node] endAt:node];
+ }
+ [params removeObjectForKey:@"equalTo"];
+ }
+
+ if (params[@"limitToFirst"]) {
+ query = [query limitToFirst:[params[@"limitToFirst"] integerValue]];
+ [params removeObjectForKey:@"limitToFirst"];
+ }
+ if (params[@"limitToLast"]) {
+ query = [query limitToLast:[params[@"limitToLast"] integerValue]];
+ [params removeObjectForKey:@"limitToLast"];
+ }
+
+ [params removeObjectForKey:@"tag"];
+ if ([params count] > 0) {
+ XCTFail(@"Unsupported query parameter: %@", params);
+ }
+ return [[FQuerySpec alloc] initWithPath:path params:query];
+}
+
+- (void) runTest:(NSDictionary *)testSpec atBasePath:(NSString *)basePath {
+ NSMutableDictionary *listens = [[NSMutableDictionary alloc] init];
+ __weak FSyncPointTests *weakSelf = self;
+
+ FListenProvider *listenProvider = [[FListenProvider alloc] init];
+ listenProvider.startListening = ^(FQuerySpec *query, NSNumber *tagId, id<FSyncTreeHash> hash, fbt_nsarray_nsstring onComplete) {
+ FQueryParams *queryParams = query.params;
+ FPath *path = query.path;
+ NSString *logTag = [NSString stringWithFormat:@"%@ (%@)", queryParams, tagId];
+ NSString *key = [weakSelf queryKeyForQuery:query tagId:tagId];
+ FFLog(@"I-RDB143001", @"Listening at %@ for %@", path, logTag);
+ id existing = listens[key];
+ NSAssert(existing == nil, @"Duplicate listen");
+ listens[key] = @YES;
+ return @[];
+ };
+
+ listenProvider.stopListening = ^(FQuerySpec *query, NSNumber *tagId) {
+ FQueryParams *queryParams = query.params;
+ FPath *path = query.path;
+ NSString *logTag = [NSString stringWithFormat:@"%@ (%@)", queryParams, tagId];
+ NSString *key = [weakSelf queryKeyForQuery:query tagId:tagId];
+ FFLog(@"I-RDB143002", @"Stop listening at %@ for %@", path, logTag);
+ id existing = listens[key];
+ XCTAssertTrue(existing != nil, @"Missing record of query that we're removing");
+ [listens removeObjectForKey:key];
+ };
+
+ FSyncTree *syncTree = [[FSyncTree alloc] initWithListenProvider:listenProvider];
+
+ NSLog(@"Running %@", testSpec[@"name"]);
+ NSInteger currentWriteId = 0;
+ for (NSDictionary *step in testSpec[@"steps"]) {
+ NSMutableDictionary *spec = [step mutableCopy];
+ if (spec[@".comment"]) {
+ NSLog(@" > %@", spec[@".comment"]);
+ }
+ if (spec[@"debug"] != nil) {
+ // TODO: Ideally we'd pause the debugger somehow (like "debugger;" in JS).
+ NSLog(@"Start debugging");
+ }
+ // Almost everything has a path...
+ FPath *path = [FPath empty];
+ if (basePath != nil) {
+ path = [path childFromString:basePath];
+ }
+ if (spec[@"path"] != nil) {
+ path = [path childFromString:spec[@"path"]];
+ }
+ NSArray *events;
+ if ([spec[@"type"] isEqualToString:@"listen"]) {
+ FQuerySpec *query = [self parseParams:spec[@"params"] forPath:path];
+ FTestEventRegistration *eventRegistration = [[FTestEventRegistration alloc] initWithSpec:spec query:query];
+ events = [syncTree addEventRegistration:eventRegistration forQuery:query];
+ [self actualEvents:events exactMatchesExpected:spec[@"events"]];
+
+ } else if ([spec[@"type"] isEqualToString:@"unlisten"]) {
+ FQuerySpec *query = [self parseParams:spec[@"params"] forPath:path];
+ FTestEventRegistration *eventRegistration = [[FTestEventRegistration alloc] initWithSpec:spec query:query];
+ events = [syncTree removeEventRegistration:eventRegistration forQuery:query cancelError:nil];
+ [self actualEvents:events exactMatchesExpected:spec[@"events"]];
+
+ } else if ([spec[@"type"] isEqualToString:@"serverUpdate"]) {
+ id<FNode> update = [FSnapshotUtilities nodeFrom:spec[@"data"]];
+ if (spec[@"tag"]) {
+ events = [syncTree applyTaggedQueryOverwriteAtPath:path newData:update tagId:spec[@"tag"]];
+ } else {
+ events = [syncTree applyServerOverwriteAtPath:path newData:update];
+ }
+ [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath];
+
+ } else if ([spec[@"type"] isEqualToString:@"serverMerge"]) {
+ FCompoundWrite *compoundWrite = [FCompoundWrite compoundWriteWithValueDictionary:spec[@"data"]];
+ if (spec[@"tag"]) {
+ events = [syncTree applyTaggedQueryMergeAtPath:path changedChildren:compoundWrite tagId:spec[@"tag"]];
+ } else {
+ events = [syncTree applyServerMergeAtPath:path changedChildren:compoundWrite];
+ }
+ [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath];
+
+ } else if ([spec[@"type"] isEqualToString:@"set"]) {
+ id<FNode> toSet = [FSnapshotUtilities nodeFrom:spec[@"data"]];
+ BOOL visible = (spec[@"visible"] != nil) ? [spec[@"visible"] boolValue] : YES;
+ events = [syncTree applyUserOverwriteAtPath:path newData:toSet writeId:currentWriteId++ isVisible:visible];
+ [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath];
+
+ } else if ([spec[@"type"] isEqualToString:@"update"]) {
+ FCompoundWrite *compoundWrite = [FCompoundWrite compoundWriteWithValueDictionary:spec[@"data"]];
+ events = [syncTree applyUserMergeAtPath:path changedChildren:compoundWrite writeId:currentWriteId++];
+ [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath];
+ } else if ([spec[@"type"] isEqualToString:@"ackUserWrite"]) {
+ NSInteger writeId = [spec[@"writeId"] integerValue];
+ BOOL revert = [spec[@"revert"] boolValue];
+ events = [syncTree ackUserWriteWithWriteId:writeId revert:revert persist:YES clock:[[FTestClock alloc] init]];
+ [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath];
+ } else if ([spec[@"type"] isEqualToString:@"suppressWarning"]) {
+ // Do nothing. This is a hack so JS's Jasmine tests don't throw warnings for "expect no errors" tests.
+ } else {
+ XCTFail(@"Unknown step: %@", spec[@"type"]);
+ }
+ }
+}
+
+- (NSArray *) loadSpecs {
+ static NSArray *json;
+ if (json == nil) {
+ NSString *syncPointSpec = [[NSBundle bundleForClass:[FSyncPointTests class]] pathForResource:@"syncPointSpec" ofType:@"json"];
+ NSLog(@"%@", syncPointSpec);
+ NSData *specData = [NSData dataWithContentsOfFile:syncPointSpec];
+ NSError *error = nil;
+ json = [NSJSONSerialization JSONObjectWithData:specData options:kNilOptions error:&error];
+
+ if (error) {
+ XCTFail(@"Error occurred parsing JSON: %@", error);
+ }
+ }
+
+ return json;
+}
+
+- (NSDictionary *) specsForName:(NSString *)name {
+ for (NSDictionary *spec in [self loadSpecs]) {
+ if ([name isEqualToString:spec[@"name"]]) {
+ return spec;
+ }
+ }
+
+ XCTFail(@"No such test: %@", name);
+ return nil;
+}
+
+- (void) runTestForName:(NSString *)name {
+ NSDictionary *spec = [self specsForName:name];
+ [self runTest:spec atBasePath:nil];
+ // run again at a deeper location
+ [self runTest:spec atBasePath:@"/foo/bar/baz"];
+}
+
+- (void) testAll {
+ NSArray *specs = [self loadSpecs];
+ for (NSDictionary *spec in specs) {
+ [self runTest:spec atBasePath:nil];
+ // run again at a deeper location
+ [self runTest:spec atBasePath:@"/foo/bar/baz"];
+ }
+}
+
+- (void) testDefaultListenHandlesParentSet {
+ [self runTestForName:@"Default listen handles a parent set"];
+}
+
+- (void) testDefaultListenHandlesASetAtTheSameLevel {
+ [self runTestForName:@"Default listen handles a set at the same level"];
+}
+
+- (void) testAQueryCanGetACompleteCacheThenAMerge {
+ [self runTestForName:@"A query can get a complete cache then a merge"];
+}
+
+- (void) testServerMergeOnListenerWithCompleteChildren {
+ [self runTestForName:@"Server merge on listener with complete children"];
+}
+
+- (void) testDeepMergeOnListenerWithCompleteChildren {
+ [self runTestForName:@"Deep merge on listener with complete children"];
+}
+
+- (void) testUpdateChildListenerTwice {
+ [self runTestForName:@"Update child listener twice"];
+}
+
+- (void) testChildOfDefaultListenThatAlreadyHasACompleteCache {
+ [self runTestForName:@"Update child of default listen that already has a complete cache"];
+}
+
+- (void) testUpdateChildOfDefaultListenThatHasNoCache {
+ [self runTestForName:@"Update child of default listen that has no cache"];
+}
+
+// failing
+- (void) testUpdateTheChildOfACoLocatedDefaultListenerAndQuery {
+ [self runTestForName:@"Update (via set) the child of a co-located default listener and query"];
+}
+
+- (void) testUpdateTheChildOfAQueryWithAFullCache {
+ [self runTestForName:@"Update (via set) the child of a query with a full cache"];
+}
+
+- (void) testUpdateAChildBelowAnEmptyQuery {
+ [self runTestForName:@"Update (via set) a child below an empty query"];
+}
+
+- (void) testUpdateDescendantOfDefaultListenerWithFullCache {
+ [self runTestForName:@"Update descendant of default listener with full cache"];
+}
+
+- (void) testDescendantSetBelowAnEmptyDefaultLIstenerIsIgnored {
+ [self runTestForName:@"Descendant set below an empty default listener is ignored"];
+}
+
+- (void) testUpdateOfAChild {
+ [self runTestForName:@"Update of a child. This can happen if a child listener is added and removed"];
+}
+
+- (void) testRevertSetWithOnlyChildCaches {
+ [self runTestForName:@"Revert set with only child caches"];
+}
+
+- (void) testCanRevertADuplicateChildSet {
+ [self runTestForName:@"Can revert a duplicate child set"];
+}
+
+- (void) testCanRevertAChildSetAndSeeTheUnderlyingData {
+ [self runTestForName:@"Can revert a child set and see the underlying data"];
+}
+
+- (void) testRevertChildSetWithNoServerData {
+ [self runTestForName:@"Revert child set with no server data"];
+}
+
+- (void) testRevertDeepSetWithNoServerData {
+ [self runTestForName:@"Revert deep set with no server data"];
+}
+
+- (void) testRevertSetCoveredByNonvisibleTransaction {
+ [self runTestForName:@"Revert set covered by non-visible transaction"];
+}
+
+- (void) testClearParentShadowingServerValuesSetWithServerChildren {
+ [self runTestForName:@"Clear parent shadowing server values set with server children"];
+}
+
+- (void) testClearChildShadowingServerValuesSetWithServerChildren {
+ [self runTestForName:@"Clear child shadowing server values set with server children"];
+}
+
+- (void) testUnrelatedMergeDoesntShadowServerUpdates {
+ [self runTestForName:@"Unrelated merge doesn't shadow server updates"];
+}
+
+- (void) testCanSetAlongsideARemoteMerge {
+ [self runTestForName:@"Can set alongside a remote merge"];
+}
+
+- (void) testSetPriorityOnALocationWithNoCache {
+ [self runTestForName:@"setPriority on a location with no cache"];
+}
+
+- (void) testDeepUpdateDeletesChildFromLimitWindowAndPullsInNewChild {
+ [self runTestForName:@"deep update deletes child from limit window and pulls in new child"];
+}
+
+- (void) testDeepSetDeletesChildFromLimitWindowAndPullsInNewChild {
+ [self runTestForName:@"deep set deletes child from limit window and pulls in new child"];
+}
+
+- (void) testEdgeCaseInNewChildForChange {
+ [self runTestForName:@"Edge case in newChildForChange_"];
+}
+
+- (void) testRevertSetInQueryWindow {
+ [self runTestForName:@"Revert set in query window"];
+}
+
+- (void) testHandlesAServerValueMovingAChildOutOfAQueryWindow {
+ [self runTestForName:@"Handles a server value moving a child out of a query window"];
+}
+
+- (void) testUpdateOfIndexedChildWorks {
+ [self runTestForName:@"Update of indexed child works"];
+}
+
+- (void) testMergeAppliedToEmptyLimit {
+ [self runTestForName:@"Merge applied to empty limit"];
+}
+
+- (void) testLimitIsRefilledFromServerDataAfterMerge {
+ [self runTestForName:@"Limit is refilled from server data after merge"];
+}
+
+- (void) testHandleRepeatedListenWithMergeAsFirstUpdate {
+ [self runTestForName:@"Handle repeated listen with merge as first update"];
+}
+
+- (void) testLimitIsRefilledFromServerDataAfterSet {
+ [self runTestForName:@"Limit is refilled from server data after set"];
+}
+
+- (void) testQueryOnWeirdPath {
+ [self runTestForName:@"query on weird path."];
+}
+
+- (void) testRunsRound2 {
+ [self runTestForName:@"runs, round2"];
+}
+
+- (void) testHandlesNestedListens {
+ [self runTestForName:@"handles nested listens"];
+}
+
+- (void) testHandlesASetBelowAListen {
+ [self runTestForName:@"Handles a set below a listen"];
+}
+
+- (void) testDoesNonDefaultQueries {
+ [self runTestForName:@"does non-default queries"];
+}
+
+- (void) testHandlesCoLocatedDefaultListenerAndQuery {
+ [self runTestForName:@"handles a co-located default listener and query"];
+}
+
+- (void) testDefaultAndNonDefaultListenerAtSameLocationWithServerUpdate {
+ [self runTestForName:@"Default and non-default listener at same location with server update"];
+}
+
+- (void) testAddAParentListenerToACompleteChildListenerExpectChildEvent {
+ [self runTestForName:@"Add a parent listener to a complete child listener, expect child event"];
+}
+
+- (void) testAddListensToASetExpectCorrectEventsIncludingAChildEvent {
+ [self runTestForName:@"Add listens to a set, expect correct events, including a child event"];
+}
+
+- (void) testServerUpdateToAChildListenerRaisesChildEventsAtParent {
+ [self runTestForName:@"ServerUpdate to a child listener raises child events at parent"];
+}
+
+- (void) testServerUpdateToAChildListenerRaisesChildEventsAtParentQuery {
+ [self runTestForName:@"ServerUpdate to a child listener raises child events at parent query"];
+}
+
+- (void) testMultipleCompleteChildrenAreHandleProperly {
+ [self runTestForName:@"Multiple complete children are handled properly"];
+}
+
+- (void) testWriteLeafNodeOverwriteAtParentNode {
+ [self runTestForName:@"Write leaf node, overwrite at parent node"];
+}
+
+- (void) testConfirmCompleteChildrenFromTheServer {
+ [self runTestForName:@"Confirm complete children from the server"];
+}
+
+- (void) testWriteLeafOverwriteFromParent {
+ [self runTestForName:@"Write leaf, overwrite from parent"];
+}
+
+- (void) testBasicUpdateTest {
+ [self runTestForName:@"Basic update test"];
+}
+
+- (void) testNoDoubleValueEventsForUserAck {
+ [self runTestForName:@"No double value events for user ack"];
+}
+
+- (void) testBasicKeyIndexSanityCheck {
+ [self runTestForName:@"Basic key index sanity check"];
+}
+
+- (void) testCollectCorrectSubviewsToListenOn {
+ [self runTestForName:@"Collect correct subviews to listen on"];
+}
+
+- (void) testLimitToFirstOneOnOrderedQuery {
+ [self runTestForName:@"Limit to first one on ordered query"];
+}
+
+- (void) testLimitToLastOneOnOrderedQuery {
+ [self runTestForName:@"Limit to last one on ordered query"];
+}
+
+- (void) testUpdateIndexedValueOnExistingChildFromLimitedQuery {
+ [self runTestForName:@"Update indexed value on existing child from limited query"];
+}
+
+- (void) testCanCreateStartAtEndAtEqualToQueriesWithBool {
+ [self runTestForName:@"Can create startAt, endAt, equalTo queries with bool"];
+}
+
+- (void) testQueryWithExistingServerSnap {
+ [self runTestForName:@"Query with existing server snap"];
+}
+
+- (void) testServerDataIsNotPurgedForNonServerIndexedQueries {
+ [self runTestForName:@"Server data is not purged for non-server-indexed queries"];
+}
+
+- (void) testStartAtEndAtDominatesLimit {
+ [self runTestForName:@"startAt/endAt dominates limit"];
+}
+
+- (void) testUpdateToSingleChildThatMovesOutOfWindow {
+ [self runTestForName:@"Update to single child that moves out of window"];
+}
+
+- (void) testLimitedQueryDoesntPullInOutOfRangeChild {
+ [self runTestForName:@"Limited query doesn't pull in out of range child"];
+}
+
+- (void) testWithCustomOrderByIsRefilledWithCorrectItem {
+ [self runTestForName:@"Limit with custom orderBy is refilled with correct item"];
+}
+
+- (void) testMergeForLocationWithDefaultAndLimitedListener {
+ [self runTestForName:@"Merge for location with default and limited listener"];
+}
+
+- (void) testUserMergePullsInCorrectValues {
+ [self runTestForName:@"User merge pulls in correct values"];
+}
+
+- (void) testUserDeepSetPullsInCorrectValues {
+ [self runTestForName:@"User deep set pulls in correct values"];
+}
+
+- (void) testQueriesWithEqualToNullWork {
+ [self runTestForName:@"Queries with equalTo(null) work"];
+}
+
+- (void) testRevertedWritesUpdateQuery {
+ [self runTestForName:@"Reverted writes update query"];
+}
+
+- (void) testDeepSetForNonLocalDataDoesntRaiseEvents {
+ [self runTestForName:@"Deep set for non-local data doesn't raise events"];
+}
+
+- (void) testUserUpdateWithNewChildrenTriggersEvents {
+ [self runTestForName:@"User update with new children triggers events"];
+}
+
+- (void) testUserWriteWithDeepOverwrite {
+ [self runTestForName:@"User write with deep user overwrite"];
+}
+
+- (void) testServerUpdatesPriority {
+ [self runTestForName:@"Server updates priority"];
+}
+
+- (void) testRevertFullUnderlyingWrite {
+ [self runTestForName:@"Revert underlying full overwrite"];
+}
+
+- (void) testUserChildOverwriteForNonexistentServerNode {
+ [self runTestForName:@"User child overwrite for non-existent server node"];
+}
+
+- (void) testRevertUserOverwriteOfChildOnLeafNode {
+ [self runTestForName:@"Revert user overwrite of child on leaf node"];
+}
+
+- (void) testServerOverwriteWithDeepUserDelete {
+ [self runTestForName:@"Server overwrite with deep user delete"];
+}
+
+- (void) testUserOverwritesLeafNodeWithPriority {
+ [self runTestForName:@"User overwrites leaf node with priority"];
+}
+
+- (void) testUserOverwritesInheritPriorityValuesFromLeafNodes {
+ [self runTestForName:@"User overwrites inherit priority values from leaf nodes"];
+}
+
+- (void) testUserUpdateOnUserSetLeafNodeWithPriorityAfterServerUpdate {
+ [self runTestForName:@"User update on user set leaf node with priority after server update"];
+}
+
+- (void) testServerDeepDeleteOnLeafNode {
+ [self runTestForName:@"Server deep delete on leaf node"];
+}
+
+- (void) testUserSetsRootPriority {
+ [self runTestForName:@"User sets root priority"];
+}
+
+- (void) testUserUpdatesPriorityOnEmptyRoot {
+ [self runTestForName:@"User updates priority on empty root"];
+}
+
+- (void) testRevertSetAtRootWithPriority {
+ [self runTestForName:@"Revert set at root with priority"];
+}
+
+- (void) testServerUpdatesPriorityAfterUserSetsPriority {
+ [self runTestForName:@"Server updates priority after user sets priority"];
+}
+
+- (void) testEmptySetDoesntPreventServerUpdates {
+ [self runTestForName:@"Empty set doesn't prevent server updates"];
+}
+
+- (void) testUserUpdatesPriorityTwiceFirstIsReverted {
+ [self runTestForName:@"User updates priority twice, first is reverted"];
+}
+
+- (void) testServerAcksRootPrioritySetAfterUserDeletesRootNode {
+ [self runTestForName:@"Server acks root priority set after user deletes root node"];
+}
+
+- (void) testADeleteInAMergeDoesntPushOutNodes {
+ [self runTestForName:@"A delete in a merge doesn't push out nodes"];
+}
+
+- (void) testATaggedQueryFiresEventsEventually {
+ [self runTestForName:@"A tagged query fires events eventually"];
+}
+
+- (void) testUserWriteOutsideOfLimitIsIgnoredForTaggedQueries {
+ [self runTestForName:@"User write outside of limit is ignored for tagged queries"];
+}
+
+- (void) testAckForMergeDoesntRaiseValueEventForLaterListen {
+ [self runTestForName:@"Ack for merge doesn't raise value event for later listen"];
+}
+
+- (void) testClearParentShadowingServerValuesMergeWithServerChildren {
+ [self runTestForName:@"Clear parent shadowing server values merge with server children"];
+}
+
+- (void) testPrioritiesDontMakeMeSick {
+ [self runTestForName:@"Priorities don't make me sick"];
+}
+
+- (void) testMergeThatMovesChildFromWindowToBoundaryDoesNotCauseChildToBeReadded {
+ [self runTestForName:@"Merge that moves child from window to boundary does not cause child to be readded"];
+}
+
+- (void) testDeepMergeAckIsHandledCorrectly {
+ [self runTestForName:@"Deep merge ack is handled correctly."];
+}
+
+- (void) testDeepMergeAckOnIncompleteDataAndWithServerValues {
+ [self runTestForName:@"Deep merge ack (on incomplete data, and with server values)"];
+}
+
+- (void) testLimitQueryHandlesDeepServerMergeForOutOfViewItem {
+ [self runTestForName:@"Limit query handles deep server merge for out-of-view item."];
+}
+
+- (void) testLimitQueryHandlesDeepUserMergeForOutOfViewItem {
+ [self runTestForName:@"Limit query handles deep user merge for out-of-view item."];
+}
+
+- (void) testLimitQueryHandlesDeepUserMergeForOutOfViewItemFollowedByServerUpdate {
+ [self runTestForName:@"Limit query handles deep user merge for out-of-view item followed by server update."];
+}
+
+- (void) testUnrelatedUntaggedUpdateIsNotCachedInTaggedListen {
+ [self runTestForName:@"Unrelated, untagged update is not cached in tagged listen"];
+}
+
+- (void) testUnrelatedAckedSetIsNotCachedInTaggedListen {
+ [self runTestForName:@"Unrelated, acked set is not cached in tagged listen"];
+}
+
+- (void) testUnrelatedAckedUpdateIsNotCachedInTaggedListen {
+ [self runTestForName:@"Unrelated, acked update is not cached in tagged listen"];
+}
+
+- (void) testdeepUpdateRaisesImmediateEventsOnlyIfHasCompleteData {
+ [self runTestForName:@"Deep update raises immediate events only if has complete data"];
+}
+
+- (void) testdeepUpdateReturnsMinimumDataRequired {
+ [self runTestForName:@"Deep update returns minimum data required"];
+}
+
+- (void) testdeepUpdateRaisesAllEvents {
+ [self runTestForName:@"Deep update raises all events"];
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FTrackedQueryManagerTest.m b/Example/Database/Tests/Unit/FTrackedQueryManagerTest.m
new file mode 100644
index 0000000..ebcf9b2
--- /dev/null
+++ b/Example/Database/Tests/Unit/FTrackedQueryManagerTest.m
@@ -0,0 +1,338 @@
+/*
+ * 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/XCTest.h>
+
+#import "FTrackedQueryManager.h"
+#import "FTrackedQuery.h"
+#import "FMockStorageEngine.h"
+#import "FPath.h"
+#import "FQuerySpec.h"
+#import "FPathIndex.h"
+#import "FSnapshotUtilities.h"
+#import "FClock.h"
+#import "FTestClock.h"
+#import "FTestHelpers.h"
+#import "FPruneForest.h"
+#import "FTestCachePolicy.h"
+
+@interface FPruneForest (Test)
+
+- (FImmutableSortedDictionary *)pruneForest;
+
+@end
+
+@interface FTrackedQueryManagerTest : XCTestCase
+
+@end
+
+@implementation FTrackedQueryManagerTest
+
+#define SAMPLE_PARAMS \
+ ([[[[[FQueryParams defaultInstance] orderBy:[[FPathIndex alloc] initWithPath:PATH(@"child")]] \
+ startAt:[FSnapshotUtilities nodeFrom:@"startVal"] childKey:@"startKey"] \
+ endAt:[FSnapshotUtilities nodeFrom:@"endVal"] childKey:@"endKey"] \
+ limitToLast:5])
+
+#define SAMPLE_QUERY \
+ ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:SAMPLE_PARAMS])
+
+#define DEFAULT_FOO_QUERY \
+ ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:[FQueryParams defaultInstance]])
+
+#define DEFAULT_BAR_QUERY \
+ ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"bar"] params:[FQueryParams defaultInstance]])
+
+- (FTrackedQueryManager *)newManager {
+ return [self newManagerWithClock:[FSystemClock clock]];
+}
+
+- (FTrackedQueryManager *)newManagerWithClock:(id<FClock>)clock {
+ return [[FTrackedQueryManager alloc] initWithStorageEngine:[[FMockStorageEngine alloc] init]
+ clock:clock];
+}
+
+- (FTrackedQueryManager *)newManagerWithStorageEngine:(id<FStorageEngine>)storageEngine {
+ return [[FTrackedQueryManager alloc] initWithStorageEngine:storageEngine clock:[FSystemClock clock]];
+}
+
+- (void)testFindTrackedQuery {
+ FTrackedQueryManager *manager = [self newManager];
+ XCTAssertNil([manager findTrackedQuery:SAMPLE_QUERY]);
+ [manager setQueryActive:SAMPLE_QUERY];
+ XCTAssertNotNil([manager findTrackedQuery:SAMPLE_QUERY]);
+}
+
+- (void)testRemoveTrackedQuery {
+ FTrackedQueryManager *manager = [self newManager];
+ [manager setQueryActive:SAMPLE_QUERY];
+ XCTAssertNotNil([manager findTrackedQuery:SAMPLE_QUERY]);
+ [manager removeTrackedQuery:SAMPLE_QUERY];
+ XCTAssertNil([manager findTrackedQuery:SAMPLE_QUERY]);
+ [manager verifyCache];
+}
+
+- (void)testSetQueryActiveAndInactive {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager setQueryActive:SAMPLE_QUERY];
+ FTrackedQuery *q = [manager findTrackedQuery:SAMPLE_QUERY];
+ XCTAssertTrue(q.isActive);
+ XCTAssertEqual(q.lastUse, clock.currentTime);
+ [manager verifyCache];
+
+ [clock tick];
+ [manager setQueryInactive:SAMPLE_QUERY];
+ q = [manager findTrackedQuery:SAMPLE_QUERY];
+ XCTAssertFalse(q.isActive);
+ XCTAssertEqual(q.lastUse, clock.currentTime);
+ [manager verifyCache];
+}
+
+- (void)testSetQueryComplete {
+ FTrackedQueryManager *manager = [self newManager];
+ [manager setQueryActive:SAMPLE_QUERY];
+ [manager setQueryComplete:SAMPLE_QUERY];
+ XCTAssertTrue([manager findTrackedQuery:SAMPLE_QUERY].isComplete);
+ [manager verifyCache];
+}
+
+- (void)testSetQueriesComplete {
+ FTrackedQueryManager *manager = [self newManager];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/bar")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"elsewhere")]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo") params:SAMPLE_PARAMS]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/baz") params:SAMPLE_PARAMS]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"elsewhere") params:SAMPLE_PARAMS]];
+
+ [manager setQueriesCompleteAtPath:PATH(@"foo")];
+
+ XCTAssertTrue([manager findTrackedQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]].isComplete);
+ XCTAssertTrue([manager findTrackedQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/bar")]].isComplete);
+ XCTAssertTrue([manager findTrackedQuery:[[FQuerySpec alloc] initWithPath:PATH(@"foo") params:SAMPLE_PARAMS]].isComplete);
+ XCTAssertTrue([manager findTrackedQuery:[[FQuerySpec alloc] initWithPath:PATH(@"foo/baz") params:SAMPLE_PARAMS]].isComplete);
+ XCTAssertFalse([manager findTrackedQuery:[FQuerySpec defaultQueryAtPath:PATH(@"elsewhere")]].isComplete);
+ XCTAssertFalse([manager findTrackedQuery:[[FQuerySpec alloc] initWithPath:PATH(@"elsewhere") params:SAMPLE_PARAMS]].isComplete);
+ [manager verifyCache];
+}
+
+- (void)testIsQueryComplete {
+ FTrackedQueryManager *manager = [self newManager];
+
+ [manager setQueryActive:SAMPLE_QUERY];
+ [manager setQueryComplete:SAMPLE_QUERY];
+
+ [manager setQueryActive:DEFAULT_BAR_QUERY];
+
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"baz")]];
+ [manager setQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"baz")]];
+
+ XCTAssertTrue([manager isQueryComplete:SAMPLE_QUERY]);
+ XCTAssertFalse([manager isQueryComplete:DEFAULT_BAR_QUERY]);
+
+ XCTAssertFalse([manager isQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"")]]);
+ XCTAssertTrue([manager isQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"baz")]]);
+ XCTAssertTrue([manager isQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"baz/quu")]]);
+}
+
+- (void)testPruneOldQueries {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"active1")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"active2")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"pinned1")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"pinned2")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive1")]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive1")]];
+ [clock tick];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive2")]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive2")]];
+ [clock tick];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive3")]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive3")]];
+ [clock tick];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive4")]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive4")]];
+ [clock tick];
+
+ // Should remove the first two inactive queries
+ FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:0.5 maxQueries:NSUIntegerMax]];
+ [self checkPruneForest:forest
+ pathsToKeep:@[@"active1", @"active2", @"pinned1", @"pinned2", @"inactive3", @"inactive4"]
+ pathsToPrune:@[@"inactive1", @"inactive2"]];
+
+ // Should remove the other two inactive queries
+ forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1 maxQueries:NSUIntegerMax]];
+ [self checkPruneForest:forest
+ pathsToKeep:@[@"active1", @"active2", @"pinned1", @"pinned2"]
+ pathsToPrune:@[@"inactive3", @"inactive4"]];
+
+ // Nothing left to prune
+ forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1 maxQueries:NSUIntegerMax]];
+ XCTAssertFalse([forest prunesAnything]);
+
+ [manager verifyCache];
+}
+
+- (void) testPruneQueriesOverMaxSize {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ for (NSUInteger i = 0; i < 10; i++) {
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]];
+ [clock tick];
+ }
+
+ FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:0.2 maxQueries:6]];
+ [self checkPruneForest:forest
+ pathsToKeep:@[@"4", @"5", @"6", @"7", @"8", @"9"]
+ pathsToPrune:@[@"0", @"1", @"2", @"3"]];
+}
+
+- (void) testPruneDefaultWithDeeperQueries {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/a") params:SAMPLE_PARAMS]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/b") params:SAMPLE_PARAMS]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]];
+
+ FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1.0 maxQueries:NSUIntegerMax]];
+ [self checkPruneForest:forest pathsToKeep:@[@"foo/a", @"foo/b"] pathsToPrune:@[@"foo"]];
+ [manager verifyCache];
+}
+
+- (void) testPruneQueriesWithDefaultQueryOnParent {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/a") params:SAMPLE_PARAMS]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/b") params:SAMPLE_PARAMS]];
+ [manager setQueryInactive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/a") params:SAMPLE_PARAMS]];
+ [manager setQueryInactive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/b") params:SAMPLE_PARAMS]];
+
+ FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1.0 maxQueries:NSUIntegerMax]];
+ [self checkPruneForest:forest pathsToKeep:@[@"foo"] pathsToPrune:@[]];
+ [manager verifyCache];
+}
+
+- (void) testPruneQueriesOverMaxSizeUsingPercent {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ for (NSUInteger i = 0; i < 10; i++) {
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]];
+ [clock tick];
+ }
+
+ FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:0.6 maxQueries:6]];
+ [self checkPruneForest:forest
+ pathsToKeep:@[@"6", @"7", @"8", @"9"]
+ pathsToPrune:@[@"0", @"1", @"2", @"3", @"4", @"5"]];
+}
+
+- (void)checkPruneForest:(FPruneForest *)pruneForest pathsToKeep:(NSArray *)toKeep pathsToPrune:(NSArray *)toPrune {
+ FPruneForest *checkForest = [FPruneForest empty];
+ for (NSString *path in toPrune) {
+ checkForest = [checkForest prunePath:PATH(path)];
+ }
+ for (NSString *path in toKeep) {
+ checkForest = [checkForest keepPath:PATH(path)];
+ }
+ XCTAssertEqualObjects([pruneForest pruneForest], [checkForest pruneForest]);
+}
+
+- (void)testKnownCompleteChildren {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithStorageEngine:engine];
+
+ XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"foo")], [NSSet set]);
+
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/a")]];
+ [manager setQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"foo/a")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/not-included")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/deep/not-included")]];
+
+ [manager setQueryActive:SAMPLE_QUERY];
+ FTrackedQuery *query = [manager findTrackedQuery:SAMPLE_QUERY];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"d", @"e"]] forQueryId:query.queryId];
+
+ XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"foo")], ([NSSet setWithArray:@[@"a", @"d", @"e"]]));
+ XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"")], [NSSet set]);
+ XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"foo/baz")], [NSSet set]);
+}
+
+- (void)testEnsureTrackedQueryForNewQuery {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager ensureCompleteTrackedQueryAtPath:PATH(@"foo")];
+ FTrackedQuery *query = [manager findTrackedQuery:DEFAULT_FOO_QUERY];
+ XCTAssertTrue(query.isComplete);
+ XCTAssertEqual(query.lastUse, clock.currentTime);
+}
+
+- (void)testEnsureTrackedQueryForAlreadyTrackedQuery {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager setQueryActive:DEFAULT_FOO_QUERY];
+
+ NSTimeInterval lastTick = clock.currentTime;
+ [clock tick];
+ [manager ensureCompleteTrackedQueryAtPath:PATH(@"foo")];
+ XCTAssertEqual([manager findTrackedQuery:DEFAULT_FOO_QUERY].lastUse, lastTick);
+}
+
+- (void)testHasActiveDefaultQuery {
+ FTrackedQueryManager *manager = [self newManager];
+
+ [manager setQueryActive:SAMPLE_QUERY];
+ [manager setQueryActive:DEFAULT_BAR_QUERY];
+ XCTAssertFalse([manager hasActiveDefaultQueryAtPath:PATH(@"foo")]);
+ XCTAssertFalse([manager hasActiveDefaultQueryAtPath:PATH(@"")]);
+ XCTAssertTrue([manager hasActiveDefaultQueryAtPath:PATH(@"bar")]);
+ XCTAssertTrue([manager hasActiveDefaultQueryAtPath:PATH(@"bar/baz")]);
+}
+
+- (void)testCacheSanity {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithStorageEngine:engine];
+
+ [manager setQueryActive:SAMPLE_QUERY];
+ [manager setQueryActive:DEFAULT_FOO_QUERY];
+ [manager verifyCache];
+
+ [manager setQueryComplete:SAMPLE_QUERY];
+ [manager verifyCache];
+
+ [manager setQueryInactive:DEFAULT_FOO_QUERY];
+ [manager verifyCache];
+
+ FTrackedQueryManager *manager2 = [self newManagerWithStorageEngine:engine];
+ XCTAssertNotNil([manager2 findTrackedQuery:SAMPLE_QUERY]);
+ XCTAssertNotNil([manager2 findTrackedQuery:DEFAULT_FOO_QUERY]);
+ [manager2 verifyCache];
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m b/Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m
new file mode 100644
index 0000000..6aee84d
--- /dev/null
+++ b/Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m
@@ -0,0 +1,574 @@
+/*
+ * 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/XCTest.h>
+
+#import "FTreeSortedDictionary.h"
+#import "FLLRBNode.h"
+#import "FLLRBEmptyNode.h"
+#import "FLLRBValueNode.h"
+
+@interface FLLRBValueNode (Tests)
+- (id<FLLRBNode>) rotateLeft;
+- (id<FLLRBNode>) rotateRight;
+@end
+
+@interface FTreeSortedDictionaryTests : XCTestCase
+
+@end
+
+@implementation FTreeSortedDictionaryTests
+
+- (NSComparator) defaultComparator {
+ return ^(id obj1, id obj2) {
+ if([obj1 respondsToSelector:@selector(compare:)] && [obj2 respondsToSelector:@selector(compare:)]) {
+ return [obj1 compare:obj2];
+ }
+ else {
+ if(obj1 < obj2) {
+ return (NSComparisonResult)NSOrderedAscending;
+ }
+ else if (obj1 > obj2) {
+ return (NSComparisonResult)NSOrderedDescending;
+ }
+ else {
+ return (NSComparisonResult)NSOrderedSame;
+ }
+ }
+ };
+}
+
+- (void)testCreateNode
+{
+ FTreeSortedDictionary* map = [[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertTrue([map.root.left isEmpty], @"Left child is properly empty");
+ XCTAssertTrue([map.root.right isEmpty], @"Right child is properly empty");
+}
+
+- (void)testGetNilReturnsNil {
+ FImmutableSortedDictionary *map1 = [[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertNil([map1 get:nil]);
+
+ FImmutableSortedDictionary *map2 = [[[FTreeSortedDictionary alloc] initWithComparator:^NSComparisonResult(id obj1, id obj2) {
+ return [obj1 compare:obj2];
+ }]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertNil([map2 get:nil]);
+}
+
+- (void)testSearchForSpecificKey {
+ FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2];
+
+ XCTAssertEqualObjects([map get:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found second object");
+ XCTAssertNil([map get:@3], @"Properly not found object");
+}
+
+- (void)testInsertNewKeyValuePair {
+ FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2];
+
+ XCTAssertEqualObjects(map.root.key, @2, @"Check the root key");
+ XCTAssertEqualObjects(map.root.left.key, @1, @"Check the root.left key");
+}
+
+- (void)testRemoveKeyValuePair {
+ FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2];
+
+ FImmutableSortedDictionary* newMap = [map removeKey:@1];
+ XCTAssertEqualObjects([newMap get:@2], @2, @"Found second object");
+ XCTAssertNil([newMap get:@1], @"Properly not found object");
+
+ // Make sure the original one is not mutated
+ XCTAssertEqualObjects([map get:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found second object");
+}
+
+- (void)testMoreRemovals {
+ FTreeSortedDictionary* map = [[[[[[[[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9]
+ insertKey:@20 withValue:@20]
+ insertKey:@18 withValue:@18]
+ insertKey:@2 withValue:@2]
+ insertKey:@71 withValue:@71]
+ insertKey:@42 withValue:@42]
+ insertKey:@88 withValue:@88];
+ XCTAssertNotNil([map get:@7], @"Found object");
+ XCTAssertNotNil([map get:@3], @"Found object");
+ XCTAssertNotNil([map get:@1], @"Found object");
+
+
+ FImmutableSortedDictionary* m1 = [map removeKey:@7];
+ FImmutableSortedDictionary* m2 = [map removeKey:@3];
+ FImmutableSortedDictionary* m3 = [map removeKey:@1];
+
+ XCTAssertNil([m1 get:@7], @"Removed object");
+ XCTAssertNotNil([m1 get:@3], @"Found object");
+ XCTAssertNotNil([m1 get:@1], @"Found object");
+
+ XCTAssertNil([m2 get:@3], @"Removed object");
+ XCTAssertNotNil([m2 get:@7], @"Found object");
+ XCTAssertNotNil([m2 get:@1], @"Found object");
+
+
+ XCTAssertNil([m3 get:@1], @"Removed object");
+ XCTAssertNotNil([m3 get:@7], @"Found object");
+ XCTAssertNotNil([m3 get:@3], @"Found object");
+}
+
+- (void) testRemovalBug {
+ FTreeSortedDictionary* map = [[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2]
+ insertKey:@3 withValue:@3];
+
+ XCTAssertEqualObjects([map get:@1], @1, @"Found object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found object");
+ XCTAssertEqualObjects([map get:@3], @3, @"Found object");
+
+ FImmutableSortedDictionary* m1 = [map removeKey:@2];
+ XCTAssertEqualObjects([m1 get:@1], @1, @"Found object");
+ XCTAssertEqualObjects([m1 get:@3], @3, @"Found object");
+ XCTAssertNil([m1 get:@2], @"Removed object");
+}
+
+- (void) testIncreasing {
+ int total = 100;
+
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ for(int i = 0; i < total; i++) {
+ NSNumber* item = [NSNumber numberWithInt:i];
+ map = [map insertKey:item withValue:item];
+ }
+
+ XCTAssertTrue([map count] == 100, @"Check if all 100 objects are in the map");
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+
+ for(int i = 0; i < total; i++) {
+ NSNumber* item = [NSNumber numberWithInt:i];
+ map = [map removeKey:item];
+ }
+
+ XCTAssertTrue([map count] == 0, @"Check if all 100 objects were removed");
+ // We can't check the depth here because the map no longer contains values, so we check that it doesn't responsd to this check
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBEmptyNode class]], @"Root is an empty node");
+ XCTAssertFalse([map respondsToSelector:@selector(checkMaxDepth)], @"The empty node doesn't respond to this selector.");
+}
+
+- (void) testStructureShouldBeValidAfterInsertionA {
+ FTreeSortedDictionary* map = [[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2]
+ insertKey:@3 withValue:@3];
+
+
+ XCTAssertEqualObjects(map.root.key, @2, @"Check root key");
+ XCTAssertEqualObjects(map.root.left.key, @1, @"Check the left key is correct");
+ XCTAssertEqualObjects(map.root.right.key, @3, @"Check the right key is correct");
+}
+
+- (void) testStructureShouldBeValidAfterInsertionB {
+ FTreeSortedDictionary* map = [[[[[[[[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9]
+ insertKey:@20 withValue:@20]
+ insertKey:@18 withValue:@18]
+ insertKey:@2 withValue:@2]
+ insertKey:@71 withValue:@71]
+ insertKey:@42 withValue:@42]
+ insertKey:@88 withValue:@88];
+
+ XCTAssertTrue([map count] == 12, @"Check if all 12 objects are in the map");
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+}
+
+- (void) testRotateLeftLeavesTreeInAValidState {
+ FLLRBValueNode* node = [[FLLRBValueNode alloc] initWithKey:@4 withValue:@4 withColor:BLACK withLeft:
+ [[FLLRBValueNode alloc] initWithKey:@2 withValue:@2 withColor:BLACK withLeft:nil withRight:nil] withRight:[[FLLRBValueNode alloc]initWithKey:@7 withValue:@7 withColor:RED withLeft:[[FLLRBValueNode alloc ]initWithKey:@5 withValue:@5 withColor:BLACK withLeft:nil withRight:nil] withRight:[[FLLRBValueNode alloc] initWithKey:@8 withValue:@8 withColor:BLACK withLeft:nil withRight:nil]]];
+
+ FLLRBValueNode* node2 = [node performSelector:@selector(rotateLeft)];
+
+ XCTAssertTrue([node2 count] == 5, @"Make sure the count is correct");
+ XCTAssertTrue([node2 checkMaxDepth], @"Check proper structure");
+}
+
+- (void) testRotateRightLeavesTreeInAValidState {
+ FLLRBValueNode* node = [[FLLRBValueNode alloc] initWithKey:@7 withValue:@7 withColor:BLACK withLeft:[[FLLRBValueNode alloc] initWithKey:@4 withValue:@4 withColor:RED withLeft:[[FLLRBValueNode alloc] initWithKey:@2 withValue:@2 withColor:BLACK withLeft:nil withRight:nil] withRight:[[FLLRBValueNode alloc] initWithKey:@5 withValue:@5 withColor:BLACK withLeft:nil withRight:nil]] withRight:[[FLLRBValueNode alloc] initWithKey:@8 withValue:@8 withColor:BLACK withLeft:nil withRight:nil]];
+
+ FLLRBValueNode* node2 = [node performSelector:@selector(rotateRight)];
+
+ XCTAssertTrue([node2 count] == 5, @"Make sure the count is correct");
+ XCTAssertEqualObjects(node2.key, @4, @"Check roots key");
+ XCTAssertEqualObjects(node2.left.key, @2, @"Check first left child key");
+ XCTAssertEqualObjects(node2.right.key, @7, @"Check first right child key");
+ XCTAssertEqualObjects(node2.right.left.key, @5, @"Check second right left key");
+ XCTAssertEqualObjects(node2.right.right.key, @8, @"Check second right left key");
+}
+
+- (void) testStructureShouldBeValidAfterInsertionC {
+ FTreeSortedDictionary* map = [[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9];
+
+ XCTAssertTrue([map count] == 6, @"Check if all 6 objects are in the map");
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+
+ FTreeSortedDictionary* m2 = [[[map insertKey:@20 withValue:@20]
+ insertKey:@18 withValue:@18]
+ insertKey:@2 withValue:@2];
+ XCTAssertTrue([m2 count] == 9, @"Check if all 9 objects are in the map");
+ XCTAssertTrue([m2.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)m2.root checkMaxDepth], @"Checking valid depth and tree structure");
+
+ FTreeSortedDictionary* m3 = [[[[m2 insertKey:@71 withValue:@71]
+ insertKey:@42 withValue:@42]
+ insertKey:@88 withValue:@88]
+ insertKey:@20 withValue:@20]; // Add a dupe to see if the size is correct
+ XCTAssertTrue([m3 count] == 12, @"Check if all 12 (minus dupe @20) objects are in the map");
+ XCTAssertTrue([m3.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)m3.root checkMaxDepth], @"Checking valid depth and tree structure");
+}
+
+- (void) testOverride {
+ FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@10 withValue:@10]
+ insertKey:@10 withValue:@8];
+
+ XCTAssertEqualObjects([map get:@10], @8, @"Found first object");
+}
+- (void) testEmpty {
+ FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@10 withValue:@10]
+ removeKey:@10];
+
+ XCTAssertTrue([map isEmpty], @"Properly empty");
+
+}
+
+- (void) testEmptyGet {
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertNil([map get:@"something"], @"Properly nil");
+}
+
+- (void) testEmptyCount {
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertTrue([map count] == 0, @"Properly zero count");
+}
+
+- (void) testEmptyRemoval {
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertTrue([[map removeKey:@"sometjhing"] count] == 0, @"Properly zero count");
+}
+
+- (void) testReverseTraversal {
+ FTreeSortedDictionary* map = [[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@5 withValue:@5]
+ insertKey:@3 withValue:@3]
+ insertKey:@2 withValue:@2]
+ insertKey:@4 withValue:@4];
+
+ __block int next = 5;
+ [map enumerateKeysAndObjectsReverse:YES usingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Properly equal");
+ next = next - 1;
+ }];
+}
+
+
+- (void) testInsertionAndRemovalOfAHundredItems {
+ int N = 100;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+ NSMutableArray* toRemove = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ [toRemove addObject:[NSNumber numberWithInt:i]];
+ }
+
+
+ [self shuffleArray:toInsert];
+ [self shuffleArray:toRemove];
+
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block int next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Correct key");
+ XCTAssertEqualObjects(value, [NSNumber numberWithInt:next], @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual(next, N, @"Check we traversed all of the items");
+
+ // remove them
+
+ for(int i = 0; i < N; i++) {
+ if([map.root isMemberOfClass:[FLLRBValueNode class]]) {
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+ }
+ map = [map removeKey:[toRemove objectAtIndex:i]];
+ }
+
+
+ XCTAssertEqual([map count], 0, @"Check we removed all of the items");
+}
+
+- (void) shuffleArray:(NSMutableArray *)array {
+ NSUInteger count = [array count];
+ for(NSUInteger i = 0; i < count; i++) {
+ NSInteger nElements = count - i;
+ NSInteger n = (arc4random() % nElements) + i;
+ [array exchangeObjectAtIndex:i withObjectAtIndex:n];
+ }
+}
+
+- (void) testBalanceProblem {
+
+ NSArray* toInsert = [[NSArray alloc] initWithObjects:@1,@7,@8,@5,@2,@6,@4,@0,@3, nil];
+
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < [toInsert count]; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+ }
+ XCTAssertTrue([map count] == [toInsert count], @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block int next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Correct key");
+ XCTAssertEqualObjects(value, [NSNumber numberWithInt:next], @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual(next, [[NSNumber numberWithUnsignedInteger:[toInsert count]] intValue], @"Check we traversed all of the items");
+
+ // removing one triggers the balance problem
+
+ map = [map removeKey:@5];
+
+ if([map.root isMemberOfClass:[FLLRBValueNode class]]) {
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+ }
+}
+
+- (void) testPredecessorKey {
+ FTreeSortedDictionary* map = [[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9];
+
+ XCTAssertNil([map getPredecessorKey:@1], @"First object doesn't have a predecessor");
+ XCTAssertEqualObjects([map getPredecessorKey:@3], @1, @"@1");
+ XCTAssertEqualObjects([map getPredecessorKey:@4], @3, @"@3");
+ XCTAssertEqualObjects([map getPredecessorKey:@7], @4, @"@4");
+ XCTAssertEqualObjects([map getPredecessorKey:@9], @7, @"@7");
+ XCTAssertEqualObjects([map getPredecessorKey:@50], @9, @"@9");
+ XCTAssertThrows([map getPredecessorKey:@777], @"Expect exception about nonexistant key");
+}
+
+- (void) testEnumerator {
+ int N = 100;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+ NSMutableArray* toRemove = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ [toRemove addObject:[NSNumber numberWithInt:i]];
+ }
+
+
+ [self shuffleArray:toInsert];
+ [self shuffleArray:toRemove];
+
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+
+ NSEnumerator* enumerator = [map keyEnumerator];
+ id next = [enumerator nextObject];
+ int correctValue = 0;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 1;
+ }
+}
+
+- (void) testReverseEnumerator {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FTreeSortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ NSEnumerator* enumerator = [map reverseKeyEnumerator];
+ id next = [enumerator nextObject];
+ int correctValue = N - 1;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue--;
+ }
+}
+
+- (void) testEnumeratorFrom {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i*2]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FTreeSortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ // Test from inbetween keys
+ {
+ NSEnumerator* enumerator = [map keyEnumeratorFrom:@11];
+ id next = [enumerator nextObject];
+ int correctValue = 12;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 2;
+ }
+ }
+
+ // Test from key in map
+ {
+ NSEnumerator* enumerator = [map keyEnumeratorFrom:@10];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 2;
+ }
+ }
+}
+
+- (void) testReverseEnumeratorFrom {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i*2]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FTreeSortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ // Test from inbetween keys
+ {
+ NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@11];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue - 2;
+ }
+ }
+
+ // Test from key in map
+ {
+ NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@10];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue - 2;
+ }
+ }
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FUtilitiesTest.m b/Example/Database/Tests/Unit/FUtilitiesTest.m
new file mode 100644
index 0000000..a012250
--- /dev/null
+++ b/Example/Database/Tests/Unit/FUtilitiesTest.m
@@ -0,0 +1,116 @@
+/*
+ * 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/UIKit.h>
+#import <XCTest/XCTest.h>
+#import "FUtilities.h"
+#import "FIRDatabase_Private.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FClock.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FWebSocketConnection.h"
+#import "FConstants.h"
+
+@interface FWebSocketConnection (Tests)
+- (NSString*)userAgent;
+@end
+
+@interface FUtilitiesTest : XCTestCase
+
+@end
+
+@implementation FUtilitiesTest
+
+- (void)testUrlWithSchema {
+ FParsedUrl *parsedUrl = [FUtilities parseUrl:@"https://repo.firebaseio.com"];
+ XCTAssertEqualObjects(parsedUrl.repoInfo.host, @"repo.firebaseio.com");
+ XCTAssertEqualObjects(parsedUrl.repoInfo.namespace, @"repo");
+ XCTAssertTrue(parsedUrl.repoInfo.secure);
+ XCTAssertEqualObjects(parsedUrl.path, [FPath empty]);
+}
+
+- (void)testUrlParsedWithoutSchema {
+ FParsedUrl *parsedUrl = [FUtilities parseUrl:@"repo.firebaseio.com"];
+ XCTAssertEqualObjects(parsedUrl.repoInfo.host, @"repo.firebaseio.com");
+ XCTAssertEqualObjects(parsedUrl.repoInfo.namespace, @"repo");
+ XCTAssertTrue(parsedUrl.repoInfo.secure);
+ XCTAssertEqualObjects(parsedUrl.path, [FPath empty]);
+}
+
+- (void)testDefaultCacheSizeIs10MB {
+ XCTAssertEqual([FIRDatabaseReference defaultConfig].persistenceCacheSizeBytes, (NSUInteger)10*1024*1024);
+ XCTAssertEqual([FIRDatabaseConfig configForName:@"test-config"].persistenceCacheSizeBytes, (NSUInteger)10*1024*1024);
+}
+
+- (void)testSettingCacheSizeToHighOrToLowThrows {
+ FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:@"config-tests-config"];
+ config.persistenceCacheSizeBytes = 5*1024*1024; // Works fine
+ XCTAssertThrows(config.persistenceCacheSizeBytes = (1024*1024-1));
+ XCTAssertThrows(config.persistenceCacheSizeBytes = 100*1024*1024+1);
+}
+
+- (void)testSystemClockMatchesCurrentTime {
+ NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
+ // Accuracy within 10ms
+ XCTAssertEqualWithAccuracy(currentTime, [[FSystemClock clock] currentTime], 0.010);
+}
+
+// This test is here for a lack of a better place to put it
+- (void)testUserAgentString {
+ FWebSocketConnection *conn = [[FWebSocketConnection alloc] init];
+
+ NSString *agent = [conn performSelector:@selector(userAgent) withObject:nil];
+
+ NSArray *parts = [agent componentsSeparatedByString:@"/"];
+ XCTAssertEqual(parts.count, (NSUInteger)5);
+ XCTAssertEqualObjects(parts[0], @"Firebase");
+ XCTAssertEqualObjects(parts[1], kWebsocketProtocolVersion); // Wire protocol version
+ XCTAssertEqualObjects(parts[2], [FIRDatabase buildVersion]); // Build version
+ XCTAssertEqualObjects(parts[3], [[UIDevice currentDevice] systemVersion]); // iOS Version
+#if TARGET_OS_IPHONE
+ NSString *deviceName = [UIDevice currentDevice].model;
+ XCTAssertEqualObjects([parts[4] componentsSeparatedByString:@"_"][0], deviceName);
+#endif
+
+}
+
+- (void)testKeyComparison {
+ NSArray *order = @[
+ @"-2147483648", @"0", @"1", @"2", @"10", @"2147483647", // Treated as integers
+ @"-2147483649", @"-2147483650", @"-a", @"2147483648", @"21474836480", @"2147483649", @"a" // treated as strings
+ ];
+ for (NSInteger i = 0; i < order.count; i++) {
+ for (NSInteger j = i + 1; j < order.count; j++) {
+ NSString *first = order[i];
+ NSString *second = order[j];
+ XCTAssertEqual([FUtilities compareKey:first toKey:second], NSOrderedAscending,
+ @"Expected %@ < %@", first, second);
+ XCTAssertEqual([FUtilities compareKey:first toKey:first], NSOrderedSame,
+ @"Expected %@ == %@", first, first);
+ XCTAssertEqual([FUtilities compareKey:second toKey:first], NSOrderedDescending,
+ @"Expected %@ > %@", second, first);
+ }
+ }
+}
+
+// Enforce a > b, b < a, a != b, because this is apparently something that happens semi-regularly
+- (void)testUnicodeKeyComparison {
+ XCTAssertEqual([FUtilities compareKey:@"유주연" toKey:@"윤규완오빠"], NSOrderedAscending);
+ XCTAssertEqual([FUtilities compareKey:@"윤규완오빠" toKey:@"유주연"], NSOrderedDescending);
+ XCTAssertNotEqual([FUtilities compareKey:@"윤규완오빠" toKey:@"유주연"], NSOrderedSame);
+}
+
+@end
diff --git a/Example/Database/Tests/en.lproj/InfoPlist.strings b/Example/Database/Tests/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/Example/Database/Tests/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+
diff --git a/Example/Database/Tests/syncPointSpec.json b/Example/Database/Tests/syncPointSpec.json
new file mode 100644
index 0000000..f39d29d
--- /dev/null
+++ b/Example/Database/Tests/syncPointSpec.json
@@ -0,0 +1,8203 @@
+[
+ {
+ "name": "Default listen handles a parent set",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Now do a set at the parent. Expect only the 'a' child to get events",
+ "type": "set",
+ "path": "",
+ "data": {
+ "a": 1,
+ "b": 2
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": 1
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Default listen handles a set at the same level",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Do a set at the same level. Expect the full value to raise events",
+ "type": "set",
+ "path": "a",
+ "data": {
+ "foo": "bar",
+ "yes": true
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "yes",
+ "prevName": "foo",
+ "data": true
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "foo": "bar",
+ "yes": true
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "A query can get a complete cache then a merge",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 3,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "a",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "d": 4
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "b",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "d": 4
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "tag": 1,
+ "path": "a",
+ "data": {
+ "a": 5,
+ "c": 3
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "d",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "a",
+ "prevName": null,
+ "data": 5
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "a": 5,
+ "b": 2,
+ "c": 3
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server merge on listener with complete children",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 1,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "a/b",
+ "data": {"c": 3, "d": 4},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": 3, "d": 4}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": 3, "d": 4}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Empty set doesn't prevent server updates",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo" : "bar"
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "empty-path",
+ "data": null,
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": { "foo": "new-bar" },
+ "events": [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "foo",
+ "prevName": null,
+ "data": "new-bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo" : "new-bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Deep merge on listener with complete children",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "a/x/y/z",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/x/y/z",
+ "data": null,
+ "events": [
+ {
+ "path": "a/x/y/z",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "a/x/y/z",
+ "data": {"c": 3, "d": 4},
+ ".comment": "No events for the top-level listener, since it's not a complete child",
+ "events": [
+ {
+ "path": "a/x/y/z",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a/x/y/z",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a/x/y/z",
+ "type": "value",
+ "data": {"c": 3, "d": 4}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update child listener twice",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 1,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b/c",
+ "data": "foo",
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": "foo"
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": "foo"}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": "foo"}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update child of default listen that already has a complete cache",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Fill the listen's cache so we can test a child set with an existing cache",
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {
+ "b": 2,
+ "c": 3
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": 2,
+ "c": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Now do a set at a child, expect the child event and a value event",
+ "type": "set",
+ "path": "a/b",
+ "data": 4,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": 4,
+ "c": 3
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update child of default listen that has no cache",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Now do a set at a child, expect the child event only",
+ "type": "set",
+ "path": "a/b",
+ "data": 4,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 4
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update (via set) the child of a co-located default listener and query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "startAt": {"index": null, "name": "b"},
+ "endAt": {"index": null, "name": "g"}
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Fill the cache. Since the default listener is there, no tag needed",
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {
+ "a": 1,
+ "c": 3,
+ "d": 4
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "a",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "c": 3, "d": 4}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"c": 3, "d": 4}
+ }
+ ]
+ },
+ {
+ ".comment": "Cache is primed. Now do the child set",
+ "type": "set",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "b":2, "c": 3, "d": 4}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b":2, "c": 3, "d": 4}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update (via set) the child of a query with a full cache",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "startAt": {"index": null, "name": "b"},
+ "endAt": {"index": null, "name": "g"}
+ },
+ "events": []
+ },
+ {
+ ".comment": "Fill the cache first",
+ "type": "serverUpdate",
+ "path": "a",
+ "tag": 1,
+ "data": {
+ "c": 3,
+ "d": 4
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"c": 3, "d": 4}
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2, "c": 3, "d": 4}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update (via set) a child below an empty query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "startAt": {"name": "b", "index": null},
+ "endAt": {"name": "g", "index": null}
+ },
+ "events": []
+ },
+ {
+ ".comment": "Set a single child, outside the window",
+ "type": "set",
+ "path": "a/h",
+ "data": 8,
+ "events": []
+ },
+ {
+ ".comment": "Now set a single child inside the window",
+ "type": "set",
+ "path": "a/e",
+ "data": 5,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "e",
+ "prevName": null,
+ "data": 5
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update descendant of default listener with full cache",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Fill the cache",
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {
+ "b": {
+ "d": 4
+ },
+ "e": 5
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "d": 4
+ }
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "e",
+ "prevName": "b",
+ "data": 5
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": {
+ "d": 4
+ },
+ "e": 5
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Now do a set at a/b/c, expect child event + new value event",
+ "type": "set",
+ "path": "a/b/c",
+ "data": 3,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "c": 3,
+ "d": 4
+ }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": {
+ "c": 3,
+ "d": 4
+ },
+ "e": 5
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Descendant set below an empty default listener is ignored",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Now do a set at a/b/c, expect no events",
+ "type": "set",
+ "path": "a/b/c",
+ "data": 3,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Update of a child. This can happen if a child listener is added and removed",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert set with only child caches",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 3,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 3
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Can revert a duplicate child set",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 3,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 3
+ }
+ ]
+ },
+ {
+ ".comment": "This set duplicates the data in the previous one, so no events expected",
+ "type": "set",
+ "path": "a/b",
+ "data": 3,
+ "events": []
+ },
+ {
+ ".comment": "Clearing the second set should have no effect, as the underlying set still exists",
+ "type": "ackUserWrite",
+ "writeId": 1,
+ "revert": true,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Can revert a child set and see the underlying data",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 3,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 3
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 4,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 4
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 3,
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 4
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": []
+ },
+ {
+ ".comment": "Clearing the second set should make the underlying set visible again, as it is now confirmed",
+ "type": "ackUserWrite",
+ "writeId": 1,
+ "revert": true,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 3
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert child set with no server data",
+ "steps": [
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": {"d": 4, "e": 5},
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {"d": 4, "e": 5}
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/c",
+ "data": {"z": 26},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": {"z": 26}
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "b",
+ "data": {"d": 4, "e": 5}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert deep set with no server data",
+ "steps": [
+ {
+ "type": "set",
+ "path": "a/b/c",
+ "data": {"d": 4, "e": 5},
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "a/x/y",
+ "data": {"z": 26},
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Revert set covered by non-visible transaction",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ ".comment": "Initial server value is X.",
+ "type": "serverUpdate",
+ "path": "",
+ "data": "X",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "X"
+ }
+ ]
+ },
+ {
+ ".comment": "Set to Y.",
+ "type": "set",
+ "path": "",
+ "data": "Y",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "Y"
+ }
+ ]
+ },
+ {
+ ".comment": "Overwrite with a non-visible 'transaction'.",
+ "type": "set",
+ "path": "",
+ "data": "Z",
+ "visible": false,
+ "events": []
+ },
+ {
+ ".comment": "Revert set to Y (e.g. security failed), so we should see it go back to Y.",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "X"
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Clear parent shadowing server values set with server children",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a",
+ "data": {"b": 28, "c": 3},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 28
+ }
+ ]
+ },
+ {
+ ".comment": "This listen should get a complete event snap, as well as complete server children",
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 28
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 28, "c": 3}
+ }
+ ]
+ },
+ {
+ ".comment": "Do a serverUpdate with a conflicting value for b, simulates a server value. It's still shadowed though",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 29,
+ "events": []
+ },
+ {
+ ".comment": "Clearing the set should result in updated values for b",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 29
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 29
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 29, "c": 3}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Clear child shadowing server values set with server children",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 28,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 28
+ }
+ ]
+ },
+ {
+ ".comment": "This listen should get an event child snap, as well as a complete server child: b",
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 28
+ }
+ ]
+ },
+ {
+ ".comment": "Do a serverUpdate with a conflicting value for b, simulates a server value. It's still shadowed though",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 29,
+ "events": []
+ },
+ {
+ ".comment": "Clearing the set should result in no events. We don't yet have the server data at the parent",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 29
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 29
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Unrelated merge doesn't shadow server updates",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a",
+ "data": {"b": 2, "c": 3},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2, "c": 3}
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/d",
+ "data": 4,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2, "c": 3, "d": 4}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Can set alongside a remote merge",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2}
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "a",
+ "data": {"b": 28, "c": 3},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2, "c": 3}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "setPriority on a location with no cache",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "a/.priority",
+ "data": "foo",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": "bar",
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": { ".priority": "foo", ".value": "bar" }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "deep update deletes child from limit window and pulls in new child",
+ "steps": [
+ {
+ "type": "set",
+ "path": "a",
+ "data": {
+ "a": {"aa": 2, "aaa": 3},
+ "b": {"bb": 2, "bbb": 3},
+ "c": {"cc": 2, "ccc": 3}
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "limitToLast": 2
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {"bb": 2, "bbb": 3}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": {"cc": 2, "ccc": 3}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": {"bb": 2, "bbb": 3},
+ "c": {"cc": 2, "ccc": 3}
+ }
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a/b",
+ "data": {
+ "bb": null,
+ "bbb": null
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "b",
+ "data": {"bb": 2, "bbb": 3}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {"aa": 2, "aaa": 3}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "a": {"aa": 2, "aaa": 3},
+ "c": {"cc": 2, "ccc": 3}
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "deep set deletes child from limit window and pulls in new child",
+ "steps": [
+ {
+ "type": "set",
+ "path": "a",
+ "data": {
+ "a": {"aa": 2},
+ "b": {"bb": 2},
+ "c": {"cc": 2}
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "limitToLast": 2
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {"bb": 2}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": {"cc": 2}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": {"bb": 2},
+ "c": {"cc": 2}
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b/bb",
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "b",
+ "data": {"bb": 2}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {"aa": 2}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "a": {"aa": 2},
+ "c": {"cc": 2}
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Edge case in newChildForChange_",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/d",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a/b/c",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/d",
+ "data": 4,
+ "events": [
+ {
+ "type": "value",
+ "path": "a/d",
+ "data": 4
+ },
+ {
+ "type": "child_added",
+ "path": "a",
+ "name": "d",
+ "prevName": null,
+ "data": 4
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b/c",
+ "data": 3,
+ "events": [
+ {
+ "path": "a/b/c",
+ "type": "value",
+ "data": 3
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert set in query window",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "limitToLast": 1,
+ "tag": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "tag": 1,
+ "data": {"b": 2},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2}
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/c",
+ "data": 3,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "b",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"c": 3}
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "revert": true,
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "c",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Handles a server value moving a child out of a query window",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": {"value": 3}, "d": {"value": 4}}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}}
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1,
+ "orderBy": "value"
+ },
+ "path": "a/b",
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": {"value": 4}
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"d": {"value": 4}}
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b/d/value",
+ "data": 5,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_moved",
+ "name": "d",
+ "prevName": null,
+ "data": {"value": 5}
+ },
+ {
+ "path": "a/b",
+ "type": "child_changed",
+ "name": "d",
+ "prevName": null,
+ "data": {"value": 5}
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"d": {"value": 5}}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": {"value": 3}, "d": {"value": 5}}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 5}}}
+ }
+ ]
+ },
+ {
+ ".comment": "The query is shadowed, so only one data update arrives. We're simulating a server value, so it's different than what was set",
+ "type": "serverUpdate",
+ "path": "a/b/d/value",
+ "data": 2,
+ "events": []
+ },
+ {
+ ".comment": "Now that we're acking the write, we should see the effect of the change",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_removed",
+ "name": "d",
+ "data": {"value": 5}
+ },
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": {"value": 3}
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": {"value": 3}}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": {"value": 3}, "d": {"value": 2}}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 2}}}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update of indexed child works",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": {"value": 3}, "d": {"value": 4}}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}}
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1,
+ "orderBy": "value"
+ },
+ "path": "a/b",
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": {"value": 4}
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"d": {"value": 4}}
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a/b/c",
+ "data": {"value": 5},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_removed",
+ "name": "d",
+ "data": {"value": 4}
+ },
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": {"value": 5}
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": {"value": 5}}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": {"value": 5}, "d": {"value": 4}}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": {"c": {"value": 5}, "d": {"value": 4}}}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Merge applied to empty limit",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "limitToLast": 1,
+ "tag": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "tag": 1,
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a",
+ "data": {"b": 1},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 1}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Limit is refilled from server data after merge",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a/b",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {"a": 1, "b": {"c": 3, "d": 4}},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": 4
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"d": 4}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": {"c": 3, "d": 4}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "b": {"c": 3, "d": 4}}
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a/b",
+ "data": {"d": null},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_removed",
+ "name": "d",
+ "data": 4
+ },
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": 3}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": "a",
+ "data": {"c": 3}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "b": {"c": 3}}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Handle repeated listen with merge as first update",
+ "steps": [
+ {
+ ".comment": "Assume that we just unlistened on this path, and before the unlisten arrives, a merge was sent by the server",
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "This happens when a merge arriving from the server while the 2nd listen is in flight",
+ "type": "serverMerge",
+ "path": "a",
+ "data": {"c": 3},
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Limit is refilled from server data after set",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a/b",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {"a": 1, "b": {"c": 3, "d": 4}},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": 4
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"d": 4}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": {"c": 3, "d": 4}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "b": {"c": 3, "d": 4}}
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b/d",
+ "data": null,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_removed",
+ "name": "d",
+ "data": 4
+ },
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": 3}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": "a",
+ "data": {"c": 3}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "b": {"c": 3}}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "query on weird path.",
+ ".comment": "We used to use '|' as a separator, which broke with paths containing |",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo|!@%^&*()_<>?+={}blah",
+ "params": {
+ "tag": 1,
+ "limitToLast": 5
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo|!@%^&*()_<>?+={}blah",
+ "tag": 1,
+ "data": { "a": "a" },
+ "events": [
+ {
+ "path": "foo|!@%^&*()_<>?+={}blah",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": "a"
+ },
+ {
+ "path": "foo|!@%^&*()_<>?+={}blah",
+ "type": "value",
+ "data": { "a": "a" }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "runs, round2",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "foo",
+ "data": "baz",
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": "baz"
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "foo/new",
+ "data": "bar",
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "new",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": { "new" : "bar"}
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": "baz",
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": { "new" : "bar"},
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 1,
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": { "new" : true, "other" : "bar"},
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "other",
+ "prevName": "new",
+ "data": "bar"
+ },
+ {
+ "path": "foo",
+ "type": "child_changed",
+ "name": "new",
+ "prevName": null,
+ "data": true
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": { "new": true, "other": "bar"}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "handles nested listens",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo/bar",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "foo": {
+ "a": 1,
+ "b": 2,
+ "bar": {
+ "c": true,
+ "d": false
+ }
+ },
+ "baz": false
+ },
+ "events": [
+ {
+ "path": "foo/bar",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": true
+ },
+ {
+ "path": "foo/bar",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": false
+ },
+ {
+ "path": "foo/bar",
+ "type": "value",
+ "data": {"c": true, "d": false}
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "bar",
+ "prevName": "b",
+ "data": {"c": true, "d": false}
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "bar": {
+ "c": true,
+ "d": false
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "foo": {
+ "a": 1,
+ "b": 2,
+ "bar": {
+ "c": false,
+ "d": false,
+ "e": true
+ },
+ "f": 3
+ },
+ "baz": false
+ },
+ "events": [
+ {
+ "path": "foo/bar",
+ "type": "child_added",
+ "name": "e",
+ "prevName": "d",
+ "data": true
+ },
+ {
+ "path": "foo/bar",
+ "type": "child_changed",
+ "name": "c",
+ "prevName": null,
+ "data": false
+ },
+ {
+ "path": "foo/bar",
+ "type": "value",
+ "data": {
+ "c": false,
+ "d": false,
+ "e": true
+ }
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "f",
+ "prevName": "bar",
+ "data": 3
+ },
+ {
+ "path": "foo",
+ "type": "child_changed",
+ "name": "bar",
+ "prevName": "b",
+ "data": {
+ "c": false,
+ "d": false,
+ "e": true
+ }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "bar": {
+ "c": false,
+ "d": false,
+ "e": true
+ },
+ "f": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Duplicate set, no events raised",
+ "type": "set",
+ "path": "",
+ "data": {
+ "foo": {
+ "a": 1,
+ "b": 2,
+ "bar": {
+ "c": false,
+ "d": false,
+ "e": true
+ },
+ "f": 3
+ },
+ "baz": false
+ },
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Handles a set below a listen",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "foo",
+ "data": 1,
+ ".comment": "We only expect a child_added, since it does not completely fill the view",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "does non-default queries",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "foo": {
+ "a": 1,
+ "b": 2
+ }
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "b": 2
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Now have the server send the same data to the query. No events result because there is no change",
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "foo",
+ "data": {
+ "b": 2
+ },
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "handles a co-located default listener and query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "foo",
+ "data": { "a": 1, "b": 2},
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": { "a": 1, "b": 2}
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {"b": 2}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Default and non-default listener at same location with server update",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": {"a": 1, "b": 2},
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "data": 1,
+ "prevName": null
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "data": 2,
+ "prevName": "a"
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {"a": 1, "b": 2}
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "data": 2,
+ "prevName": null
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {"b": 2}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Add a parent listener to a complete child listener, expect child event",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": 1,
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Add listens to a set, expect correct events, including a child event",
+ "steps": [
+ {
+ "type": "set",
+ "path": "foo",
+ "data": {"bar": 1, "baz": 2},
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo/bar",
+ "events": [
+ {
+ "path": "foo/bar",
+ "type": "value",
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": {"bar": 1, "baz": 2}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "ServerUpdate to a child listener raises child events at parent",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": 1,
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "ServerUpdate to a child listener raises child events at parent query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": 1,
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Multiple complete children are handled properly",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo/a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo/b",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo/a",
+ "data": 1,
+ "events": [
+ {
+ "path": "foo/a",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "foo/b",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Write leaf node, overwrite at parent node",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/aa",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "a/aa",
+ "data": 1,
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "aa",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a",
+ "data": {
+ "aa": 2
+ },
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "aa",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "aa": 2
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Confirm complete children from the server",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/aa",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/aa",
+ "data": 1,
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "aa",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ ".comment": "At some point in the future, we might consider sending a hash here to avoid duplicate data",
+ "data": {"aa": 1},
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"aa": 1}
+ }
+ ]
+ },
+ {
+ ".comment": "Now, delete the same child and make sure we get the right events",
+ "type": "serverUpdate",
+ "path": "a/aa",
+ "data": null,
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": null
+ },
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "aa",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": null
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Write leaf, overwrite from parent",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/aa",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a/bb",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "First set is at leaf. Expect only a child_added for the parent, nothing for the sibling",
+ "type": "set",
+ "path": "a/aa",
+ "data": 1,
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "aa",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ ".comment": "Now set at the parent. Expect value events for everyone",
+ "type": "set",
+ "path": "a",
+ "data": {"aa": 2},
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "a/bb",
+ "type": "value",
+ "data": null
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "aa",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"aa": 2}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Basic update test",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "b",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ ".comment": "Initial data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "b",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Now update two children. Not b, there should be no events at b",
+ "type": "update",
+ "path": "",
+ "data": {
+ "a": true,
+ "c": false
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": true
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "a",
+ "prevName": null,
+ "data": true
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "c",
+ "prevName": "b",
+ "data": false
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": true,
+ "b": 2,
+ "c": false
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "No double value events for user ack",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1,
+ "endAt": {"index": null, "name": "d"}
+ },
+ "events": []
+ },
+ {
+ ".comment": "user sets data",
+ "type": "set",
+ "path": "foo",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": { "c": 3 }
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ }
+ ]
+ },
+ {
+ ".comment": "server acks data, but local overwrite causes no events to fire",
+ "type": "serverUpdate",
+ "path": "foo",
+ "tag": 1,
+ "data": null,
+ "events": []
+ },
+ {
+ ".comment": "server sends data with merge",
+ "type": "serverMerge",
+ "path": "foo",
+ "tag": 1,
+ "data": {
+ "c": 3
+ },
+ "events": [ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "foo/d",
+ "data": 4,
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": { "d": 4 }
+ },
+ {
+ "path": "foo",
+ "type": "child_removed",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": 4
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "foo",
+ "tag": 1,
+ "data": {
+ "d": 4
+ },
+ "events": [ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 1,
+ "revert": false,
+ "events": []
+ }
+ ]
+ },
+ {
+ "name": "Basic key index sanity check",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderByKey": true,
+ "startAt": { "index": "aa" },
+ "endAt": { "index": "e" }
+ },
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "a": { ".priority": 10, ".value": "a" },
+ "b": { ".priority": 5, ".value": "b" },
+ "c": { ".priority": 20, ".value": "c" },
+ "d": { ".priority": 7, ".value": "d" },
+ "e": { ".priority": 30, ".value": "e" },
+ "f": { ".priority": 8, ".value": "f" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {".priority": 5, ".value": "b"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": {".priority": 20, ".value": "c"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": {".priority": 7, ".value": "d"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "e",
+ "prevName": "d",
+ "data": {".priority": 30, ".value": "e"}
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { ".priority": 5, ".value": "b" },
+ "c": { ".priority": 20, ".value": "c" },
+ "d": { ".priority": 7, ".value": "d" },
+ "e": { ".priority": 30, ".value": "e" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Add a new item outside of range and make sure we get events.",
+ "type": "set",
+ "path": "a",
+ "data": "hello!",
+ "events": [ ]
+ },
+ {
+ ".comment": "Add a new item within range and ensure we get ld_added.",
+ "type": "set",
+ "path": "bass",
+ "data": 3.14,
+ "events":
+ [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "bass",
+ "prevName": "b",
+ "data": 3.14
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { ".priority": 5, ".value": "b" },
+ "bass": 3.14,
+ "c": { ".priority": 20, ".value": "c" },
+ "d": { ".priority": 7, ".value": "d" },
+ "e": { ".priority": 30, ".value": "e" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Modify an item and ensure we get child_changed.",
+ "type": "set",
+ "path": "b",
+ "data": 42,
+ "events":
+ [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 42
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": 42,
+ "bass": 3.14,
+ "c": { ".priority": 20, ".value": "c" },
+ "d": { ".priority": 7, ".value": "d" },
+ "e": { ".priority": 30, ".value": "e" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Collect correct subviews to listen on",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "callbackId": 1,
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "callbackId": 1,
+ "path": "/a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "callbackId": 1,
+ "path": "/a/b",
+ "events": []
+ },
+ {
+ ".comment": "should not cause /a/b to be listened upon",
+ "type": "unlisten",
+ "callbackId": 1,
+ "path": "",
+ "events": []
+ },
+ {
+ ".comment": "should now cause /a/b to be listened upon",
+ "type": "unlisten",
+ "callbackId": 1,
+ "path": "/a",
+ "events": []
+ }
+ ]
+ },
+ {
+ "name": "Limit to first one on ordered query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "vanished",
+ "limitToFirst": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "triceratops": {"vanished": -66000000},
+ "stegosaurus": {"vanished": -155000000},
+ "pterodactyl": {"vanished": -75000000}
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "stegosaurus",
+ "prevName": null,
+ "data": {"vanished": -155000000}
+ },
+ {
+ "path": "",
+ "type": "value",
+ "name": "",
+ "prevName": null,
+ "data": {"stegosaurus": {"vanished": -155000000}}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Limit to last one on ordered query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "vanished",
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "triceratops": {"vanished": -66000000},
+ "stegosaurus": {"vanished": -155000000},
+ "pterodactyl": {"vanished": -75000000}
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "triceratops",
+ "prevName": null,
+ "data": {"vanished": -66000000}
+ },
+ {
+ "path": "",
+ "type": "value",
+ "name": "",
+ "prevName": null,
+ "data": {"triceratops": {"vanished": -66000000}}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Update indexed value on existing child from limited query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "age",
+ "limitToLast": 4
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "4": { "age": 41, "highscore": 400, "name": "old mama"},
+ "5": { "age": 18, "highscore": 1200, "name": "young mama"},
+ "6": { "age": 20, "highscore": 1003, "name": "micheal blub"},
+ "7": { "age": 30, "highscore": 10000, "name": "no. 7"}
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "4",
+ "prevName": "7",
+ "data": { "age": 41, "highscore": 400, "name": "old mama"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "5",
+ "prevName": null,
+ "data": { "age": 18, "highscore": 1200, "name": "young mama"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "6",
+ "prevName": "5",
+ "data": { "age": 20, "highscore": 1003, "name": "micheal blub"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "7",
+ "prevName": "6",
+ "data": { "age": 30, "highscore": 10000, "name": "no. 7"}
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "4": { "age": 41, "highscore": 400, "name": "old mama"},
+ "5": { "age": 18, "highscore": 1200, "name": "young mama"},
+ "6": { "age": 20, "highscore": 1003, "name": "micheal blub"},
+ "7": { "age": 30, "highscore": 10000, "name": "no. 7"}
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update the order by value, should cause a new value event",
+ "type": "serverUpdate",
+ "path": "4/age",
+ "tag": 1,
+ "data": 25,
+ "events": [
+ {
+ "path": "",
+ "type": "child_moved",
+ "name": "4",
+ "prevName": "6",
+ "data": { "age": 25, "highscore": 400, "name": "old mama"}
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "4",
+ "prevName": "6",
+ "data": { "age": 25, "highscore": 400, "name": "old mama"}
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "4": { "age": 25, "highscore": 400, "name": "old mama"},
+ "5": { "age": 18, "highscore": 1200, "name": "young mama"},
+ "6": { "age": 20, "highscore": 1003, "name": "micheal blub"},
+ "7": { "age": 30, "highscore": 10000, "name": "no. 7"}
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Can create startAt, endAt, equalTo queries with bool",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "boolKey",
+ "startAt": {"index": true}
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 2,
+ "orderBy": "boolKey",
+ "endAt": {"index": true}
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 3,
+ "orderBy": "boolKey",
+ "equalTo": {"index": true}
+ },
+ "events": []
+ },
+ {
+ "type": "suppressWarning",
+ "events": []
+ }
+ ]
+ },
+ {
+ "name": "Query with existing server snap",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "age"
+ },
+ "events": []
+ },
+ {
+ ".comment": "untagged update, since index doesn't exist",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": { "age": 10, "score": 100, "bar": "baz" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": { "age": 10, "score": 100, "bar": "baz" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": { "age": 10, "score": 100, "bar": "baz" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "new listen should use existing data and index correctly",
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 2,
+ "orderBy": "score"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": { "age": 10, "score": 100, "bar": "baz" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": { "age": 10, "score": 100, "bar": "baz" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Server data is not purged for non-server-indexed queries",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "highscore",
+ "limitToLast": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "server has no index, so it sends down everything",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "highscore": 100, "value": "a" },
+ "b": { "highscore": 200, "value": "b" },
+ "c": { "highscore": 0, "value": "c" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "highscore": 100, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "highscore": 200, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "highscore": 100, "value": "a" },
+ "b": { "highscore": 200, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update of highscore leads to only a partial update",
+ "type": "serverUpdate",
+ "path": "c/highscore",
+ "data": 300,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "prevName": null,
+ "data": { "highscore": 100, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "highscore": 300, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "highscore": 200, "value": "b" },
+ "c": { "highscore": 300, "value": "c" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Limit with custom orderBy is refilled with correct item",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "age",
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "age": 4 },
+ "b": { "age": 3 },
+ "c": { "age": 2 },
+ "d": { "age": 1 }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "age": 4 }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "age": 4 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "delete 'a' and make sure 'b' comes into view.",
+ "type": "set",
+ "path": "a",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "age": 4 }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": { "age": 3 }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "age": 3 }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "startAt/endAt dominates limit",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 2 },
+ "limitToFirst": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "server has no index, so it sends down everything",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 1000, "value": "b" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to fill limit and beyond",
+ "type": "serverMerge",
+ "path": "",
+ "data": {
+ "b": { "index": 1, "value": "b" },
+ "c": { "index": 2, "value": "c" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 1, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 1, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to move entry out of window",
+ "type": "serverUpdate",
+ "path": "a/index",
+ "data": 1000,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 2, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "index": 1, "value": "b" },
+ "c": { "index": 2, "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to move all but one entry out of window",
+ "type": "serverUpdate",
+ "path": "b/index",
+ "data": 1000,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "b",
+ "data": { "index": 1, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "c": { "index": 2, "value": "c" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Update to single child that moves out of window",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends all data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to move child out of query",
+ "type": "serverUpdate",
+ "path": "a/index",
+ "data": -1,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to move child out of query",
+ "type": "serverUpdate",
+ "path": "b/index",
+ "data": -1,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "b",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "c": { "index": 3, "value": "c" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Limited query doesn't pull in out of range child",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends all data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 1000, "value": "c" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to move child out of query",
+ "type": "serverUpdate",
+ "path": "a/index",
+ "data": -1,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "index": 2, "value": "b" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Merge for location with default and limited listener",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ ".comment": "complete update",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" },
+ "d": { "index": 4, "value": "d" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": { "index": 4, "value": "d" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" },
+ "d": { "index": 4, "value": "d" }
+ }
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "limitToFirst": 2
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server pulls in other node",
+ "type": "serverMerge",
+ "path": "",
+ "data": {
+ "a": null,
+ "d": null
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "d",
+ "data": { "index": 4, "value": "d" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "User merge pulls in correct values",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 3
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends all data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" },
+ "d": { "index": 1000, "value": "d" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user merge pulls in existing value",
+ "type": "update",
+ "path": "d",
+ "data": { "index": 2 },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "c",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "b",
+ "data": { "index": 2, "value": "d" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "d": { "index": 2, "value": "d" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "User deep set pulls in correct values",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 3
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends all data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" },
+ "d": { "index": 1000, "value": "d" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user deep set pulls in existing value",
+ "type": "set",
+ "path": "d/index",
+ "data": 2,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "c",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "b",
+ "data": { "index": 2, "value": "d" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "d": { "index": 2, "value": "d" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Queries with equalTo(null) work",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": null },
+ "endAt": { "index": null }
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends all data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "value": "a" },
+ "b": { "value": "b" },
+ "c": { "value": "c", "index": 1 }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "value": "a" },
+ "b": { "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "server updates existing value (bringing c into query)",
+ "type": "serverUpdate",
+ "path": "c/index",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "value": "a" },
+ "b": { "value": "b" },
+ "c": { "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "server updates existing value (sending c out of query)",
+ "type": "serverUpdate",
+ "path": "c/index",
+ "data": 1,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "c",
+ "data": { "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "value": "a" },
+ "b": { "value": "b" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Reverted writes update query",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends only query data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 5, "value": "b" },
+ "d": { "index": 6, "value": "d" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 5, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 5, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user adds new value should update query",
+ "type": "set",
+ "path": "",
+ "data": {
+ "c": { "index": 2, "value": "c" },
+ "a": { "index": 1, "value": "a" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "b",
+ "data": { "index": 5, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "a",
+ "data": { "index": 2, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "c": { "index": 2, "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "write is reverted should revert query to old state",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "c",
+ "data": { "index": 2, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 5, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 5, "value": "b" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Deep set for non-local data doesn't raise events",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends only query data",
+ "type": "serverUpdate",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 5, "value": "b" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 5, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 5, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user updates a value for node outside of query, should trigger no events",
+ "type": "set",
+ "path": "c/index",
+ "data": 1,
+ "events": [ ]
+ },
+ {
+ ".comment": "update from server now contains complete data",
+ "type": "serverMerge",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "c": { "index": 1, "value": "c" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "b",
+ "data": { "index": 5, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "a",
+ "data": { "index": 1, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "c": { "index": 1, "value": "c" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "User update with new children triggers events",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "orderBy": "value",
+ "tag": 1
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends query data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "value": 5 },
+ "c": { "value": 3 }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": { "value": 3 }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": "c",
+ "data": { "value": 5 }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "c": { "value": 3 },
+ "a": { "value": 5 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user adds new children through an update",
+ "type": "update",
+ "path": "",
+ "data": {
+ "b": { "value": 4 },
+ "d": { "value": 2 }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": { "value": 2 }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "c",
+ "data": { "value": 4 }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "d": { "value": 2 },
+ "c": { "value": 3 },
+ "b": { "value": 4 },
+ "a": { "value": 5 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "server send new server",
+ "type": "serverMerge",
+ "path": "",
+ "data": {
+ "b": { "value": 4 },
+ "d": { "value": 2 }
+ },
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": [ ]
+ }
+ ]
+ },
+ {
+ "name": "User write with deep user overwrite",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "/foo",
+ "params": {
+ "orderBy": "value",
+ "tag": 1
+ },
+ "events": []
+ },
+ {
+ ".comment": "user sets initial data",
+ "type": "set",
+ "path": "/foo",
+ "data": {
+ "a": { "value": 1 },
+ "b": { "value": 5 },
+ "c": { "value": 10 }
+ },
+ "events": [
+ {
+ "path": "/foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "value": 1 }
+ },
+ {
+ "path": "/foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "value": 5 }
+ },
+ {
+ "path": "/foo",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "value": 10 }
+ },
+ {
+ "path": "/foo",
+ "type": "value",
+ "data": {
+ "a": { "value": 1 },
+ "b": { "value": 5 },
+ "c": { "value": 10 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user quickly overwrites value",
+ "type": "set",
+ "path": "/foo/c/value",
+ "data": 3,
+ "events": [
+ {
+ "path": "/foo",
+ "type": "child_moved",
+ "name": "c",
+ "prevName": "a",
+ "data": { "value": 3 }
+ },
+ {
+ "path": "/foo",
+ "type": "child_changed",
+ "name": "c",
+ "prevName": "a",
+ "data": { "value": 3 }
+ },
+ {
+ "path": "/foo",
+ "type": "value",
+ "data": {
+ "a": { "value": 1 },
+ "c": { "value": 3 },
+ "b": { "value": 5 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "server sends complete but outdated data",
+ "type": "serverUpdate",
+ "path": "/foo",
+ "data": {
+ "a": { "value": 1 },
+ "b": { "value": 5 },
+ "c": { "value": 10 }
+ },
+ "events": [ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [ ]
+ },
+ {
+ ".comment": "server sends update",
+ "type": "serverUpdate",
+ "path": "/foo/c/value",
+ "data": 3,
+ "events": [ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 1,
+ "events": [ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Deep server merge",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": {
+ "bar1" : { "a": "baz1", "b": "qux1" },
+ "bar2" : { "a": "baz2", "b": "qux2" }
+ }
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "bar1",
+ "prevName": null,
+ "data": { "a": "baz1", "b": "qux1" }
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "bar2",
+ "prevName": "bar1",
+ "data": { "a": "baz2", "b": "qux2" }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "bar1" : { "a": "baz1", "b": "qux1" },
+ "bar2" : { "a": "baz2", "b": "qux2" }
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "foo",
+ "data": {
+ "bar1/a": "newbaz1",
+ "bar2/b": "newqux2"
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_changed",
+ "name": "bar1",
+ "prevName": null,
+ "data": { "a": "newbaz1", "b": "qux1" }
+ },
+ {
+ "path": "foo",
+ "type": "child_changed",
+ "name": "bar2",
+ "prevName": "bar1",
+ "data": { "a": "baz2", "b": "newqux2" }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "bar1" : { "a": "newbaz1", "b": "qux1" },
+ "bar2" : { "a": "baz2", "b": "newqux2" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server updates priority",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a/foo",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": { "foo": "bar" },
+ "events": [
+ {
+ "path": "a/foo",
+ "type": "value",
+ "data": "bar"
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": { "foo": "bar" }
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/foo/.priority",
+ "data": "qux",
+ "events": [
+ {
+ "path": "a/foo",
+ "type": "value",
+ "data": { ".value": "bar", ".priority": "qux" }
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "foo",
+ "prevName": null,
+ "data": { ".value": "bar", ".priority": "qux" }
+ },
+ {
+ "path": "a",
+ "type": "child_moved",
+ "name": "foo",
+ "prevName": null,
+ "data": { ".value": "bar", ".priority": "qux" }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "foo": { ".value": "bar", ".priority": "qux" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert underlying full overwrite",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "key-a": "val-a",
+ "key-b": "val-b"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-a",
+ "prevName": null,
+ "data": "val-a"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-b",
+ "prevName": "key-a",
+ "data": "val-b"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-a": "val-a",
+ "key-b": "val-b"
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "key-c": "val-c",
+ "key-d": "val-d"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-a",
+ "data": "val-a"
+ },
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-b",
+ "data": "val-b"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-c",
+ "prevName": null,
+ "data": "val-c"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-d",
+ "prevName": "key-c",
+ "data": "val-d"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-c": "val-c",
+ "key-d": "val-d"
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "key-e": "val-e",
+ "key-f": "val-f"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-c",
+ "data": "val-c"
+ },
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-d",
+ "data": "val-d"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-e",
+ "prevName": null,
+ "data": "val-e"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-f",
+ "prevName": "key-e",
+ "data": "val-f"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-e": "val-e",
+ "key-f": "val-f"
+ }
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User child overwrite for non-existent server node",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "foo",
+ "data": { "bar": "qux" },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": { "bar": "qux" }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert user overwrite of child on leaf node",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": "foo",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "foo"
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "key",
+ "data": "value",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key",
+ "prevName": null,
+ "data": "value"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": { "key": "value" }
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key",
+ "prevName": null,
+ "data": "value"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": "foo"
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server overwrite with deep user delete",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "key-1": "value-1"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": "value-1"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": "value-1"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User deletes non-existent key, which shouldn't trigger events",
+ "type": "set",
+ "path": "key-2/non-key",
+ "data": null,
+ "events": []
+ },
+ {
+ ".comment": "Server updates node with deep user delete",
+ "type": "serverUpdate",
+ "path": "key-2",
+ "data": {
+ "deep-key": "deep-value"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-2",
+ "prevName": "key-1",
+ "data": {
+ "deep-key": "deep-value"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": "value-1",
+ "key-2": {
+ "deep-key": "deep-value"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User overwrites leaf node with priority",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Overwrite leaf with children node",
+ "type": "set",
+ "path": "foo",
+ "data": "bar",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "foo": "bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User overwrites inherit priority values from leaf nodes",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user updates the node",
+ "type": "set",
+ "path": "foo",
+ "data": "foo-value",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "foo-value"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "foo": "foo-value"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "The server updates the data for the set",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ },
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ },
+ {
+ ".comment": "Add another update, should not have old priority",
+ "type": "set",
+ "path": "bar",
+ "data": "bar-value",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "bar",
+ "prevName": null,
+ "data": "bar-value"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "foo": "foo-value",
+ "bar": "bar-value"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User update on user set leaf node with priority after server update",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user overwrite shadows server data",
+ "type": "serverMerge",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": [ ]
+ },
+ {
+ ".comment": "user updates the node",
+ "type": "update",
+ "path": "deep/deeper",
+ "data": {
+ "0-key": null,
+ "key": "value"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "deep",
+ "prevName": null,
+ "data": { "deeper": { "key": "value" } }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "deep": { "deeper": { "key": "value" } }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server deep delete on leaf node",
+ ".comment": "This is a contrived example, as the server will probably not send null updates to leaf nodes",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": "foo",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "foo"
+ }
+ ]
+ },
+ {
+ ".comment": "this should trigger no events",
+ "type": "serverUpdate",
+ "path": "deep/child",
+ "data": null,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "User sets root priority",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar"
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": ".priority",
+ "data": "prio",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "foo": "bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User updates priority on empty root",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ ".comment": "Priority on empty root should not trigger events",
+ "type": "set",
+ "path": ".priority",
+ "data": "prio",
+ "events": []
+ },
+ {
+ ".comment": "This should a value event without priority",
+ "type": "serverUpdate",
+ "path": "",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ ".comment": "This should now have the user priority",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "foo": "bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert set at root with priority",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User overwrites root",
+ "type": "set",
+ "path": "",
+ "data": {
+ "baz": "qux",
+ ".priority": "prio"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "baz",
+ "prevName": null,
+ "data": "qux"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "baz": "qux"
+ }
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "baz",
+ "prevName": null,
+ "data": "qux"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server updates priority after user sets priority",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": { ".value": "foo", ".priority": "prio" },
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": { ".value": "foo", ".priority": "prio" }
+ }
+ ]
+ },
+ {
+ ".comment": "User overwrites priority",
+ "type": "set",
+ "path": ".priority",
+ "data": "prio-2",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": { ".value": "foo", ".priority": "prio-2" }
+ }
+ ]
+ },
+ {
+ ".comment": "this should not trigger any events since a user write is shadowing",
+ "type": "serverUpdate",
+ "path": ".priority",
+ "data": null,
+ "events": [ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User updates priority twice, first is reverted",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": { "foo": "bar" },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": { "foo": "bar" }
+ }
+ ]
+ },
+ {
+ ".comment": "User overwrites priority first time",
+ "type": "set",
+ "path": ".priority",
+ "data": "prio-1",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar",
+ ".priority": "prio-1"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User overwrites priority second time",
+ "type": "set",
+ "path": ".priority",
+ "data": "prio-2",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar",
+ ".priority": "prio-2"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "revert should not trigger event",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": "new-bar",
+ "events": [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "foo",
+ "prevName": null,
+ "data": "new-bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "new-bar",
+ ".priority": "prio-2"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server acks root priority set after user deletes root node",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": "foo",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "foo"
+ }
+ ]
+ },
+ {
+ ".comment": "User overwrites root priority",
+ "type": "set",
+ "path": ".priority",
+ "data": "prio",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".value": "foo",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User deletes root node",
+ "type": "set",
+ "path": "",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": ".priority",
+ "data": "prio",
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "A delete in a merge doesn't push out nodes",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 3,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "key-1": 1,
+ "key-3": 3,
+ "key-4": 4
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-3",
+ "prevName": "key-1",
+ "data": 3
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-4",
+ "prevName": "key-3",
+ "data": 4
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": 1,
+ "key-3": 3,
+ "key-4": 4
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Since key-3 is deleted, key-5 should still remain in the query",
+ "type": "serverMerge",
+ "path": "",
+ "data": {
+ "key-3": null,
+ "key-2": 2
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-3",
+ "data": 3
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-2",
+ "prevName": "key-1",
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": 1,
+ "key-2": 2,
+ "key-4": 4
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "A tagged query fires events eventually",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "limitToLast": 2
+ },
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "key-1": 1,
+ "key-2": 2,
+ "key-3": 3
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-2",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-3",
+ "prevName": "key-2",
+ "data": 3
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-2": 2,
+ "key-3": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server updates tagged data, should filter key-1 node",
+ "type": "serverUpdate",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "key-2": 2,
+ "key-3": 3
+ },
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ },
+ {
+ ".comment": "User deletes element, only child removed event is fired, since data is not available",
+ "type": "set",
+ "path": "key-2",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-2",
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-3": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server updates tagged data, should filter key-1 node",
+ "type": "serverMerge",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "key-1": 1,
+ "key-2": null
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "value",
+ "name": "",
+ "data": {
+ "key-1": 1,
+ "key-3": 3
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "A server update that leaves user sets unchanged is not ignored",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "key-1": 1,
+ "key-2": 2
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-2",
+ "prevName": "key-1",
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": 1,
+ "key-2": 2
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user adds a new node",
+ "type": "set",
+ "path": "key-3",
+ "data": 3,
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-3",
+ "prevName": "key-2",
+ "data": 3
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": 1,
+ "key-2": 2,
+ "key-3": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server adds new children with full overwrite",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "key-1": 1,
+ "key-2": 2,
+ "key-4": 4
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-4",
+ "prevName": "key-3",
+ "data": 4
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": 1,
+ "key-2": 2,
+ "key-3": 3,
+ "key-4": 4
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User write outside of limit is ignored for tagged queries",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 2,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ },
+ "key-4": {
+ "index": 4,
+ "other-key": "bar"
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-4",
+ "prevName": "key-1",
+ "data": {
+ "index": 4,
+ "other-key": "bar"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ },
+ "key-4": {
+ "index": 4,
+ "other-key": "bar"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user updates index of child outside, which should bring it in view eventually, but not before the server sends the complete node",
+ "type": "set",
+ "path": "key-2/index",
+ "data": 2,
+ "events": []
+ },
+ {
+ ".comment": "In the meantime the server adds another node",
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ },
+ "key-3": {
+ "index": 3,
+ "other-key": "baz"
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-4",
+ "data": {
+ "index": 4,
+ "other-key": "bar"
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-3",
+ "prevName": "key-1",
+ "data": {
+ "index": 3,
+ "other-key": "baz"
+ }
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ },
+ "key-3": {
+ "index": 3,
+ "other-key": "baz"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server now incorperates user update",
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "key-2",
+ "data": {
+ "index": 2,
+ "other-key": "qux"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-3",
+ "data": {
+ "index": 3,
+ "other-key": "baz"
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-2",
+ "prevName": "key-1",
+ "data": {
+ "index": 2,
+ "other-key": "qux"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ },
+ "key-2": {
+ "index": 2,
+ "other-key": "qux"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Ack for merge doesn't raise value event for later listen",
+ "steps": [
+ {
+ "type": "update",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ ".comment": "This acks a merge, so we can't raise a value event yet",
+ "writeId": 0,
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": "bar",
+ "qux": "quux"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "qux",
+ "prevName": "foo",
+ "data": "quux"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar",
+ "qux": "quux"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Clear parent shadowing server values merge with server children",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a",
+ "data": {"b": 28, "c": 3},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 28
+ }
+ ]
+ },
+ {
+ ".comment": "This listen should get a complete event snap, as well as complete server children",
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 28
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ }
+ ]
+ },
+ {
+ ".comment": "Do a serverUpdate with a conflicting value for b, simulates a server value. It's still shadowed though",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 29,
+ "events": []
+ },
+ {
+ ".comment": "Clearing the set should result in updated values for b",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 29
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 29
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Priorities don't make me sick",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "a/foo",
+ "data": {
+ "bar": "baz",
+ ".priority": "prio"
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": {
+ "bar": "baz",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/foo/bar",
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "foo",
+ "data": {
+ "bar": "baz",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "this caused vomitting in the past...",
+ "type": "set",
+ "path": "a/foo/bar",
+ "data": null,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Merge that moves child from window to boundary does not cause child to be readded",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": [],
+ "params": {
+ "tag": 1,
+ "limitToFirst": 2,
+ "startAt": {"index": 1},
+ "orderBy": "index"
+ }
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "tag": 1,
+ "data": {
+ "2-a": {
+ "index": 10
+ },
+ "1-b": {
+ "index": 20
+ }
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "2-a",
+ "prevName": null,
+ "data": {
+ "index": 10
+ }
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "1-b",
+ "prevName": "2-a",
+ "data": {
+ "index": 20
+ }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "2-a": { "index": 10 },
+ "1-b": { "index": 20 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "2-a will be the 'next' child after the old '1-b' which will be updated first, but it shouldn't be added because it will actually be out of the window...",
+ "type": "update",
+ "path": "a",
+ "data": {
+ "1-b": { "index": 0 },
+ "2-a": { "index": 30 },
+ "3-c": { "index": 5 },
+ "4-d": { "index": 6 }
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "2-a",
+ "data": { "index": 10 }
+ },
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "1-b",
+ "data": { "index": 20 }
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "3-c",
+ "prevName": null,
+ "data": { "index": 5 }
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "4-d",
+ "prevName": "3-c",
+ "data": { "index": 6 }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "3-c": { "index": 5 },
+ "4-d": { "index": 6 }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Deep merge ack is handled correctly.",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Initial server data.",
+ "type": "serverUpdate",
+ "path": "a",
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ ".comment": "Do deep merge.",
+ "type": "update",
+ "path": "a/b",
+ "data": {
+ "c": 42,
+ "d": "hi"
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "c": 42,
+ "d": "hi"
+ }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": {
+ "c": 42,
+ "d": "hi"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server update for our deep merge.",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": {
+ "c": 42,
+ "d": "hi"
+ },
+ "events": []
+ },
+ {
+ ".comment": "ack deep merge.",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Deep merge ack (on incomplete data, and with server values)",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ ".comment": "Initial server data.",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": {
+ "c": "original-server-value"
+ },
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "data": "original-server-value",
+ "prevName": null
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {
+ "c": "original-server-value"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Do deep merge.",
+ "type": "update",
+ "path": "a/b",
+ "data": {
+ "c": "user-merge-value"
+ },
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_changed",
+ "name": "c",
+ "data": "user-merge-value",
+ "prevName": null
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {
+ "c": "user-merge-value"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Listen on a (which won't have complete data).",
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "c": "user-merge-value"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server update for our deep merge, but change data (simulate server value).",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": {
+ "c": "user-merge-value-after-server-resolution"
+ },
+ "events": []
+ },
+ {
+ ".comment": "ack deep merge.",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_changed",
+ "name": "c",
+ "data": "user-merge-value-after-server-resolution",
+ "prevName": null
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {
+ "c": "user-merge-value-after-server-resolution"
+ }
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "c": "user-merge-value-after-server-resolution"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Limit query handles deep server merge for out-of-view item.",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 1
+ },
+ "events": []
+ },
+ {
+ ".comment": "Initial server data.",
+ "type": "serverUpdate",
+ "path": "foo",
+ "tag": 1,
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server merge for out-of-view child 'b' (perhaps for another listener). Shouldn't trigger events since we don't have complete data.",
+ "type": "serverMerge",
+ "path": "foo/b",
+ "data": {
+ "val": "b-val"
+ },
+ "events": [ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Limit query handles deep user merge for out-of-view item.",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 1
+ },
+ "events": []
+ },
+ {
+ ".comment": "Initial server data.",
+ "type": "serverUpdate",
+ "path": "foo",
+ "tag": 1,
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User merge for out-of-view child 'b'. Shouldn't trigger events since we don't have complete data.",
+ "type": "update",
+ "path": "foo/b",
+ "data": {
+ "val": "b-val"
+ },
+ "events": [ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Limit query handles deep user merge for out-of-view item followed by server update.",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 1
+ },
+ "events": []
+ },
+ {
+ ".comment": "Initial server data.",
+ "type": "serverUpdate",
+ "path": "foo",
+ "tag": 1,
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User merge for out-of-view child 'b'. Shouldn't trigger events since we don't have complete data.",
+ "type": "update",
+ "path": "foo/b",
+ "data": {
+ "val": "b-val-new"
+ },
+ "events": [ ]
+ },
+ {
+ ".comment": "Server update for 'b', bringing it into view.",
+ "type": "serverUpdate",
+ "path": "foo/b",
+ "data": {
+ "val": "b-val-old",
+ "val2": "b-val2"
+ },
+ "events": [
+ {
+ "type": "child_removed",
+ "path": "foo",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ {
+ "type": "child_added",
+ "path": "foo",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "val": "b-val-new",
+ "val2": "b-val2"
+ }
+ },
+ {
+ "type": "value",
+ "path": "foo",
+ "data": {
+ "b": {
+ "val": "b-val-new",
+ "val2": "b-val2"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Unrelated, untagged update is not cached in tagged listen",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "limitToFirst": 1,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "server sends update for key-2 which should not be cached or marked complete",
+ "type": "serverUpdate",
+ "path": "key-2",
+ "data": {
+ "index": 2,
+ "other-key": "bar"
+ },
+ "events": []
+ },
+ {
+ ".comment": "Now an update for key-1 comes in, marking query as filtered",
+ "type": "serverMerge",
+ "tag": 1,
+ "path": "key-1",
+ "data": {
+ "other-key": "new-foo"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server now updates node out of view, should not mark view unfiltered",
+ "type": "serverUpdate",
+ "path": "key-3",
+ "data": { "index": 3, "other-key": "qux" },
+ "events": []
+ },
+ {
+ ".comment": "Server now updates node out of view, should not raise any events",
+ "type": "serverMerge",
+ "path": "key-2",
+ "data": { "index": 0 },
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Unrelated, acked set is not cached in tagged listen",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "limitToFirst": 1,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "key-1/other-key",
+ "data": "new-foo",
+ "events": [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "key-1/other-key",
+ "tag": 1,
+ "data": "new-foo",
+ "events": []
+ },
+ {
+ ".comment": "The ack should not mark key-2 complete in tagged listen",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ },
+ {
+ ".comment": "Server now updates node out of view, should not raise any events",
+ "type": "serverMerge",
+ "path": "key-2",
+ "data": { "index": 0 },
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Unrelated, acked update is not cached in tagged listen",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "limitToFirst": 1,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "key-1",
+ "data": {
+ "other-key": "new-foo"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "key-1",
+ "tag": 1,
+ "data": {
+ "other-key": "new-foo"
+ },
+ "events": []
+ },
+ {
+ ".comment": "The ack should not mark key-2 complete in tagged listen",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ },
+ {
+ ".comment": "Server now updates node out of view, should not raise any events",
+ "type": "serverMerge",
+ "path": "key-2",
+ "data": { "index": 0 },
+ "events": []
+ }
+ ]
+ },
+ {
+ "name": "Deep update raises immediate events only if has complete data",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "age",
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "a": {
+ "age": 4
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "age": 4
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": {
+ "age": 4
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "",
+ "data": {
+ "a/age": 0,
+ "e": {
+ "age": 4
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": {
+ "age": 4
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "e",
+ "prevName": null,
+ "data": {
+ "age": 4
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "e": {
+ "age": 4
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Now we don't have a full data for child /f, don't raise the event. The events for child /e are correct, although may be confusing for customers.",
+ "type": "update",
+ "path": "",
+ "data": {
+ "e/age": 0,
+ "f/age": 4
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_moved",
+ "name": "e",
+ "prevName": null,
+ "data": {
+ "age": 0
+ }
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "e",
+ "prevName": null,
+ "data": {
+ "age": 0
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "e": {
+ "age": 0
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "f": {
+ "age": 4
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "e",
+ "data": {
+ "age": 0
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "f",
+ "prevName": null,
+ "data": {
+ "age": 4
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "f": {
+ "age": 4
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Deep update returns minimum data required",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "idx",
+ "equalTo": { "index": true }
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "a": {
+ "name": "foo",
+ "idx": true
+ },
+ "b": {
+ "name": "bar",
+ "idx": true
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "name": "foo",
+ "idx": true
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": {
+ "name": "bar",
+ "idx": true
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": {
+ "name": "foo",
+ "idx": true
+ },
+ "b": {
+ "name": "bar",
+ "idx": true
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "a/idx": false,
+ "b/name": "blah",
+ "c": {
+ "name": "bar",
+ "idx": true
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": {
+ "name": "foo",
+ "idx": true
+ }
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "name": "blah",
+ "idx": true
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": {
+ "name": "bar",
+ "idx": true
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": {
+ "name": "blah",
+ "idx": true
+ },
+ "c": {
+ "name": "bar",
+ "idx": true
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Deep update raises all events",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "aa": 1, "ab": 2 },
+ "b": { "ba": 3, "bb": 4 }
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "prevName": null,
+ "name": "aa",
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "prevName": "aa",
+ "name": "ab",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "aa": 1,
+ "ab": 2
+ }
+ },
+ {
+ "path": "b",
+ "type": "child_added",
+ "prevName": null,
+ "name": "ba",
+ "data": 3
+ },
+ {
+ "path": "b",
+ "type": "child_added",
+ "prevName": "ba",
+ "name": "bb",
+ "data": 4
+ },
+ {
+ "path": "b",
+ "type": "value",
+ "data": {
+ "ba": 3,
+ "bb": 4
+ }
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "",
+ "data": {
+ "a/aa": 0,
+ "b/ba": 0
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_changed",
+ "prevName": null,
+ "name": "aa",
+ "data": 0
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "aa": 0,
+ "ab": 2
+ }
+ },
+ {
+ "path": "b",
+ "type": "child_changed",
+ "prevName": null,
+ "name": "ba",
+ "data": 0
+ },
+ {
+ "path": "b",
+ "type": "value",
+ "data": {
+ "ba": 0,
+ "bb": 4
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "a",
+ "data": {
+ "ab/abc": 1,
+ "ac/acd": 2
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_changed",
+ "prevName": "aa",
+ "name": "ab",
+ "data": { "abc": 1 }
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "prevName": "ab",
+ "name": "ac",
+ "data": { "acd": 2 }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "aa": 0,
+ "ab": { "abc": 1 },
+ "ac": { "acd": 2 }
+ }
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/Example/Database/Tests/third_party/Base64.h b/Example/Database/Tests/third_party/Base64.h
new file mode 100644
index 0000000..6db1028
--- /dev/null
+++ b/Example/Database/Tests/third_party/Base64.h
@@ -0,0 +1,53 @@
+//
+// Base64.h
+//
+// Version 1.1
+//
+// Created by Nick Lockwood on 12/01/2012.
+// Copyright (C) 2012 Charcoal Design
+//
+// Distributed under the permissive zlib License
+// Get the latest version from here:
+//
+// https://github.com/nicklockwood/Base64
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#import <Foundation/Foundation.h>
+
+
+@interface NSData (Base64)
+
++ (NSData *)dataWithBase64EncodedString:(NSString *)string;
+- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth;
+- (NSString *)base64EncodedString;
+
+@end
+
+
+@interface NSString (Base64)
+
++ (NSString *)stringWithBase64EncodedString:(NSString *)string;
+- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth;
+- (NSString *)base64EncodedString;
+- (NSString *)base64DecodedString;
+- (NSData *)base64DecodedData;
+
+@end
diff --git a/Example/Database/Tests/third_party/Base64.m b/Example/Database/Tests/third_party/Base64.m
new file mode 100644
index 0000000..b3d73db
--- /dev/null
+++ b/Example/Database/Tests/third_party/Base64.m
@@ -0,0 +1,202 @@
+//
+// Base64.m
+//
+// Version 1.1
+//
+// Created by Nick Lockwood on 12/01/2012.
+// Copyright (C) 2012 Charcoal Design
+//
+// Distributed under the permissive zlib License
+// Get the latest version from here:
+//
+// https://github.com/nicklockwood/Base64
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#import "Base64.h"
+
+
+#import <Availability.h>
+#if !__has_feature(objc_arc)
+#error This library requires automatic reference counting
+#endif
+
+
+@implementation NSData (Base64)
+
++ (NSData *)dataWithBase64EncodedString:(NSString *)string
+{
+ const char lookup[] =
+ {
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99,
+ 99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99,
+ 99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99
+ };
+
+ NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
+ long long inputLength = [inputData length];
+ const unsigned char *inputBytes = [inputData bytes];
+
+ long long maxOutputLength = (inputLength / 4 + 1) * 3;
+ NSMutableData *outputData = [NSMutableData dataWithLength:maxOutputLength];
+ unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes];
+
+ int accumulator = 0;
+ long long outputLength = 0;
+ unsigned char accumulated[] = {0, 0, 0, 0};
+ for (long long i = 0; i < inputLength; i++)
+ {
+ unsigned char decoded = lookup[inputBytes[i] & 0x7F];
+ if (decoded != 99)
+ {
+ accumulated[accumulator] = decoded;
+ if (accumulator == 3)
+ {
+ outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4);
+ outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2);
+ outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3];
+ }
+ accumulator = (accumulator + 1) % 4;
+ }
+ }
+
+ //handle left-over data
+ if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4);
+ if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2);
+ if (accumulator > 2) outputLength++;
+
+ //truncate data to match actual output length
+ outputData.length = outputLength;
+ return outputLength? outputData: nil;
+}
+
+- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth
+{
+ //ensure wrapWidth is a multiple of 4
+ wrapWidth = (wrapWidth / 4) * 4;
+
+ const char lookup[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ long long inputLength = [self length];
+ const unsigned char *inputBytes = [self bytes];
+
+ long long maxOutputLength = (inputLength / 3 + 1) * 4;
+ maxOutputLength += wrapWidth? (maxOutputLength / wrapWidth) * 2: 0;
+ unsigned char *outputBytes = (unsigned char *)malloc(maxOutputLength);
+
+ long long i;
+ long long outputLength = 0;
+ for (i = 0; i < inputLength - 2; i += 3)
+ {
+ outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
+ outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)];
+ outputBytes[outputLength++] = lookup[((inputBytes[i + 1] & 0x0F) << 2) | ((inputBytes[i + 2] & 0xC0) >> 6)];
+ outputBytes[outputLength++] = lookup[inputBytes[i + 2] & 0x3F];
+
+ //add line break
+ if (wrapWidth && (outputLength + 2) % (wrapWidth + 2) == 0)
+ {
+ outputBytes[outputLength++] = '\r';
+ outputBytes[outputLength++] = '\n';
+ }
+ }
+
+ //handle left-over data
+ if (i == inputLength - 2)
+ {
+ // = terminator
+ outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
+ outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)];
+ outputBytes[outputLength++] = lookup[(inputBytes[i + 1] & 0x0F) << 2];
+ outputBytes[outputLength++] = '=';
+ }
+ else if (i == inputLength - 1)
+ {
+ // == terminator
+ outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
+ outputBytes[outputLength++] = lookup[(inputBytes[i] & 0x03) << 4];
+ outputBytes[outputLength++] = '=';
+ outputBytes[outputLength++] = '=';
+ }
+
+ if (outputLength >= 4)
+ {
+ //truncate data to match actual output length
+ outputBytes = realloc(outputBytes, outputLength);
+ return [[NSString alloc] initWithBytesNoCopy:outputBytes
+ length:outputLength
+ encoding:NSASCIIStringEncoding
+ freeWhenDone:YES];
+ }
+ else if (outputBytes)
+ {
+ free(outputBytes);
+ }
+ return nil;
+}
+
+- (NSString *)base64EncodedString
+{
+ return [self base64EncodedStringWithWrapWidth:0];
+}
+
+@end
+
+
+@implementation NSString (Base64)
+
++ (NSString *)stringWithBase64EncodedString:(NSString *)string
+{
+ NSData *data = [NSData dataWithBase64EncodedString:string];
+ if (data)
+ {
+ return [[self alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ }
+ return nil;
+}
+
+- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth
+{
+ NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
+ return [data base64EncodedStringWithWrapWidth:wrapWidth];
+}
+
+- (NSString *)base64EncodedString
+{
+ NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
+ return [data base64EncodedString];
+}
+
+- (NSString *)base64DecodedString
+{
+ return [NSString stringWithBase64EncodedString:self];
+}
+
+- (NSData *)base64DecodedData
+{
+ return [NSData dataWithBase64EncodedString:self];
+}
+
+@end
diff --git a/Example/Firebase.xcodeproj/project.pbxproj b/Example/Firebase.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..bd2692f
--- /dev/null
+++ b/Example/Firebase.xcodeproj/project.pbxproj
@@ -0,0 +1,3524 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ DE3373891E73773400881891 /* AllUnitTests */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = DE33738A1E73773400881891 /* Build configuration list for PBXAggregateTarget "AllUnitTests" */;
+ buildPhases = (
+ );
+ dependencies = (
+ DE6F01BA1E957157004AEE01 /* PBXTargetDependency */,
+ DEB5185A1E9008CB0089C938 /* PBXTargetDependency */,
+ DE9315871E86E9990083EDBF /* PBXTargetDependency */,
+ DEE14E0B1E844FDC006FA992 /* PBXTargetDependency */,
+ DE3373981E73776F00881891 /* PBXTargetDependency */,
+ );
+ name = AllUnitTests;
+ productName = AllTests;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 0624F3EB1EC0ED0800E5940D /* FConnectionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB46F1EBA7AEF00038A59 /* FConnectionTest.m */; };
+ 0624F3EC1EC0ED1B00E5940D /* FData.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4711EBA7AEF00038A59 /* FData.m */; };
+ 0624F3ED1EC0ED2300E5940D /* FDotInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4731EBA7AEF00038A59 /* FDotInfo.m */; };
+ 0624F3EE1EC0ED2A00E5940D /* FEventTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4751EBA7AEF00038A59 /* FEventTests.m */; };
+ 0624F3EF1EC0ED3000E5940D /* FIRAuthTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4761EBA7AEF00038A59 /* FIRAuthTests.m */; };
+ 0624F3F01EC0ED3500E5940D /* FIRDatabaseQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4781EBA7AEF00038A59 /* FIRDatabaseQueryTests.m */; };
+ 0624F3F11EC0ED3A00E5940D /* FIRDatabaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4791EBA7AEF00038A59 /* FIRDatabaseTests.m */; };
+ 0624F3F21EC0ED3F00E5940D /* FKeepSyncedTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB47F1EBA7AEF00038A59 /* FKeepSyncedTest.m */; };
+ 0624F3F31EC0ED4300E5940D /* FOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4811EBA7AEF00038A59 /* FOrder.m */; };
+ 0624F3F41EC0ED4800E5940D /* FOrderByTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4831EBA7AEF00038A59 /* FOrderByTests.m */; };
+ 0624F3F51EC0ED4D00E5940D /* FPersist.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4851EBA7AEF00038A59 /* FPersist.m */; };
+ 0624F3F61EC0ED5100E5940D /* FRealtime.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4871EBA7AEF00038A59 /* FRealtime.m */; };
+ 0624F3F71EC0ED5600E5940D /* FTransactionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB48F1EBA7AEF00038A59 /* FTransactionTest.m */; };
+ 0637BA651EC0F99700CAEFD4 /* FirebaseDev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0637BA641EC0F99700CAEFD4 /* FirebaseDev.framework */; };
+ 0637BA671EC0F9BA00CAEFD4 /* FDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D791E8EF202009EB6DF /* FDevice.m */; };
+ 0637BA681EC0F9BD00CAEFD4 /* FEventTester.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D7B1E8EF202009EB6DF /* FEventTester.m */; };
+ 0637BA691EC0F9C100CAEFD4 /* FIRFakeApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB47E1EBA7AEF00038A59 /* FIRFakeApp.m */; };
+ 0637BA6A1EC0F9C400CAEFD4 /* FIRTestAuthTokenProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D7D1E8EF202009EB6DF /* FIRTestAuthTokenProvider.m */; };
+ 0637BA6B1EC0F9C700CAEFD4 /* FMockStorageEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D7F1E8EF202009EB6DF /* FMockStorageEngine.m */; };
+ 0637BA6C1EC0F9CB00CAEFD4 /* FTestAuthTokenGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D811E8EF202009EB6DF /* FTestAuthTokenGenerator.m */; };
+ 0637BA6D1EC0F9CF00CAEFD4 /* FTestBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB45A1EBA7AE200038A59 /* FTestBase.m */; };
+ 0637BA6E1EC0F9D200CAEFD4 /* FTestCachePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D831E8EF202009EB6DF /* FTestCachePolicy.m */; };
+ 0637BA6F1EC0F9D500CAEFD4 /* FTestClock.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D851E8EF202009EB6DF /* FTestClock.m */; };
+ 0637BA701EC0F9D900CAEFD4 /* FTestExpectations.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D871E8EF202009EB6DF /* FTestExpectations.m */; };
+ 0637BA711EC0F9DD00CAEFD4 /* FTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D891E8EF202009EB6DF /* FTestHelpers.m */; };
+ 0637BA721EC0F9E000CAEFD4 /* FTupleEventTypeString.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D8B1E8EF203009EB6DF /* FTupleEventTypeString.m */; };
+ 0637BA731EC0F9E400CAEFD4 /* SenTest+FWaiter.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D8D1E8EF203009EB6DF /* SenTest+FWaiter.m */; };
+ 063CB49A1EBA7AEF00038A59 /* FirebaseTests-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 063CB47C1EBA7AEF00038A59 /* FirebaseTests-Info.plist */; };
+ 063CB4A71EBA7B0B00038A59 /* FCompoundWriteTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB46E1EBA7AEF00038A59 /* FCompoundWriteTest.m */; };
+ 063CB4BE1EBA7B3100038A59 /* FIRDataSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB47B1EBA7AEF00038A59 /* FIRDataSnapshotTests.m */; };
+ 063CB4BF1EBA7B3100038A59 /* FIRFakeApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB47E1EBA7AEF00038A59 /* FIRFakeApp.m */; };
+ 063CB4C81EBA7B3100038A59 /* FTreeSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4901EBA7AEF00038A59 /* FTreeSortedDictionaryTests.m */; };
+ 063CB4C91EBA7B4600038A59 /* FArraySortedDictionaryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4471EBA7AE200038A59 /* FArraySortedDictionaryTest.m */; };
+ 063CB4CA1EBA7B4600038A59 /* FCompoundHashTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4481EBA7AE200038A59 /* FCompoundHashTest.m */; };
+ 063CB4CB1EBA7B4600038A59 /* FIRMutableDataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB44A1EBA7AE200038A59 /* FIRMutableDataTests.m */; };
+ 063CB4CC1EBA7B4600038A59 /* FLevelDBStorageEngineTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB44B1EBA7AE200038A59 /* FLevelDBStorageEngineTests.m */; };
+ 063CB4CD1EBA7B4600038A59 /* FNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB44C1EBA7AE200038A59 /* FNodeTests.m */; };
+ 063CB4CE1EBA7B4600038A59 /* FPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB44E1EBA7AE200038A59 /* FPathTests.m */; };
+ 063CB4CF1EBA7B4600038A59 /* FPersistenceManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB44F1EBA7AE200038A59 /* FPersistenceManagerTest.m */; };
+ 063CB4D01EBA7B4600038A59 /* FPruneForestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4501EBA7AE200038A59 /* FPruneForestTest.m */; };
+ 063CB4D11EBA7B4600038A59 /* FPruningTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4511EBA7AE200038A59 /* FPruningTest.m */; };
+ 063CB4D21EBA7B4600038A59 /* FQueryParamsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4521EBA7AE200038A59 /* FQueryParamsTest.m */; };
+ 063CB4D31EBA7B4600038A59 /* FRangeMergeTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4531EBA7AE200038A59 /* FRangeMergeTest.m */; };
+ 063CB4D41EBA7B4600038A59 /* FRepoInfoTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4541EBA7AE200038A59 /* FRepoInfoTest.m */; };
+ 063CB4D51EBA7B4600038A59 /* FSparseSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4561EBA7AE200038A59 /* FSparseSnapshotTests.m */; };
+ 063CB4D71EBA7B4600038A59 /* FTestBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB45A1EBA7AE200038A59 /* FTestBase.m */; };
+ 063CB4D81EBA7B4600038A59 /* FTrackedQueryManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB45B1EBA7AE200038A59 /* FTrackedQueryManagerTest.m */; };
+ 063CB4D91EBA7B4600038A59 /* FUtilitiesTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB45C1EBA7AE200038A59 /* FUtilitiesTest.m */; };
+ 063CB4DB1EBAA89E00038A59 /* FSyncPointTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4581EBA7AE200038A59 /* FSyncPointTests.m */; };
+ 0672F2F21EBBA7D900818E87 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0672F2F11EBBA7D900818E87 /* GoogleService-Info.plist */; };
+ 0672F2F31EBBA7D900818E87 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0672F2F11EBBA7D900818E87 /* GoogleService-Info.plist */; };
+ 069428831EC3B38C00F7BC69 /* 1mb.dat in Resources */ = {isa = PBXBuildFile; fileRef = 069428801EC3B35A00F7BC69 /* 1mb.dat */; };
+ 0697B1221EC13D8A00542174 /* Base64.m in Sources */ = {isa = PBXBuildFile; fileRef = 0697B1211EC13D8A00542174 /* Base64.m */; };
+ 06B47E8C1EC39ADF00170C02 /* FirebaseDev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06B47E8B1EC39ADF00170C02 /* FirebaseDev.framework */; };
+ 06C24A061EC39BCB005208CA /* FIRStorageIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 06121ECA1EC39A0B0008D70E /* FIRStorageIntegrationTests.m */; };
+ 22DD1E787F5347BD66CC842B /* Pods_Auth_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1EEA0F965ABC48C695972509 /* Pods_Auth_Example.framework */; };
+ 260F4B35536ACE792D9BD6C6 /* Pods_Database_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64928F2997FAF0EAEAC9B8CA /* Pods_Database_Tests.framework */; };
+ 3054DA05818345789EA0C5B0 /* Pods_Core_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A821396D7D1089ECE810EF /* Pods_Core_Example.framework */; };
+ 4768966C0C99B8D4215826A5 /* Pods_Auth_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAB9666F29A81704CA956317 /* Pods_Auth_Tests.framework */; };
+ 48402D5F3CB17E091298C7FF /* Pods_Database_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66C7EEA21795A3320088DEBE /* Pods_Database_Example.framework */; };
+ 7EA36B802D84DD89CE6203A0 /* Pods_Storage_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16E92590A6B517109A2B219F /* Pods_Storage_Tests.framework */; };
+ 83C9C772827554752364B400 /* Pods_Messaging_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8A6D15690286B6BB4CB8023 /* Pods_Messaging_Example.framework */; };
+ 8CE9133C8720B1C600F7C731 /* Pods_Core_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D52CEDD0146DF63640A4C3A5 /* Pods_Core_Tests.framework */; };
+ 8D14BB390A3E191CCF78BF91 /* Pods_Storage_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 36DF4C7B93E6FE7AD8F88A38 /* Pods_Storage_IntegrationTests.framework */; };
+ 9653E6AB7DDD8B5E4814442D /* Pods_Database_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E3DEB3CBB1440528DFE1E197 /* Pods_Database_IntegrationTests.framework */; };
+ AFAF36F51EC28C25004BDEE5 /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */; };
+ AFAF36F61EC28C25004BDEE5 /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */; };
+ AFAF36F71EC28C25004BDEE5 /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */; };
+ AFAF36F81EC28C25004BDEE5 /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */; };
+ AFAF36F91EC28C25004BDEE5 /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */; };
+ AFC8BA9D1EBD230E00B8EEAE /* NotificationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFC8BA9C1EBD230E00B8EEAE /* NotificationsController.swift */; };
+ AFC8BA9F1EBD51A700B8EEAE /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFC8BA9E1EBD51A700B8EEAE /* Environment.swift */; };
+ AFC8BAA71EC257D800B8EEAE /* FIRSampleAppUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = AFC8BAA31EC257D800B8EEAE /* FIRSampleAppUtilities.m */; };
+ AFD5630C1EB1400900EA2233 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AFD563081EB1400900EA2233 /* LaunchScreen.storyboard */; };
+ AFD5630D1EB1400900EA2233 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AFD5630A1EB1400900EA2233 /* Main.storyboard */; };
+ AFD5630E1EB1402300EA2233 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFD562FF1EB13DF200EA2233 /* AppDelegate.swift */; };
+ AFD5630F1EB1402300EA2233 /* MessagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFD563011EB13DF200EA2233 /* MessagingViewController.swift */; };
+ AFD563151EB29EDE00EA2233 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AFD563131EB1466100EA2233 /* GoogleService-Info.plist */; };
+ AFD563171EBBEF7B00EA2233 /* Data+MessagingExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFD563161EBBEF7B00EA2233 /* Data+MessagingExtensions.swift */; };
+ BDE625D72CA3B8918088E0F5 /* Pods_Storage_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA7879CD6EE51EE4E20937C8 /* Pods_Storage_Example.framework */; };
+ DE0E5BBB1EA7D92E00FAA825 /* FIRVerifyClientRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE0E5BB91EA7D92E00FAA825 /* FIRVerifyClientRequestTest.m */; };
+ DE0E5BBC1EA7D92E00FAA825 /* FIRVerifyClientResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE0E5BBA1EA7D92E00FAA825 /* FIRVerifyClientResponseTests.m */; };
+ DE0E5BBD1EA7D93100FAA825 /* FIRAuthAppCredentialTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE0E5BB51EA7D91C00FAA825 /* FIRAuthAppCredentialTests.m */; };
+ DE0E5BBE1EA7D93500FAA825 /* FIRAuthAppDelegateProxyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE0E5BB61EA7D91C00FAA825 /* FIRAuthAppDelegateProxyTests.m */; };
+ DE4E711B1E953ABC00070092 /* FirebaseDev.podspec in Resources */ = {isa = PBXBuildFile; fileRef = DE4E711A1E953ABC00070092 /* FirebaseDev.podspec */; };
+ DE6F01B01E95675E004AEE01 /* FirebaseDev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE6F01AC1E95673C004AEE01 /* FirebaseDev.framework */; };
+ DE750DBD1EB3DD5B00A75E47 /* FIRAuthAPNSTokenTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE750DB61EB3DD4000A75E47 /* FIRAuthAPNSTokenTests.m */; };
+ DE750DBE1EB3DD6800A75E47 /* FIRAuthAPNSTokenManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE750DB51EB3DD4000A75E47 /* FIRAuthAPNSTokenManagerTests.m */; };
+ DE750DBF1EB3DD6C00A75E47 /* FIRAuthAppCredentialManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE750DB71EB3DD4000A75E47 /* FIRAuthAppCredentialManagerTests.m */; };
+ DE750DC01EB3DD6F00A75E47 /* FIRAuthNotificationManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE750DB81EB3DD4000A75E47 /* FIRAuthNotificationManagerTests.m */; };
+ DE7B8DB61E8EF203009EB6DF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DE7B8D691E8EF202009EB6DF /* InfoPlist.strings */; };
+ DE7B8DBE1E8EF203009EB6DF /* FDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D791E8EF202009EB6DF /* FDevice.m */; };
+ DE7B8DBF1E8EF203009EB6DF /* FEventTester.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D7B1E8EF202009EB6DF /* FEventTester.m */; };
+ DE7B8DC01E8EF203009EB6DF /* FIRTestAuthTokenProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D7D1E8EF202009EB6DF /* FIRTestAuthTokenProvider.m */; };
+ DE7B8DC11E8EF203009EB6DF /* FMockStorageEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D7F1E8EF202009EB6DF /* FMockStorageEngine.m */; };
+ DE7B8DC21E8EF203009EB6DF /* FTestAuthTokenGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D811E8EF202009EB6DF /* FTestAuthTokenGenerator.m */; };
+ DE7B8DC31E8EF203009EB6DF /* FTestCachePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D831E8EF202009EB6DF /* FTestCachePolicy.m */; };
+ DE7B8DC41E8EF203009EB6DF /* FTestClock.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D851E8EF202009EB6DF /* FTestClock.m */; };
+ DE7B8DC51E8EF203009EB6DF /* FTestExpectations.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D871E8EF202009EB6DF /* FTestExpectations.m */; };
+ DE7B8DC61E8EF203009EB6DF /* FTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D891E8EF202009EB6DF /* FTestHelpers.m */; };
+ DE7B8DC71E8EF203009EB6DF /* FTupleEventTypeString.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D8B1E8EF203009EB6DF /* FTupleEventTypeString.m */; };
+ DE7B8DC81E8EF203009EB6DF /* SenTest+FWaiter.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D8D1E8EF203009EB6DF /* SenTest+FWaiter.m */; };
+ DE7B8DC91E8EF203009EB6DF /* syncPointSpec.json in Resources */ = {isa = PBXBuildFile; fileRef = DE7B8D8E1E8EF203009EB6DF /* syncPointSpec.json */; };
+ DE7B8DCA1E8EF23A009EB6DF /* FIRAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D321E8EF202009EB6DF /* FIRAppDelegate.m */; };
+ DE7B8DCB1E8EF23A009EB6DF /* FIRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D341E8EF202009EB6DF /* FIRViewController.m */; };
+ 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 */; };
+ DE7B8DD31E8F1CA7009EB6DF /* Database-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DE7B8DD21E8F1CA7009EB6DF /* Database-Info.plist */; };
+ DE9315261E86C6FF0083EDBF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE9314ED1E86C6FF0083EDBF /* LaunchScreen.storyboard */; };
+ DE9315271E86C6FF0083EDBF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE9314EF1E86C6FF0083EDBF /* Main.storyboard */; };
+ DE9315291E86C6FF0083EDBF /* FIRAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9314F31E86C6FF0083EDBF /* FIRAppDelegate.m */; };
+ DE93152A1E86C6FF0083EDBF /* FIRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9314F51E86C6FF0083EDBF /* FIRViewController.m */; };
+ DE93152B1E86C6FF0083EDBF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DE9314F61E86C6FF0083EDBF /* GoogleService-Info.plist */; };
+ DE93152D1E86C6FF0083EDBF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9314F81E86C6FF0083EDBF /* main.m */; };
+ DE9315571E86C71C0083EDBF /* FIRAdditionalUserInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9314FA1E86C6FF0083EDBF /* FIRAdditionalUserInfoTests.m */; };
+ DE9315581E86C71C0083EDBF /* FIRApp+FIRAuthUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9314FC1E86C6FF0083EDBF /* FIRApp+FIRAuthUnitTests.m */; };
+ DE9315591E86C71C0083EDBF /* FIRAuthBackendCreateAuthURITests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9314FD1E86C6FF0083EDBF /* FIRAuthBackendCreateAuthURITests.m */; };
+ DE93155A1E86C71C0083EDBF /* FIRAuthBackendRPCImplementationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9314FE1E86C6FF0083EDBF /* FIRAuthBackendRPCImplementationTests.m */; };
+ DE93155B1E86C71C0083EDBF /* FIRAuthDispatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9314FF1E86C6FF0083EDBF /* FIRAuthDispatcherTests.m */; };
+ DE93155C1E86C71C0083EDBF /* FIRAuthGlobalWorkQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315001E86C6FF0083EDBF /* FIRAuthGlobalWorkQueueTests.m */; };
+ DE93155D1E86C71C0083EDBF /* FIRAuthKeychainTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315011E86C6FF0083EDBF /* FIRAuthKeychainTests.m */; };
+ DE93155E1E86C71C0083EDBF /* FIRAuthSerialTaskQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315021E86C6FF0083EDBF /* FIRAuthSerialTaskQueueTests.m */; };
+ DE93155F1E86C71C0083EDBF /* FIRAuthTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315031E86C6FF0083EDBF /* FIRAuthTests.m */; };
+ DE9315601E86C71C0083EDBF /* FIRAuthUserDefaultsStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315041E86C6FF0083EDBF /* FIRAuthUserDefaultsStorageTests.m */; };
+ DE9315611E86C71C0083EDBF /* FIRCreateAuthURIRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315051E86C6FF0083EDBF /* FIRCreateAuthURIRequestTests.m */; };
+ DE9315621E86C71C0083EDBF /* FIRCreateAuthURIResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315061E86C6FF0083EDBF /* FIRCreateAuthURIResponseTests.m */; };
+ DE9315631E86C71C0083EDBF /* FIRDeleteAccountRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315071E86C6FF0083EDBF /* FIRDeleteAccountRequestTests.m */; };
+ DE9315641E86C71C0083EDBF /* FIRDeleteAccountResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315081E86C6FF0083EDBF /* FIRDeleteAccountResponseTests.m */; };
+ DE9315651E86C71C0083EDBF /* FIRFakeBackendRPCIssuer.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93150A1E86C6FF0083EDBF /* FIRFakeBackendRPCIssuer.m */; };
+ DE9315661E86C71C0083EDBF /* FIRGetAccountInfoRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93150B1E86C6FF0083EDBF /* FIRGetAccountInfoRequestTests.m */; };
+ DE9315671E86C71C0083EDBF /* FIRGetAccountInfoResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93150C1E86C6FF0083EDBF /* FIRGetAccountInfoResponseTests.m */; };
+ DE9315681E86C71C0083EDBF /* FIRGetOOBConfirmationCodeRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93150D1E86C6FF0083EDBF /* FIRGetOOBConfirmationCodeRequestTests.m */; };
+ DE9315691E86C71C0083EDBF /* FIRGetOOBConfirmationCodeResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93150E1E86C6FF0083EDBF /* FIRGetOOBConfirmationCodeResponseTests.m */; };
+ DE93156A1E86C71C0083EDBF /* FIRGitHubAuthProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93150F1E86C6FF0083EDBF /* FIRGitHubAuthProviderTests.m */; };
+ DE93156C1E86C71C0083EDBF /* FIRResetPasswordRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315111E86C6FF0083EDBF /* FIRResetPasswordRequestTests.m */; };
+ DE93156D1E86C71C0083EDBF /* FIRResetPasswordResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315121E86C6FF0083EDBF /* FIRResetPasswordResponseTests.m */; };
+ DE93156E1E86C71C0083EDBF /* FIRSendVerificationCodeRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315131E86C6FF0083EDBF /* FIRSendVerificationCodeRequestTests.m */; };
+ DE93156F1E86C71C0083EDBF /* FIRSendVerificationCodeResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315141E86C6FF0083EDBF /* FIRSendVerificationCodeResponseTests.m */; };
+ DE9315701E86C71C0083EDBF /* FIRSetAccountInfoRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315151E86C6FF0083EDBF /* FIRSetAccountInfoRequestTests.m */; };
+ DE9315711E86C71C0083EDBF /* FIRSetAccountInfoResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315161E86C6FF0083EDBF /* FIRSetAccountInfoResponseTests.m */; };
+ DE9315721E86C71C0083EDBF /* FIRSignUpNewUserRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315171E86C6FF0083EDBF /* FIRSignUpNewUserRequestTests.m */; };
+ DE9315731E86C71C0083EDBF /* FIRSignUpNewUserResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315181E86C6FF0083EDBF /* FIRSignUpNewUserResponseTests.m */; };
+ DE9315741E86C71C0083EDBF /* FIRTwitterAuthProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315191E86C6FF0083EDBF /* FIRTwitterAuthProviderTests.m */; };
+ DE9315751E86C71C0083EDBF /* FIRUserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93151A1E86C6FF0083EDBF /* FIRUserTests.m */; };
+ DE9315761E86C71C0083EDBF /* FIRVerifyAssertionRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93151B1E86C6FF0083EDBF /* FIRVerifyAssertionRequestTests.m */; };
+ DE9315771E86C71C0083EDBF /* FIRVerifyAssertionResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93151C1E86C6FF0083EDBF /* FIRVerifyAssertionResponseTests.m */; };
+ DE9315781E86C71C0083EDBF /* FIRVerifyCustomTokenRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93151D1E86C6FF0083EDBF /* FIRVerifyCustomTokenRequestTests.m */; };
+ DE9315791E86C71C0083EDBF /* FIRVerifyCustomTokenResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93151E1E86C6FF0083EDBF /* FIRVerifyCustomTokenResponseTests.m */; };
+ DE93157A1E86C71C0083EDBF /* FIRVerifyPasswordRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE93151F1E86C6FF0083EDBF /* FIRVerifyPasswordRequestTest.m */; };
+ DE93157B1E86C71C0083EDBF /* FIRVerifyPasswordResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315201E86C6FF0083EDBF /* FIRVerifyPasswordResponseTests.m */; };
+ DE93157C1E86C71C0083EDBF /* FIRVerifyPhoneNumberRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315211E86C6FF0083EDBF /* FIRVerifyPhoneNumberRequestTests.m */; };
+ DE93157D1E86C71C0083EDBF /* FIRVerifyPhoneNumberResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315221E86C6FF0083EDBF /* FIRVerifyPhoneNumberResponseTests.m */; };
+ DE93157E1E86C71C0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315241E86C6FF0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.m */; };
+ DE9315F41E8738E60083EDBF /* FIRMessagingClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C31E8738B70083EDBF /* FIRMessagingClientTest.m */; };
+ DE9315F51E8738E60083EDBF /* FIRMessagingCodedInputStreamTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C41E8738B70083EDBF /* FIRMessagingCodedInputStreamTest.m */; };
+ DE9315F61E8738E60083EDBF /* FIRMessagingConnectionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C51E8738B70083EDBF /* FIRMessagingConnectionTest.m */; };
+ DE9315F71E8738E60083EDBF /* FIRMessagingContextManagerServiceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C61E8738B70083EDBF /* FIRMessagingContextManagerServiceTest.m */; };
+ DE9315F81E8738E60083EDBF /* FIRMessagingDataMessageManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C71E8738B70083EDBF /* FIRMessagingDataMessageManagerTest.m */; };
+ DE9315F91E8738E60083EDBF /* FIRMessagingFakeConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C91E8738B70083EDBF /* FIRMessagingFakeConnection.m */; };
+ DE9315FA1E8738E60083EDBF /* FIRMessagingFakeSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CB1E8738B70083EDBF /* FIRMessagingFakeSocket.m */; };
+ DE9315FB1E8738E60083EDBF /* FIRMessagingLinkHandlingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CC1E8738B70083EDBF /* FIRMessagingLinkHandlingTest.m */; };
+ DE9315FC1E8738E60083EDBF /* FIRMessagingPendingTopicsListTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CD1E8738B70083EDBF /* FIRMessagingPendingTopicsListTest.m */; };
+ DE9315FD1E8738E60083EDBF /* FIRMessagingPubSubTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CE1E8738B70083EDBF /* FIRMessagingPubSubTest.m */; };
+ DE9315FE1E8738E60083EDBF /* FIRMessagingRegistrarTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CF1E8738B70083EDBF /* FIRMessagingRegistrarTest.m */; };
+ DE9315FF1E8738E60083EDBF /* FIRMessagingRemoteNotificationsProxyTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D01E8738B70083EDBF /* FIRMessagingRemoteNotificationsProxyTest.m */; };
+ DE9316001E8738E60083EDBF /* FIRMessagingRmqManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D11E8738B70083EDBF /* FIRMessagingRmqManagerTest.m */; };
+ DE9316011E8738E60083EDBF /* FIRMessagingSecureSocketTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D21E8738B70083EDBF /* FIRMessagingSecureSocketTest.m */; };
+ DE9316021E8738E60083EDBF /* FIRMessagingServiceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D31E8738B70083EDBF /* FIRMessagingServiceTest.m */; };
+ 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 */; };
+ DEB139F41E73506A00AC236D /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; };
+ DEB139F51E73506A00AC236D /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
+ DEB139F61E73506A00AC236D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; };
+ DEB13A271E73518B00AC236D /* FIRStorageDeleteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB139C11E734D9D00AC236D /* FIRStorageDeleteTests.m */; };
+ DEB13A281E73518B00AC236D /* FIRStorageGetMetadataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB139C21E734D9D00AC236D /* FIRStorageGetMetadataTests.m */; };
+ DEB13A291E73518B00AC236D /* FIRStorageMetadataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB139C31E734D9D00AC236D /* FIRStorageMetadataTests.m */; };
+ DEB13A2A1E73518B00AC236D /* FIRStoragePathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB139C41E734D9D00AC236D /* FIRStoragePathTests.m */; };
+ DEB13A2B1E73518B00AC236D /* FIRStorageReferenceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB139C51E734D9D00AC236D /* FIRStorageReferenceTests.m */; };
+ DEB13A2C1E73518B00AC236D /* FIRStorageTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB139C71E734D9D00AC236D /* FIRStorageTestHelpers.m */; };
+ DEB13A2D1E73518B00AC236D /* FIRStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB139C81E734D9D00AC236D /* FIRStorageTests.m */; };
+ DEB13A2E1E73518B00AC236D /* FIRStorageTokenAuthorizerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB139C91E734D9D00AC236D /* FIRStorageTokenAuthorizerTests.m */; };
+ DEB13A2F1E73518B00AC236D /* FIRStorageUpdateMetadataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB139CA1E734D9D00AC236D /* FIRStorageUpdateMetadataTests.m */; };
+ DEB13A301E73518B00AC236D /* FIRStorageUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB139CB1E734D9D00AC236D /* FIRStorageUtilsTests.m */; };
+ DEB61EC51E7C5DBB00C04B96 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEB61EB91E7C5DBB00C04B96 /* LaunchScreen.storyboard */; };
+ DEB61EC61E7C5DBB00C04B96 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEB61EBB1E7C5DBB00C04B96 /* Main.storyboard */; };
+ DEB61EC71E7C5DBB00C04B96 /* FIRAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB61EBE1E7C5DBB00C04B96 /* FIRAppDelegate.m */; };
+ DEB61EC81E7C5DBB00C04B96 /* FIRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB61EC01E7C5DBB00C04B96 /* FIRViewController.m */; };
+ DEB61EC91E7C5DBB00C04B96 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DEB61EC11E7C5DBB00C04B96 /* GoogleService-Info.plist */; };
+ DEB61ECB1E7C5DBB00C04B96 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DEB61EC31E7C5DBB00C04B96 /* main.m */; };
+ DEC0EE0D1EA427CC007E2177 /* FirebaseDev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE6F01B31E9567F1004AEE01 /* FirebaseDev.framework */; };
+ DEC0EE0F1EA42D5D007E2177 /* FirebaseDev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEC0EE0E1EA42D5D007E2177 /* FirebaseDev.framework */; };
+ DEC0EE111EA42D73007E2177 /* FirebaseDev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEC0EE101EA42D73007E2177 /* FirebaseDev.framework */; };
+ DECE039B1E9ED01600164CA4 /* FIRPhoneAuthProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DECE03991E9ECFF500164CA4 /* FIRPhoneAuthProviderTests.m */; };
+ DEE13AA11EA170D500D1BABA /* FirebaseDev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE6F01B11E9567BF004AEE01 /* FirebaseDev.framework */; };
+ DEE14D7E1E844677006FA992 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEE14D681E844677006FA992 /* LaunchScreen.storyboard */; };
+ DEE14D7F1E844677006FA992 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEE14D6A1E844677006FA992 /* Main.storyboard */; };
+ DEE14D811E844677006FA992 /* FIRAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D6E1E844677006FA992 /* FIRAppDelegate.m */; };
+ DEE14D821E844677006FA992 /* FIRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D701E844677006FA992 /* FIRViewController.m */; };
+ DEE14D831E844677006FA992 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DEE14D711E844677006FA992 /* GoogleService-Info.plist */; };
+ DEE14D851E844677006FA992 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D731E844677006FA992 /* main.m */; };
+ DEE14D8E1E84468D006FA992 /* FIRAppAssociationRegistrationUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D751E844677006FA992 /* FIRAppAssociationRegistrationUnitTests.m */; };
+ DEE14D8F1E84468D006FA992 /* FIRAppTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D761E844677006FA992 /* FIRAppTest.m */; };
+ DEE14D901E84468D006FA992 /* FIRBundleUtilTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D771E844677006FA992 /* FIRBundleUtilTest.m */; };
+ DEE14D911E84468D006FA992 /* FIRConfigurationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D781E844677006FA992 /* FIRConfigurationTest.m */; };
+ DEE14D921E84468D006FA992 /* FIRLoggerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D791E844677006FA992 /* FIRLoggerTest.m */; };
+ DEE14D931E84468D006FA992 /* FIROptionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D7A1E844677006FA992 /* FIROptionsTest.m */; };
+ DEE14D941E84468D006FA992 /* FIRTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D7C1E844677006FA992 /* FIRTestCase.m */; };
+ EA9A4B8DCCA67EB6F9B4008F /* Pods_Messaging_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9D28B14E5B756D3A1938CB2 /* Pods_Messaging_Tests.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 06121EC61EC399D40008D70E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DEB139E01E73506A00AC236D;
+ remoteInfo = Storage_Example;
+ };
+ 0624F3E61EC0ECFA00E5940D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7B8D041E8EF077009EB6DF;
+ remoteInfo = Database_Example;
+ };
+ AFD563111EB140E100EA2233 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = AFD562E41EB13C6D00EA2233;
+ remoteInfo = Messaging_Example;
+ };
+ DE3373971E73776F00881891 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DEB13A0A1E73507E00AC236D;
+ remoteInfo = Storage_Tests;
+ };
+ DE6F01B91E957157004AEE01 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DE9315A61E8738460083EDBF;
+ remoteInfo = Messaging_Tests;
+ };
+ DE7B8D1E1E8EF078009EB6DF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7B8D041E8EF077009EB6DF;
+ remoteInfo = Database_Example;
+ };
+ DE9314DF1E86C6BE0083EDBF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DE9314C51E86C6BD0083EDBF;
+ remoteInfo = Auth_Example;
+ };
+ DE9315861E86E9990083EDBF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DE9314DD1E86C6BE0083EDBF;
+ remoteInfo = Auth_Tests;
+ };
+ DEB13A251E73512500AC236D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DEB139E01E73506A00AC236D;
+ remoteInfo = Storage_Example;
+ };
+ DEB518591E9008CB0089C938 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7B8D1C1E8EF078009EB6DF;
+ remoteInfo = Database_Tests;
+ };
+ DEE14D5A1E84464D006FA992 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DEE14D401E84464D006FA992;
+ remoteInfo = Core_Example;
+ };
+ DEE14E0A1E844FDC006FA992 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DEE14D581E84464D006FA992;
+ remoteInfo = Core_Tests;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ 06121EBC1EC399C50008D70E /* Storage_IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Storage_IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 06121ECA1EC39A0B0008D70E /* FIRStorageIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStorageIntegrationTests.m; sourceTree = "<group>"; };
+ 0624F3E11EC0ECFA00E5940D /* Database_IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Database_IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0637BA641EC0F99700CAEFD4 /* FirebaseDev.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FirebaseDev.framework; path = "../../../Library/Developer/Xcode/DerivedData/Firebase-dajssrvxpeovebatpcchwkfhwcjh/Build/Products/Debug-iphonesimulator/FirebaseDev-Core-Database-Root/FirebaseDev.framework"; sourceTree = "<group>"; };
+ 063CB4471EBA7AE200038A59 /* FArraySortedDictionaryTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FArraySortedDictionaryTest.m; path = Database/Tests/Unit/FArraySortedDictionaryTest.m; sourceTree = SOURCE_ROOT; };
+ 063CB4481EBA7AE200038A59 /* FCompoundHashTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FCompoundHashTest.m; path = Database/Tests/Unit/FCompoundHashTest.m; sourceTree = SOURCE_ROOT; };
+ 063CB4491EBA7AE200038A59 /* FIRMutableDataTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FIRMutableDataTests.h; path = Database/Tests/Unit/FIRMutableDataTests.h; sourceTree = SOURCE_ROOT; };
+ 063CB44A1EBA7AE200038A59 /* FIRMutableDataTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRMutableDataTests.m; path = Database/Tests/Unit/FIRMutableDataTests.m; sourceTree = SOURCE_ROOT; };
+ 063CB44B1EBA7AE200038A59 /* FLevelDBStorageEngineTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLevelDBStorageEngineTests.m; path = Database/Tests/Unit/FLevelDBStorageEngineTests.m; sourceTree = SOURCE_ROOT; };
+ 063CB44C1EBA7AE200038A59 /* FNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FNodeTests.m; path = Database/Tests/Unit/FNodeTests.m; sourceTree = SOURCE_ROOT; };
+ 063CB44D1EBA7AE200038A59 /* FPathTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FPathTests.h; path = Database/Tests/Unit/FPathTests.h; sourceTree = SOURCE_ROOT; };
+ 063CB44E1EBA7AE200038A59 /* FPathTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FPathTests.m; path = Database/Tests/Unit/FPathTests.m; sourceTree = SOURCE_ROOT; };
+ 063CB44F1EBA7AE200038A59 /* FPersistenceManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FPersistenceManagerTest.m; path = Database/Tests/Unit/FPersistenceManagerTest.m; sourceTree = SOURCE_ROOT; };
+ 063CB4501EBA7AE200038A59 /* FPruneForestTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FPruneForestTest.m; path = Database/Tests/Unit/FPruneForestTest.m; sourceTree = SOURCE_ROOT; };
+ 063CB4511EBA7AE200038A59 /* FPruningTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FPruningTest.m; path = Database/Tests/Unit/FPruningTest.m; sourceTree = SOURCE_ROOT; };
+ 063CB4521EBA7AE200038A59 /* FQueryParamsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FQueryParamsTest.m; path = Database/Tests/Unit/FQueryParamsTest.m; sourceTree = SOURCE_ROOT; };
+ 063CB4531EBA7AE200038A59 /* FRangeMergeTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FRangeMergeTest.m; path = Database/Tests/Unit/FRangeMergeTest.m; sourceTree = SOURCE_ROOT; };
+ 063CB4541EBA7AE200038A59 /* FRepoInfoTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FRepoInfoTest.m; path = Database/Tests/Unit/FRepoInfoTest.m; sourceTree = SOURCE_ROOT; };
+ 063CB4551EBA7AE200038A59 /* FSparseSnapshotTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FSparseSnapshotTests.h; path = Database/Tests/Unit/FSparseSnapshotTests.h; sourceTree = SOURCE_ROOT; };
+ 063CB4561EBA7AE200038A59 /* FSparseSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSparseSnapshotTests.m; path = Database/Tests/Unit/FSparseSnapshotTests.m; sourceTree = SOURCE_ROOT; };
+ 063CB4571EBA7AE200038A59 /* FSyncPointTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FSyncPointTests.h; path = Database/Tests/Unit/FSyncPointTests.h; sourceTree = SOURCE_ROOT; };
+ 063CB4581EBA7AE200038A59 /* FSyncPointTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSyncPointTests.m; path = Database/Tests/Unit/FSyncPointTests.m; sourceTree = SOURCE_ROOT; };
+ 063CB4591EBA7AE200038A59 /* FTestBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FTestBase.h; path = Database/Tests/Helpers/FTestBase.h; sourceTree = SOURCE_ROOT; };
+ 063CB45A1EBA7AE200038A59 /* FTestBase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FTestBase.m; path = Database/Tests/Helpers/FTestBase.m; sourceTree = SOURCE_ROOT; };
+ 063CB45B1EBA7AE200038A59 /* FTrackedQueryManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FTrackedQueryManagerTest.m; path = Database/Tests/Unit/FTrackedQueryManagerTest.m; sourceTree = SOURCE_ROOT; };
+ 063CB45C1EBA7AE200038A59 /* FUtilitiesTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FUtilitiesTest.m; path = Database/Tests/Unit/FUtilitiesTest.m; sourceTree = SOURCE_ROOT; };
+ 063CB46E1EBA7AEF00038A59 /* FCompoundWriteTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FCompoundWriteTest.m; sourceTree = "<group>"; };
+ 063CB46F1EBA7AEF00038A59 /* FConnectionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FConnectionTest.m; path = Integration/FConnectionTest.m; sourceTree = "<group>"; };
+ 063CB4701EBA7AEF00038A59 /* FData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FData.h; path = Integration/FData.h; sourceTree = "<group>"; };
+ 063CB4711EBA7AEF00038A59 /* FData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FData.m; path = Integration/FData.m; sourceTree = "<group>"; };
+ 063CB4721EBA7AEF00038A59 /* FDotInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FDotInfo.h; path = Integration/FDotInfo.h; sourceTree = "<group>"; };
+ 063CB4731EBA7AEF00038A59 /* FDotInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FDotInfo.m; path = Integration/FDotInfo.m; sourceTree = "<group>"; };
+ 063CB4741EBA7AEF00038A59 /* FEventTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FEventTests.h; path = Integration/FEventTests.h; sourceTree = "<group>"; };
+ 063CB4751EBA7AEF00038A59 /* FEventTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FEventTests.m; path = Integration/FEventTests.m; sourceTree = "<group>"; };
+ 063CB4761EBA7AEF00038A59 /* FIRAuthTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthTests.m; path = Integration/FIRAuthTests.m; sourceTree = "<group>"; };
+ 063CB4771EBA7AEF00038A59 /* FIRDatabaseQueryTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FIRDatabaseQueryTests.h; path = Integration/FIRDatabaseQueryTests.h; sourceTree = "<group>"; };
+ 063CB4781EBA7AEF00038A59 /* FIRDatabaseQueryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRDatabaseQueryTests.m; path = Integration/FIRDatabaseQueryTests.m; sourceTree = "<group>"; };
+ 063CB4791EBA7AEF00038A59 /* FIRDatabaseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRDatabaseTests.m; path = Integration/FIRDatabaseTests.m; sourceTree = "<group>"; };
+ 063CB47A1EBA7AEF00038A59 /* FIRDataSnapshotTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRDataSnapshotTests.h; sourceTree = "<group>"; };
+ 063CB47B1EBA7AEF00038A59 /* FIRDataSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRDataSnapshotTests.m; sourceTree = "<group>"; };
+ 063CB47C1EBA7AEF00038A59 /* FirebaseTests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "FirebaseTests-Info.plist"; sourceTree = "<group>"; };
+ 063CB47D1EBA7AEF00038A59 /* FIRFakeApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRFakeApp.h; sourceTree = "<group>"; };
+ 063CB47E1EBA7AEF00038A59 /* FIRFakeApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRFakeApp.m; sourceTree = "<group>"; };
+ 063CB47F1EBA7AEF00038A59 /* FKeepSyncedTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FKeepSyncedTest.m; path = Integration/FKeepSyncedTest.m; sourceTree = "<group>"; };
+ 063CB4801EBA7AEF00038A59 /* FOrder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FOrder.h; path = Integration/FOrder.h; sourceTree = "<group>"; };
+ 063CB4811EBA7AEF00038A59 /* FOrder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FOrder.m; path = Integration/FOrder.m; sourceTree = "<group>"; };
+ 063CB4821EBA7AEF00038A59 /* FOrderByTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FOrderByTests.h; path = Integration/FOrderByTests.h; sourceTree = "<group>"; };
+ 063CB4831EBA7AEF00038A59 /* FOrderByTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FOrderByTests.m; path = Integration/FOrderByTests.m; sourceTree = "<group>"; };
+ 063CB4841EBA7AEF00038A59 /* FPersist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FPersist.h; path = Integration/FPersist.h; sourceTree = "<group>"; };
+ 063CB4851EBA7AEF00038A59 /* FPersist.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FPersist.m; path = Integration/FPersist.m; sourceTree = "<group>"; };
+ 063CB4861EBA7AEF00038A59 /* FRealtime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FRealtime.h; path = Integration/FRealtime.h; sourceTree = "<group>"; };
+ 063CB4871EBA7AEF00038A59 /* FRealtime.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FRealtime.m; path = Integration/FRealtime.m; sourceTree = "<group>"; };
+ 063CB48D1EBA7AEF00038A59 /* FTestContants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FTestContants.h; sourceTree = "<group>"; };
+ 063CB48E1EBA7AEF00038A59 /* FTransactionTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FTransactionTest.h; path = Integration/FTransactionTest.h; sourceTree = "<group>"; };
+ 063CB48F1EBA7AEF00038A59 /* FTransactionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FTransactionTest.m; path = Integration/FTransactionTest.m; sourceTree = "<group>"; };
+ 063CB4901EBA7AEF00038A59 /* FTreeSortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FTreeSortedDictionaryTests.m; sourceTree = "<group>"; };
+ 0672F2F11EBBA7D900818E87 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
+ 069428801EC3B35A00F7BC69 /* 1mb.dat */ = {isa = PBXFileReference; lastKnownFileType = file; path = 1mb.dat; sourceTree = "<group>"; };
+ 0697B1201EC13D8A00542174 /* Base64.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Base64.h; sourceTree = "<group>"; };
+ 0697B1211EC13D8A00542174 /* Base64.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Base64.m; sourceTree = "<group>"; };
+ 06B47E8B1EC39ADF00170C02 /* FirebaseDev.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FirebaseDev.framework; path = "../../../Library/Developer/Xcode/DerivedData/Firebase-dajssrvxpeovebatpcchwkfhwcjh/Build/Products/Debug-iphonesimulator/FirebaseDev-Core-Root-Storage/FirebaseDev.framework"; sourceTree = "<group>"; };
+ 08A821396D7D1089ECE810EF /* Pods_Core_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Core_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0B1BDA534E1F49931795B5E6 /* Pods-Core_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Core_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Core_Tests/Pods-Core_Tests.release.xcconfig"; sourceTree = "<group>"; };
+ 16E92590A6B517109A2B219F /* Pods_Storage_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Storage_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 18B5255FF5BEBF6F72C40F39 /* Pods-Auth_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Auth_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Auth_Tests/Pods-Auth_Tests.debug.xcconfig"; sourceTree = "<group>"; };
+ 1EEA0F965ABC48C695972509 /* Pods_Auth_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Auth_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 2F002D4E7FA7F07A830CCFDA /* Pods-Auth_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Auth_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Auth_Example/Pods-Auth_Example.release.xcconfig"; sourceTree = "<group>"; };
+ 3673564CCB64DE360C8CB97F /* Pods-Storage_IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Storage_IntegrationTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Storage_IntegrationTests/Pods-Storage_IntegrationTests.debug.xcconfig"; sourceTree = "<group>"; };
+ 36DF4C7B93E6FE7AD8F88A38 /* Pods_Storage_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Storage_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3E84D28D93B8196D6A483F15 /* Pods-Storage_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Storage_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Storage_Tests/Pods-Storage_Tests.debug.xcconfig"; sourceTree = "<group>"; };
+ 4A8B7AE7C053949F6BBBDD3E /* Pods-Database_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Database_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Database_Tests/Pods-Database_Tests.release.xcconfig"; sourceTree = "<group>"; };
+ 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; };
+ 60FCE4043D8FE42648646A7F /* Pods-Auth_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Auth_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Auth_Tests/Pods-Auth_Tests.release.xcconfig"; sourceTree = "<group>"; };
+ 64928F2997FAF0EAEAC9B8CA /* Pods_Database_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Database_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 66C7EEA21795A3320088DEBE /* Pods_Database_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Database_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 6A0FCB2A37144B3C05E519F6 /* Pods-Storage_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Storage_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Storage_Example/Pods-Storage_Example.debug.xcconfig"; sourceTree = "<group>"; };
+ 6BAD1CF3DDEDDD76EC87052D /* Pods-Storage_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Storage_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Storage_Example/Pods-Storage_Example.release.xcconfig"; sourceTree = "<group>"; };
+ 6D2E4A9396D707C5DEF9B74B /* Pods-Messaging_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Messaging_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Messaging_Tests/Pods-Messaging_Tests.release.xcconfig"; sourceTree = "<group>"; };
+ 6E974DE29EBB9602E723757E /* Pods-Messaging_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Messaging_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Messaging_Tests/Pods-Messaging_Tests.debug.xcconfig"; sourceTree = "<group>"; };
+ 7727BC17692B98E2B7D0EA7A /* Pods-Database_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Database_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Database_Example/Pods-Database_Example.debug.xcconfig"; sourceTree = "<group>"; };
+ 8496034D8156555C5FCF8F14 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
+ 884B87C50C7C950BC18E9091 /* Pods-Messaging_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Messaging_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Messaging_Example/Pods-Messaging_Example.debug.xcconfig"; sourceTree = "<group>"; };
+ 8E32E359BE29C3100CF51FC4 /* Pods-Core_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Core_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Core_Tests/Pods-Core_Tests.debug.xcconfig"; sourceTree = "<group>"; };
+ 8F77C04C2E764FBB0F6C05C6 /* Pods-Core_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Core_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Core_Example/Pods-Core_Example.release.xcconfig"; sourceTree = "<group>"; };
+ A6903B88963F6FD1857889E6 /* Pods-Messaging_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Messaging_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Messaging_Example/Pods-Messaging_Example.release.xcconfig"; sourceTree = "<group>"; };
+ AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Shared.xcassets; path = Shared/Shared.xcassets; sourceTree = "<group>"; };
+ AFC8BA9C1EBD230E00B8EEAE /* NotificationsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotificationsController.swift; path = App/NotificationsController.swift; sourceTree = "<group>"; };
+ AFC8BA9E1EBD51A700B8EEAE /* Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Environment.swift; path = App/Environment.swift; sourceTree = "<group>"; };
+ AFC8BAA11EC257D700B8EEAE /* Messaging_Example-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Messaging_Example-Bridging-Header.h"; sourceTree = "<group>"; };
+ AFC8BAA21EC257D800B8EEAE /* FIRSampleAppUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FIRSampleAppUtilities.h; path = Shared/FIRSampleAppUtilities.h; sourceTree = "<group>"; };
+ AFC8BAA31EC257D800B8EEAE /* FIRSampleAppUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRSampleAppUtilities.m; path = Shared/FIRSampleAppUtilities.m; sourceTree = "<group>"; };
+ AFD562E51EB13C6D00EA2233 /* Messaging_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Messaging_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ AFD562FF1EB13DF200EA2233 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = App/AppDelegate.swift; sourceTree = "<group>"; };
+ AFD563001EB13DF200EA2233 /* Messaging-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Messaging-Info.plist"; path = "App/Messaging-Info.plist"; sourceTree = "<group>"; };
+ AFD563011EB13DF200EA2233 /* MessagingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessagingViewController.swift; path = App/MessagingViewController.swift; sourceTree = "<group>"; };
+ AFD563091EB1400900EA2233 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = App/Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ AFD5630B1EB1400900EA2233 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = App/Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ AFD563131EB1466100EA2233 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "App/GoogleService-Info.plist"; sourceTree = "<group>"; };
+ AFD563141EB29B8C00EA2233 /* Messaging_Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Messaging_Example.entitlements; path = App/Messaging_Example.entitlements; sourceTree = "<group>"; };
+ AFD563161EBBEF7B00EA2233 /* Data+MessagingExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Data+MessagingExtensions.swift"; path = "App/Data+MessagingExtensions.swift"; sourceTree = "<group>"; };
+ BEEA177FFAAB9FA02F898C51 /* Pods-Database_IntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Database_IntegrationTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Database_IntegrationTests/Pods-Database_IntegrationTests.release.xcconfig"; sourceTree = "<group>"; };
+ C45949C3AB12F54D27702387 /* Pods-Auth_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Auth_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Auth_Example/Pods-Auth_Example.debug.xcconfig"; sourceTree = "<group>"; };
+ C8A6D15690286B6BB4CB8023 /* Pods_Messaging_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Messaging_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ CA86AD35456DA6130F7DE02C /* Pods-Storage_IntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Storage_IntegrationTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Storage_IntegrationTests/Pods-Storage_IntegrationTests.release.xcconfig"; sourceTree = "<group>"; };
+ D52CEDD0146DF63640A4C3A5 /* Pods_Core_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Core_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ D58064F9C4DE303997B89D2E /* Pods-Storage_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Storage_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Storage_Tests/Pods-Storage_Tests.release.xcconfig"; sourceTree = "<group>"; };
+ DA7879CD6EE51EE4E20937C8 /* Pods_Storage_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Storage_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ DE0E5BB51EA7D91C00FAA825 /* FIRAuthAppCredentialTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthAppCredentialTests.m; sourceTree = "<group>"; };
+ DE0E5BB61EA7D91C00FAA825 /* FIRAuthAppDelegateProxyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthAppDelegateProxyTests.m; sourceTree = "<group>"; };
+ DE0E5BB91EA7D92E00FAA825 /* FIRVerifyClientRequestTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyClientRequestTest.m; sourceTree = "<group>"; };
+ DE0E5BBA1EA7D92E00FAA825 /* FIRVerifyClientResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyClientResponseTests.m; sourceTree = "<group>"; };
+ DE45C6641E7DA8CB009E6ACD /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
+ DE4E711A1E953ABC00070092 /* FirebaseDev.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = FirebaseDev.podspec; path = ../FirebaseDev.podspec; sourceTree = "<group>"; };
+ DE6F01AC1E95673C004AEE01 /* FirebaseDev.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FirebaseDev.framework; path = "../../../Library/Developer/Xcode/DerivedData/Firebase-chgkzndqfwnawrfmbrhkugiybjre/Build/Products/Debug-iphonesimulator/FirebaseDev-Core-Root-Storage/FirebaseDev.framework"; sourceTree = "<group>"; };
+ DE6F01B11E9567BF004AEE01 /* FirebaseDev.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FirebaseDev.framework; path = "../../../Library/Developer/Xcode/DerivedData/Firebase-chgkzndqfwnawrfmbrhkugiybjre/Build/Products/Debug-iphonesimulator/FirebaseDev-Auth-Core-Root/FirebaseDev.framework"; sourceTree = "<group>"; };
+ DE6F01B31E9567F1004AEE01 /* FirebaseDev.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FirebaseDev.framework; path = "../../../Library/Developer/Xcode/DerivedData/Firebase-chgkzndqfwnawrfmbrhkugiybjre/Build/Products/Debug-iphonesimulator/FirebaseDev-Core/FirebaseDev.framework"; sourceTree = "<group>"; };
+ DE750DB51EB3DD4000A75E47 /* FIRAuthAPNSTokenManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthAPNSTokenManagerTests.m; sourceTree = "<group>"; };
+ DE750DB61EB3DD4000A75E47 /* FIRAuthAPNSTokenTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthAPNSTokenTests.m; sourceTree = "<group>"; };
+ DE750DB71EB3DD4000A75E47 /* FIRAuthAppCredentialManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthAppCredentialManagerTests.m; sourceTree = "<group>"; };
+ DE750DB81EB3DD4000A75E47 /* FIRAuthNotificationManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthNotificationManagerTests.m; sourceTree = "<group>"; };
+ DE7B8D051E8EF077009EB6DF /* Database_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Database_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ DE7B8D1D1E8EF078009EB6DF /* Database_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Database_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ DE7B8D2D1E8EF202009EB6DF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ DE7B8D2F1E8EF202009EB6DF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ DE7B8D311E8EF202009EB6DF /* FIRAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRAppDelegate.h; sourceTree = "<group>"; };
+ DE7B8D321E8EF202009EB6DF /* FIRAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAppDelegate.m; sourceTree = "<group>"; };
+ DE7B8D331E8EF202009EB6DF /* FIRViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRViewController.h; sourceTree = "<group>"; };
+ DE7B8D341E8EF202009EB6DF /* FIRViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRViewController.m; sourceTree = "<group>"; };
+ DE7B8D371E8EF202009EB6DF /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ DE7B8D6A1E8EF202009EB6DF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+ DE7B8D781E8EF202009EB6DF /* FDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FDevice.h; sourceTree = "<group>"; };
+ DE7B8D791E8EF202009EB6DF /* FDevice.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FDevice.m; sourceTree = "<group>"; };
+ DE7B8D7A1E8EF202009EB6DF /* FEventTester.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FEventTester.h; sourceTree = "<group>"; };
+ DE7B8D7B1E8EF202009EB6DF /* FEventTester.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FEventTester.m; sourceTree = "<group>"; };
+ DE7B8D7C1E8EF202009EB6DF /* FIRTestAuthTokenProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRTestAuthTokenProvider.h; sourceTree = "<group>"; };
+ DE7B8D7D1E8EF202009EB6DF /* FIRTestAuthTokenProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRTestAuthTokenProvider.m; sourceTree = "<group>"; };
+ DE7B8D7E1E8EF202009EB6DF /* FMockStorageEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMockStorageEngine.h; sourceTree = "<group>"; };
+ DE7B8D7F1E8EF202009EB6DF /* FMockStorageEngine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMockStorageEngine.m; sourceTree = "<group>"; };
+ DE7B8D801E8EF202009EB6DF /* FTestAuthTokenGenerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FTestAuthTokenGenerator.h; sourceTree = "<group>"; };
+ DE7B8D811E8EF202009EB6DF /* FTestAuthTokenGenerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FTestAuthTokenGenerator.m; sourceTree = "<group>"; };
+ DE7B8D821E8EF202009EB6DF /* FTestCachePolicy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FTestCachePolicy.h; sourceTree = "<group>"; };
+ DE7B8D831E8EF202009EB6DF /* FTestCachePolicy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FTestCachePolicy.m; sourceTree = "<group>"; };
+ DE7B8D841E8EF202009EB6DF /* FTestClock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FTestClock.h; sourceTree = "<group>"; };
+ DE7B8D851E8EF202009EB6DF /* FTestClock.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FTestClock.m; sourceTree = "<group>"; };
+ DE7B8D861E8EF202009EB6DF /* FTestExpectations.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FTestExpectations.h; sourceTree = "<group>"; };
+ DE7B8D871E8EF202009EB6DF /* FTestExpectations.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FTestExpectations.m; sourceTree = "<group>"; };
+ DE7B8D881E8EF202009EB6DF /* FTestHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FTestHelpers.h; sourceTree = "<group>"; };
+ DE7B8D891E8EF202009EB6DF /* FTestHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FTestHelpers.m; sourceTree = "<group>"; };
+ DE7B8D8A1E8EF203009EB6DF /* FTupleEventTypeString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FTupleEventTypeString.h; sourceTree = "<group>"; };
+ DE7B8D8B1E8EF203009EB6DF /* FTupleEventTypeString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FTupleEventTypeString.m; sourceTree = "<group>"; };
+ DE7B8D8C1E8EF203009EB6DF /* SenTest+FWaiter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SenTest+FWaiter.h"; sourceTree = "<group>"; };
+ DE7B8D8D1E8EF203009EB6DF /* SenTest+FWaiter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SenTest+FWaiter.m"; sourceTree = "<group>"; };
+ DE7B8D8E1E8EF203009EB6DF /* syncPointSpec.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = syncPointSpec.json; sourceTree = "<group>"; };
+ DE7B8DD21E8F1CA7009EB6DF /* Database-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Database-Info.plist"; sourceTree = "<group>"; };
+ DE9314C61E86C6BD0083EDBF /* Auth_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Auth_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ DE9314DE1E86C6BE0083EDBF /* Auth_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Auth_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ DE9314EE1E86C6FF0083EDBF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ DE9314F01E86C6FF0083EDBF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ DE9314F21E86C6FF0083EDBF /* FIRAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRAppDelegate.h; sourceTree = "<group>"; };
+ DE9314F31E86C6FF0083EDBF /* FIRAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAppDelegate.m; sourceTree = "<group>"; };
+ DE9314F41E86C6FF0083EDBF /* FIRViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRViewController.h; sourceTree = "<group>"; };
+ DE9314F51E86C6FF0083EDBF /* FIRViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRViewController.m; sourceTree = "<group>"; };
+ DE9314F61E86C6FF0083EDBF /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
+ DE9314F81E86C6FF0083EDBF /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ DE9314FA1E86C6FF0083EDBF /* FIRAdditionalUserInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAdditionalUserInfoTests.m; sourceTree = "<group>"; };
+ DE9314FB1E86C6FF0083EDBF /* FIRApp+FIRAuthUnitTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FIRApp+FIRAuthUnitTests.h"; sourceTree = "<group>"; };
+ DE9314FC1E86C6FF0083EDBF /* FIRApp+FIRAuthUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "FIRApp+FIRAuthUnitTests.m"; sourceTree = "<group>"; };
+ DE9314FD1E86C6FF0083EDBF /* FIRAuthBackendCreateAuthURITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthBackendCreateAuthURITests.m; sourceTree = "<group>"; };
+ DE9314FE1E86C6FF0083EDBF /* FIRAuthBackendRPCImplementationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthBackendRPCImplementationTests.m; sourceTree = "<group>"; };
+ DE9314FF1E86C6FF0083EDBF /* FIRAuthDispatcherTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthDispatcherTests.m; sourceTree = "<group>"; };
+ DE9315001E86C6FF0083EDBF /* FIRAuthGlobalWorkQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthGlobalWorkQueueTests.m; sourceTree = "<group>"; };
+ DE9315011E86C6FF0083EDBF /* FIRAuthKeychainTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthKeychainTests.m; sourceTree = "<group>"; };
+ DE9315021E86C6FF0083EDBF /* FIRAuthSerialTaskQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthSerialTaskQueueTests.m; sourceTree = "<group>"; };
+ DE9315031E86C6FF0083EDBF /* FIRAuthTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthTests.m; sourceTree = "<group>"; };
+ DE9315041E86C6FF0083EDBF /* FIRAuthUserDefaultsStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthUserDefaultsStorageTests.m; sourceTree = "<group>"; };
+ DE9315051E86C6FF0083EDBF /* FIRCreateAuthURIRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRCreateAuthURIRequestTests.m; sourceTree = "<group>"; };
+ DE9315061E86C6FF0083EDBF /* FIRCreateAuthURIResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRCreateAuthURIResponseTests.m; sourceTree = "<group>"; };
+ DE9315071E86C6FF0083EDBF /* FIRDeleteAccountRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRDeleteAccountRequestTests.m; sourceTree = "<group>"; };
+ DE9315081E86C6FF0083EDBF /* FIRDeleteAccountResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRDeleteAccountResponseTests.m; sourceTree = "<group>"; };
+ DE9315091E86C6FF0083EDBF /* FIRFakeBackendRPCIssuer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRFakeBackendRPCIssuer.h; sourceTree = "<group>"; };
+ DE93150A1E86C6FF0083EDBF /* FIRFakeBackendRPCIssuer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRFakeBackendRPCIssuer.m; sourceTree = "<group>"; };
+ DE93150B1E86C6FF0083EDBF /* FIRGetAccountInfoRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRGetAccountInfoRequestTests.m; sourceTree = "<group>"; };
+ DE93150C1E86C6FF0083EDBF /* FIRGetAccountInfoResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRGetAccountInfoResponseTests.m; sourceTree = "<group>"; };
+ DE93150D1E86C6FF0083EDBF /* FIRGetOOBConfirmationCodeRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRGetOOBConfirmationCodeRequestTests.m; sourceTree = "<group>"; };
+ DE93150E1E86C6FF0083EDBF /* FIRGetOOBConfirmationCodeResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRGetOOBConfirmationCodeResponseTests.m; sourceTree = "<group>"; };
+ DE93150F1E86C6FF0083EDBF /* FIRGitHubAuthProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRGitHubAuthProviderTests.m; sourceTree = "<group>"; };
+ DE9315111E86C6FF0083EDBF /* FIRResetPasswordRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRResetPasswordRequestTests.m; sourceTree = "<group>"; };
+ DE9315121E86C6FF0083EDBF /* FIRResetPasswordResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRResetPasswordResponseTests.m; sourceTree = "<group>"; };
+ DE9315131E86C6FF0083EDBF /* FIRSendVerificationCodeRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRSendVerificationCodeRequestTests.m; sourceTree = "<group>"; };
+ DE9315141E86C6FF0083EDBF /* FIRSendVerificationCodeResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRSendVerificationCodeResponseTests.m; sourceTree = "<group>"; };
+ DE9315151E86C6FF0083EDBF /* FIRSetAccountInfoRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRSetAccountInfoRequestTests.m; sourceTree = "<group>"; };
+ DE9315161E86C6FF0083EDBF /* FIRSetAccountInfoResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRSetAccountInfoResponseTests.m; sourceTree = "<group>"; };
+ DE9315171E86C6FF0083EDBF /* FIRSignUpNewUserRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRSignUpNewUserRequestTests.m; sourceTree = "<group>"; };
+ DE9315181E86C6FF0083EDBF /* FIRSignUpNewUserResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRSignUpNewUserResponseTests.m; sourceTree = "<group>"; };
+ DE9315191E86C6FF0083EDBF /* FIRTwitterAuthProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRTwitterAuthProviderTests.m; sourceTree = "<group>"; };
+ DE93151A1E86C6FF0083EDBF /* FIRUserTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRUserTests.m; sourceTree = "<group>"; };
+ DE93151B1E86C6FF0083EDBF /* FIRVerifyAssertionRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyAssertionRequestTests.m; sourceTree = "<group>"; };
+ DE93151C1E86C6FF0083EDBF /* FIRVerifyAssertionResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyAssertionResponseTests.m; sourceTree = "<group>"; };
+ DE93151D1E86C6FF0083EDBF /* FIRVerifyCustomTokenRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyCustomTokenRequestTests.m; sourceTree = "<group>"; };
+ DE93151E1E86C6FF0083EDBF /* FIRVerifyCustomTokenResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyCustomTokenResponseTests.m; sourceTree = "<group>"; };
+ DE93151F1E86C6FF0083EDBF /* FIRVerifyPasswordRequestTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyPasswordRequestTest.m; sourceTree = "<group>"; };
+ DE9315201E86C6FF0083EDBF /* FIRVerifyPasswordResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyPasswordResponseTests.m; sourceTree = "<group>"; };
+ DE9315211E86C6FF0083EDBF /* FIRVerifyPhoneNumberRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyPhoneNumberRequestTests.m; sourceTree = "<group>"; };
+ DE9315221E86C6FF0083EDBF /* FIRVerifyPhoneNumberResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyPhoneNumberResponseTests.m; sourceTree = "<group>"; };
+ DE9315231E86C6FF0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCMStubRecorder+FIRAuthUnitTests.h"; sourceTree = "<group>"; };
+ DE9315241E86C6FF0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OCMStubRecorder+FIRAuthUnitTests.m"; sourceTree = "<group>"; };
+ DE9315251E86C6FF0083EDBF /* Tests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = "<group>"; };
+ DE9315801E86C7F70083EDBF /* Auth-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Auth-Info.plist"; sourceTree = "<group>"; };
+ DE9315A71E8738460083EDBF /* Messaging_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Messaging_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ DE9315C31E8738B70083EDBF /* FIRMessagingClientTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingClientTest.m; sourceTree = "<group>"; };
+ DE9315C41E8738B70083EDBF /* FIRMessagingCodedInputStreamTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingCodedInputStreamTest.m; sourceTree = "<group>"; };
+ DE9315C51E8738B70083EDBF /* FIRMessagingConnectionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingConnectionTest.m; sourceTree = "<group>"; };
+ DE9315C61E8738B70083EDBF /* FIRMessagingContextManagerServiceTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingContextManagerServiceTest.m; sourceTree = "<group>"; };
+ DE9315C71E8738B70083EDBF /* FIRMessagingDataMessageManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingDataMessageManagerTest.m; sourceTree = "<group>"; };
+ DE9315C81E8738B70083EDBF /* FIRMessagingFakeConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRMessagingFakeConnection.h; sourceTree = "<group>"; };
+ DE9315C91E8738B70083EDBF /* FIRMessagingFakeConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingFakeConnection.m; sourceTree = "<group>"; };
+ DE9315CA1E8738B70083EDBF /* FIRMessagingFakeSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRMessagingFakeSocket.h; sourceTree = "<group>"; };
+ DE9315CB1E8738B70083EDBF /* FIRMessagingFakeSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingFakeSocket.m; sourceTree = "<group>"; };
+ DE9315CC1E8738B70083EDBF /* FIRMessagingLinkHandlingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingLinkHandlingTest.m; sourceTree = "<group>"; };
+ DE9315CD1E8738B70083EDBF /* FIRMessagingPendingTopicsListTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingPendingTopicsListTest.m; sourceTree = "<group>"; };
+ DE9315CE1E8738B70083EDBF /* FIRMessagingPubSubTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingPubSubTest.m; sourceTree = "<group>"; };
+ DE9315CF1E8738B70083EDBF /* FIRMessagingRegistrarTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingRegistrarTest.m; sourceTree = "<group>"; };
+ DE9315D01E8738B70083EDBF /* FIRMessagingRemoteNotificationsProxyTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingRemoteNotificationsProxyTest.m; sourceTree = "<group>"; };
+ DE9315D11E8738B70083EDBF /* FIRMessagingRmqManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingRmqManagerTest.m; sourceTree = "<group>"; };
+ DE9315D21E8738B70083EDBF /* FIRMessagingSecureSocketTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingSecureSocketTest.m; sourceTree = "<group>"; };
+ DE9315D31E8738B70083EDBF /* FIRMessagingServiceTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingServiceTest.m; sourceTree = "<group>"; };
+ DE9315D41E8738B70083EDBF /* FIRMessagingSyncMessageManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingSyncMessageManagerTest.m; sourceTree = "<group>"; };
+ DE9315D51E8738B70083EDBF /* FIRMessagingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingTest.m; sourceTree = "<group>"; };
+ DE9315D61E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRMessagingTestNotificationUtilities.h; sourceTree = "<group>"; };
+ DE9315D71E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingTestNotificationUtilities.m; sourceTree = "<group>"; };
+ DE9315D81E8738B70083EDBF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ DEB139C11E734D9D00AC236D /* FIRStorageDeleteTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStorageDeleteTests.m; sourceTree = "<group>"; };
+ DEB139C21E734D9D00AC236D /* FIRStorageGetMetadataTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStorageGetMetadataTests.m; sourceTree = "<group>"; };
+ DEB139C31E734D9D00AC236D /* FIRStorageMetadataTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStorageMetadataTests.m; sourceTree = "<group>"; };
+ DEB139C41E734D9D00AC236D /* FIRStoragePathTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStoragePathTests.m; sourceTree = "<group>"; };
+ DEB139C51E734D9D00AC236D /* FIRStorageReferenceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStorageReferenceTests.m; sourceTree = "<group>"; };
+ DEB139C61E734D9D00AC236D /* FIRStorageTestHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRStorageTestHelpers.h; sourceTree = "<group>"; };
+ DEB139C71E734D9D00AC236D /* FIRStorageTestHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStorageTestHelpers.m; sourceTree = "<group>"; };
+ DEB139C81E734D9D00AC236D /* FIRStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStorageTests.m; sourceTree = "<group>"; };
+ DEB139C91E734D9D00AC236D /* FIRStorageTokenAuthorizerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStorageTokenAuthorizerTests.m; sourceTree = "<group>"; };
+ DEB139CA1E734D9D00AC236D /* FIRStorageUpdateMetadataTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStorageUpdateMetadataTests.m; sourceTree = "<group>"; };
+ DEB139CB1E734D9D00AC236D /* FIRStorageUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRStorageUtilsTests.m; sourceTree = "<group>"; };
+ DEB139CC1E734D9D00AC236D /* Tests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = "<group>"; };
+ DEB13A081E73506A00AC236D /* Storage_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Storage_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ DEB13A231E73507E00AC236D /* Storage_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Storage_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ DEB61E781E7C542600C04B96 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; };
+ DEB61EBA1E7C5DBB00C04B96 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ DEB61EBC1E7C5DBB00C04B96 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ DEB61EBD1E7C5DBB00C04B96 /* FIRAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRAppDelegate.h; sourceTree = "<group>"; };
+ DEB61EBE1E7C5DBB00C04B96 /* FIRAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAppDelegate.m; sourceTree = "<group>"; };
+ DEB61EBF1E7C5DBB00C04B96 /* FIRViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRViewController.h; sourceTree = "<group>"; };
+ DEB61EC01E7C5DBB00C04B96 /* FIRViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRViewController.m; sourceTree = "<group>"; };
+ DEB61EC11E7C5DBB00C04B96 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
+ DEB61EC31E7C5DBB00C04B96 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ DEB61EC41E7C5DBB00C04B96 /* Storage-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Storage-Info.plist"; sourceTree = "<group>"; };
+ DEC0EE0E1EA42D5D007E2177 /* FirebaseDev.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FirebaseDev.framework; path = "../../../Library/Developer/Xcode/DerivedData/Firebase-chgkzndqfwnawrfmbrhkugiybjre/Build/Products/Debug-iphonesimulator/FirebaseDev-Core-Messaging-Root/FirebaseDev.framework"; sourceTree = "<group>"; };
+ DEC0EE101EA42D73007E2177 /* FirebaseDev.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FirebaseDev.framework; path = "../../../Library/Developer/Xcode/DerivedData/Firebase-chgkzndqfwnawrfmbrhkugiybjre/Build/Products/Debug-iphonesimulator/FirebaseDev-Core-Database-Root/FirebaseDev.framework"; sourceTree = "<group>"; };
+ DECE03991E9ECFF500164CA4 /* FIRPhoneAuthProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRPhoneAuthProviderTests.m; sourceTree = "<group>"; };
+ DEE14D411E84464D006FA992 /* Core_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Core_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ DEE14D591E84464D006FA992 /* Core_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Core_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ DEE14D691E844677006FA992 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ DEE14D6B1E844677006FA992 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ DEE14D6C1E844677006FA992 /* Core-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Core-Info.plist"; sourceTree = "<group>"; };
+ DEE14D6D1E844677006FA992 /* FIRAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRAppDelegate.h; sourceTree = "<group>"; };
+ DEE14D6E1E844677006FA992 /* FIRAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAppDelegate.m; sourceTree = "<group>"; };
+ DEE14D6F1E844677006FA992 /* FIRViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRViewController.h; sourceTree = "<group>"; };
+ DEE14D701E844677006FA992 /* FIRViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRViewController.m; sourceTree = "<group>"; };
+ DEE14D711E844677006FA992 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
+ DEE14D731E844677006FA992 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ DEE14D751E844677006FA992 /* FIRAppAssociationRegistrationUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAppAssociationRegistrationUnitTests.m; sourceTree = "<group>"; };
+ DEE14D761E844677006FA992 /* FIRAppTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAppTest.m; sourceTree = "<group>"; };
+ DEE14D771E844677006FA992 /* FIRBundleUtilTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRBundleUtilTest.m; sourceTree = "<group>"; };
+ DEE14D781E844677006FA992 /* FIRConfigurationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRConfigurationTest.m; sourceTree = "<group>"; };
+ DEE14D791E844677006FA992 /* FIRLoggerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRLoggerTest.m; sourceTree = "<group>"; };
+ DEE14D7A1E844677006FA992 /* FIROptionsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIROptionsTest.m; sourceTree = "<group>"; };
+ DEE14D7B1E844677006FA992 /* FIRTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRTestCase.h; sourceTree = "<group>"; };
+ DEE14D7C1E844677006FA992 /* FIRTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRTestCase.m; sourceTree = "<group>"; };
+ DEE14D7D1E844677006FA992 /* Tests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = "<group>"; };
+ E2C2834C90DBAB56D568189F /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
+ E3DEB3CBB1440528DFE1E197 /* Pods_Database_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Database_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ E9D28B14E5B756D3A1938CB2 /* Pods_Messaging_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Messaging_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ EE077EBC5A738E61E06B5FA2 /* Pods-Database_IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Database_IntegrationTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Database_IntegrationTests/Pods-Database_IntegrationTests.debug.xcconfig"; sourceTree = "<group>"; };
+ EEA5C6257533CD27D37A14FC /* Pods-Database_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Database_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Database_Example/Pods-Database_Example.release.xcconfig"; sourceTree = "<group>"; };
+ F0A9002767E1A9D63CEECFF6 /* Pods-Database_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Database_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Database_Tests/Pods-Database_Tests.debug.xcconfig"; sourceTree = "<group>"; };
+ FAB9666F29A81704CA956317 /* Pods_Auth_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Auth_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ FF57915145DB00008E7C56A8 /* Pods-Core_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Core_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Core_Example/Pods-Core_Example.debug.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 06121EB91EC399C50008D70E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 06B47E8C1EC39ADF00170C02 /* FirebaseDev.framework in Frameworks */,
+ 8D14BB390A3E191CCF78BF91 /* Pods_Storage_IntegrationTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 0624F3DE1EC0ECFA00E5940D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0637BA651EC0F99700CAEFD4 /* FirebaseDev.framework in Frameworks */,
+ 9653E6AB7DDD8B5E4814442D /* Pods_Database_IntegrationTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ AFD562E21EB13C6D00EA2233 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 83C9C772827554752364B400 /* Pods_Messaging_Example.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE7B8D021E8EF077009EB6DF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 48402D5F3CB17E091298C7FF /* Pods_Database_Example.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE7B8D1A1E8EF078009EB6DF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEC0EE111EA42D73007E2177 /* FirebaseDev.framework in Frameworks */,
+ 260F4B35536ACE792D9BD6C6 /* Pods_Database_Tests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE9314C31E86C6BD0083EDBF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 22DD1E787F5347BD66CC842B /* Pods_Auth_Example.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE9314DB1E86C6BE0083EDBF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEE13AA11EA170D500D1BABA /* FirebaseDev.framework in Frameworks */,
+ 4768966C0C99B8D4215826A5 /* Pods_Auth_Tests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE9315A41E8738460083EDBF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEC0EE0F1EA42D5D007E2177 /* FirebaseDev.framework in Frameworks */,
+ EA9A4B8DCCA67EB6F9B4008F /* Pods_Messaging_Tests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEB139F31E73506A00AC236D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEB139F41E73506A00AC236D /* CoreGraphics.framework in Frameworks */,
+ DEB139F51E73506A00AC236D /* UIKit.framework in Frameworks */,
+ DEB139F61E73506A00AC236D /* Foundation.framework in Frameworks */,
+ BDE625D72CA3B8918088E0F5 /* Pods_Storage_Example.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEB13A161E73507E00AC236D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE6F01B01E95675E004AEE01 /* FirebaseDev.framework in Frameworks */,
+ 7EA36B802D84DD89CE6203A0 /* Pods_Storage_Tests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE14D3E1E84464D006FA992 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3054DA05818345789EA0C5B0 /* Pods_Core_Example.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE14D561E84464D006FA992 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEC0EE0D1EA427CC007E2177 /* FirebaseDev.framework in Frameworks */,
+ 8CE9133C8720B1C600F7C731 /* Pods_Core_Tests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 06121EC81EC399E50008D70E /* Unit */ = {
+ isa = PBXGroup;
+ children = (
+ DEB139C61E734D9D00AC236D /* FIRStorageTestHelpers.h */,
+ DEB139C11E734D9D00AC236D /* FIRStorageDeleteTests.m */,
+ DEB139C21E734D9D00AC236D /* FIRStorageGetMetadataTests.m */,
+ DEB139C31E734D9D00AC236D /* FIRStorageMetadataTests.m */,
+ DEB139C41E734D9D00AC236D /* FIRStoragePathTests.m */,
+ DEB139C51E734D9D00AC236D /* FIRStorageReferenceTests.m */,
+ DEB139C71E734D9D00AC236D /* FIRStorageTestHelpers.m */,
+ DEB139C81E734D9D00AC236D /* FIRStorageTests.m */,
+ DEB139C91E734D9D00AC236D /* FIRStorageTokenAuthorizerTests.m */,
+ DEB139CA1E734D9D00AC236D /* FIRStorageUpdateMetadataTests.m */,
+ DEB139CB1E734D9D00AC236D /* FIRStorageUtilsTests.m */,
+ );
+ path = Unit;
+ sourceTree = "<group>";
+ };
+ 06121EC91EC39A020008D70E /* Integration */ = {
+ isa = PBXGroup;
+ children = (
+ 06121ECA1EC39A0B0008D70E /* FIRStorageIntegrationTests.m */,
+ );
+ path = Integration;
+ sourceTree = "<group>";
+ };
+ 063CB43C1EBA752300038A59 /* Integration */ = {
+ isa = PBXGroup;
+ children = (
+ 063CB46F1EBA7AEF00038A59 /* FConnectionTest.m */,
+ 063CB4701EBA7AEF00038A59 /* FData.h */,
+ 063CB4711EBA7AEF00038A59 /* FData.m */,
+ 063CB4721EBA7AEF00038A59 /* FDotInfo.h */,
+ 063CB4731EBA7AEF00038A59 /* FDotInfo.m */,
+ 063CB4741EBA7AEF00038A59 /* FEventTests.h */,
+ 063CB4751EBA7AEF00038A59 /* FEventTests.m */,
+ 063CB4761EBA7AEF00038A59 /* FIRAuthTests.m */,
+ 063CB4771EBA7AEF00038A59 /* FIRDatabaseQueryTests.h */,
+ 063CB4781EBA7AEF00038A59 /* FIRDatabaseQueryTests.m */,
+ 063CB4791EBA7AEF00038A59 /* FIRDatabaseTests.m */,
+ 063CB47F1EBA7AEF00038A59 /* FKeepSyncedTest.m */,
+ 063CB4801EBA7AEF00038A59 /* FOrder.h */,
+ 063CB4811EBA7AEF00038A59 /* FOrder.m */,
+ 063CB4821EBA7AEF00038A59 /* FOrderByTests.h */,
+ 063CB4831EBA7AEF00038A59 /* FOrderByTests.m */,
+ 063CB4841EBA7AEF00038A59 /* FPersist.h */,
+ 063CB4851EBA7AEF00038A59 /* FPersist.m */,
+ 063CB4861EBA7AEF00038A59 /* FRealtime.h */,
+ 063CB4871EBA7AEF00038A59 /* FRealtime.m */,
+ 063CB48E1EBA7AEF00038A59 /* FTransactionTest.h */,
+ 063CB48F1EBA7AEF00038A59 /* FTransactionTest.m */,
+ );
+ name = Integration;
+ sourceTree = "<group>";
+ };
+ 0697B11F1EC13D7800542174 /* third_party */ = {
+ isa = PBXGroup;
+ children = (
+ 0697B1201EC13D8A00542174 /* Base64.h */,
+ 0697B1211EC13D8A00542174 /* Base64.m */,
+ );
+ path = third_party;
+ sourceTree = "<group>";
+ };
+ 6003F581195388D10070C39A = {
+ isa = PBXGroup;
+ children = (
+ 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */,
+ DE9314EB1E86C6FF0083EDBF /* Auth */,
+ DEE14D661E844677006FA992 /* Core */,
+ DE7B8D2A1E8EF202009EB6DF /* Database */,
+ DE9315B41E8738B70083EDBF /* Messaging */,
+ AFC8BAA01EC24B1600B8EEAE /* Shared */,
+ DEB139B31E734D9D00AC236D /* Storage */,
+ 6003F58C195388D20070C39A /* Frameworks */,
+ 6003F58B195388D20070C39A /* Products */,
+ BDA0613720DCD29C1C3C3791 /* Pods */,
+ );
+ sourceTree = "<group>";
+ };
+ 6003F58B195388D20070C39A /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ DEB13A081E73506A00AC236D /* Storage_Example.app */,
+ DEB13A231E73507E00AC236D /* Storage_Tests.xctest */,
+ DEE14D411E84464D006FA992 /* Core_Example.app */,
+ DEE14D591E84464D006FA992 /* Core_Tests.xctest */,
+ DE9314C61E86C6BD0083EDBF /* Auth_Example.app */,
+ DE9314DE1E86C6BE0083EDBF /* Auth_Tests.xctest */,
+ DE9315A71E8738460083EDBF /* Messaging_Tests.xctest */,
+ DE7B8D051E8EF077009EB6DF /* Database_Example.app */,
+ DE7B8D1D1E8EF078009EB6DF /* Database_Tests.xctest */,
+ AFD562E51EB13C6D00EA2233 /* Messaging_Example.app */,
+ 0624F3E11EC0ECFA00E5940D /* Database_IntegrationTests.xctest */,
+ 06121EBC1EC399C50008D70E /* Storage_IntegrationTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 6003F58C195388D20070C39A /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 06B47E8B1EC39ADF00170C02 /* FirebaseDev.framework */,
+ 0637BA641EC0F99700CAEFD4 /* FirebaseDev.framework */,
+ DEC0EE101EA42D73007E2177 /* FirebaseDev.framework */,
+ DEC0EE0E1EA42D5D007E2177 /* FirebaseDev.framework */,
+ DE6F01B31E9567F1004AEE01 /* FirebaseDev.framework */,
+ DE6F01B11E9567BF004AEE01 /* FirebaseDev.framework */,
+ DE6F01AC1E95673C004AEE01 /* FirebaseDev.framework */,
+ DE45C6641E7DA8CB009E6ACD /* XCTest.framework */,
+ DEB61E781E7C542600C04B96 /* libsqlite3.tbd */,
+ 6003F58D195388D20070C39A /* Foundation.framework */,
+ 6003F58F195388D20070C39A /* CoreGraphics.framework */,
+ 6003F591195388D20070C39A /* UIKit.framework */,
+ 1EEA0F965ABC48C695972509 /* Pods_Auth_Example.framework */,
+ FAB9666F29A81704CA956317 /* Pods_Auth_Tests.framework */,
+ 08A821396D7D1089ECE810EF /* Pods_Core_Example.framework */,
+ D52CEDD0146DF63640A4C3A5 /* Pods_Core_Tests.framework */,
+ 66C7EEA21795A3320088DEBE /* Pods_Database_Example.framework */,
+ 64928F2997FAF0EAEAC9B8CA /* Pods_Database_Tests.framework */,
+ C8A6D15690286B6BB4CB8023 /* Pods_Messaging_Example.framework */,
+ E9D28B14E5B756D3A1938CB2 /* Pods_Messaging_Tests.framework */,
+ DA7879CD6EE51EE4E20937C8 /* Pods_Storage_Example.framework */,
+ 16E92590A6B517109A2B219F /* Pods_Storage_Tests.framework */,
+ E3DEB3CBB1440528DFE1E197 /* Pods_Database_IntegrationTests.framework */,
+ 36DF4C7B93E6FE7AD8F88A38 /* Pods_Storage_IntegrationTests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */ = {
+ isa = PBXGroup;
+ children = (
+ DE4E711A1E953ABC00070092 /* FirebaseDev.podspec */,
+ 8496034D8156555C5FCF8F14 /* README.md */,
+ E2C2834C90DBAB56D568189F /* LICENSE */,
+ );
+ name = "Podspec Metadata";
+ sourceTree = "<group>";
+ };
+ AFC8BAA01EC24B1600B8EEAE /* Shared */ = {
+ isa = PBXGroup;
+ children = (
+ AFC8BAA21EC257D800B8EEAE /* FIRSampleAppUtilities.h */,
+ AFC8BAA31EC257D800B8EEAE /* FIRSampleAppUtilities.m */,
+ AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */,
+ );
+ name = Shared;
+ sourceTree = "<group>";
+ };
+ AFD562F71EB13CC700EA2233 /* App */ = {
+ isa = PBXGroup;
+ children = (
+ AFD563001EB13DF200EA2233 /* Messaging-Info.plist */,
+ AFD563141EB29B8C00EA2233 /* Messaging_Example.entitlements */,
+ AFD562FF1EB13DF200EA2233 /* AppDelegate.swift */,
+ AFD563161EBBEF7B00EA2233 /* Data+MessagingExtensions.swift */,
+ AFC8BA9E1EBD51A700B8EEAE /* Environment.swift */,
+ AFD563011EB13DF200EA2233 /* MessagingViewController.swift */,
+ AFC8BA9C1EBD230E00B8EEAE /* NotificationsController.swift */,
+ AFD563081EB1400900EA2233 /* LaunchScreen.storyboard */,
+ AFD5630A1EB1400900EA2233 /* Main.storyboard */,
+ AFD563131EB1466100EA2233 /* GoogleService-Info.plist */,
+ );
+ name = App;
+ sourceTree = "<group>";
+ };
+ BDA0613720DCD29C1C3C3791 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ C45949C3AB12F54D27702387 /* Pods-Auth_Example.debug.xcconfig */,
+ 2F002D4E7FA7F07A830CCFDA /* Pods-Auth_Example.release.xcconfig */,
+ 18B5255FF5BEBF6F72C40F39 /* Pods-Auth_Tests.debug.xcconfig */,
+ 60FCE4043D8FE42648646A7F /* Pods-Auth_Tests.release.xcconfig */,
+ FF57915145DB00008E7C56A8 /* Pods-Core_Example.debug.xcconfig */,
+ 8F77C04C2E764FBB0F6C05C6 /* Pods-Core_Example.release.xcconfig */,
+ 8E32E359BE29C3100CF51FC4 /* Pods-Core_Tests.debug.xcconfig */,
+ 0B1BDA534E1F49931795B5E6 /* Pods-Core_Tests.release.xcconfig */,
+ 7727BC17692B98E2B7D0EA7A /* Pods-Database_Example.debug.xcconfig */,
+ EEA5C6257533CD27D37A14FC /* Pods-Database_Example.release.xcconfig */,
+ F0A9002767E1A9D63CEECFF6 /* Pods-Database_Tests.debug.xcconfig */,
+ 4A8B7AE7C053949F6BBBDD3E /* Pods-Database_Tests.release.xcconfig */,
+ 884B87C50C7C950BC18E9091 /* Pods-Messaging_Example.debug.xcconfig */,
+ A6903B88963F6FD1857889E6 /* Pods-Messaging_Example.release.xcconfig */,
+ 6E974DE29EBB9602E723757E /* Pods-Messaging_Tests.debug.xcconfig */,
+ 6D2E4A9396D707C5DEF9B74B /* Pods-Messaging_Tests.release.xcconfig */,
+ 6A0FCB2A37144B3C05E519F6 /* Pods-Storage_Example.debug.xcconfig */,
+ 6BAD1CF3DDEDDD76EC87052D /* Pods-Storage_Example.release.xcconfig */,
+ 3E84D28D93B8196D6A483F15 /* Pods-Storage_Tests.debug.xcconfig */,
+ D58064F9C4DE303997B89D2E /* Pods-Storage_Tests.release.xcconfig */,
+ EE077EBC5A738E61E06B5FA2 /* Pods-Database_IntegrationTests.debug.xcconfig */,
+ BEEA177FFAAB9FA02F898C51 /* Pods-Database_IntegrationTests.release.xcconfig */,
+ 3673564CCB64DE360C8CB97F /* Pods-Storage_IntegrationTests.debug.xcconfig */,
+ CA86AD35456DA6130F7DE02C /* Pods-Storage_IntegrationTests.release.xcconfig */,
+ );
+ name = Pods;
+ sourceTree = "<group>";
+ };
+ DE7B8D2A1E8EF202009EB6DF /* Database */ = {
+ isa = PBXGroup;
+ children = (
+ DE7B8D2B1E8EF202009EB6DF /* App */,
+ DE7B8D381E8EF202009EB6DF /* Tests */,
+ );
+ path = Database;
+ sourceTree = "<group>";
+ };
+ DE7B8D2B1E8EF202009EB6DF /* App */ = {
+ isa = PBXGroup;
+ children = (
+ 0672F2F11EBBA7D900818E87 /* GoogleService-Info.plist */,
+ DE7B8DD21E8F1CA7009EB6DF /* Database-Info.plist */,
+ DE7B8D311E8EF202009EB6DF /* FIRAppDelegate.h */,
+ DE7B8D331E8EF202009EB6DF /* FIRViewController.h */,
+ DE7B8D321E8EF202009EB6DF /* FIRAppDelegate.m */,
+ DE7B8D341E8EF202009EB6DF /* FIRViewController.m */,
+ DE7B8D371E8EF202009EB6DF /* main.m */,
+ DE7B8D2C1E8EF202009EB6DF /* LaunchScreen.storyboard */,
+ DE7B8D2E1E8EF202009EB6DF /* Main.storyboard */,
+ );
+ path = App;
+ sourceTree = "<group>";
+ };
+ DE7B8D381E8EF202009EB6DF /* Tests */ = {
+ isa = PBXGroup;
+ children = (
+ 063CB47C1EBA7AEF00038A59 /* FirebaseTests-Info.plist */,
+ DE7B8D751E8EF202009EB6DF /* Helpers */,
+ DE7B8D691E8EF202009EB6DF /* InfoPlist.strings */,
+ 063CB43C1EBA752300038A59 /* Integration */,
+ DE7B8D8E1E8EF203009EB6DF /* syncPointSpec.json */,
+ 0697B11F1EC13D7800542174 /* third_party */,
+ DE7B8D391E8EF202009EB6DF /* Unit */,
+ );
+ path = Tests;
+ sourceTree = "<group>";
+ };
+ DE7B8D391E8EF202009EB6DF /* Unit */ = {
+ isa = PBXGroup;
+ children = (
+ 063CB4471EBA7AE200038A59 /* FArraySortedDictionaryTest.m */,
+ 063CB4481EBA7AE200038A59 /* FCompoundHashTest.m */,
+ 063CB46E1EBA7AEF00038A59 /* FCompoundWriteTest.m */,
+ 063CB47A1EBA7AEF00038A59 /* FIRDataSnapshotTests.h */,
+ 063CB47B1EBA7AEF00038A59 /* FIRDataSnapshotTests.m */,
+ 063CB4491EBA7AE200038A59 /* FIRMutableDataTests.h */,
+ 063CB44A1EBA7AE200038A59 /* FIRMutableDataTests.m */,
+ 063CB44B1EBA7AE200038A59 /* FLevelDBStorageEngineTests.m */,
+ 063CB44C1EBA7AE200038A59 /* FNodeTests.m */,
+ 063CB44D1EBA7AE200038A59 /* FPathTests.h */,
+ 063CB44E1EBA7AE200038A59 /* FPathTests.m */,
+ 063CB44F1EBA7AE200038A59 /* FPersistenceManagerTest.m */,
+ 063CB4501EBA7AE200038A59 /* FPruneForestTest.m */,
+ 063CB4511EBA7AE200038A59 /* FPruningTest.m */,
+ 063CB4521EBA7AE200038A59 /* FQueryParamsTest.m */,
+ 063CB4531EBA7AE200038A59 /* FRangeMergeTest.m */,
+ 063CB4541EBA7AE200038A59 /* FRepoInfoTest.m */,
+ 063CB4551EBA7AE200038A59 /* FSparseSnapshotTests.h */,
+ 063CB4561EBA7AE200038A59 /* FSparseSnapshotTests.m */,
+ 063CB4571EBA7AE200038A59 /* FSyncPointTests.h */,
+ 063CB4581EBA7AE200038A59 /* FSyncPointTests.m */,
+ 063CB45B1EBA7AE200038A59 /* FTrackedQueryManagerTest.m */,
+ 063CB4901EBA7AEF00038A59 /* FTreeSortedDictionaryTests.m */,
+ 063CB45C1EBA7AE200038A59 /* FUtilitiesTest.m */,
+ );
+ path = Unit;
+ sourceTree = "<group>";
+ };
+ DE7B8D751E8EF202009EB6DF /* Helpers */ = {
+ isa = PBXGroup;
+ children = (
+ DE7B8D781E8EF202009EB6DF /* FDevice.h */,
+ DE7B8D791E8EF202009EB6DF /* FDevice.m */,
+ DE7B8D7A1E8EF202009EB6DF /* FEventTester.h */,
+ DE7B8D7B1E8EF202009EB6DF /* FEventTester.m */,
+ 063CB47D1EBA7AEF00038A59 /* FIRFakeApp.h */,
+ 063CB47E1EBA7AEF00038A59 /* FIRFakeApp.m */,
+ DE7B8D7C1E8EF202009EB6DF /* FIRTestAuthTokenProvider.h */,
+ DE7B8D7D1E8EF202009EB6DF /* FIRTestAuthTokenProvider.m */,
+ DE7B8D7E1E8EF202009EB6DF /* FMockStorageEngine.h */,
+ DE7B8D7F1E8EF202009EB6DF /* FMockStorageEngine.m */,
+ DE7B8D801E8EF202009EB6DF /* FTestAuthTokenGenerator.h */,
+ DE7B8D811E8EF202009EB6DF /* FTestAuthTokenGenerator.m */,
+ 063CB4591EBA7AE200038A59 /* FTestBase.h */,
+ 063CB45A1EBA7AE200038A59 /* FTestBase.m */,
+ DE7B8D821E8EF202009EB6DF /* FTestCachePolicy.h */,
+ DE7B8D831E8EF202009EB6DF /* FTestCachePolicy.m */,
+ DE7B8D841E8EF202009EB6DF /* FTestClock.h */,
+ DE7B8D851E8EF202009EB6DF /* FTestClock.m */,
+ 063CB48D1EBA7AEF00038A59 /* FTestContants.h */,
+ DE7B8D861E8EF202009EB6DF /* FTestExpectations.h */,
+ DE7B8D871E8EF202009EB6DF /* FTestExpectations.m */,
+ DE7B8D881E8EF202009EB6DF /* FTestHelpers.h */,
+ DE7B8D891E8EF202009EB6DF /* FTestHelpers.m */,
+ DE7B8D8A1E8EF203009EB6DF /* FTupleEventTypeString.h */,
+ DE7B8D8B1E8EF203009EB6DF /* FTupleEventTypeString.m */,
+ DE7B8D8C1E8EF203009EB6DF /* SenTest+FWaiter.h */,
+ DE7B8D8D1E8EF203009EB6DF /* SenTest+FWaiter.m */,
+ );
+ path = Helpers;
+ sourceTree = "<group>";
+ };
+ DE9314EB1E86C6FF0083EDBF /* Auth */ = {
+ isa = PBXGroup;
+ children = (
+ DE9314EC1E86C6FF0083EDBF /* App */,
+ DE9314F91E86C6FF0083EDBF /* Tests */,
+ );
+ path = Auth;
+ sourceTree = "<group>";
+ };
+ DE9314EC1E86C6FF0083EDBF /* App */ = {
+ isa = PBXGroup;
+ children = (
+ DE9315801E86C7F70083EDBF /* Auth-Info.plist */,
+ DE9314ED1E86C6FF0083EDBF /* LaunchScreen.storyboard */,
+ DE9314EF1E86C6FF0083EDBF /* Main.storyboard */,
+ DE9314F21E86C6FF0083EDBF /* FIRAppDelegate.h */,
+ DE9314F31E86C6FF0083EDBF /* FIRAppDelegate.m */,
+ DE9314F41E86C6FF0083EDBF /* FIRViewController.h */,
+ DE9314F51E86C6FF0083EDBF /* FIRViewController.m */,
+ DE9314F61E86C6FF0083EDBF /* GoogleService-Info.plist */,
+ DE9314F81E86C6FF0083EDBF /* main.m */,
+ );
+ path = App;
+ sourceTree = "<group>";
+ };
+ DE9314F91E86C6FF0083EDBF /* Tests */ = {
+ isa = PBXGroup;
+ children = (
+ DE750DB51EB3DD4000A75E47 /* FIRAuthAPNSTokenManagerTests.m */,
+ DE750DB61EB3DD4000A75E47 /* FIRAuthAPNSTokenTests.m */,
+ DE750DB71EB3DD4000A75E47 /* FIRAuthAppCredentialManagerTests.m */,
+ DE750DB81EB3DD4000A75E47 /* FIRAuthNotificationManagerTests.m */,
+ DE0E5BB91EA7D92E00FAA825 /* FIRVerifyClientRequestTest.m */,
+ DE0E5BBA1EA7D92E00FAA825 /* FIRVerifyClientResponseTests.m */,
+ DE0E5BB51EA7D91C00FAA825 /* FIRAuthAppCredentialTests.m */,
+ DE0E5BB61EA7D91C00FAA825 /* FIRAuthAppDelegateProxyTests.m */,
+ DECE03991E9ECFF500164CA4 /* FIRPhoneAuthProviderTests.m */,
+ DE9314FB1E86C6FF0083EDBF /* FIRApp+FIRAuthUnitTests.h */,
+ DE9315091E86C6FF0083EDBF /* FIRFakeBackendRPCIssuer.h */,
+ DE9315231E86C6FF0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.h */,
+ DE9314FA1E86C6FF0083EDBF /* FIRAdditionalUserInfoTests.m */,
+ DE9314FC1E86C6FF0083EDBF /* FIRApp+FIRAuthUnitTests.m */,
+ DE9314FD1E86C6FF0083EDBF /* FIRAuthBackendCreateAuthURITests.m */,
+ DE9314FE1E86C6FF0083EDBF /* FIRAuthBackendRPCImplementationTests.m */,
+ DE9314FF1E86C6FF0083EDBF /* FIRAuthDispatcherTests.m */,
+ DE9315001E86C6FF0083EDBF /* FIRAuthGlobalWorkQueueTests.m */,
+ DE9315011E86C6FF0083EDBF /* FIRAuthKeychainTests.m */,
+ DE9315021E86C6FF0083EDBF /* FIRAuthSerialTaskQueueTests.m */,
+ DE9315031E86C6FF0083EDBF /* FIRAuthTests.m */,
+ DE9315041E86C6FF0083EDBF /* FIRAuthUserDefaultsStorageTests.m */,
+ DE9315051E86C6FF0083EDBF /* FIRCreateAuthURIRequestTests.m */,
+ DE9315061E86C6FF0083EDBF /* FIRCreateAuthURIResponseTests.m */,
+ DE9315071E86C6FF0083EDBF /* FIRDeleteAccountRequestTests.m */,
+ DE9315081E86C6FF0083EDBF /* FIRDeleteAccountResponseTests.m */,
+ DE93150A1E86C6FF0083EDBF /* FIRFakeBackendRPCIssuer.m */,
+ DE93150B1E86C6FF0083EDBF /* FIRGetAccountInfoRequestTests.m */,
+ DE93150C1E86C6FF0083EDBF /* FIRGetAccountInfoResponseTests.m */,
+ DE93150D1E86C6FF0083EDBF /* FIRGetOOBConfirmationCodeRequestTests.m */,
+ DE93150E1E86C6FF0083EDBF /* FIRGetOOBConfirmationCodeResponseTests.m */,
+ DE93150F1E86C6FF0083EDBF /* FIRGitHubAuthProviderTests.m */,
+ DE9315111E86C6FF0083EDBF /* FIRResetPasswordRequestTests.m */,
+ DE9315121E86C6FF0083EDBF /* FIRResetPasswordResponseTests.m */,
+ DE9315131E86C6FF0083EDBF /* FIRSendVerificationCodeRequestTests.m */,
+ DE9315141E86C6FF0083EDBF /* FIRSendVerificationCodeResponseTests.m */,
+ DE9315151E86C6FF0083EDBF /* FIRSetAccountInfoRequestTests.m */,
+ DE9315161E86C6FF0083EDBF /* FIRSetAccountInfoResponseTests.m */,
+ DE9315171E86C6FF0083EDBF /* FIRSignUpNewUserRequestTests.m */,
+ DE9315181E86C6FF0083EDBF /* FIRSignUpNewUserResponseTests.m */,
+ DE9315191E86C6FF0083EDBF /* FIRTwitterAuthProviderTests.m */,
+ DE93151A1E86C6FF0083EDBF /* FIRUserTests.m */,
+ DE93151B1E86C6FF0083EDBF /* FIRVerifyAssertionRequestTests.m */,
+ DE93151C1E86C6FF0083EDBF /* FIRVerifyAssertionResponseTests.m */,
+ DE93151D1E86C6FF0083EDBF /* FIRVerifyCustomTokenRequestTests.m */,
+ DE93151E1E86C6FF0083EDBF /* FIRVerifyCustomTokenResponseTests.m */,
+ DE93151F1E86C6FF0083EDBF /* FIRVerifyPasswordRequestTest.m */,
+ DE9315201E86C6FF0083EDBF /* FIRVerifyPasswordResponseTests.m */,
+ DE9315211E86C6FF0083EDBF /* FIRVerifyPhoneNumberRequestTests.m */,
+ DE9315221E86C6FF0083EDBF /* FIRVerifyPhoneNumberResponseTests.m */,
+ DE9315241E86C6FF0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.m */,
+ DE9315251E86C6FF0083EDBF /* Tests-Info.plist */,
+ );
+ path = Tests;
+ sourceTree = "<group>";
+ };
+ DE9315B41E8738B70083EDBF /* Messaging */ = {
+ isa = PBXGroup;
+ children = (
+ AFC8BAA11EC257D700B8EEAE /* Messaging_Example-Bridging-Header.h */,
+ AFD562F71EB13CC700EA2233 /* App */,
+ DE9315C21E8738B70083EDBF /* Tests */,
+ );
+ path = Messaging;
+ sourceTree = "<group>";
+ };
+ DE9315C21E8738B70083EDBF /* Tests */ = {
+ isa = PBXGroup;
+ children = (
+ DE9315C81E8738B70083EDBF /* FIRMessagingFakeConnection.h */,
+ DE9315CA1E8738B70083EDBF /* FIRMessagingFakeSocket.h */,
+ DE9315D61E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.h */,
+ DE9315C31E8738B70083EDBF /* FIRMessagingClientTest.m */,
+ DE9315C41E8738B70083EDBF /* FIRMessagingCodedInputStreamTest.m */,
+ DE9315C51E8738B70083EDBF /* FIRMessagingConnectionTest.m */,
+ DE9315C61E8738B70083EDBF /* FIRMessagingContextManagerServiceTest.m */,
+ DE9315C71E8738B70083EDBF /* FIRMessagingDataMessageManagerTest.m */,
+ DE9315C91E8738B70083EDBF /* FIRMessagingFakeConnection.m */,
+ DE9315CB1E8738B70083EDBF /* FIRMessagingFakeSocket.m */,
+ DE9315CC1E8738B70083EDBF /* FIRMessagingLinkHandlingTest.m */,
+ DE9315CD1E8738B70083EDBF /* FIRMessagingPendingTopicsListTest.m */,
+ DE9315CE1E8738B70083EDBF /* FIRMessagingPubSubTest.m */,
+ DE9315CF1E8738B70083EDBF /* FIRMessagingRegistrarTest.m */,
+ DE9315D01E8738B70083EDBF /* FIRMessagingRemoteNotificationsProxyTest.m */,
+ DE9315D11E8738B70083EDBF /* FIRMessagingRmqManagerTest.m */,
+ DE9315D21E8738B70083EDBF /* FIRMessagingSecureSocketTest.m */,
+ DE9315D31E8738B70083EDBF /* FIRMessagingServiceTest.m */,
+ DE9315D41E8738B70083EDBF /* FIRMessagingSyncMessageManagerTest.m */,
+ DE9315D51E8738B70083EDBF /* FIRMessagingTest.m */,
+ DE9315D71E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.m */,
+ DE9315D81E8738B70083EDBF /* Info.plist */,
+ );
+ path = Tests;
+ sourceTree = "<group>";
+ };
+ DEB139B31E734D9D00AC236D /* Storage */ = {
+ isa = PBXGroup;
+ children = (
+ DEB61EB81E7C5DBB00C04B96 /* App */,
+ DEB139C01E734D9D00AC236D /* Tests */,
+ );
+ path = Storage;
+ sourceTree = "<group>";
+ };
+ DEB139C01E734D9D00AC236D /* Tests */ = {
+ isa = PBXGroup;
+ children = (
+ 06121EC91EC39A020008D70E /* Integration */,
+ 06121EC81EC399E50008D70E /* Unit */,
+ DEB139CC1E734D9D00AC236D /* Tests-Info.plist */,
+ );
+ path = Tests;
+ sourceTree = "<group>";
+ };
+ DEB61EB81E7C5DBB00C04B96 /* App */ = {
+ isa = PBXGroup;
+ children = (
+ 069428801EC3B35A00F7BC69 /* 1mb.dat */,
+ DEB61EB91E7C5DBB00C04B96 /* LaunchScreen.storyboard */,
+ DEB61EBB1E7C5DBB00C04B96 /* Main.storyboard */,
+ DEB61EBD1E7C5DBB00C04B96 /* FIRAppDelegate.h */,
+ DEB61EBE1E7C5DBB00C04B96 /* FIRAppDelegate.m */,
+ DEB61EBF1E7C5DBB00C04B96 /* FIRViewController.h */,
+ DEB61EC01E7C5DBB00C04B96 /* FIRViewController.m */,
+ DEB61EC11E7C5DBB00C04B96 /* GoogleService-Info.plist */,
+ DEB61EC31E7C5DBB00C04B96 /* main.m */,
+ DEB61EC41E7C5DBB00C04B96 /* Storage-Info.plist */,
+ );
+ path = App;
+ sourceTree = "<group>";
+ };
+ DEE14D661E844677006FA992 /* Core */ = {
+ isa = PBXGroup;
+ children = (
+ DEE14D671E844677006FA992 /* App */,
+ DEE14D741E844677006FA992 /* Tests */,
+ );
+ path = Core;
+ sourceTree = "<group>";
+ };
+ DEE14D671E844677006FA992 /* App */ = {
+ isa = PBXGroup;
+ children = (
+ DEE14D681E844677006FA992 /* LaunchScreen.storyboard */,
+ DEE14D6A1E844677006FA992 /* Main.storyboard */,
+ DEE14D6C1E844677006FA992 /* Core-Info.plist */,
+ DEE14D6D1E844677006FA992 /* FIRAppDelegate.h */,
+ DEE14D6E1E844677006FA992 /* FIRAppDelegate.m */,
+ DEE14D6F1E844677006FA992 /* FIRViewController.h */,
+ DEE14D701E844677006FA992 /* FIRViewController.m */,
+ DEE14D711E844677006FA992 /* GoogleService-Info.plist */,
+ DEE14D731E844677006FA992 /* main.m */,
+ );
+ path = App;
+ sourceTree = "<group>";
+ };
+ DEE14D741E844677006FA992 /* Tests */ = {
+ isa = PBXGroup;
+ children = (
+ DEE14D7B1E844677006FA992 /* FIRTestCase.h */,
+ DEE14D751E844677006FA992 /* FIRAppAssociationRegistrationUnitTests.m */,
+ DEE14D761E844677006FA992 /* FIRAppTest.m */,
+ DEE14D771E844677006FA992 /* FIRBundleUtilTest.m */,
+ DEE14D781E844677006FA992 /* FIRConfigurationTest.m */,
+ DEE14D791E844677006FA992 /* FIRLoggerTest.m */,
+ DEE14D7A1E844677006FA992 /* FIROptionsTest.m */,
+ DEE14D7C1E844677006FA992 /* FIRTestCase.m */,
+ DEE14D7D1E844677006FA992 /* Tests-Info.plist */,
+ );
+ path = Tests;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 06121EBB1EC399C50008D70E /* Storage_IntegrationTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 06121EC51EC399C50008D70E /* Build configuration list for PBXNativeTarget "Storage_IntegrationTests" */;
+ buildPhases = (
+ BCC67418B8EBA4E90488CD55 /* [CP] Check Pods Manifest.lock */,
+ 06121EB81EC399C50008D70E /* Sources */,
+ 06121EB91EC399C50008D70E /* Frameworks */,
+ 06121EBA1EC399C50008D70E /* Resources */,
+ 0840546A7D90530C21375416 /* [CP] Embed Pods Frameworks */,
+ B84CF6076850A6EA9E66592F /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 06121EC71EC399D40008D70E /* PBXTargetDependency */,
+ );
+ name = Storage_IntegrationTests;
+ productName = Storage_IntegrationTests;
+ productReference = 06121EBC1EC399C50008D70E /* Storage_IntegrationTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 0624F3E01EC0ECFA00E5940D /* Database_IntegrationTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 0624F3E81EC0ECFA00E5940D /* Build configuration list for PBXNativeTarget "Database_IntegrationTests" */;
+ buildPhases = (
+ BF9A61F6BBE9655CED5E897D /* [CP] Check Pods Manifest.lock */,
+ 0624F3DD1EC0ECFA00E5940D /* Sources */,
+ 0624F3DE1EC0ECFA00E5940D /* Frameworks */,
+ 0624F3DF1EC0ECFA00E5940D /* Resources */,
+ 744AF9F9B2DB5C511EB9969A /* [CP] Embed Pods Frameworks */,
+ 597B88E0E6B632C48707E8EB /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 0624F3E71EC0ECFA00E5940D /* PBXTargetDependency */,
+ );
+ name = Database_IntegrationTests;
+ productName = Database_IntegratioNtests;
+ productReference = 0624F3E11EC0ECFA00E5940D /* Database_IntegrationTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ AFD562E41EB13C6D00EA2233 /* Messaging_Example */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = AFD562F41EB13C6D00EA2233 /* Build configuration list for PBXNativeTarget "Messaging_Example" */;
+ buildPhases = (
+ 26F9869011740630E2119D0D /* [CP] Check Pods Manifest.lock */,
+ AFD562E11EB13C6D00EA2233 /* Sources */,
+ AFD562E21EB13C6D00EA2233 /* Frameworks */,
+ AFD562E31EB13C6D00EA2233 /* Resources */,
+ ADFC988CE33AA0C8F0C59177 /* [CP] Embed Pods Frameworks */,
+ A39D405E17BE3A6646B8E38E /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Messaging_Example;
+ productName = Messaging_Example;
+ productReference = AFD562E51EB13C6D00EA2233 /* Messaging_Example.app */;
+ productType = "com.apple.product-type.application";
+ };
+ DE7B8D041E8EF077009EB6DF /* Database_Example */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DE7B8D281E8EF078009EB6DF /* Build configuration list for PBXNativeTarget "Database_Example" */;
+ buildPhases = (
+ 4363D4BBFAAC4D505B9B18EC /* [CP] Check Pods Manifest.lock */,
+ DE7B8D011E8EF077009EB6DF /* Sources */,
+ DE7B8D021E8EF077009EB6DF /* Frameworks */,
+ DE7B8D031E8EF077009EB6DF /* Resources */,
+ E9A4ADE4EE7390DC85A9FC17 /* [CP] Embed Pods Frameworks */,
+ 5AE0F6A81F9A499BD752D5E9 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Database_Example;
+ productName = Database_Example;
+ productReference = DE7B8D051E8EF077009EB6DF /* Database_Example.app */;
+ productType = "com.apple.product-type.application";
+ };
+ DE7B8D1C1E8EF078009EB6DF /* Database_Tests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DE7B8D291E8EF078009EB6DF /* Build configuration list for PBXNativeTarget "Database_Tests" */;
+ buildPhases = (
+ EE21F035A2A97C35635C2F3C /* [CP] Check Pods Manifest.lock */,
+ DE7B8D191E8EF078009EB6DF /* Sources */,
+ DE7B8D1A1E8EF078009EB6DF /* Frameworks */,
+ DE7B8D1B1E8EF078009EB6DF /* Resources */,
+ 5EFAE1A18DA8F6BFC0C191E8 /* [CP] Embed Pods Frameworks */,
+ 634E392CCD4D5E88B96D3EF1 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ DE7B8D1F1E8EF078009EB6DF /* PBXTargetDependency */,
+ );
+ name = Database_Tests;
+ productName = Database_ExampleTests;
+ productReference = DE7B8D1D1E8EF078009EB6DF /* Database_Tests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DE9314C51E86C6BD0083EDBF /* Auth_Example */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DE9314E91E86C6BE0083EDBF /* Build configuration list for PBXNativeTarget "Auth_Example" */;
+ buildPhases = (
+ 7CC2EB21DFB0E48B1B8171B2 /* [CP] Check Pods Manifest.lock */,
+ DE9314C21E86C6BD0083EDBF /* Sources */,
+ DE9314C31E86C6BD0083EDBF /* Frameworks */,
+ DE9314C41E86C6BD0083EDBF /* Resources */,
+ B293C142610E914FBE2CA4C9 /* [CP] Embed Pods Frameworks */,
+ A2DF8D8C8D3B6639CBD9CB5B /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Auth_Example;
+ productName = Auth_Example;
+ productReference = DE9314C61E86C6BD0083EDBF /* Auth_Example.app */;
+ productType = "com.apple.product-type.application";
+ };
+ DE9314DD1E86C6BE0083EDBF /* Auth_Tests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DE9314EA1E86C6BE0083EDBF /* Build configuration list for PBXNativeTarget "Auth_Tests" */;
+ buildPhases = (
+ 016A3201E8E0C5ABE835F645 /* [CP] Check Pods Manifest.lock */,
+ DE9314DA1E86C6BE0083EDBF /* Sources */,
+ DE9314DB1E86C6BE0083EDBF /* Frameworks */,
+ DE9314DC1E86C6BE0083EDBF /* Resources */,
+ 54175166C251F5F698B6B1C3 /* [CP] Embed Pods Frameworks */,
+ 9D8053D74F91F866DB0D0199 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ DE9314E01E86C6BE0083EDBF /* PBXTargetDependency */,
+ );
+ name = Auth_Tests;
+ productName = Auth_ExampleTests;
+ productReference = DE9314DE1E86C6BE0083EDBF /* Auth_Tests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DE9315A61E8738460083EDBF /* Messaging_Tests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DE9315B31E8738460083EDBF /* Build configuration list for PBXNativeTarget "Messaging_Tests" */;
+ buildPhases = (
+ 66C488F8D840BC12E26BEE9C /* [CP] Check Pods Manifest.lock */,
+ DE9315A31E8738460083EDBF /* Sources */,
+ DE9315A41E8738460083EDBF /* Frameworks */,
+ DE9315A51E8738460083EDBF /* Resources */,
+ 3D9876DCE9EFE13441346E50 /* [CP] Embed Pods Frameworks */,
+ AA723E4B93CCA8A3A4C24F4E /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ AFD563121EB140E100EA2233 /* PBXTargetDependency */,
+ );
+ name = Messaging_Tests;
+ productName = Messaging_ExampleTests;
+ productReference = DE9315A71E8738460083EDBF /* Messaging_Tests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DEB139E01E73506A00AC236D /* Storage_Example */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DEB13A051E73506A00AC236D /* Build configuration list for PBXNativeTarget "Storage_Example" */;
+ buildPhases = (
+ 7E9B7B5115CCC4F0FCBED014 /* [CP] Check Pods Manifest.lock */,
+ DEB139E21E73506A00AC236D /* Sources */,
+ DEB139F31E73506A00AC236D /* Frameworks */,
+ DEB139F91E73506A00AC236D /* Resources */,
+ E8657FA5227C3B5EB7B83B40 /* [CP] Embed Pods Frameworks */,
+ 5BE3F4C8BA697C65D55C055E /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Storage_Example;
+ productName = Firebase;
+ productReference = DEB13A081E73506A00AC236D /* Storage_Example.app */;
+ productType = "com.apple.product-type.application";
+ };
+ DEB13A0A1E73507E00AC236D /* Storage_Tests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DEB13A201E73507E00AC236D /* Build configuration list for PBXNativeTarget "Storage_Tests" */;
+ buildPhases = (
+ AD42F17297AF3C18062D4C51 /* [CP] Check Pods Manifest.lock */,
+ DEB13A0E1E73507E00AC236D /* Sources */,
+ DEB13A161E73507E00AC236D /* Frameworks */,
+ DEB13A1D1E73507E00AC236D /* Resources */,
+ 57EF5C8DAF88A5F43BE0C6FE /* [CP] Embed Pods Frameworks */,
+ 12F6D0DD1D452316A3123EED /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ DEB13A261E73512500AC236D /* PBXTargetDependency */,
+ );
+ name = Storage_Tests;
+ productName = FirebaseTests;
+ productReference = DEB13A231E73507E00AC236D /* Storage_Tests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DEE14D401E84464D006FA992 /* Core_Example */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DEE14D641E84464D006FA992 /* Build configuration list for PBXNativeTarget "Core_Example" */;
+ buildPhases = (
+ AB5B6984AF16CF03E74EA522 /* [CP] Check Pods Manifest.lock */,
+ DEE14D3D1E84464D006FA992 /* Sources */,
+ DEE14D3E1E84464D006FA992 /* Frameworks */,
+ DEE14D3F1E84464D006FA992 /* Resources */,
+ DEFCD9C6026936498AE2778C /* [CP] Embed Pods Frameworks */,
+ 883CE42B54A7BB78295FBCDA /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Core_Example;
+ productName = Core_Example;
+ productReference = DEE14D411E84464D006FA992 /* Core_Example.app */;
+ productType = "com.apple.product-type.application";
+ };
+ DEE14D581E84464D006FA992 /* Core_Tests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DEE14D651E84464D006FA992 /* Build configuration list for PBXNativeTarget "Core_Tests" */;
+ buildPhases = (
+ C3AFD8761D910A99E506F606 /* [CP] Check Pods Manifest.lock */,
+ DEE14D551E84464D006FA992 /* Sources */,
+ DEE14D561E84464D006FA992 /* Frameworks */,
+ DEE14D571E84464D006FA992 /* Resources */,
+ 316D851DAC53422509F9B7B4 /* [CP] Embed Pods Frameworks */,
+ 9D6CD2CDCDD281E43FDD3492 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ DEE14D5B1E84464D006FA992 /* PBXTargetDependency */,
+ );
+ name = Core_Tests;
+ productName = Core_ExampleTests;
+ productReference = DEE14D591E84464D006FA992 /* Core_Tests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 6003F582195388D10070C39A /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ CLASSPREFIX = FIR;
+ LastSwiftUpdateCheck = 0830;
+ LastUpgradeCheck = 0820;
+ ORGANIZATIONNAME = "Paul Beusterien";
+ TargetAttributes = {
+ 06121EBB1EC399C50008D70E = {
+ CreatedOnToolsVersion = 8.2.1;
+ ProvisioningStyle = Automatic;
+ TestTargetID = DEB139E01E73506A00AC236D;
+ };
+ 0624F3E01EC0ECFA00E5940D = {
+ CreatedOnToolsVersion = 8.2.1;
+ ProvisioningStyle = Automatic;
+ TestTargetID = DE7B8D041E8EF077009EB6DF;
+ };
+ AFD562E41EB13C6D00EA2233 = {
+ CreatedOnToolsVersion = 8.3.2;
+ DevelopmentTeam = EQHXZ8M8AV;
+ LastSwiftMigration = 0830;
+ ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.BackgroundModes = {
+ enabled = 1;
+ };
+ com.apple.Push = {
+ enabled = 1;
+ };
+ };
+ };
+ DE3373891E73773400881891 = {
+ CreatedOnToolsVersion = 8.2.1;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ };
+ DE7B8D041E8EF077009EB6DF = {
+ CreatedOnToolsVersion = 8.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ };
+ DE7B8D1C1E8EF078009EB6DF = {
+ CreatedOnToolsVersion = 8.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ TestTargetID = DE7B8D041E8EF077009EB6DF;
+ };
+ DE9314C51E86C6BD0083EDBF = {
+ CreatedOnToolsVersion = 8.2.1;
+ DevelopmentTeam = EQHXZ8M8AV;
+ LastSwiftMigration = 0830;
+ ProvisioningStyle = Automatic;
+ };
+ DE9314DD1E86C6BE0083EDBF = {
+ CreatedOnToolsVersion = 8.2.1;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ TestTargetID = DE9314C51E86C6BD0083EDBF;
+ };
+ DE9315A61E8738460083EDBF = {
+ CreatedOnToolsVersion = 8.2.1;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ TestTargetID = AFD562E41EB13C6D00EA2233;
+ };
+ DEB139E01E73506A00AC236D = {
+ DevelopmentTeam = EQHXZ8M8AV;
+ };
+ DEB13A0A1E73507E00AC236D = {
+ DevelopmentTeam = EQHXZ8M8AV;
+ TestTargetID = DEB139E01E73506A00AC236D;
+ };
+ DEE14D401E84464D006FA992 = {
+ CreatedOnToolsVersion = 8.2.1;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ };
+ DEE14D581E84464D006FA992 = {
+ CreatedOnToolsVersion = 8.2.1;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ TestTargetID = DEE14D401E84464D006FA992;
+ };
+ };
+ };
+ buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firebase" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 6003F581195388D10070C39A;
+ productRefGroup = 6003F58B195388D20070C39A /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ DE9314C51E86C6BD0083EDBF /* Auth_Example */,
+ DE9314DD1E86C6BE0083EDBF /* Auth_Tests */,
+ DEE14D401E84464D006FA992 /* Core_Example */,
+ DEE14D581E84464D006FA992 /* Core_Tests */,
+ DE7B8D041E8EF077009EB6DF /* Database_Example */,
+ DE7B8D1C1E8EF078009EB6DF /* Database_Tests */,
+ 0624F3E01EC0ECFA00E5940D /* Database_IntegrationTests */,
+ AFD562E41EB13C6D00EA2233 /* Messaging_Example */,
+ DE9315A61E8738460083EDBF /* Messaging_Tests */,
+ DEB139E01E73506A00AC236D /* Storage_Example */,
+ DEB13A0A1E73507E00AC236D /* Storage_Tests */,
+ 06121EBB1EC399C50008D70E /* Storage_IntegrationTests */,
+ DE3373891E73773400881891 /* AllUnitTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 06121EBA1EC399C50008D70E /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 0624F3DF1EC0ECFA00E5940D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ AFD562E31EB13C6D00EA2233 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ AFD5630D1EB1400900EA2233 /* Main.storyboard in Resources */,
+ AFAF36F81EC28C25004BDEE5 /* Shared.xcassets in Resources */,
+ AFD563151EB29EDE00EA2233 /* GoogleService-Info.plist in Resources */,
+ AFD5630C1EB1400900EA2233 /* LaunchScreen.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE7B8D031E8EF077009EB6DF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ AFAF36F71EC28C25004BDEE5 /* Shared.xcassets in Resources */,
+ 0672F2F21EBBA7D900818E87 /* GoogleService-Info.plist in Resources */,
+ DE7B8DD11E8EF24F009EB6DF /* Main.storyboard in Resources */,
+ DE7B8DD01E8EF246009EB6DF /* LaunchScreen.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE7B8D1B1E8EF078009EB6DF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE7B8DB61E8EF203009EB6DF /* InfoPlist.strings in Resources */,
+ DE7B8DC91E8EF203009EB6DF /* syncPointSpec.json in Resources */,
+ 0672F2F31EBBA7D900818E87 /* GoogleService-Info.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE9314C41E86C6BD0083EDBF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE93152B1E86C6FF0083EDBF /* GoogleService-Info.plist in Resources */,
+ AFAF36F51EC28C25004BDEE5 /* Shared.xcassets in Resources */,
+ DE9315261E86C6FF0083EDBF /* LaunchScreen.storyboard in Resources */,
+ DE9315271E86C6FF0083EDBF /* Main.storyboard in Resources */,
+ DE4E711B1E953ABC00070092 /* FirebaseDev.podspec in Resources */,
+ DE7B8DD31E8F1CA7009EB6DF /* Database-Info.plist in Resources */,
+ 063CB49A1EBA7AEF00038A59 /* FirebaseTests-Info.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE9314DC1E86C6BE0083EDBF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE9315A51E8738460083EDBF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEB139F91E73506A00AC236D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 069428831EC3B38C00F7BC69 /* 1mb.dat in Resources */,
+ DEB61EC51E7C5DBB00C04B96 /* LaunchScreen.storyboard in Resources */,
+ AFAF36F91EC28C25004BDEE5 /* Shared.xcassets in Resources */,
+ DEB61EC61E7C5DBB00C04B96 /* Main.storyboard in Resources */,
+ DEB61EC91E7C5DBB00C04B96 /* GoogleService-Info.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEB13A1D1E73507E00AC236D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE14D3F1E84464D006FA992 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ AFAF36F61EC28C25004BDEE5 /* Shared.xcassets in Resources */,
+ DEE14D831E844677006FA992 /* GoogleService-Info.plist in Resources */,
+ DEE14D7E1E844677006FA992 /* LaunchScreen.storyboard in Resources */,
+ DEE14D7F1E844677006FA992 /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE14D571E84464D006FA992 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 016A3201E8E0C5ABE835F645 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ 0840546A7D90530C21375416 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Storage_IntegrationTests/Pods-Storage_IntegrationTests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 12F6D0DD1D452316A3123EED /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Storage_Tests/Pods-Storage_Tests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 26F9869011740630E2119D0D /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ 316D851DAC53422509F9B7B4 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Core_Tests/Pods-Core_Tests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3D9876DCE9EFE13441346E50 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Messaging_Tests/Pods-Messaging_Tests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 4363D4BBFAAC4D505B9B18EC /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ 54175166C251F5F698B6B1C3 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Auth_Tests/Pods-Auth_Tests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 57EF5C8DAF88A5F43BE0C6FE /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Storage_Tests/Pods-Storage_Tests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 597B88E0E6B632C48707E8EB /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Database_IntegrationTests/Pods-Database_IntegrationTests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 5AE0F6A81F9A499BD752D5E9 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Database_Example/Pods-Database_Example-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 5BE3F4C8BA697C65D55C055E /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Storage_Example/Pods-Storage_Example-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 5EFAE1A18DA8F6BFC0C191E8 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Database_Tests/Pods-Database_Tests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 634E392CCD4D5E88B96D3EF1 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Database_Tests/Pods-Database_Tests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 66C488F8D840BC12E26BEE9C /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ 744AF9F9B2DB5C511EB9969A /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Database_IntegrationTests/Pods-Database_IntegrationTests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 7CC2EB21DFB0E48B1B8171B2 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ 7E9B7B5115CCC4F0FCBED014 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ 883CE42B54A7BB78295FBCDA /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Core_Example/Pods-Core_Example-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9D6CD2CDCDD281E43FDD3492 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Core_Tests/Pods-Core_Tests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9D8053D74F91F866DB0D0199 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Auth_Tests/Pods-Auth_Tests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ A2DF8D8C8D3B6639CBD9CB5B /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Auth_Example/Pods-Auth_Example-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ A39D405E17BE3A6646B8E38E /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Messaging_Example/Pods-Messaging_Example-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ AA723E4B93CCA8A3A4C24F4E /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Messaging_Tests/Pods-Messaging_Tests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ AB5B6984AF16CF03E74EA522 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ AD42F17297AF3C18062D4C51 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ ADFC988CE33AA0C8F0C59177 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Messaging_Example/Pods-Messaging_Example-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ B293C142610E914FBE2CA4C9 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Auth_Example/Pods-Auth_Example-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ B84CF6076850A6EA9E66592F /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Storage_IntegrationTests/Pods-Storage_IntegrationTests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ BCC67418B8EBA4E90488CD55 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ BF9A61F6BBE9655CED5E897D /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ C3AFD8761D910A99E506F606 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+ DEFCD9C6026936498AE2778C /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Core_Example/Pods-Core_Example-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ E8657FA5227C3B5EB7B83B40 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Storage_Example/Pods-Storage_Example-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ E9A4ADE4EE7390DC85A9FC17 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Database_Example/Pods-Database_Example-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ EE21F035A2A97C35635C2F3C /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ 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";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 06121EB81EC399C50008D70E /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 06C24A061EC39BCB005208CA /* FIRStorageIntegrationTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 0624F3DD1EC0ECFA00E5940D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0624F3EC1EC0ED1B00E5940D /* FData.m in Sources */,
+ 0637BA6D1EC0F9CF00CAEFD4 /* FTestBase.m in Sources */,
+ 0637BA721EC0F9E000CAEFD4 /* FTupleEventTypeString.m in Sources */,
+ 0637BA6C1EC0F9CB00CAEFD4 /* FTestAuthTokenGenerator.m in Sources */,
+ 0637BA6B1EC0F9C700CAEFD4 /* FMockStorageEngine.m in Sources */,
+ 0624F3EB1EC0ED0800E5940D /* FConnectionTest.m in Sources */,
+ 0624F3F21EC0ED3F00E5940D /* FKeepSyncedTest.m in Sources */,
+ 0624F3F71EC0ED5600E5940D /* FTransactionTest.m in Sources */,
+ 0624F3F01EC0ED3500E5940D /* FIRDatabaseQueryTests.m in Sources */,
+ 0637BA701EC0F9D900CAEFD4 /* FTestExpectations.m in Sources */,
+ 0637BA6F1EC0F9D500CAEFD4 /* FTestClock.m in Sources */,
+ 0637BA691EC0F9C100CAEFD4 /* FIRFakeApp.m in Sources */,
+ 0624F3EE1EC0ED2A00E5940D /* FEventTests.m in Sources */,
+ 0624F3ED1EC0ED2300E5940D /* FDotInfo.m in Sources */,
+ 0624F3F61EC0ED5100E5940D /* FRealtime.m in Sources */,
+ 0637BA671EC0F9BA00CAEFD4 /* FDevice.m in Sources */,
+ 0637BA6A1EC0F9C400CAEFD4 /* FIRTestAuthTokenProvider.m in Sources */,
+ 0637BA731EC0F9E400CAEFD4 /* SenTest+FWaiter.m in Sources */,
+ 0624F3F11EC0ED3A00E5940D /* FIRDatabaseTests.m in Sources */,
+ 0624F3F41EC0ED4800E5940D /* FOrderByTests.m in Sources */,
+ 0624F3F51EC0ED4D00E5940D /* FPersist.m in Sources */,
+ 0624F3EF1EC0ED3000E5940D /* FIRAuthTests.m in Sources */,
+ 0624F3F31EC0ED4300E5940D /* FOrder.m in Sources */,
+ 0637BA6E1EC0F9D200CAEFD4 /* FTestCachePolicy.m in Sources */,
+ 0637BA711EC0F9DD00CAEFD4 /* FTestHelpers.m in Sources */,
+ 0637BA681EC0F9BD00CAEFD4 /* FEventTester.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ AFD562E11EB13C6D00EA2233 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ AFD563171EBBEF7B00EA2233 /* Data+MessagingExtensions.swift in Sources */,
+ AFD5630E1EB1402300EA2233 /* AppDelegate.swift in Sources */,
+ AFC8BA9D1EBD230E00B8EEAE /* NotificationsController.swift in Sources */,
+ AFD5630F1EB1402300EA2233 /* MessagingViewController.swift in Sources */,
+ AFC8BAA71EC257D800B8EEAE /* FIRSampleAppUtilities.m in Sources */,
+ AFC8BA9F1EBD51A700B8EEAE /* Environment.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE7B8D011E8EF077009EB6DF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE7B8DCB1E8EF23A009EB6DF /* FIRViewController.m in Sources */,
+ DE7B8DCC1E8EF23A009EB6DF /* main.m in Sources */,
+ DE7B8DCA1E8EF23A009EB6DF /* FIRAppDelegate.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE7B8D191E8EF078009EB6DF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE7B8DC61E8EF203009EB6DF /* FTestHelpers.m in Sources */,
+ 063CB4CE1EBA7B4600038A59 /* FPathTests.m in Sources */,
+ DE7B8DC31E8EF203009EB6DF /* FTestCachePolicy.m in Sources */,
+ 063CB4D11EBA7B4600038A59 /* FPruningTest.m in Sources */,
+ DE7B8DC41E8EF203009EB6DF /* FTestClock.m in Sources */,
+ DE7B8DC01E8EF203009EB6DF /* FIRTestAuthTokenProvider.m in Sources */,
+ 063CB4D31EBA7B4600038A59 /* FRangeMergeTest.m in Sources */,
+ 063CB4C81EBA7B3100038A59 /* FTreeSortedDictionaryTests.m in Sources */,
+ 063CB4CD1EBA7B4600038A59 /* FNodeTests.m in Sources */,
+ 063CB4CB1EBA7B4600038A59 /* FIRMutableDataTests.m in Sources */,
+ 063CB4BE1EBA7B3100038A59 /* FIRDataSnapshotTests.m in Sources */,
+ DE7B8DC11E8EF203009EB6DF /* FMockStorageEngine.m in Sources */,
+ DE7B8DC71E8EF203009EB6DF /* FTupleEventTypeString.m in Sources */,
+ 063CB4CC1EBA7B4600038A59 /* FLevelDBStorageEngineTests.m in Sources */,
+ 063CB4D41EBA7B4600038A59 /* FRepoInfoTest.m in Sources */,
+ 063CB4CA1EBA7B4600038A59 /* FCompoundHashTest.m in Sources */,
+ 063CB4D81EBA7B4600038A59 /* FTrackedQueryManagerTest.m in Sources */,
+ 063CB4D91EBA7B4600038A59 /* FUtilitiesTest.m in Sources */,
+ 063CB4D51EBA7B4600038A59 /* FSparseSnapshotTests.m in Sources */,
+ 063CB4D71EBA7B4600038A59 /* FTestBase.m in Sources */,
+ DE7B8DBE1E8EF203009EB6DF /* FDevice.m in Sources */,
+ 063CB4CF1EBA7B4600038A59 /* FPersistenceManagerTest.m in Sources */,
+ 063CB4A71EBA7B0B00038A59 /* FCompoundWriteTest.m in Sources */,
+ DE7B8DC81E8EF203009EB6DF /* SenTest+FWaiter.m in Sources */,
+ 063CB4D01EBA7B4600038A59 /* FPruneForestTest.m in Sources */,
+ DE7B8DC51E8EF203009EB6DF /* FTestExpectations.m in Sources */,
+ 063CB4BF1EBA7B3100038A59 /* FIRFakeApp.m in Sources */,
+ DE7B8DBF1E8EF203009EB6DF /* FEventTester.m in Sources */,
+ 063CB4D21EBA7B4600038A59 /* FQueryParamsTest.m in Sources */,
+ 063CB4DB1EBAA89E00038A59 /* FSyncPointTests.m in Sources */,
+ 063CB4C91EBA7B4600038A59 /* FArraySortedDictionaryTest.m in Sources */,
+ DE7B8DC21E8EF203009EB6DF /* FTestAuthTokenGenerator.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE9314C21E86C6BD0083EDBF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0697B1221EC13D8A00542174 /* Base64.m in Sources */,
+ DE93152A1E86C6FF0083EDBF /* FIRViewController.m in Sources */,
+ DE93152D1E86C6FF0083EDBF /* main.m in Sources */,
+ DE9315291E86C6FF0083EDBF /* FIRAppDelegate.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE9314DA1E86C6BE0083EDBF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE9315691E86C71C0083EDBF /* FIRGetOOBConfirmationCodeResponseTests.m in Sources */,
+ DE9315661E86C71C0083EDBF /* FIRGetAccountInfoRequestTests.m in Sources */,
+ DE9315731E86C71C0083EDBF /* FIRSignUpNewUserResponseTests.m in Sources */,
+ DE9315681E86C71C0083EDBF /* FIRGetOOBConfirmationCodeRequestTests.m in Sources */,
+ DE9315571E86C71C0083EDBF /* FIRAdditionalUserInfoTests.m in Sources */,
+ DE750DBF1EB3DD6C00A75E47 /* FIRAuthAppCredentialManagerTests.m in Sources */,
+ DE93157B1E86C71C0083EDBF /* FIRVerifyPasswordResponseTests.m in Sources */,
+ DE93155B1E86C71C0083EDBF /* FIRAuthDispatcherTests.m in Sources */,
+ DE9315791E86C71C0083EDBF /* FIRVerifyCustomTokenResponseTests.m in Sources */,
+ DE9315601E86C71C0083EDBF /* FIRAuthUserDefaultsStorageTests.m in Sources */,
+ DE9315641E86C71C0083EDBF /* FIRDeleteAccountResponseTests.m in Sources */,
+ DE9315741E86C71C0083EDBF /* FIRTwitterAuthProviderTests.m in Sources */,
+ DE750DC01EB3DD6F00A75E47 /* FIRAuthNotificationManagerTests.m in Sources */,
+ DE93156A1E86C71C0083EDBF /* FIRGitHubAuthProviderTests.m in Sources */,
+ DE9315761E86C71C0083EDBF /* FIRVerifyAssertionRequestTests.m in Sources */,
+ DE9315781E86C71C0083EDBF /* FIRVerifyCustomTokenRequestTests.m in Sources */,
+ DE93157C1E86C71C0083EDBF /* FIRVerifyPhoneNumberRequestTests.m in Sources */,
+ DE9315651E86C71C0083EDBF /* FIRFakeBackendRPCIssuer.m in Sources */,
+ DE9315591E86C71C0083EDBF /* FIRAuthBackendCreateAuthURITests.m in Sources */,
+ DE0E5BBB1EA7D92E00FAA825 /* FIRVerifyClientRequestTest.m in Sources */,
+ DE93156F1E86C71C0083EDBF /* FIRSendVerificationCodeResponseTests.m in Sources */,
+ DE93156C1E86C71C0083EDBF /* FIRResetPasswordRequestTests.m in Sources */,
+ DE93156D1E86C71C0083EDBF /* FIRResetPasswordResponseTests.m in Sources */,
+ DE9315611E86C71C0083EDBF /* FIRCreateAuthURIRequestTests.m in Sources */,
+ DE93156E1E86C71C0083EDBF /* FIRSendVerificationCodeRequestTests.m in Sources */,
+ DE93155D1E86C71C0083EDBF /* FIRAuthKeychainTests.m in Sources */,
+ DE93155C1E86C71C0083EDBF /* FIRAuthGlobalWorkQueueTests.m in Sources */,
+ DE9315631E86C71C0083EDBF /* FIRDeleteAccountRequestTests.m in Sources */,
+ DECE039B1E9ED01600164CA4 /* FIRPhoneAuthProviderTests.m in Sources */,
+ DE750DBE1EB3DD6800A75E47 /* FIRAuthAPNSTokenManagerTests.m in Sources */,
+ DE93157A1E86C71C0083EDBF /* FIRVerifyPasswordRequestTest.m in Sources */,
+ DE9315621E86C71C0083EDBF /* FIRCreateAuthURIResponseTests.m in Sources */,
+ DE93155A1E86C71C0083EDBF /* FIRAuthBackendRPCImplementationTests.m in Sources */,
+ DE93157D1E86C71C0083EDBF /* FIRVerifyPhoneNumberResponseTests.m in Sources */,
+ DE93157E1E86C71C0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.m in Sources */,
+ DE9315771E86C71C0083EDBF /* FIRVerifyAssertionResponseTests.m in Sources */,
+ DE9315721E86C71C0083EDBF /* FIRSignUpNewUserRequestTests.m in Sources */,
+ DE9315671E86C71C0083EDBF /* FIRGetAccountInfoResponseTests.m in Sources */,
+ DE9315701E86C71C0083EDBF /* FIRSetAccountInfoRequestTests.m in Sources */,
+ DE0E5BBD1EA7D93100FAA825 /* FIRAuthAppCredentialTests.m in Sources */,
+ DE93155E1E86C71C0083EDBF /* FIRAuthSerialTaskQueueTests.m in Sources */,
+ DE9315581E86C71C0083EDBF /* FIRApp+FIRAuthUnitTests.m in Sources */,
+ DE9315711E86C71C0083EDBF /* FIRSetAccountInfoResponseTests.m in Sources */,
+ DE93155F1E86C71C0083EDBF /* FIRAuthTests.m in Sources */,
+ DE750DBD1EB3DD5B00A75E47 /* FIRAuthAPNSTokenTests.m in Sources */,
+ DE0E5BBE1EA7D93500FAA825 /* FIRAuthAppDelegateProxyTests.m in Sources */,
+ DE0E5BBC1EA7D92E00FAA825 /* FIRVerifyClientResponseTests.m in Sources */,
+ DE9315751E86C71C0083EDBF /* FIRUserTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE9315A31E8738460083EDBF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE9315F41E8738E60083EDBF /* FIRMessagingClientTest.m in Sources */,
+ DE9315F51E8738E60083EDBF /* FIRMessagingCodedInputStreamTest.m in Sources */,
+ DE9315F71E8738E60083EDBF /* FIRMessagingContextManagerServiceTest.m in Sources */,
+ DE9315FD1E8738E60083EDBF /* FIRMessagingPubSubTest.m in Sources */,
+ DE9316011E8738E60083EDBF /* FIRMessagingSecureSocketTest.m in Sources */,
+ DE9315FB1E8738E60083EDBF /* FIRMessagingLinkHandlingTest.m in Sources */,
+ DE9315FC1E8738E60083EDBF /* FIRMessagingPendingTopicsListTest.m in Sources */,
+ DE9316001E8738E60083EDBF /* FIRMessagingRmqManagerTest.m in Sources */,
+ DE9315F91E8738E60083EDBF /* FIRMessagingFakeConnection.m in Sources */,
+ DE9316021E8738E60083EDBF /* FIRMessagingServiceTest.m in Sources */,
+ DE9315FE1E8738E60083EDBF /* FIRMessagingRegistrarTest.m in Sources */,
+ DE9316031E8738E60083EDBF /* FIRMessagingSyncMessageManagerTest.m in Sources */,
+ DE9315FF1E8738E60083EDBF /* FIRMessagingRemoteNotificationsProxyTest.m in Sources */,
+ DE9315F81E8738E60083EDBF /* FIRMessagingDataMessageManagerTest.m in Sources */,
+ DE9316051E8738E60083EDBF /* FIRMessagingTestNotificationUtilities.m in Sources */,
+ DE9315F61E8738E60083EDBF /* FIRMessagingConnectionTest.m in Sources */,
+ DE9316041E8738E60083EDBF /* FIRMessagingTest.m in Sources */,
+ DE9315FA1E8738E60083EDBF /* FIRMessagingFakeSocket.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEB139E21E73506A00AC236D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEB61EC81E7C5DBB00C04B96 /* FIRViewController.m in Sources */,
+ DEB61ECB1E7C5DBB00C04B96 /* main.m in Sources */,
+ DEB61EC71E7C5DBB00C04B96 /* FIRAppDelegate.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEB13A0E1E73507E00AC236D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEB13A301E73518B00AC236D /* FIRStorageUtilsTests.m in Sources */,
+ DEB13A2D1E73518B00AC236D /* FIRStorageTests.m in Sources */,
+ DEB13A281E73518B00AC236D /* FIRStorageGetMetadataTests.m in Sources */,
+ DEB13A2F1E73518B00AC236D /* FIRStorageUpdateMetadataTests.m in Sources */,
+ DEB13A271E73518B00AC236D /* FIRStorageDeleteTests.m in Sources */,
+ DEB13A2C1E73518B00AC236D /* FIRStorageTestHelpers.m in Sources */,
+ DEB13A291E73518B00AC236D /* FIRStorageMetadataTests.m in Sources */,
+ DEB13A2E1E73518B00AC236D /* FIRStorageTokenAuthorizerTests.m in Sources */,
+ DEB13A2B1E73518B00AC236D /* FIRStorageReferenceTests.m in Sources */,
+ DEB13A2A1E73518B00AC236D /* FIRStoragePathTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE14D3D1E84464D006FA992 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEE14D821E844677006FA992 /* FIRViewController.m in Sources */,
+ DEE14D851E844677006FA992 /* main.m in Sources */,
+ DEE14D811E844677006FA992 /* FIRAppDelegate.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DEE14D551E84464D006FA992 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DEE14D8E1E84468D006FA992 /* FIRAppAssociationRegistrationUnitTests.m in Sources */,
+ DEE14D8F1E84468D006FA992 /* FIRAppTest.m in Sources */,
+ DEE14D911E84468D006FA992 /* FIRConfigurationTest.m in Sources */,
+ DEE14D921E84468D006FA992 /* FIRLoggerTest.m in Sources */,
+ DEE14D931E84468D006FA992 /* FIROptionsTest.m in Sources */,
+ DEE14D901E84468D006FA992 /* FIRBundleUtilTest.m in Sources */,
+ DEE14D941E84468D006FA992 /* FIRTestCase.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 06121EC71EC399D40008D70E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DEB139E01E73506A00AC236D /* Storage_Example */;
+ targetProxy = 06121EC61EC399D40008D70E /* PBXContainerItemProxy */;
+ };
+ 0624F3E71EC0ECFA00E5940D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DE7B8D041E8EF077009EB6DF /* Database_Example */;
+ targetProxy = 0624F3E61EC0ECFA00E5940D /* PBXContainerItemProxy */;
+ };
+ AFD563121EB140E100EA2233 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = AFD562E41EB13C6D00EA2233 /* Messaging_Example */;
+ targetProxy = AFD563111EB140E100EA2233 /* PBXContainerItemProxy */;
+ };
+ DE3373981E73776F00881891 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DEB13A0A1E73507E00AC236D /* Storage_Tests */;
+ targetProxy = DE3373971E73776F00881891 /* PBXContainerItemProxy */;
+ };
+ DE6F01BA1E957157004AEE01 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DE9315A61E8738460083EDBF /* Messaging_Tests */;
+ targetProxy = DE6F01B91E957157004AEE01 /* PBXContainerItemProxy */;
+ };
+ DE7B8D1F1E8EF078009EB6DF /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DE7B8D041E8EF077009EB6DF /* Database_Example */;
+ targetProxy = DE7B8D1E1E8EF078009EB6DF /* PBXContainerItemProxy */;
+ };
+ DE9314E01E86C6BE0083EDBF /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DE9314C51E86C6BD0083EDBF /* Auth_Example */;
+ targetProxy = DE9314DF1E86C6BE0083EDBF /* PBXContainerItemProxy */;
+ };
+ DE9315871E86E9990083EDBF /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DE9314DD1E86C6BE0083EDBF /* Auth_Tests */;
+ targetProxy = DE9315861E86E9990083EDBF /* PBXContainerItemProxy */;
+ };
+ DEB13A261E73512500AC236D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DEB139E01E73506A00AC236D /* Storage_Example */;
+ targetProxy = DEB13A251E73512500AC236D /* PBXContainerItemProxy */;
+ };
+ DEB5185A1E9008CB0089C938 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DE7B8D1C1E8EF078009EB6DF /* Database_Tests */;
+ targetProxy = DEB518591E9008CB0089C938 /* PBXContainerItemProxy */;
+ };
+ DEE14D5B1E84464D006FA992 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DEE14D401E84464D006FA992 /* Core_Example */;
+ targetProxy = DEE14D5A1E84464D006FA992 /* PBXContainerItemProxy */;
+ };
+ DEE14E0B1E844FDC006FA992 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DEE14D581E84464D006FA992 /* Core_Tests */;
+ targetProxy = DEE14E0A1E844FDC006FA992 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ AFD563081EB1400900EA2233 /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ AFD563091EB1400900EA2233 /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+ AFD5630A1EB1400900EA2233 /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ AFD5630B1EB1400900EA2233 /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+ DE7B8D2C1E8EF202009EB6DF /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DE7B8D2D1E8EF202009EB6DF /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+ DE7B8D2E1E8EF202009EB6DF /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DE7B8D2F1E8EF202009EB6DF /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+ DE7B8D691E8EF202009EB6DF /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DE7B8D6A1E8EF202009EB6DF /* en */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "<group>";
+ };
+ DE9314ED1E86C6FF0083EDBF /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DE9314EE1E86C6FF0083EDBF /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+ DE9314EF1E86C6FF0083EDBF /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DE9314F01E86C6FF0083EDBF /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+ DEB61EB91E7C5DBB00C04B96 /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DEB61EBA1E7C5DBB00C04B96 /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+ DEB61EBB1E7C5DBB00C04B96 /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DEB61EBC1E7C5DBB00C04B96 /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+ DEE14D681E844677006FA992 /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DEE14D691E844677006FA992 /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+ DEE14D6A1E844677006FA992 /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ DEE14D6B1E844677006FA992 /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 06121EC31EC399C50008D70E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 3673564CCB64DE360C8CB97F /* Pods-Storage_IntegrationTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/../../Firebase/Storage/Private\"",
+ "\"$(SRCROOT)/../Firebase/Core/Private\"",
+ );
+ INFOPLIST_FILE = "Storage/Tests/Tests-Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 10.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.firebase.mobile.Storage-IntegrationTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Storage_Example.app/Storage_Example";
+ };
+ name = Debug;
+ };
+ 06121EC41EC399C50008D70E /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = CA86AD35456DA6130F7DE02C /* Pods-Storage_IntegrationTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/../../Firebase/Storage/Private\"",
+ "\"$(SRCROOT)/../Firebase/Core/Private\"",
+ );
+ INFOPLIST_FILE = "Storage/Tests/Tests-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.firebase.mobile.Storage-IntegrationTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Storage_Example.app/Storage_Example";
+ };
+ name = Release;
+ };
+ 0624F3E91EC0ECFA00E5940D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = EE077EBC5A738E61E06B5FA2 /* Pods-Database_IntegrationTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ 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/FImmutableSortedDictionary/FImmutableSortedDictionary\"",
+ "\"${PODS_ROOT}/../../Firebase/Database/Core/View\"",
+ "\"$(SRCROOT)/../Firebase/Core/Private\"",
+ );
+ 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 = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Database-IntegrationTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Database_Example.app/Database_Example";
+ };
+ name = Debug;
+ };
+ 0624F3EA1EC0ECFA00E5940D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = BEEA177FFAAB9FA02F898C51 /* Pods-Database_IntegrationTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ COPY_PHASE_STRIP = NO;
+ 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/FImmutableSortedDictionary/FImmutableSortedDictionary\"",
+ "\"${PODS_ROOT}/../../Firebase/Database/Core/View\"",
+ "\"$(SRCROOT)/../Firebase/Core/Private\"",
+ );
+ 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";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Database_Example.app/Database_Example";
+ };
+ name = Release;
+ };
+ 6003F5BD195388D20070C39A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ 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_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ 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 = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ 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 = 8.3;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 6003F5BE195388D20070C39A /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ 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_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ 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 = 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 = 8.3;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ AFD562F51EB13C6D00EA2233 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 884B87C50C7C950BC18E9091 /* Pods-Messaging_Example.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CODE_SIGN_ENTITLEMENTS = Messaging/App/Messaging_Example.entitlements;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "Messaging/App/Messaging-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseMessagingSample.dev;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OBJC_BRIDGING_HEADER = "Messaging/Messaging_Example-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Debug;
+ };
+ AFD562F61EB13C6D00EA2233 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = A6903B88963F6FD1857889E6 /* Pods-Messaging_Example.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CODE_SIGN_ENTITLEMENTS = Messaging/App/Messaging_Example.entitlements;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "Messaging/App/Messaging-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = NO;
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseMessagingSample.dev;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Messaging/Messaging_Example-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Release;
+ };
+ DE33738B1E73773400881891 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ DE33738C1E73773400881891 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+ DE7B8D241E8EF078009EB6DF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7727BC17692B98E2B7D0EA7A /* Pods-Database_Example.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "$(SRCROOT)/Database/App/Database-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Database-Example";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ DE7B8D251E8EF078009EB6DF /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = EEA5C6257533CD27D37A14FC /* Pods-Database_Example.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "$(SRCROOT)/Database/App/Database-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = NO;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Database-Example";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+ DE7B8D261E8EF078009EB6DF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = F0A9002767E1A9D63CEECFF6 /* Pods-Database_Tests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ 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/FImmutableSortedDictionary/FImmutableSortedDictionary\"",
+ "\"${PODS_ROOT}/../../Firebase/Database/Core/View\"",
+ "\"$(SRCROOT)/../Firebase/Core/Private\"",
+ );
+ 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 = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Database-ExampleTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Database_Example.app/Database_Example";
+ };
+ name = Debug;
+ };
+ DE7B8D271E8EF078009EB6DF /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 4A8B7AE7C053949F6BBBDD3E /* Pods-Database_Tests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ 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/FImmutableSortedDictionary/FImmutableSortedDictionary\"",
+ "\"${PODS_ROOT}/../../Firebase/Database/Core/View\"",
+ "\"$(SRCROOT)/../Firebase/Core/Private\"",
+ );
+ 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-ExampleTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Database_Example.app/Database_Example";
+ };
+ name = Release;
+ };
+ DE9314E51E86C6BE0083EDBF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = C45949C3AB12F54D27702387 /* Pods-Auth_Example.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "$(SRCROOT)/Auth/App/Auth-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-Example";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Debug;
+ };
+ DE9314E61E86C6BE0083EDBF /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 2F002D4E7FA7F07A830CCFDA /* Pods-Auth_Example.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "$(SRCROOT)/Auth/App/Auth-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = NO;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-Example";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Release;
+ };
+ DE9314E71E86C6BE0083EDBF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 18B5255FF5BEBF6F72C40F39 /* Pods-Auth_Tests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ HEADER_SEARCH_PATHS = (
+ "\"${PODS_ROOT}/../../Firebase/Auth/Source/RPCs\"",
+ "$(inherited)",
+ "\"${PODS_ROOT}/../../Firebase/Auth/Source/Private\"",
+ "\"${PODS_ROOT}/../../Firebase/Auth/Source/AuthProviders\"",
+ "\"${PODS_ROOT}/../../Firebase/Core/Private\"",
+ );
+ INFOPLIST_FILE = "Auth/Tests/Tests-Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 10.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-ExampleTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Auth_Example.app/Auth_Example";
+ };
+ name = Debug;
+ };
+ DE9314E81E86C6BE0083EDBF /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 60FCE4043D8FE42648646A7F /* Pods-Auth_Tests.release.xcconfig */;
+ 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 = EQHXZ8M8AV;
+ HEADER_SEARCH_PATHS = (
+ "\"${PODS_ROOT}/../../Firebase/Auth/Source/RPCs\"",
+ "$(inherited)",
+ "\"${PODS_ROOT}/../../Firebase/Auth/Source/Private\"",
+ "\"${PODS_ROOT}/../../Firebase/Auth/Source/AuthProviders\"",
+ "\"${PODS_ROOT}/../../Firebase/Core/Private\"",
+ );
+ INFOPLIST_FILE = "Auth/Tests/Tests-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.Auth-ExampleTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Auth_Example.app/Auth_Example";
+ };
+ name = Release;
+ };
+ DE9315B01E8738460083EDBF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 6E974DE29EBB9602E723757E /* Pods-Messaging_Tests.debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "COCOAPODS=1",
+ "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1",
+ );
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/Headers/Public\"",
+ "\"${PODS_ROOT}/Headers/Public/FirebaseAnalytics\"",
+ "\"${PODS_ROOT}/../../Firebase/Messaging\"",
+ "\"${PODS_ROOT}/../../Firebase/Core/Private\"",
+ );
+ 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.Messaging-ExampleTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Messaging_Example.app/Messaging_Example";
+ };
+ name = Debug;
+ };
+ DE9315B11E8738460083EDBF /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 6D2E4A9396D707C5DEF9B74B /* Pods-Messaging_Tests.release.xcconfig */;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ 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 = EQHXZ8M8AV;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "COCOAPODS=1",
+ "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1",
+ );
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/Headers/Public\"",
+ "\"${PODS_ROOT}/Headers/Public/FirebaseAnalytics\"",
+ "\"${PODS_ROOT}/../../Firebase/Messaging\"",
+ "\"${PODS_ROOT}/../../Firebase/Core/Private\"",
+ );
+ 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.Messaging-ExampleTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Messaging_Example.app/Messaging_Example";
+ };
+ name = Release;
+ };
+ DEB13A061E73506A00AC236D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 6A0FCB2A37144B3C05E519F6 /* Pods-Storage_Example.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = "$(inherited)";
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ HEADER_SEARCH_PATHS = (
+ "\"${PODS_ROOT}/../../Firebase/Storage/Private\"",
+ "$(inherited)",
+ );
+ INFOPLIST_FILE = "$(SRCROOT)/Storage/App/Storage-Info.plist";
+ MODULE_NAME = ExampleApp;
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Debug;
+ };
+ DEB13A071E73506A00AC236D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 6BAD1CF3DDEDDD76EC87052D /* Pods-Storage_Example.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = "$(inherited)";
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ HEADER_SEARCH_PATHS = (
+ "\"${PODS_ROOT}/../../Firebase/Storage/Private\"",
+ "$(inherited)",
+ );
+ INFOPLIST_FILE = "$(SRCROOT)/Storage/App/Storage-Info.plist";
+ MODULE_NAME = ExampleApp;
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Release;
+ };
+ DEB13A211E73507E00AC236D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 3E84D28D93B8196D6A483F15 /* Pods-Storage_Tests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(SDKROOT)/Developer/Library/Frameworks",
+ "$(inherited)",
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/../../Firebase/Storage/Private\"",
+ "\"${PODS_ROOT}/../../Firebase/Core/Private\"",
+ );
+ INFOPLIST_FILE = "Storage/Tests/Tests-Info.plist";
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Storage_Example.app/Storage_Example";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Debug;
+ };
+ DEB13A221E73507E00AC236D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = D58064F9C4DE303997B89D2E /* Pods-Storage_Tests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(SDKROOT)/Developer/Library/Frameworks",
+ "$(inherited)",
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/../../Firebase/Storage/Private\"",
+ "\"${PODS_ROOT}/../../Firebase/Core/Private\"",
+ );
+ INFOPLIST_FILE = "Storage/Tests/Tests-Info.plist";
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Storage_Example.app/Storage_Example";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Release;
+ };
+ DEE14D601E84464D006FA992 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = FF57915145DB00008E7C56A8 /* Pods-Core_Example.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "$(SRCROOT)/Core/App/Core-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Core-Example";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ DEE14D611E84464D006FA992 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 8F77C04C2E764FBB0F6C05C6 /* Pods-Core_Example.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ INFOPLIST_FILE = "$(SRCROOT)/Core/App/Core-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = NO;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Core-Example";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+ DEE14D621E84464D006FA992 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 8E32E359BE29C3100CF51FC4 /* Pods-Core_Tests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/build/Debug-iphoneos/FirebaseCore",
+ );
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/../../Firebase/Core/Private\"",
+ );
+ INFOPLIST_FILE = "Core/Tests/Tests-Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 10.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Core-ExampleTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Core_Example.app/Core_Example";
+ };
+ name = Debug;
+ };
+ DEE14D631E84464D006FA992 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 0B1BDA534E1F49931795B5E6 /* Pods-Core_Tests.release.xcconfig */;
+ 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 = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/build/Debug-iphoneos/FirebaseCore",
+ );
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/../../Firebase/Core/Private\"",
+ );
+ INFOPLIST_FILE = "Core/Tests/Tests-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.Core-ExampleTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Core_Example.app/Core_Example";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 06121EC51EC399C50008D70E /* Build configuration list for PBXNativeTarget "Storage_IntegrationTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 06121EC31EC399C50008D70E /* Debug */,
+ 06121EC41EC399C50008D70E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 0624F3E81EC0ECFA00E5940D /* Build configuration list for PBXNativeTarget "Database_IntegrationTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0624F3E91EC0ECFA00E5940D /* Debug */,
+ 0624F3EA1EC0ECFA00E5940D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firebase" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6003F5BD195388D20070C39A /* Debug */,
+ 6003F5BE195388D20070C39A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ AFD562F41EB13C6D00EA2233 /* Build configuration list for PBXNativeTarget "Messaging_Example" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ AFD562F51EB13C6D00EA2233 /* Debug */,
+ AFD562F61EB13C6D00EA2233 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE33738A1E73773400881891 /* Build configuration list for PBXAggregateTarget "AllUnitTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE33738B1E73773400881891 /* Debug */,
+ DE33738C1E73773400881891 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE7B8D281E8EF078009EB6DF /* Build configuration list for PBXNativeTarget "Database_Example" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE7B8D241E8EF078009EB6DF /* Debug */,
+ DE7B8D251E8EF078009EB6DF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE7B8D291E8EF078009EB6DF /* Build configuration list for PBXNativeTarget "Database_Tests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE7B8D261E8EF078009EB6DF /* Debug */,
+ DE7B8D271E8EF078009EB6DF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE9314E91E86C6BE0083EDBF /* Build configuration list for PBXNativeTarget "Auth_Example" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE9314E51E86C6BE0083EDBF /* Debug */,
+ DE9314E61E86C6BE0083EDBF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE9314EA1E86C6BE0083EDBF /* Build configuration list for PBXNativeTarget "Auth_Tests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE9314E71E86C6BE0083EDBF /* Debug */,
+ DE9314E81E86C6BE0083EDBF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE9315B31E8738460083EDBF /* Build configuration list for PBXNativeTarget "Messaging_Tests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE9315B01E8738460083EDBF /* Debug */,
+ DE9315B11E8738460083EDBF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DEB13A051E73506A00AC236D /* Build configuration list for PBXNativeTarget "Storage_Example" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DEB13A061E73506A00AC236D /* Debug */,
+ DEB13A071E73506A00AC236D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DEB13A201E73507E00AC236D /* Build configuration list for PBXNativeTarget "Storage_Tests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DEB13A211E73507E00AC236D /* Debug */,
+ DEB13A221E73507E00AC236D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DEE14D641E84464D006FA992 /* Build configuration list for PBXNativeTarget "Core_Example" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DEE14D601E84464D006FA992 /* Debug */,
+ DEE14D611E84464D006FA992 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DEE14D651E84464D006FA992 /* Build configuration list for PBXNativeTarget "Core_Tests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DEE14D621E84464D006FA992 /* Debug */,
+ DEE14D631E84464D006FA992 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 6003F582195388D10070C39A /* Project object */;
+}
diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/AllUnitTests.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/AllUnitTests.xcscheme
new file mode 100644
index 0000000..f356606
--- /dev/null
+++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/AllUnitTests.xcscheme
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0820"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE3373891E73773400881891"
+ BuildableName = "AllUnitTests"
+ BlueprintName = "AllUnitTests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE7B8D1C1E8EF078009EB6DF"
+ BuildableName = "Database_Tests.xctest"
+ BlueprintName = "Database_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE9315A61E8738460083EDBF"
+ BuildableName = "Messaging_Tests.xctest"
+ BlueprintName = "Messaging_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE9314DD1E86C6BE0083EDBF"
+ BuildableName = "Auth_Tests.xctest"
+ BlueprintName = "Auth_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE14D581E84464D006FA992"
+ BuildableName = "Core_Tests.xctest"
+ BlueprintName = "Core_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEB13A0A1E73507E00AC236D"
+ BuildableName = "Storage_Tests.xctest"
+ BlueprintName = "Storage_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE9315A61E8738460083EDBF"
+ BuildableName = "Messaging_Tests.xctest"
+ BlueprintName = "Messaging_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE9314DD1E86C6BE0083EDBF"
+ BuildableName = "Auth_Tests.xctest"
+ BlueprintName = "Auth_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE7B8D1C1E8EF078009EB6DF"
+ BuildableName = "Database_Tests.xctest"
+ BlueprintName = "Database_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "0624F3E01EC0ECFA00E5940D"
+ BuildableName = "Database_IntegrationTests.xctest"
+ BlueprintName = "Database_IntegrationTests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "06121EBB1EC399C50008D70E"
+ BuildableName = "Storage_IntegrationTests.xctest"
+ BlueprintName = "Storage_IntegrationTests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE3373891E73773400881891"
+ BuildableName = "AllUnitTests"
+ BlueprintName = "AllUnitTests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE3373891E73773400881891"
+ BuildableName = "AllUnitTests"
+ BlueprintName = "AllUnitTests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE3373891E73773400881891"
+ BuildableName = "AllUnitTests"
+ BlueprintName = "AllUnitTests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_Tests.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_Tests.xcscheme
new file mode 100644
index 0000000..28bc109
--- /dev/null
+++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_Tests.xcscheme
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE9314DD1E86C6BE0083EDBF"
+ BuildableName = "Auth_Tests.xctest"
+ BlueprintName = "Auth_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Core_Tests.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Core_Tests.xcscheme
new file mode 100644
index 0000000..1c301e1
--- /dev/null
+++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Core_Tests.xcscheme
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEE14D581E84464D006FA992"
+ BuildableName = "Core_Tests.xctest"
+ BlueprintName = "Core_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Database_IntegrationTests.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Database_IntegrationTests.xcscheme
new file mode 100644
index 0000000..ab12465
--- /dev/null
+++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Database_IntegrationTests.xcscheme
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "0624F3E01EC0ECFA00E5940D"
+ BuildableName = "Database_IntegrationTests.xctest"
+ BlueprintName = "Database_IntegrationTests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Database_Tests.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Database_Tests.xcscheme
new file mode 100644
index 0000000..fbc84c6
--- /dev/null
+++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Database_Tests.xcscheme
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE7B8D1C1E8EF078009EB6DF"
+ BuildableName = "Database_Tests.xctest"
+ BlueprintName = "Database_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Example.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Example.xcscheme
new file mode 100644
index 0000000..6e13a6f
--- /dev/null
+++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Example.xcscheme
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "AFD562E41EB13C6D00EA2233"
+ BuildableName = "Messaging_Example.app"
+ BlueprintName = "Messaging_Example"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE9315A61E8738460083EDBF"
+ BuildableName = "Messaging_Tests.xctest"
+ BlueprintName = "Messaging_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "AFD562E41EB13C6D00EA2233"
+ BuildableName = "Messaging_Example.app"
+ BlueprintName = "Messaging_Example"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "AFD562E41EB13C6D00EA2233"
+ BuildableName = "Messaging_Example.app"
+ BlueprintName = "Messaging_Example"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "AFD562E41EB13C6D00EA2233"
+ BuildableName = "Messaging_Example.app"
+ BlueprintName = "Messaging_Example"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Tests.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Tests.xcscheme
new file mode 100644
index 0000000..768524f
--- /dev/null
+++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Tests.xcscheme
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE9315A61E8738460083EDBF"
+ BuildableName = "Messaging_Tests.xctest"
+ BlueprintName = "Messaging_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Storage_IntegrationTests.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Storage_IntegrationTests.xcscheme
new file mode 100644
index 0000000..752ef80
--- /dev/null
+++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Storage_IntegrationTests.xcscheme
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "06121EBB1EC399C50008D70E"
+ BuildableName = "Storage_IntegrationTests.xctest"
+ BlueprintName = "Storage_IntegrationTests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Storage_Tests.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Storage_Tests.xcscheme
new file mode 100644
index 0000000..c3274cb
--- /dev/null
+++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Storage_Tests.xcscheme
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DEB13A0A1E73507E00AC236D"
+ BuildableName = "Storage_Tests.xctest"
+ BlueprintName = "Storage_Tests"
+ ReferencedContainer = "container:Firebase.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Example/Messaging/App/AppDelegate.swift b/Example/Messaging/App/AppDelegate.swift
new file mode 100644
index 0000000..0f40a4e
--- /dev/null
+++ b/Example/Messaging/App/AppDelegate.swift
@@ -0,0 +1,114 @@
+/*
+ * 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 FirebaseDev
+import UserNotifications
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+
+ static let isWithinUnitTest: Bool = {
+ if let testClass = NSClassFromString("XCTestCase") {
+ return true
+ } else {
+ return false
+ }
+ }()
+
+ static var hasPresentedInvalidServiceInfoPlistAlert = false
+
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+ guard !AppDelegate.isWithinUnitTest else {
+ // During unit tests, we don't want to initialize Firebase, since by default we want to able
+ // to run unit tests without requiring a non-dummy GoogleService-Info.plist file
+ return true
+ }
+
+ guard SampleAppUtilities.appContainsRealServiceInfoPlist() else {
+ // We can't run because the GoogleService-Info.plist file is likely the dummy file which needs
+ // to be replaced with a real one, or somehow the file has been removed from the app bundle.
+ // See: https://github.com/firebase/firebase-ios-sdk/
+ // We'll present a friendly alert when the app becomes active.
+ return true
+ }
+
+ FirebaseApp.configure()
+ Messaging.messaging().delegate = self
+
+ NotificationsController.configure()
+
+ if #available(iOS 8.0, *) {
+ // Always register for remote notifications. This will not show a prompt to the user, as by
+ // default it will provision silent notifications. We can use UNUserNotificationCenter to
+ // request authorization for user-facing notifications.
+ application.registerForRemoteNotifications()
+ } else {
+ // iOS 7 didn't differentiate between user-facing and other notifications, so we should just
+ // register for remote notifications
+ NotificationsController.shared.registerForUserFacingNotificationsFor(application)
+ }
+
+ printFCMToken()
+ return true
+ }
+
+ func printFCMToken() {
+ if let token = Messaging.messaging().fcmToken {
+ print("FCM Token: \(token)")
+ } else {
+ print("FCM Token: nil")
+ }
+ }
+
+ func application(_ application: UIApplication,
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
+ print("APNS Token: \(deviceToken.hexByteString)")
+ NotificationCenter.default.post(name: APNSTokenReceivedNotification, object: nil)
+ if #available(iOS 8.0, *) {
+ } else {
+ // On iOS 7, receiving a device token also means our user notifications were granted, so fire
+ // the notification to update our user notifications UI
+ NotificationCenter.default.post(name: UserNotificationsChangedNotification, object: nil)
+ }
+ }
+
+ func application(_ application: UIApplication,
+ didRegister notificationSettings: UIUserNotificationSettings) {
+ NotificationCenter.default.post(name: UserNotificationsChangedNotification, object: nil)
+ }
+
+ func applicationDidBecomeActive(_ application: UIApplication) {
+ // If the app didn't start property due to an invalid GoogleService-Info.plist file, show an
+ // alert to the developer.
+ if !SampleAppUtilities.appContainsRealServiceInfoPlist() &&
+ !AppDelegate.hasPresentedInvalidServiceInfoPlistAlert {
+ if let vc = window?.rootViewController {
+ SampleAppUtilities.presentAlertForInvalidServiceInfoPlistFrom(vc)
+ AppDelegate.hasPresentedInvalidServiceInfoPlistAlert = true
+ }
+ }
+ }
+}
+
+extension AppDelegate: MessagingDelegate {
+ func messaging(_ messaging: Messaging, didRefreshRegistrationToken fcmToken: String) {
+ printFCMToken()
+ }
+}
+
diff --git a/Example/Messaging/App/Base.lproj/LaunchScreen.storyboard b/Example/Messaging/App/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..fdf3f97
--- /dev/null
+++ b/Example/Messaging/App/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Messaging/App/Base.lproj/Main.storyboard b/Example/Messaging/App/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..6df1a82
--- /dev/null
+++ b/Example/Messaging/App/Base.lproj/Main.storyboard
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="taE-sK-BOl">
+ <device id="retina4_7" orientation="portrait">
+ <adaptation id="fullscreen"/>
+ </device>
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--Firebase Cloud Messaging-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="MessagingViewController" customModule="Messaging_Example" customModuleProvider="target" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ </view>
+ <navigationItem key="navigationItem" title="Firebase Cloud Messaging" id="z1u-kE-qKb"/>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="698" y="164"/>
+ </scene>
+ <!--Navigation Controller-->
+ <scene sceneID="rmF-xz-rwn">
+ <objects>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="Ju1-Bj-8eG" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ <navigationController id="taE-sK-BOl" sceneMemberID="viewController">
+ <navigationBar key="navigationBar" contentMode="scaleToFill" id="iTL-Kg-11w">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </navigationBar>
+ <connections>
+ <segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="04R-HZ-bi6"/>
+ </connections>
+ </navigationController>
+ </objects>
+ <point key="canvasLocation" x="-92" y="165"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Messaging/App/Data+MessagingExtensions.swift b/Example/Messaging/App/Data+MessagingExtensions.swift
new file mode 100644
index 0000000..99ded25
--- /dev/null
+++ b/Example/Messaging/App/Data+MessagingExtensions.swift
@@ -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 Foundation
+
+extension Data {
+ // Print Data as a string of bytes in hex, such as the common representation of APNs device tokens
+ // See: http://stackoverflow.com/a/40031342/9849
+ var hexByteString: String {
+ return self.map { String(format: "%02.2hhx", $0) }.joined()
+ }
+}
diff --git a/Example/Messaging/App/Environment.swift b/Example/Messaging/App/Environment.swift
new file mode 100644
index 0000000..5219c64
--- /dev/null
+++ b/Example/Messaging/App/Environment.swift
@@ -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 Foundation
+
+struct Environment {
+ static let isSimulator: Bool = {
+ var isSim = false
+ #if arch(i386) || arch(x86_64)
+ isSim = true
+ #endif
+
+ return isSim
+ }()
+}
diff --git a/Example/Messaging/App/GoogleService-Info.plist b/Example/Messaging/App/GoogleService-Info.plist
new file mode 100644
index 0000000..89afffe
--- /dev/null
+++ b/Example/Messaging/App/GoogleService-Info.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>API_KEY</key>
+ <string>correct_api_key</string>
+ <key>TRACKING_ID</key>
+ <string>correct_tracking_id</string>
+ <key>CLIENT_ID</key>
+ <string>correct_client_id</string>
+ <key>REVERSED_CLIENT_ID</key>
+ <string>correct_reversed_client_id</string>
+ <key>ANDROID_CLIENT_ID</key>
+ <string>correct_android_client_id</string>
+ <key>GOOGLE_APP_ID</key>
+ <string>1:123:ios:123abc</string>
+ <key>GCM_SENDER_ID</key>
+ <string>correct_gcm_sender_id</string>
+ <key>PLIST_VERSION</key>
+ <string>1</string>
+ <key>BUNDLE_ID</key>
+ <string>com.google.FirebaseSDKTests</string>
+ <key>PROJECT_ID</key>
+ <string>abc-xyz-123</string>
+ <key>DATABASE_URL</key>
+ <string>https://abc-xyz-123.firebaseio.com</string>
+ <key>STORAGE_BUCKET</key>
+ <string>project-id-123.storage.firebase.com</string>
+</dict>
+</plist>
diff --git a/Example/Messaging/App/Messaging-Info.plist b/Example/Messaging/App/Messaging-Info.plist
new file mode 100644
index 0000000..e42f39d
--- /dev/null
+++ b/Example/Messaging/App/Messaging-Info.plist
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UIBackgroundModes</key>
+ <array>
+ <string>remote-notification</string>
+ </array>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Messaging/App/MessagingViewController.swift b/Example/Messaging/App/MessagingViewController.swift
new file mode 100644
index 0000000..00ed3ff
--- /dev/null
+++ b/Example/Messaging/App/MessagingViewController.swift
@@ -0,0 +1,332 @@
+/*
+ * 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 FirebaseDev
+
+enum Row: String {
+ case apnsToken = "apnsToken"
+ case apnsStatus = "apnsStatus"
+ case requestAPNSPermissions = "requestAPNSPermissions"
+ case fcmToken = "fcmToken"
+}
+
+enum PermissionsButtonTitle: String {
+ case requestPermissions = "Request User Notifications"
+ case noAPNS = "Cannot Request Permissions (No APNs)"
+ case alreadyRequested = "Already Requested Permissions"
+ case simulator = "Cannot Request Permissions (Simulator)"
+}
+
+class MessagingViewController: UIViewController {
+
+ let tableView: UITableView
+
+ var sections = [[Row]]()
+ var sectionHeaderTitles = [String?]()
+
+ var allowedNotificationTypes: [NotificationsControllerAllowedNotificationType]?
+
+ // Cached rows by Row type. Since this is largely a fixed table view, we'll
+ // keep track of our created cells and UI, rather than have all the logic
+
+ required init?(coder aDecoder: NSCoder) {
+ tableView = UITableView(frame: CGRect.zero, style: .grouped)
+ tableView.rowHeight = UITableViewAutomaticDimension
+ tableView.estimatedRowHeight = 44
+ // Allow UI Controls within the table to be immediately responsive
+ tableView.delaysContentTouches = false
+ super.init(coder: aDecoder)
+ tableView.dataSource = self
+ tableView.delegate = self
+ }
+
+ override func loadView() {
+ super.loadView()
+ view = UIView(frame: CGRect.zero)
+ view.addSubview(self.tableView)
+ // Ensure that the tableView always is the size of the view
+ tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ let center = NotificationCenter.default
+ center.addObserver(self,
+ selector: #selector(onAPNSTokenReceived) ,
+ name: APNSTokenReceivedNotification,
+ object: nil)
+ center.addObserver(self,
+ selector: #selector(onUserNotificationSettingsChanged),
+ name: UserNotificationsChangedNotification,
+ object: nil)
+ center.addObserver(self,
+ selector: #selector(onFCMTokenRefreshed),
+ name: Notification.Name.MessagingRegistrationTokenRefreshed,
+ object: nil)
+ updateAllowedNotificationTypes {
+ self.resetTableContents()
+ self.tableView.reloadData()
+ }
+ }
+
+ func onAPNSTokenReceived() {
+ // Reload the appropriate cells
+ updateAllowedNotificationTypes {
+ if let tokenPath = self.indexPathFor(.apnsToken),
+ let statusPath = self.indexPathFor(.apnsStatus),
+ let requestPath = self.indexPathFor(.requestAPNSPermissions) {
+ self.updateIndexPaths(indexPaths: [tokenPath, statusPath, requestPath])
+ }
+ }
+ }
+
+ func onFCMTokenRefreshed() {
+ if let indexPath = indexPathFor(.fcmToken) {
+ updateIndexPaths(indexPaths: [indexPath])
+ }
+ }
+
+ func onUserNotificationSettingsChanged() {
+ updateAllowedNotificationTypes {
+ if let statusPath = self.indexPathFor(.apnsStatus),
+ let requestPath = self.indexPathFor(.requestAPNSPermissions) {
+ self.updateIndexPaths(indexPaths: [statusPath, requestPath])
+ }
+ }
+ }
+
+ private func updateIndexPaths(indexPaths: [IndexPath]) {
+ tableView.beginUpdates()
+ tableView.reloadRows(at: indexPaths, with: .none)
+ tableView.endUpdates()
+ }
+
+ fileprivate func updateAllowedNotificationTypes(_ completion: (() -> Void)?) {
+ NotificationsController.shared.getAllowedNotificationTypes { (types) in
+ self.allowedNotificationTypes = types
+ self.updateRequestAPNSButton()
+ completion?()
+ }
+ }
+
+ fileprivate func updateRequestAPNSButton() {
+ guard !Environment.isSimulator else {
+ requestPermissionsButton.isEnabled = false
+ requestPermissionsButton.setTitle(PermissionsButtonTitle.simulator.rawValue, for: .normal)
+ return
+ }
+ guard let allowedTypes = allowedNotificationTypes else {
+ requestPermissionsButton.isEnabled = false
+ requestPermissionsButton.setTitle(PermissionsButtonTitle.noAPNS.rawValue, for: .normal)
+ return
+ }
+
+ requestPermissionsButton.isEnabled =
+ (allowedTypes.count == 1 && allowedTypes.first! == .silent)
+
+ let title: PermissionsButtonTitle =
+ (requestPermissionsButton.isEnabled ? .requestPermissions : .alreadyRequested)
+ requestPermissionsButton.setTitle(title.rawValue, for: .normal)
+ }
+
+ // MARK: UI (Cells and Buttons) Defined as lazy properties
+ lazy var apnsTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: .subtitle, reuseIdentifier: Row.apnsToken.rawValue)
+ cell.textLabel?.numberOfLines = 0
+ cell.textLabel?.lineBreakMode = .byWordWrapping
+ return cell
+ }()
+
+ lazy var apnsStatusTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: Row.apnsStatus.rawValue)
+ cell.textLabel?.text = "Allowed:"
+ cell.detailTextLabel?.numberOfLines = 0
+ cell.detailTextLabel?.lineBreakMode = .byWordWrapping
+ return cell
+ }()
+
+ lazy var requestPermissionsButton: UIButton = {
+ let button = UIButton(type: .system)
+ button.setTitle(PermissionsButtonTitle.requestPermissions.rawValue, for: .normal)
+ button.setTitleColor(UIColor.gray, for: .highlighted)
+ button.setTitleColor(UIColor.gray, for: .disabled)
+ button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
+ button.addTarget(self,
+ action: #selector(onRequestUserNotificationsButtonTapped),
+ for: .touchUpInside)
+ return button
+ }()
+
+ lazy var apnsRequestPermissionsTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: .default,
+ reuseIdentifier: Row.requestAPNSPermissions.rawValue)
+ cell.selectionStyle = .none
+ cell.contentView.addSubview(self.requestPermissionsButton)
+ self.requestPermissionsButton.frame = cell.contentView.bounds
+ self.requestPermissionsButton.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ return cell
+ }()
+
+ lazy var fcmTokenTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: .subtitle, reuseIdentifier: Row.fcmToken.rawValue)
+ cell.textLabel?.numberOfLines = 0
+ cell.textLabel?.lineBreakMode = .byCharWrapping
+ return cell
+ }()
+}
+
+// MARK: - Configuring the table view and cells with information
+extension MessagingViewController {
+ func resetTableContents() {
+ sections.removeAll()
+ sectionHeaderTitles.removeAll()
+
+ // APNS
+ let apnsSection: [Row] = [.apnsToken, .apnsStatus, .requestAPNSPermissions]
+ sections.append(apnsSection)
+ sectionHeaderTitles.append("APNs")
+
+ // FCM
+ let fcmSection: [Row] = [.fcmToken]
+ sections.append(fcmSection)
+ sectionHeaderTitles.append("FCM Token")
+
+ }
+
+ func indexPathFor(_ rowId: Row) -> IndexPath? {
+ var sectionIndex = 0
+ for section in sections {
+ var rowIndex = 0
+ for row in section {
+ if row == rowId {
+ return IndexPath(row: rowIndex, section: sectionIndex)
+ }
+ rowIndex += 1
+ }
+ sectionIndex += 1
+ }
+ return nil
+ }
+
+ func configureCell(_ cell: UITableViewCell, withAPNSToken apnsToken: Data?) {
+ guard !Environment.isSimulator else {
+ cell.textLabel?.text = "APNs notifications are not supported in the simulator."
+ cell.detailTextLabel?.text = nil
+ return
+ }
+ if let apnsToken = apnsToken {
+ cell.textLabel?.text = apnsToken.hexByteString
+ cell.detailTextLabel?.text = "Tap to Share"
+ } else {
+ cell.textLabel?.text = "None"
+ cell.detailTextLabel?.text = nil
+ }
+ }
+
+ func configureCellWithAPNSStatus(_ cell: UITableViewCell) {
+ if let allowedNotificationTypes = allowedNotificationTypes {
+ let displayableTypes: [String] = allowedNotificationTypes.map { return $0.rawValue }
+ cell.detailTextLabel?.text = displayableTypes.joined(separator: ", ")
+ } else {
+ cell.detailTextLabel?.text = "Retrieving..."
+ }
+ }
+
+ func configureCell(_ cell: UITableViewCell, withFCMToken fcmToken: String?) {
+ if let fcmToken = fcmToken {
+ cell.textLabel?.text = fcmToken
+ cell.detailTextLabel?.text = "Tap to Share"
+ } else {
+ cell.textLabel?.text = "None"
+ cell.detailTextLabel?.text = nil
+ }
+ }
+}
+
+// MARK: - UITableViewDataSource
+extension MessagingViewController: UITableViewDataSource {
+ func numberOfSections(in tableView: UITableView) -> Int {
+ return sections.count
+ }
+
+ public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return sections[section].count
+ }
+
+ public func tableView(_ tableView: UITableView,
+ cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let row = sections[indexPath.section][indexPath.row]
+
+ let cell: UITableViewCell
+ switch row {
+ case .apnsToken:
+ cell = apnsTableCell
+ configureCell(cell, withAPNSToken: Messaging.messaging().apnsToken)
+ case .apnsStatus:
+ cell = apnsStatusTableCell
+ configureCellWithAPNSStatus(cell)
+ case .requestAPNSPermissions:
+ cell = apnsRequestPermissionsTableCell
+ case .fcmToken:
+ cell = fcmTokenTableCell
+ configureCell(cell, withFCMToken: Messaging.messaging().fcmToken)
+ }
+ return cell
+ }
+
+ func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ return sectionHeaderTitles[section]
+ }
+}
+
+// MARK: - UITableViewDelegate
+extension MessagingViewController: UITableViewDelegate {
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+
+ let row = sections[indexPath.section][indexPath.row]
+ switch row {
+ case .apnsToken:
+ if let apnsToken = Messaging.messaging().apnsToken {
+ showActivityViewControllerFor(sharedItem: apnsToken.hexByteString)
+ }
+ case .fcmToken:
+ if let fcmToken = Messaging.messaging().fcmToken {
+ showActivityViewControllerFor(sharedItem: fcmToken)
+ }
+ default: break
+ }
+ }
+}
+
+// MARK: - UI Controls
+extension MessagingViewController {
+ func onRequestUserNotificationsButtonTapped(sender: UIButton) {
+ NotificationsController.shared.registerForUserFacingNotificationsFor(UIApplication.shared)
+ }
+}
+
+// MARK: - Activity View Controller
+extension MessagingViewController {
+ func showActivityViewControllerFor(sharedItem: Any) {
+ let activityViewController = UIActivityViewController(activityItems: [sharedItem],
+ applicationActivities: nil)
+ present(activityViewController, animated: true, completion: nil)
+ }
+}
+
diff --git a/Example/Messaging/App/Messaging_Example.entitlements b/Example/Messaging/App/Messaging_Example.entitlements
new file mode 100644
index 0000000..903def2
--- /dev/null
+++ b/Example/Messaging/App/Messaging_Example.entitlements
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>aps-environment</key>
+ <string>development</string>
+</dict>
+</plist>
diff --git a/Example/Messaging/App/NotificationsController.swift b/Example/Messaging/App/NotificationsController.swift
new file mode 100644
index 0000000..726d980
--- /dev/null
+++ b/Example/Messaging/App/NotificationsController.swift
@@ -0,0 +1,132 @@
+/*
+ * 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 UserNotifications
+
+import FirebaseDev
+
+enum NotificationsControllerAllowedNotificationType: String {
+ case none = "None"
+ case silent = "Silent Updates"
+ case alert = "Alerts"
+ case badge = "Badges"
+ case sound = "Sounds"
+}
+
+let APNSTokenReceivedNotification: Notification.Name
+ = Notification.Name(rawValue: "APNSTokenReceivedNotification")
+let UserNotificationsChangedNotification: Notification.Name
+ = Notification.Name(rawValue: "UserNotificationsChangedNotification")
+
+class NotificationsController: NSObject {
+
+ static let shared: NotificationsController = {
+ let instance = NotificationsController()
+ return instance
+ }()
+
+ class func configure() {
+ let sharedController = NotificationsController.shared
+ // Always become the delegate of UNUserNotificationCenter, even before we've requested user
+ // permissions
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current().delegate = sharedController
+ }
+ }
+
+ func registerForUserFacingNotificationsFor(_ application: UIApplication) {
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current()
+ .requestAuthorization(options: [.alert, .badge, .sound],
+ completionHandler: { (granted, error) in
+ NotificationCenter.default.post(name: UserNotificationsChangedNotification, object: nil)
+ })
+ } else if #available(iOS 8.0, *) {
+ let userNotificationSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound],
+ categories: [])
+ application.registerUserNotificationSettings(userNotificationSettings)
+
+ } else {
+ application.registerForRemoteNotifications(matching: [.alert, .badge, .sound])
+ }
+ }
+
+ func getAllowedNotificationTypes(_ completion:
+ @escaping (_ allowedTypes: [NotificationsControllerAllowedNotificationType]) -> Void) {
+
+ guard Messaging.messaging().apnsToken != nil else {
+ completion([.none])
+ return
+ }
+
+ var types: [NotificationsControllerAllowedNotificationType] = [.silent]
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { (settings) in
+ if settings.alertSetting == .enabled {
+ types.append(.alert)
+ }
+ if settings.badgeSetting == .enabled {
+ types.append(.badge)
+ }
+ if settings.soundSetting == .enabled {
+ types.append(.sound)
+ }
+ DispatchQueue.main.async {
+ completion(types)
+ }
+ })
+ } else if #available(iOS 8.0, *) {
+ if let userNotificationSettings = UIApplication.shared.currentUserNotificationSettings {
+ if userNotificationSettings.types.contains(.alert) {
+ types.append(.alert)
+ }
+ if userNotificationSettings.types.contains(.badge) {
+ types.append(.badge)
+ }
+ if userNotificationSettings.types.contains(.sound) {
+ types.append(.sound)
+ }
+ }
+ completion(types)
+ } else {
+ let enabledTypes = UIApplication.shared.enabledRemoteNotificationTypes()
+ if enabledTypes.contains(.alert) {
+ types.append(.alert)
+ }
+ if enabledTypes.contains(.badge) {
+ types.append(.badge)
+ }
+ if enabledTypes.contains(.sound) {
+ types.append(.sound)
+ }
+ completion(types)
+ }
+ }
+}
+
+// MARK: - UNUserNotificationCenterDelegate
+@available(iOS 10.0, *)
+extension NotificationsController: UNUserNotificationCenterDelegate {
+
+ func userNotificationCenter(_ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification,
+ withCompletionHandler completionHandler:
+ @escaping (UNNotificationPresentationOptions) -> Void) {
+ // Always show the incoming notification, even if the app is in foreground
+ completionHandler([.alert, .badge, .sound])
+ }
+}
diff --git a/Example/Messaging/Messaging_Example-Bridging-Header.h b/Example/Messaging/Messaging_Example-Bridging-Header.h
new file mode 100644
index 0000000..6bbbdba
--- /dev/null
+++ b/Example/Messaging/Messaging_Example-Bridging-Header.h
@@ -0,0 +1,17 @@
+/*
+ * 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 "FIRSampleAppUtilities.h"
diff --git a/Example/Messaging/Tests/FIRMessagingClientTest.m b/Example/Messaging/Tests/FIRMessagingClientTest.m
new file mode 100644
index 0000000..f36ec53
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingClientTest.m
@@ -0,0 +1,308 @@
+/*
+ * 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;
+
+#import <OCMock/OCMock.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingFakeConnection.h"
+#import "FIRMessagingRegistrar.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSecureSocket.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+#import "FIRReachabilityChecker.h"
+
+static NSString *const kFIRMessagingUserDefaultsSuite = @"FIRMessagingClientTestUserDefaultsSuite";
+
+static NSString *const kDeviceAuthId = @"123456";
+static NSString *const kSecretToken = @"56789";
+static NSString *const kDigest = @"com.google.digest";
+static NSString *const kVersionInfo = @"1.0";
+static NSString *const kSubscriptionID = @"abcdef-subscription-id";
+static NSString *const kDeletedSubscriptionID = @"deleted-abcdef-subscription-id";
+static NSString *const kFIRMessagingAppIDToken = @"1234xyzdef56789";
+static NSString *const kTopicToSubscribeTo = @"/topics/abcdef/hello-world";
+
+@interface FIRMessagingRegistrar ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
+
+@end
+
+@interface FIRMessagingClient () <FIRMessagingConnectionDelegate>
+
+@property(nonatomic, readwrite, strong) FIRMessagingConnection *connection;
+@property(nonatomic, readwrite, strong) FIRMessagingRegistrar *registrar;
+
+@property(nonatomic, readwrite, assign) int64_t lastConnectedTimestamp;
+@property(nonatomic, readwrite, assign) int64_t lastDisconnectedTimestamp;
+@property(nonatomic, readwrite, assign) NSUInteger subscribeRetryCount;
+@property(nonatomic, readwrite, assign) NSUInteger connectRetryCount;
+
+- (NSTimeInterval)connectionTimeoutInterval;
+- (void)setupConnection;
+
+@end
+
+@interface FIRMessagingConnection () <FIRMessagingSecureSocketDelegate>
+
+@property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;
+
+- (void)setupConnectionSocket;
+- (void)connectToSocket:(FIRMessagingSecureSocket *)socket;
+- (NSTimeInterval)connectionTimeoutInterval;
+- (void)sendHeartbeatPing;
+
+@end
+
+@interface FIRMessagingSecureSocket ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+
+@end
+
+@interface FIRMessagingClientTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
+@property(nonatomic, readwrite, strong) id mockClient;
+@property(nonatomic, readwrite, strong) id mockReachability;
+@property(nonatomic, readwrite, strong) id mockRmqManager;
+@property(nonatomic, readwrite, strong) id mockClientDelegate;
+@property(nonatomic, readwrite, strong) id mockDataMessageManager;
+@property(nonatomic, readwrite, strong) id mockRegistrar;
+
+// argument callback blocks
+@property(nonatomic, readwrite, copy) FIRMessagingConnectCompletionHandler connectCompletion;
+@property(nonatomic, readwrite, copy) FIRMessagingTopicOperationCompletion subscribeCompletion;
+
+@end
+
+@implementation FIRMessagingClientTest
+
+- (void)setUp {
+ [super setUp];
+ _mockClientDelegate =
+ OCMStrictProtocolMock(@protocol(FIRMessagingClientDelegate));
+ _mockReachability = OCMClassMock([FIRReachabilityChecker class]);
+ _mockRmqManager = OCMClassMock([FIRMessagingRmqManager class]);
+ _client = [[FIRMessagingClient alloc] initWithDelegate:_mockClientDelegate
+ reachability:_mockReachability
+ rmq2Manager:_mockRmqManager];
+ _mockClient = OCMPartialMock(_client);
+ _mockRegistrar = OCMPartialMock([_client registrar]);
+ [_mockClient setRegistrar:_mockRegistrar];
+ _mockDataMessageManager = OCMClassMock([FIRMessagingDataMessageManager class]);
+ [_mockClient setDataMessageManager:_mockDataMessageManager];
+}
+
+- (void)tearDown {
+ // remove all handlers
+ [self tearDownMocksAndHandlers];
+ // Mock all sockets to disconnect in a nice way
+ [[[(id)self.client.connection.socket stub] andDo:^(NSInvocation *invocation) {
+ self.client.connection.socket.state = kFIRMessagingSecureSocketClosed;
+ }] disconnect];
+
+ [self.client teardown];
+ [super tearDown];
+}
+
+- (void)tearDownMocksAndHandlers {
+ self.connectCompletion = nil;
+ self.subscribeCompletion = nil;
+}
+
+- (void)setupConnectionWithFakeLoginResult:(BOOL)loginResult
+ heartbeatTimeout:(NSTimeInterval)heartbeatTimeout {
+ [self setupFakeConnectionWithClass:[FIRMessagingFakeConnection class]
+ withSetupCompletionHandler:^(FIRMessagingConnection *connection) {
+ FIRMessagingFakeConnection *fakeConnection = (FIRMessagingFakeConnection *)connection;
+ fakeConnection.shouldFakeSuccessLogin = loginResult;
+ fakeConnection.fakeConnectionTimeout = heartbeatTimeout;
+ }];
+}
+
+- (void)testSetupConnection {
+ XCTAssertNil(self.client.connection);
+ [self.client setupConnection];
+ XCTAssertNotNil(self.client.connection);
+ XCTAssertNotNil(self.client.connection.delegate);
+}
+
+- (void)testConnectSuccess_withCachedFcmDefaults {
+ [self addFIRMessagingPreferenceKeysToUserDefaults];
+
+ // login request should be successful
+ [self setupConnectionWithFakeLoginResult:YES heartbeatTimeout:1.0];
+
+ XCTestExpectation *setupConnection = [self
+ expectationWithDescription:@"Fcm should successfully setup a connection"];
+
+ [self.client connectWithHandler:^(NSError *error) {
+ XCTAssertNil(error);
+ [setupConnection fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:1.0 handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+}
+
+- (void)testsConnectWithNoNetworkError_withCachedFcmDefaults {
+ // connection timeout interval is 1s
+ [[[self.mockClient stub] andReturnValue:@(1)] connectionTimeoutInterval];
+ [self addFIRMessagingPreferenceKeysToUserDefaults];
+
+ [self setupFakeConnectionWithClass:[FIRMessagingFakeFailConnection class]
+ withSetupCompletionHandler:^(FIRMessagingConnection *connection) {
+ FIRMessagingFakeFailConnection *fakeConnection = (FIRMessagingFakeFailConnection *)connection;
+ fakeConnection.shouldFakeSuccessLogin = NO;
+ // should fail only once
+ fakeConnection.failCount = 1;
+ }];
+
+ XCTestExpectation *connectExpectation = [self
+ expectationWithDescription:@"Should retry connection if once failed"];
+ [self.client connectWithHandler:^(NSError *error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqual(kFIRMessagingErrorCodeNetwork, error.code);
+ [connectExpectation fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:10.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+}
+
+- (void)testConnectSuccessOnSecondTry_withCachedFcmDefaults {
+ // connection timeout interval is 1s
+ [[[self.mockClient stub] andReturnValue:@(1)] connectionTimeoutInterval];
+ [self addFIRMessagingPreferenceKeysToUserDefaults];
+
+ // the network is available
+ [[[self.mockReachability stub]
+ andReturnValue:@(kFIRReachabilityViaWifi)] reachabilityStatus];
+
+ [self setupFakeConnectionWithClass:[FIRMessagingFakeFailConnection class]
+ withSetupCompletionHandler:^(FIRMessagingConnection *connection) {
+ FIRMessagingFakeFailConnection *fakeConnection = (FIRMessagingFakeFailConnection *)connection;
+ fakeConnection.shouldFakeSuccessLogin = NO;
+ // should fail only once
+ fakeConnection.failCount = 1;
+ }];
+
+ XCTestExpectation *connectExpectation = [self
+ expectationWithDescription:@"Should retry connection if once failed"];
+ [self.client connectWithHandler:^(NSError *error) {
+ XCTAssertNil(error);
+ [connectExpectation fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:10.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertTrue(
+ [self.client isConnectionActive]);
+ }];
+}
+
+- (void)testDisconnectAfterConnect {
+ // setup the connection
+ [self addFIRMessagingPreferenceKeysToUserDefaults];
+
+ // login request should be successful
+ // Connection should not timeout because of heartbeat failure. Therefore set heartbeatTimeout
+ // to a large value.
+ [self setupConnectionWithFakeLoginResult:YES heartbeatTimeout:100.0];
+
+ [[[self.mockClient stub] andReturnValue:@(1)] connectionTimeoutInterval];
+
+ // the network is available
+ [[[self.mockReachability stub]
+ andReturnValue:@(kFIRReachabilityViaWifi)] reachabilityStatus];
+
+ XCTestExpectation *setupConnection =
+ [self expectationWithDescription:@"Fcm should successfully setup a connection"];
+
+ __block int timesConnected = 0;
+ FIRMessagingConnectCompletionHandler handler = ^(NSError *error) {
+ XCTAssertNil(error);
+ timesConnected++;
+ if (timesConnected == 1) {
+ [setupConnection fulfill];
+ // disconnect the connection after some time
+ FIRMessagingFakeConnection *fakeConnection = (FIRMessagingFakeConnection *)[self.mockClient connection];
+ dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (0.2 * NSEC_PER_SEC));
+ dispatch_after(time, dispatch_get_main_queue(), ^{
+ // disconnect now
+ [(FIRMessagingFakeConnection *)fakeConnection mockSocketDisconnect];
+ [(FIRMessagingFakeConnection *)fakeConnection disconnectNow];
+ });
+ } else {
+ XCTFail(@"Fcm should only connect at max 2 times");
+ }
+ };
+ [self.mockClient connectWithHandler:handler];
+
+ // reconnect after disconnect
+ XCTAssertTrue(self.client.isConnectionActive);
+
+ [self waitForExpectationsWithTimeout:10.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertNotEqual(self.client.lastDisconnectedTimestamp, 0);
+ XCTAssertTrue(self.client.isConnectionActive);
+ }];
+}
+
+#pragma mark - Private Helpers
+
+- (void)setupFakeConnectionWithClass:(Class)connectionClass
+ withSetupCompletionHandler:(void (^)(FIRMessagingConnection *))handler {
+ [[[self.mockClient stub] andDo:^(NSInvocation *invocation) {
+ self.client.connection =
+ [[connectionClass alloc] initWithAuthID:kDeviceAuthId
+ token:kSecretToken
+ host:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ runLoop:[NSRunLoop mainRunLoop]
+ rmq2Manager:self.mockRmqManager
+ fcmManager:self.mockDataMessageManager];
+ self.client.connection.delegate = self.client;
+ handler(self.client.connection);
+ }] setupConnection];
+}
+
+- (void)addFIRMessagingPreferenceKeysToUserDefaults {
+ id mockCheckinService = OCMClassMock([FIRMessagingCheckinService class]);
+ [[[mockCheckinService stub] andReturn:kDeviceAuthId] deviceAuthID];
+ [[[mockCheckinService stub] andReturn:kSecretToken] secretToken];
+ [[[mockCheckinService stub] andReturnValue:@YES] hasValidCheckinInfo];
+
+ [[[self.mockRegistrar stub] andReturn:mockCheckinService] checkinService];
+}
+
+@end
+
diff --git a/Example/Messaging/Tests/FIRMessagingCodedInputStreamTest.m b/Example/Messaging/Tests/FIRMessagingCodedInputStreamTest.m
new file mode 100644
index 0000000..7cc2d97
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingCodedInputStreamTest.m
@@ -0,0 +1,116 @@
+/*
+ * 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;
+
+#import "FIRMessagingCodedInputStream.h"
+
+@interface FIRMessagingCodedInputStreamTest : XCTestCase
+@end
+
+@implementation FIRMessagingCodedInputStreamTest
+
+- (void)testReadingSmallDataStream {
+ FIRMessagingCodedInputStream *stream =
+ [[FIRMessagingCodedInputStream alloc] initWithData:[[self class] sampleData1]];
+ int8_t actualTag = 2;
+ int8_t tag;
+ XCTAssertTrue([stream readTag:&tag]);
+ XCTAssertEqual(actualTag, tag);
+
+ // test length
+ int32_t actualLength = 4;
+ int32_t length;
+ XCTAssertTrue([stream readLength:&length]);
+ XCTAssertEqual(actualLength, length);
+
+ NSData *actualData = [[self class] packetDataForSampleData1];
+ NSData *data = [stream readDataWithLength:length];
+ XCTAssertTrue([actualData isEqualToData:data]);
+}
+
+- (void)testReadingLargeDataStream {
+ FIRMessagingCodedInputStream *stream =
+ [[FIRMessagingCodedInputStream alloc] initWithData:[[self class] sampleData2]];
+ int8_t actualTag = 5;
+ int8_t tag;
+ XCTAssertTrue([stream readTag:&tag]);
+ XCTAssertEqual(actualTag, tag);
+
+ int32_t actualLength = 257;
+ int32_t length;
+ XCTAssertTrue([stream readLength:&length]);
+ XCTAssertEqual(actualLength, length);
+
+ NSData *actualData = [[self class] packetDataForSampleData2];
+ NSData *data = [stream readDataWithLength:length];
+ XCTAssertTrue([actualData isEqualToData:data]);
+}
+
+- (void)testReadingInvalidDataStream {
+ FIRMessagingCodedInputStream *stream =
+ [[FIRMessagingCodedInputStream alloc] initWithData:[[self class] invalidData]];
+ int8_t actualTag = 7;
+ int8_t tag;
+ XCTAssertTrue([stream readTag:&tag]);
+ XCTAssertEqual(actualTag, tag);
+
+ int32_t actualLength = 2;
+ int32_t length;
+ XCTAssertTrue([stream readLength:&length]);
+ XCTAssertEqual(actualLength, length);
+
+ XCTAssertNil([stream readDataWithLength:length]);
+}
+
++ (NSData *)sampleData1 {
+ // tag = 2,
+ // length = 4,
+ // data = integer 255
+ const char data[] = { 0x02, 0x04, 0x80, 0x00, 0x00, 0xff };
+ return [NSData dataWithBytes:data length:6];
+}
+
++ (NSData *)packetDataForSampleData1 {
+ const char data[] = { 0x80, 0x00, 0x00, 0xff };
+ return [NSData dataWithBytes:data length:4];
+}
+
++ (NSData *)sampleData2 {
+ // test reading varint properly
+ // tag = 5,
+ // length = 257,
+ // data = length 257
+ const char tagAndLength[] = { 0x05, 0x81, 0x02 };
+ NSMutableData *data = [NSMutableData dataWithBytes:tagAndLength length:3];
+ [data appendData:[self packetDataForSampleData2]];
+ return data;
+}
+
++ (NSData *)packetDataForSampleData2 {
+ char packetData[257] = { 0xff, 0xff, 0xff };
+ return [NSData dataWithBytes:packetData length:257];
+}
+
++ (NSData *)invalidData {
+ // tag = 7,
+ // length = 2,
+ // data = (length 1)
+ const char data[] = { 0x07, 0x02, 0xff };
+ return [NSData dataWithBytes:data length:3];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingConnectionTest.m b/Example/Messaging/Tests/FIRMessagingConnectionTest.m
new file mode 100644
index 0000000..47b29d2
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingConnectionTest.m
@@ -0,0 +1,480 @@
+/*
+ * 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;
+
+#import <OCMock/OCMock.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+#import <GoogleToolboxForMac/GTMDefines.h>
+
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingFakeConnection.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSecureSocket.h"
+#import "FIRMessagingUtilities.h"
+
+static NSString *const kDeviceAuthId = @"123456";
+static NSString *const kSecretToken = @"56789";
+
+// used to verify if we are sending in the right proto or not.
+// set it to negative value to disable this check
+static FIRMessagingProtoTag currentProtoSendTag;
+
+@interface FIRMessagingSecureSocket ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+
+@end
+
+@interface FIRMessagingSecureSocket (test_FIRMessagingConnection)
+
+- (void)_successconnectToHost:(NSString *)host
+ port:(NSUInteger)port
+ onRunLoop:(NSRunLoop *)runLoop;
+- (void)_fakeSuccessfulSocketConnect;
+
+@end
+
+@implementation FIRMessagingSecureSocket (test_FIRMessagingConnection)
+
+- (void)_successconnectToHost:(NSString *)host
+ port:(NSUInteger)port
+ onRunLoop:(NSRunLoop *)runLoop {
+ // created ports, opened streams
+ // invoke callback async
+ [self _fakeSuccessfulSocketConnect];
+}
+
+- (void)_fakeSuccessfulSocketConnect {
+ self.state = kFIRMessagingSecureSocketOpen;
+ [self.delegate secureSocketDidConnect:self];
+}
+
+@end
+
+// make sure these are defined in FIRMessagingConnection
+@interface FIRMessagingConnection () <FIRMessagingSecureSocketDelegate>
+
+@property(nonatomic, readwrite, assign) int64_t lastLoginServerTimestamp;
+@property(nonatomic, readwrite, assign) int lastStreamIdAcked;
+@property(nonatomic, readwrite, assign) int inStreamId;
+@property(nonatomic, readwrite, assign) int outStreamId;
+
+@property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;
+
+@property(nonatomic, readwrite, strong) NSMutableArray *unackedS2dIds;
+@property(nonatomic, readwrite, strong) NSMutableDictionary *ackedS2dMap;
+@property(nonatomic, readwrite, strong) NSMutableArray *d2sInfos;
+
+- (void)setupConnectionSocket;
+- (void)connectToSocket:(FIRMessagingSecureSocket *)socket;
+- (NSTimeInterval)connectionTimeoutInterval;
+- (void)sendHeartbeatPing;
+
+@end
+
+
+@interface FIRMessagingConnectionTest : XCTestCase
+
+@property(nonatomic, readwrite, assign) BOOL didSuccessfullySendData;
+
+@property(nonatomic, readwrite, strong) NSUserDefaults *userDefaults;
+@property(nonatomic, readwrite, strong) FIRMessagingConnection *fakeConnection;
+@property(nonatomic, readwrite, strong) id mockClient;
+@property(nonatomic, readwrite, strong) id mockConnection;
+@property(nonatomic, readwrite, strong) id mockRmq;
+@property(nonatomic, readwrite, strong) id mockDataMessageManager;
+
+@end
+
+@implementation FIRMessagingConnectionTest
+
+- (void)setUp {
+ [super setUp];
+ _userDefaults = [[NSUserDefaults alloc] init];
+ _mockRmq = OCMClassMock([FIRMessagingRmqManager class]);
+ _mockDataMessageManager = OCMClassMock([FIRMessagingDataMessageManager class]);
+ // fake connection is only used to simulate the socket behavior
+ _fakeConnection = [[FIRMessagingFakeConnection alloc] initWithAuthID:kDeviceAuthId
+ token:kSecretToken
+ host:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ runLoop:[NSRunLoop currentRunLoop]
+ rmq2Manager:_mockRmq
+ fcmManager:_mockDataMessageManager];
+
+ _mockClient = OCMClassMock([FIRMessagingClient class]);
+ _fakeConnection.delegate = _mockClient;
+ _mockConnection = OCMPartialMock(_fakeConnection);
+ _didSuccessfullySendData = NO;
+}
+
+- (void)tearDown {
+ [self.fakeConnection teardown];
+ [super tearDown];
+}
+
+- (void)testInitialConnectionNotConnected {
+ XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
+}
+
+- (void)testSuccessfulSocketConnection {
+ [self.fakeConnection signIn];
+
+ // should be connected now
+ XCTAssertEqual(kFIRMessagingConnectionConnected, self.fakeConnection.state);
+ XCTAssertEqual(0, self.fakeConnection.lastStreamIdAcked);
+ XCTAssertEqual(0, self.fakeConnection.inStreamId);
+ XCTAssertEqual(0, self.fakeConnection.ackedS2dMap.count);
+ XCTAssertEqual(0, self.fakeConnection.unackedS2dIds.count);
+
+ [self stubSocketDisconnect:self.fakeConnection.socket];
+}
+
+- (void)testSignInAndThenSignOut {
+ [self.fakeConnection signIn];
+ [self stubSocketDisconnect:self.fakeConnection.socket];
+ [self.fakeConnection signOut];
+ XCTAssertEqual(kFIRMessagingSecureSocketClosed, self.fakeConnection.socket.state);
+}
+
+- (void)testSuccessfulSignIn {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+ XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionSignedIn);
+ XCTAssertEqual(self.fakeConnection.outStreamId, 2);
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testSignOut_whenSignedIn {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+
+ // should be signed in now
+ id mockSocket = self.fakeConnection.socket;
+ [self.fakeConnection signOut];
+ XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
+ XCTAssertEqual(self.fakeConnection.outStreamId, 3);
+ XCTAssertNil([(FIRMessagingSecureSocket *)mockSocket delegate]);
+ XCTAssertTrue(self.didSuccessfullySendData);
+ OCMVerify([mockSocket sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagClose
+ rmqId:[OCMArg isNil]]);
+}
+
+- (void)testReceiveCloseProto {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+
+ id mockSocket = self.fakeConnection.socket;
+ GtalkClose *close = [[GtalkClose alloc] init];
+ [self.fakeConnection secureSocket:mockSocket
+ didReceiveData:[close data]
+ withTag:kFIRMessagingProtoTagClose];
+ XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testLoginRequest {
+ XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
+ [self.fakeConnection setupConnectionSocket];
+
+ id socketMock = OCMPartialMock(self.fakeConnection.socket);
+ self.fakeConnection.socket = socketMock;
+
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ [socketMock _fakeSuccessfulSocketConnect];
+ }]
+ connectToHost:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ onRunLoop:[OCMArg any]];
+
+ [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
+ // do nothing
+ sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagLoginRequest
+ rmqId:[OCMArg isNil]];
+
+ // swizzle disconnect socket
+ OCMVerify([[[socketMock stub] andCall:@selector(_disconnectSocket)
+ onObject:self] disconnect]);
+
+ currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
+ // send login request
+ [self.fakeConnection connectToSocket:socketMock];
+
+ // verify login request sent
+ XCTAssertEqual(1, self.fakeConnection.outStreamId);
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testLoginRequest_withPendingMessagesInRmq {
+ // TODO: add fake messages to rmq and test login request with them
+}
+
+- (void)testLoginRequest_withSuccessfulResponse {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+
+ OCMVerify([self.mockClient didLoginWithConnection:[OCMArg isEqual:self.fakeConnection]]);
+
+ // should send a heartbeat ping too
+ XCTAssertEqual(self.fakeConnection.outStreamId, 2);
+ // update for the received login response proto
+ XCTAssertEqual(self.fakeConnection.inStreamId, 1);
+ // did send data during login
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testConnectionTimeout {
+ XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
+
+ [self.fakeConnection setupConnectionSocket];
+
+ id socketMock = OCMPartialMock(self.fakeConnection.socket);
+ self.fakeConnection.socket = socketMock;
+
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ [socketMock _fakeSuccessfulSocketConnect];
+ }]
+ connectToHost:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ onRunLoop:[OCMArg any]];
+
+ [self.fakeConnection connectToSocket:socketMock];
+ XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionConnected);
+
+ GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
+ [response setId_p:@""];
+
+ // connection timeout has been scheduled
+ // should disconnect since we wait for more time
+ XCTestExpectation *disconnectExpectation =
+ [self expectationWithDescription:
+ @"FCM connection should timeout without receiving "
+ @"any data for a timeout interval"];
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ [self _disconnectSocket];
+ [disconnectExpectation fulfill];
+ }] disconnect];
+
+ // simulate connection receiving login response
+ [self.fakeConnection secureSocket:socketMock
+ didReceiveData:[response data]
+ withTag:kFIRMessagingProtoTagLoginResponse];
+
+ [self waitForExpectationsWithTimeout:2.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+
+ [socketMock verify];
+ XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
+}
+
+- (void)testDataMessageReceive {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+ GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
+ [stanza setCategory:@"special"];
+ [stanza setFrom:@"xyz"];
+ [self.fakeConnection secureSocket:self.fakeConnection.socket
+ didReceiveData:[stanza data]
+ withTag:kFIRMessagingProtoTagDataMessageStanza];
+
+ OCMVerify([self.mockClient connectionDidRecieveMessage:[OCMArg checkWithBlock:^BOOL(id obj) {
+ GtalkDataMessageStanza *message = (GtalkDataMessageStanza *)obj;
+ return [[message category] isEqual:@"special"] && [[message from] isEqual:@"xyz"];
+ }]]);
+ // did send data while login
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testDataMessageReceiveWithInvalidTag {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+ GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
+ BOOL didCauseException = NO;
+ @try {
+ [self.fakeConnection secureSocket:self.fakeConnection.socket
+ didReceiveData:[stanza data]
+ withTag:kFIRMessagingProtoTagInvalid];
+ } @catch (NSException *exception) {
+ didCauseException = YES;
+ } @finally {
+ }
+ XCTAssertFalse(didCauseException);
+}
+
+- (void)testDataMessageReceiveWithTagThatDoesntEquateToClass {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+ GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
+ BOOL didCauseException = NO;
+ int8_t tagWhichDoesntEquateToClass = INT8_MAX;
+ @try {
+ [self.fakeConnection secureSocket:self.fakeConnection.socket
+ didReceiveData:[stanza data]
+ withTag:tagWhichDoesntEquateToClass];
+ } @catch (NSException *exception) {
+ didCauseException = YES;
+ } @finally {
+ }
+ XCTAssertFalse(didCauseException);
+}
+
+- (void)testHeartbeatSend {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection]; // outstreamId should be 2
+ XCTAssertEqual(self.fakeConnection.outStreamId, 2);
+ [self.fakeConnection sendHeartbeatPing];
+ id mockSocket = self.fakeConnection.socket;
+ OCMVerify([mockSocket sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagHeartbeatPing
+ rmqId:[OCMArg isNil]]);
+ XCTAssertEqual(self.fakeConnection.outStreamId, 3);
+ // did send data
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+- (void)testHeartbeatReceived {
+ [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
+ XCTAssertEqual(self.fakeConnection.outStreamId, 2);
+ GtalkHeartbeatPing *ping = [[GtalkHeartbeatPing alloc] init];
+ [self.fakeConnection secureSocket:self.fakeConnection.socket
+ didReceiveData:[ping data]
+ withTag:kFIRMessagingProtoTagHeartbeatPing];
+ XCTAssertEqual(self.fakeConnection.inStreamId, 2);
+ id mockSocket = self.fakeConnection.socket;
+ OCMVerify([mockSocket sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagHeartbeatAck
+ rmqId:[OCMArg isNil]]);
+ XCTAssertEqual(self.fakeConnection.outStreamId, 3);
+ // did send data
+ XCTAssertTrue(self.didSuccessfullySendData);
+}
+
+// TODO: Add tests for Selective/Stream ACK's
+
+#pragma mark - Stubs
+
+- (void)_disconnectSocket {
+ self.fakeConnection.socket.state = kFIRMessagingSecureSocketClosed;
+}
+
+- (void)_sendData:(NSData *)data withTag:(int8_t)tag rmqId:(NSString *)rmqId {
+ _GTMDevLog(@"FIRMessaging Socket: Send data with Tag: %d rmq: %@", tag, rmqId);
+ if (currentProtoSendTag > 0) {
+ XCTAssertEqual(tag, currentProtoSendTag);
+ }
+ self.didSuccessfullySendData = YES;
+}
+
+#pragma mark - Private Helpers
+
+/**
+ * Stub socket disconnect to prevent spurious assert. Since we mock the socket object being
+ * used by the connection, while we teardown the client we also disconnect the socket to tear
+ * it down. Since we are using mock sockets we need to stub the `disconnect` to prevent some
+ * assertions from taking place.
+ * The `_disconectSocket` has the gist of the actual socket disconnect without any assertions.
+ */
+- (void)stubSocketDisconnect:(id)mockSocket {
+ [[[mockSocket stub] andCall:@selector(_disconnectSocket)
+ onObject:self] disconnect];
+
+ [mockSocket verify];
+}
+
+- (void)mockSuccessfulSignIn {
+ XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
+ [self.fakeConnection setupConnectionSocket];
+
+ id socketMock = OCMPartialMock(self.fakeConnection.socket);
+ self.fakeConnection.socket = socketMock;
+
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ [socketMock _fakeSuccessfulSocketConnect];
+ }]
+ connectToHost:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ onRunLoop:[OCMArg any]];
+
+ [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
+ // do nothing
+ sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagLoginRequest
+ rmqId:[OCMArg isNil]];
+
+ // send login request
+ currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
+ [self.fakeConnection connectToSocket:socketMock];
+
+ GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
+ [response setId_p:@""];
+
+ // simulate connection receiving login response
+ [self.fakeConnection secureSocket:socketMock
+ didReceiveData:[response data]
+ withTag:kFIRMessagingProtoTagLoginResponse];
+
+ OCMVerify([self.mockClient didLoginWithConnection:[OCMArg isEqual:self.fakeConnection]]);
+
+ // should receive data
+ XCTAssertTrue(self.didSuccessfullySendData);
+ // should send a heartbeat ping too
+ XCTAssertEqual(self.fakeConnection.outStreamId, 2);
+ // update for the received login response proto
+ XCTAssertEqual(self.fakeConnection.inStreamId, 1);
+}
+
+- (void)setupSuccessfulLoginRequestWithConnection:(FIRMessagingConnection *)fakeConnection {
+ [fakeConnection setupConnectionSocket];
+
+ id socketMock = OCMPartialMock(fakeConnection.socket);
+ fakeConnection.socket = socketMock;
+
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ [socketMock _fakeSuccessfulSocketConnect];
+ }]
+ connectToHost:[FIRMessagingFakeConnection fakeHost]
+ port:[FIRMessagingFakeConnection fakePort]
+ onRunLoop:[OCMArg any]];
+
+ [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
+ // do nothing
+ sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagLoginRequest
+ rmqId:[OCMArg isNil]];
+
+ // swizzle disconnect socket
+ [[[socketMock stub] andCall:@selector(_disconnectSocket)
+ onObject:self] disconnect];
+
+ // send login request
+ currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
+ [fakeConnection connectToSocket:socketMock];
+
+ GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
+ [response setId_p:@""];
+
+ // simulate connection receiving login response
+ [fakeConnection secureSocket:socketMock
+ didReceiveData:[response data]
+ withTag:kFIRMessagingProtoTagLoginResponse];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m b/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m
new file mode 100644
index 0000000..cb48e7f
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m
@@ -0,0 +1,183 @@
+/*
+ * 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/XCTest.h>
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessagingContextManagerService.h"
+
+@interface FIRMessagingContextManagerServiceTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) NSDateFormatter *dateFormatter;
+@property(nonatomic, readwrite, strong) NSMutableArray *scheduledLocalNotifications;
+
+@end
+
+@implementation FIRMessagingContextManagerServiceTest
+
+- (void)setUp {
+ [super setUp];
+ self.dateFormatter = [[NSDateFormatter alloc] init];
+ [self.dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
+ self.scheduledLocalNotifications = [NSMutableArray array];
+ [self mockSchedulingLocalNotifications];
+}
+
+- (void)tearDown {
+ [super tearDown];
+}
+
+/**
+ * Test invalid context manager message, missing lt_start string.
+ */
+- (void)testInvalidContextManagerMessage_missingStartTime {
+ NSDictionary *message = @{
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([FIRMessagingContextManagerService isContextManagerMessage:message]);
+}
+
+/**
+ * Test valid context manager message.
+ */
+- (void)testValidContextManagerMessage {
+ NSDictionary *message = @{
+ kFIRMessagingContextManagerLocalTimeStart: @"2015-12-12 00:00:00",
+ @"hello" : @"world",
+ };
+ XCTAssertTrue([FIRMessagingContextManagerService isContextManagerMessage:message]);
+}
+
+// TODO: Enable these tests. They fail because we cannot schedule local
+// notifications on OSX without permission. It's better to mock AppDelegate's
+// scheduleLocalNotification to mock scheduling behavior.
+
+/**
+ * Context Manager message with future start date should be successfully scheduled.
+ */
+- (void)testMessageWithFutureStartTime {
+ NSString *messageIdentifier = @"fcm-cm-test1";
+ NSString *startTimeString = @"2020-01-12 12:00:00"; // way into the future
+ NSDictionary *message = @{
+ kFIRMessagingContextManagerLocalTimeStart: startTimeString,
+ kFIRMessagingContextManagerBodyKey : @"Hello world!",
+ @"id": messageIdentifier,
+ @"hello" : @"world"
+ };
+
+ 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);
+}
+
+/**
+ * Context Manager message with past end date should not be scheduled.
+ */
+- (void)testMessageWithPastEndTime {
+ 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
+ NSDictionary *message = @{
+ kFIRMessagingContextManagerLocalTimeStart: startTimeString,
+ kFIRMessagingContextManagerLocalTimeEnd : endTimeString,
+ kFIRMessagingContextManagerBodyKey : @"Hello world!",
+ @"id": messageIdentifier,
+ @"hello" : @"world"
+ };
+
+ XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
+ XCTAssertEqual(self.scheduledLocalNotifications.count, 0);
+}
+
+/**
+ * Context Manager message with past start and future end date should be successfully
+ * scheduled.
+ */
+- (void)testMessageWithPastStartAndFutureEndTime {
+ NSString *messageIdentifier = @"fcm-cm-test1";
+ NSDate *startDate = [NSDate dateWithTimeIntervalSinceNow:-1000]; // past
+ NSDate *endDate = [NSDate dateWithTimeIntervalSinceNow:1000]; // future
+ NSString *startTimeString = [self.dateFormatter stringFromDate:startDate];
+ NSString *endTimeString = [self.dateFormatter stringFromDate:endDate];
+
+ NSDictionary *message = @{
+ kFIRMessagingContextManagerLocalTimeStart : startTimeString,
+ kFIRMessagingContextManagerLocalTimeEnd : endTimeString,
+ kFIRMessagingContextManagerBodyKey : @"Hello world!",
+ @"id": messageIdentifier,
+ @"hello" : @"world"
+ };
+
+ XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
+
+ XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
+ UILocalNotification *notification = [self.scheduledLocalNotifications firstObject];
+ // schedule notification after start date
+ XCTAssertEqual([notification.fireDate compare:startDate], NSOrderedDescending);
+ // schedule notification after end date
+ XCTAssertEqual([notification.fireDate compare:endDate], NSOrderedAscending);
+}
+
+/**
+ * Test correctly parsing user data in local notifications.
+ */
+- (void)testTimedNotificationsUserInfo {
+ NSString *messageIdentifierKey = @"message.id";
+ NSString *messageIdentifier = @"fcm-cm-test1";
+ NSString *startTimeString = @"2020-01-12 12:00:00"; // way into the future
+
+ NSString *customDataKey = @"hello";
+ NSString *customData = @"world";
+ NSDictionary *message = @{
+ kFIRMessagingContextManagerLocalTimeStart : startTimeString,
+ kFIRMessagingContextManagerBodyKey : @"Hello world!",
+ messageIdentifierKey : messageIdentifier,
+ customDataKey : customData,
+ };
+
+ XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
+
+ XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
+ UILocalNotification *notification = [self.scheduledLocalNotifications firstObject];
+ XCTAssertEqualObjects(notification.userInfo[messageIdentifierKey], messageIdentifier);
+ XCTAssertEqualObjects(notification.userInfo[customDataKey], customData);
+}
+
+#pragma mark - Private Helpers
+
+- (void)mockSchedulingLocalNotifications {
+ id mockApplication = OCMPartialMock([UIApplication sharedApplication]);
+ __block UILocalNotification *notificationToSchedule;
+ [[[mockApplication stub]
+ andDo:^(NSInvocation *invocation) {
+ // Mock scheduling a notification
+ if (notificationToSchedule) {
+ [self.scheduledLocalNotifications addObject:notificationToSchedule];
+ }
+ }] scheduleLocalNotification:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[UILocalNotification class]]) {
+ notificationToSchedule = obj;
+ return YES;
+ }
+ return NO;
+ }]];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingDataMessageManagerTest.m b/Example/Messaging/Tests/FIRMessagingDataMessageManagerTest.m
new file mode 100644
index 0000000..2b4f407
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingDataMessageManagerTest.m
@@ -0,0 +1,662 @@
+/*
+ * 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;
+
+#import <OCMock/OCMock.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessaging.h"
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingReceiver.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSyncMessageManager.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRMessaging_Private.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import "NSError+FIRMessaging.h"
+
+static NSString *const kFIRMessagingUserDefaultsSuite = @"FIRMessagingClientTestUserDefaultsSuite";
+
+static NSString *const kFIRMessagingAppIDToken = @"1234abcdef789";
+
+static NSString *const kMessagePersistentID = @"abcdef123";
+static NSString *const kMessageFrom = @"com.example.gcm";
+static NSString *const kMessageTo = @"123456789";
+static NSString *const kCollapseKey = @"collapse-1";
+static NSString *const kAppDataItemKey = @"hello";
+static NSString *const kAppDataItemValue = @"world";
+static NSString *const kAppDataItemInvalidKey = @"google.hello";
+
+static NSString *const kRmqDatabaseName = @"gcm-dmm-test";
+
+@interface FIRMessagingDataMessageManager()
+
+@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
+
+- (NSString *)categoryForUpstreamMessages;
+
+@end
+
+@interface FIRMessagingDataMessageManagerTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) id mockClient;
+@property(nonatomic, readwrite, strong) id mockRmqManager;
+@property(nonatomic, readwrite, strong) id mockReceiver;
+@property(nonatomic, readwrite, strong) id mockSyncMessageManager;
+@property(nonatomic, readwrite, strong) FIRMessagingDataMessageManager *dataMessageManager;
+@property(nonatomic, readwrite, strong) id mockDataMessageManager;
+
+@end
+
+@implementation FIRMessagingDataMessageManagerTest
+
+- (void)setUp {
+ [super setUp];
+ _mockClient = OCMClassMock([FIRMessagingClient class]);
+ _mockReceiver = OCMClassMock([FIRMessagingReceiver class]);
+ _mockRmqManager = OCMClassMock([FIRMessagingRmqManager class]);
+ _mockSyncMessageManager = OCMClassMock([FIRMessagingSyncMessageManager class]);
+ _dataMessageManager = [[FIRMessagingDataMessageManager alloc]
+ initWithDelegate:_mockReceiver
+ client:_mockClient
+ rmq2Manager:_mockRmqManager
+ syncMessageManager:_mockSyncMessageManager];
+ [_dataMessageManager refreshDelayedMessages];
+ _mockDataMessageManager = OCMPartialMock(_dataMessageManager);
+}
+
+
+- (void)testSendValidMessage_withNoConnection {
+ // mock no connection initially
+ NSString *messageID = @"1";
+ BOOL mockConnectionActive = NO;
+ [[[self.mockClient stub] andDo:^(NSInvocation *invocation) {
+ NSValue *returnValue = [NSValue valueWithBytes:&mockConnectionActive
+ objCType:@encode(BOOL)];
+ [invocation setReturnValue:&returnValue];
+ }] isConnectionActive];
+
+ BOOL(^isValidStanza)(id obj) = ^BOOL(id obj) {
+ if ([obj isKindOfClass:[GtalkDataMessageStanza class]]) {
+ GtalkDataMessageStanza *message = (GtalkDataMessageStanza *)obj;
+ return ([message.id_p isEqualToString:messageID] && [message.to isEqualToString:kMessageTo]);
+ }
+ return NO;
+ };
+ OCMExpect([self.mockReceiver willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg isNil]]);
+ [[[self.mockRmqManager stub] andReturnValue:@YES]
+ saveRmqMessage:[OCMArg checkWithBlock:isValidStanza]
+ error:[OCMArg anyObjectRef]];
+
+ // should be logged into the service
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ // try to send messages with no connection should be queued into RMQ
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:-1 delay:0];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+ OCMVerifyAll(self.mockRmqManager);
+}
+
+- (void)testSendValidMessage_withoutCheckinAuthentication {
+ NSString *messageID = @"1";
+ NSMutableDictionary *message = [self standardFIRMessagingMessageWithMessageID:messageID];
+
+ OCMExpect([self.mockReceiver
+ willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSError class]]) {
+ NSError *error = (NSError *)obj;
+ return error.code == kFIRMessagingErrorCodeMissingDeviceID;
+ }
+ return NO;
+ }]]);
+
+ // do not log into checkin service
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+}
+
+- (void)testSendInvalidMessage_withNoTo {
+ NSString *messageID = @"1";
+ NSMutableDictionary *message =
+ [FIRMessaging createFIRMessagingMessageWithMessage:@{ kAppDataItemKey : kAppDataItemValue}
+ to:@""
+ withID:messageID
+ timeToLive:-1
+ delay:0];
+
+ OCMExpect([self.mockReceiver
+ willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSError class]]) {
+ NSError *error = (NSError *)obj;
+ return error.code == kFIRMessagingErrorMissingTo;
+ }
+ return NO;
+ }]]);
+
+ // should be logged into the service
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+}
+
+- (void)testSendInvalidMessage_withSizeExceeded {
+ NSString *messageID = @"1";
+ NSString *veryLargeString = [@"a" stringByPaddingToLength:4 * 1024 // 4kB
+ withString:@"b"
+ startingAtIndex:0];
+ NSMutableDictionary *message =
+ [FIRMessaging createFIRMessagingMessageWithMessage:@{ kAppDataItemKey : veryLargeString }
+ to:kMessageTo
+ withID:messageID
+ timeToLive:-1
+ delay:0];
+
+ OCMExpect([self.mockReceiver
+ willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSError class]]) {
+ NSError *error = (NSError *)obj;
+ return error.code == kFIRMessagingErrorSizeExceeded;
+ }
+ return NO;
+ }]]);
+
+ [self addFakeFIRMessagingRegistrationToken];
+ // should be logged into the service
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+}
+
+// TODO: Add test with rawData exceeding 4KB in size
+
+- (void)testSendValidMessage_withRmqSaveError {
+ NSString *messageID = @"1";
+ NSMutableDictionary *message = [self standardFIRMessagingMessageWithMessageID:messageID];
+ [[[self.mockRmqManager stub] andReturnValue:@NO]
+ saveRmqMessage:[OCMArg any] error:[OCMArg anyObjectRef]];
+
+ OCMExpect([self.mockReceiver
+ willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSError class]]) {
+ NSError *error = (NSError *)obj;
+ return error.code == kFIRMessagingErrorSave;
+ }
+ return NO;
+ }]]);
+
+ // should be logged into the service
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+}
+
+- (void)testSendValidMessage_withTTL0 {
+ // simulate a valid connection
+ [[[self.mockClient stub] andReturnValue:@YES] isConnectionActive];
+ NSString *messageID = @"1";
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:0 delay:0];
+
+ BOOL(^isValidStanza)(id obj) = ^BOOL(id obj) {
+ if ([obj isKindOfClass:[GtalkDataMessageStanza class]]) {
+ GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)obj;
+ return ([stanza.id_p isEqualToString:messageID] &&
+ [stanza.to isEqualToString:kMessageTo] &&
+ stanza.ttl == 0);
+ }
+ return NO;
+ };
+
+ OCMExpect([self.mockClient sendMessage:[OCMArg checkWithBlock:isValidStanza]]);
+
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockClient);
+}
+
+// TODO: This is failing on simulator 7.1 & 8.2, take this out temporarily
+- (void)XXX_testSendValidMessage_withTTL0AndNoFIRMessagingConnection {
+ // simulate a invalid connection
+ [[[self.mockClient stub] andReturnValue:@NO] isConnectionActive];
+
+ // simulate network reachability
+ FIRMessaging *service = [FIRMessaging messaging];
+ id mockService = OCMPartialMock(service);
+ [[[mockService stub] andReturnValue:@YES] isNetworkAvailable];
+
+ NSString *messageID = @"1";
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:0 delay:0];
+
+
+ BOOL(^isValidStanza)(id obj) = ^BOOL(id obj) {
+ if ([obj isKindOfClass:[GtalkDataMessageStanza class]]) {
+ GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)obj;
+ return ([stanza.id_p isEqualToString:messageID] &&
+ [stanza.to isEqualToString:kMessageTo] &&
+ stanza.ttl == 0);
+ }
+ return NO;
+ };
+
+ // should save the message to be sent when we reconnect the next time
+ OCMExpect([self.mockClient sendOnConnectOrDrop:[OCMArg checkWithBlock:isValidStanza]]);
+ // should also try to reconnect immediately
+ OCMExpect([self.mockClient retryConnectionImmediately:[OCMArg isEqual:@YES]]);
+
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockClient);
+}
+
+// TODO: Investigate why this test is flaky
+- (void)xxx_testSendValidMessage_withTTL0AndNoNetwork {
+ // simulate a invalid connection
+ [[[self.mockClient stub] andReturnValue:@NO] isConnectionActive];
+
+ NSString *messageID = @"1";
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:0 delay:0];
+
+
+ // should drop the message since there is no network
+ OCMExpect([self.mockReceiver willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSError class]]) {
+ NSError *error = (NSError *)obj;
+ return error.code == kFIRMessagingErrorCodeNetwork;
+ }
+ return NO;
+ }]]);
+
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockReceiver);
+}
+
+// TODO: This failed on simulator 7.1 & 8.2, take this out temporarily
+- (void)XXX_testDelayedMessagesBeingResentOnReconnect {
+ static BOOL isConnectionActive = NO;
+ OCMStub([self.mockClient isConnectionActive]).andDo(^(NSInvocation *invocation) {
+ [invocation setReturnValue:&isConnectionActive];
+ });
+
+ // message that lives for 2 seconds
+ NSString *messageID = @"1";
+ int ttl = 2;
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:ttl delay:1];
+
+ __block GtalkDataMessageStanza *firstMessageStanza;
+
+ OCMStub([self.mockRmqManager saveRmqMessage:[OCMArg any]
+ error:[OCMArg anyObjectRef]]).andReturn(YES);
+
+ OCMExpect([self.mockReceiver willSendDataMessageWithID:[OCMArg isEqual:messageID]
+ error:[OCMArg isNil]]);
+
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ __block FIRMessagingDataMessageHandler dataMessageHandler;
+
+ [[[self.mockRmqManager stub] andDo:^(NSInvocation *invocation) {
+ dataMessageHandler([FIRMessagingGetRmq2Id(firstMessageStanza) longLongValue],
+ firstMessageStanza);
+ }]
+ scanWithRmqMessageHandler:[OCMArg isNil]
+ dataMessageHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
+ dataMessageHandler = obj;
+ return YES;
+ }]];
+
+ // expect both 1 and 2 messages to be sent once we regain connection
+ __block BOOL firstMessageSent = NO;
+ __block BOOL secondMessageSent = NO;
+ XCTestExpectation *didSendAllMessages =
+ [self expectationWithDescription:@"Did send all messages"];
+ OCMExpect([self.mockClient sendMessage:[OCMArg checkWithBlock:^BOOL(id obj) {
+ // [didSendAllMessages fulfill];
+ if ([obj isKindOfClass:[GtalkDataMessageStanza class]]) {
+ GtalkDataMessageStanza *message = (GtalkDataMessageStanza *)obj;
+ if ([@"1" isEqualToString:message.id_p]) {
+ firstMessageSent = YES;
+ } else if ([@"2" isEqualToString:message.id_p]) {
+ secondMessageSent = YES;
+ }
+ if (firstMessageSent && secondMessageSent) {
+ [didSendAllMessages fulfill];
+ }
+ return firstMessageSent || secondMessageSent;
+ }
+ return NO;
+ }]]);
+
+ // send the second message after some delay
+ [NSThread sleepForTimeInterval:2.0];
+
+ isConnectionActive = YES;
+ // simulate active connection
+ NSString *newMessageID = @"2";
+ NSMutableDictionary *newMessage = [self upstreamMessageWithID:newMessageID
+ ttl:0
+ delay:0];
+ // send another message to resend not sent messages
+ [self.dataMessageManager sendDataMessageStanza:newMessage];
+
+ [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
+ XCTAssertNil(error);
+ OCMVerifyAll(self.mockClient);
+ OCMVerifyAll(self.mockReceiver);
+ }];
+}
+
+- (void)testSendDelayedMessage_shouldNotSend {
+ // should not send a delayed message even with an active connection
+ // simulate active connection
+ [[[self.mockClient stub] andReturnValue:OCMOCK_VALUE(YES)] isConnectionActive];
+ [[self.mockClient reject] sendMessage:[OCMArg any]];
+
+ [[self.mockReceiver reject] didSendDataMessageWithID:[OCMArg any]];
+
+ // delayed message
+ NSString *messageID = @"1";
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ NSMutableDictionary *message = [self upstreamMessageWithID:messageID ttl:0 delay:1];
+ [self.dataMessageManager sendDataMessageStanza:message];
+
+ OCMVerifyAll(self.mockClient);
+ OCMVerifyAll(self.mockReceiver);
+}
+
+- (void)testProcessPacket_withValidPacket {
+ GtalkDataMessageStanza *message = [self validDataMessagePacket];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingFromKey], message.from);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingCollapseKey], message.token);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingMessageIDKey], kMessagePersistentID);
+ XCTAssertEqualObjects(parsedMessage[kAppDataItemKey], kAppDataItemValue);
+ XCTAssertEqual(4, parsedMessage.count);
+}
+
+- (void)testProcessPacket_withOnlyFrom {
+ GtalkDataMessageStanza *message = [self validDataMessageWithOnlyFrom];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingFromKey], message.from);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingMessageIDKey], kMessagePersistentID);
+ XCTAssertEqual(2, parsedMessage.count);
+}
+
+- (void)testProcessPacket_withInvalidPacket {
+ GtalkDataMessageStanza *message = [self invalidDataMessageUsingReservedKeyword];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingFromKey], message.from);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingMessageIDKey], kMessagePersistentID);
+ XCTAssertEqual(2, parsedMessage.count);
+}
+
+/**
+ * Test parsing a duplex message.
+ */
+- (void)testProcessPacket_withDuplexMessage {
+ GtalkDataMessageStanza *stanza = [self validDuplexmessage];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:stanza];
+ XCTAssertEqual(5, parsedMessage.count);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingFromKey], stanza.from);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingCollapseKey], stanza.token);
+ XCTAssertEqualObjects(parsedMessage[kFIRMessagingMessageIDKey], kMessagePersistentID);
+ XCTAssertEqualObjects(parsedMessage[kAppDataItemKey], kAppDataItemValue);
+ XCTAssertTrue([parsedMessage[kFIRMessagingMessageSyncViaMCSKey] boolValue]);
+}
+
+- (void)testReceivingParsedMessage {
+ NSDictionary *message = @{ @"hello" : @"world" };
+ OCMStub([self.mockReceiver didReceiveMessage:[OCMArg isEqual:message] withIdentifier:[OCMArg any]]);
+ [self.dataMessageManager didReceiveParsedMessage:message];
+ OCMVerify([self.mockReceiver didReceiveMessage:message withIdentifier:[OCMArg any]]);
+}
+
+/**
+ * Test receiving a new duplex message notifies the receiver callback.
+ */
+- (void)testReceivingNewDuplexMessage {
+ GtalkDataMessageStanza *message = [self validDuplexmessage];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ [[[self.mockSyncMessageManager stub] andReturnValue:@(NO)]
+ didReceiveMCSSyncMessage:parsedMessage];
+ OCMStub([self.mockReceiver didReceiveMessage:[OCMArg isEqual:message] withIdentifier:[OCMArg any]]);
+ [self.dataMessageManager didReceiveParsedMessage:parsedMessage];
+ OCMVerify([self.mockReceiver didReceiveMessage:[OCMArg any] withIdentifier:[OCMArg any]]);
+}
+
+/**
+ * Test receiving a duplicated duplex message does not notify the receiver callback.
+ */
+- (void)testReceivingDuplicateDuplexMessage {
+ GtalkDataMessageStanza *message = [self validDuplexmessage];
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ [[[self.mockSyncMessageManager stub] andReturnValue:@(YES)]
+ didReceiveMCSSyncMessage:parsedMessage];
+ [[self.mockReceiver reject] didReceiveMessage:[OCMArg any] withIdentifier:[OCMArg any]];
+ [self.dataMessageManager didReceiveParsedMessage:parsedMessage];
+}
+
+/**
+ * In this test we simulate a real RMQ manager and send messages simulating no
+ * active connection. Then we simulate a new connection being established and
+ * the client receives a Streaming ACK which should result in resending RMQ messages.
+ */
+- (void)testResendSavedMessages {
+ static BOOL isClientConnected = NO;
+ [[[self.mockClient stub] andDo:^(NSInvocation *invocation) {
+ [invocation setReturnValue:&isClientConnected];
+ }] isConnectionActive];
+
+ // Set a fake, valid bundle identifier
+ [[[self.mockDataMessageManager stub] andReturn:@"gcm-dmm-test"] categoryForUpstreamMessages];
+
+ [FIRMessagingRmqManager removeDatabaseWithName:kRmqDatabaseName];
+ FIRMessagingRmqManager *newRmqManager =
+ [[FIRMessagingRmqManager alloc] initWithDatabaseName:kRmqDatabaseName];
+ [newRmqManager loadRmqId];
+ // have a real RMQ store
+ [self.dataMessageManager setRmq2Manager:newRmqManager];
+
+ [self addFakeFIRMessagingRegistrationToken];
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+
+ // send a couple of message with no connection should be saved to RMQ
+ [self.dataMessageManager sendDataMessageStanza:
+ [self upstreamMessageWithID:@"1" ttl:20000 delay:0]];
+ [self.dataMessageManager sendDataMessageStanza:
+ [self upstreamMessageWithID:@"2" ttl:20000 delay:0]];
+
+ [NSThread sleepForTimeInterval:1.0];
+ isClientConnected = YES;
+ // after the usual version, login assertion we would receive a SelectiveAck
+ // assuming we we weren't able to send any messages we won't delete anything
+ // from the RMQ but try to resend whatever is there
+ __block int didRecieveMessages = 0;
+ id mockConnection = OCMClassMock([FIRMessagingConnection class]);
+
+ BOOL (^resendMessageBlock)(id obj) = ^BOOL(id obj) {
+ if ([obj isKindOfClass:[GtalkDataMessageStanza class]]) {
+ GtalkDataMessageStanza *message = (GtalkDataMessageStanza *)obj;
+ NSLog(@"hello resending %@, %d", message.id_p, didRecieveMessages);
+ if ([@"1" isEqualToString:message.id_p]) {
+ didRecieveMessages |= 1; // right most bit for 1st message
+ return YES;
+ } else if ([@"2" isEqualToString:message.id_p]) {
+ didRecieveMessages |= (1<<1); // second from RMB for 2nd message
+ return YES;
+ }
+ }
+ return NO;
+ };
+ [[[mockConnection stub] andDo:^(NSInvocation *invocation) {
+ // pass
+ }] sendProto:[OCMArg checkWithBlock:resendMessageBlock]];
+
+ [self.dataMessageManager resendMessagesWithConnection:mockConnection];
+
+ // should send both messages
+ XCTAssert(didRecieveMessages == 3);
+ OCMVerifyAll(mockConnection);
+}
+
+- (void)testResendingExpiredMessagesFails {
+ // TODO: Test that expired messages should not be sent on resend
+ static BOOL isClientConnected = NO;
+ [[[self.mockClient stub] andDo:^(NSInvocation *invocation) {
+ [invocation setReturnValue:&isClientConnected];
+ }] isConnectionActive];
+
+ // Set a fake, valid bundle identifier
+ [[[self.mockDataMessageManager stub] andReturn:@"gcm-dmm-test"] categoryForUpstreamMessages];
+
+ [FIRMessagingRmqManager removeDatabaseWithName:kRmqDatabaseName];
+ FIRMessagingRmqManager *newRmqManager =
+ [[FIRMessagingRmqManager alloc] initWithDatabaseName:kRmqDatabaseName];
+ [newRmqManager loadRmqId];
+ // have a real RMQ store
+ [self.dataMessageManager setRmq2Manager:newRmqManager];
+
+ [self.dataMessageManager setDeviceAuthID:@"auth-id" secretToken:@"secret-token"];
+ // send a message that expires in 1 sec
+ [self.dataMessageManager sendDataMessageStanza:
+ [self upstreamMessageWithID:@"1" ttl:1 delay:0]];
+
+ // wait for 2 seconds (let the above message expire)
+ [NSThread sleepForTimeInterval:2.0];
+ isClientConnected = YES;
+
+ id mockConnection = OCMClassMock([FIRMessagingConnection class]);
+
+ [[mockConnection reject] sendProto:[OCMArg any]];
+ [self.dataMessageManager resendMessagesWithConnection:mockConnection];
+
+ // rmq should not have any pending messages
+ [newRmqManager scanWithRmqMessageHandler:^(int64_t rmqId, int8_t tag, NSData *data) {
+ XCTFail(@"RMQ should not have any message");
+ }
+ dataMessageHandler:nil];
+}
+
+#pragma mark - Private
+
+- (void)addFakeFIRMessagingRegistrationToken {
+ // [[FIRMessagingDefaultsManager sharedInstance] saveAppIDToken:kFIRMessagingAppIDToken];
+}
+
+#pragma mark - Create Packet
+
+- (GtalkDataMessageStanza *)validDataMessagePacket {
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ message.from = kMessageFrom;
+ message.token = kCollapseKey;
+ message.persistentId = kMessagePersistentID;
+ GtalkAppData *item = [[GtalkAppData alloc] init];
+ item.key = kAppDataItemKey;
+ item.value = kAppDataItemValue;
+ message.appDataArray = [NSMutableArray arrayWithObject:item];
+ return message;
+}
+
+- (GtalkDataMessageStanza *)validDataMessageWithOnlyFrom {
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ message.from = kMessageFrom;
+ message.persistentId = kMessagePersistentID;
+ return message;
+}
+
+- (GtalkDataMessageStanza *)invalidDataMessageUsingReservedKeyword {
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ message.from = kMessageFrom;
+ message.persistentId = kMessagePersistentID;
+ GtalkAppData *item = [[GtalkAppData alloc] init];
+ item.key = kAppDataItemInvalidKey;
+ item.value = kAppDataItemValue;
+ message.appDataArray = [NSMutableArray arrayWithObject:item];
+ return message;
+}
+
+- (GtalkDataMessageStanza *)validDataMessageForFIRMessaging {
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ message.from = kMessageFrom;
+ message.token = @"com.google.gcm";
+ return message;
+}
+
+- (GtalkDataMessageStanza *)validDuplexmessage {
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ message.from = kMessageFrom;
+ message.token = kCollapseKey;
+ message.persistentId = kMessagePersistentID;
+ GtalkAppData *item = [[GtalkAppData alloc] init];
+ item.key = kAppDataItemKey;
+ item.value = kAppDataItemValue;
+ GtalkAppData *duplexItem = [[GtalkAppData alloc] init];
+ duplexItem.key = @"gcm.duplex";
+ duplexItem.value = @"1";
+ message.appDataArray = [NSMutableArray arrayWithObjects:item, duplexItem, nil];
+ return message;
+}
+
+#pragma mark - Create Message
+
+- (NSMutableDictionary *)standardFIRMessagingMessageWithMessageID:(NSString *)messageID {
+ NSDictionary *message = @{ kAppDataItemKey : kAppDataItemValue };
+ return [FIRMessaging createFIRMessagingMessageWithMessage:message
+ to:kMessageTo
+ withID:messageID
+ timeToLive:-1
+ delay:0];
+}
+
+- (NSMutableDictionary *)upstreamMessageWithID:(NSString *)messageID
+ ttl:(int64_t)ttl
+ delay:(int)delay {
+ NSDictionary *message = @{ kAppDataItemInvalidKey : kAppDataItemValue };
+ return [FIRMessaging createFIRMessagingMessageWithMessage:message
+ to:kMessageTo
+ withID:messageID
+ timeToLive:ttl
+ delay:delay];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingFakeConnection.h b/Example/Messaging/Tests/FIRMessagingFakeConnection.h
new file mode 100644
index 0000000..7fe52bf
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingFakeConnection.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 "FIRMessagingConnection.h"
+
+/**
+ * A bunch of different fake connections are used to simulate various connection behaviours.
+ * A fake connection that successfully conects to remote host.
+ */
+// TODO: Split FIRMessagingConnection to make it more testable.
+@interface FIRMessagingFakeConnection : FIRMessagingConnection
+
+@property(nonatomic, readwrite, assign) BOOL shouldFakeSuccessLogin;
+
+// timeout caused by heartbeat failure (defaults to 0.5s)
+@property(nonatomic, readwrite, assign) NSTimeInterval fakeConnectionTimeout;
+
+/**
+ * Should stub the socket disconnect to not fail when called
+ */
+- (void)mockSocketDisconnect;
+
+/**
+ * Calls disconnect on the socket(which should theoretically be mocked by the above method) and
+ * let the socket delegate know that it has been disconnected.
+ */
+- (void)disconnectNow;
+
+/**
+ * The fake host to connect to.
+ */
++ (NSString *)fakeHost;
+
+/**
+ * The fake port used to connect.
+ */
++ (int)fakePort;
+
+@end
+
+/**
+ * A fake connection that simulates failure a certain number of times before success.
+ */
+// TODO: Coalesce this with the FIRMessagingFakeConnection itself.
+@interface FIRMessagingFakeFailConnection : FIRMessagingFakeConnection
+
+@property(nonatomic, readwrite, assign) int failCount;
+@property(nonatomic, readwrite, assign) int signInRequests;
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingFakeConnection.m b/Example/Messaging/Tests/FIRMessagingFakeConnection.m
new file mode 100644
index 0000000..de2e0bb
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingFakeConnection.m
@@ -0,0 +1,150 @@
+/*
+ * 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 "FIRMessagingFakeConnection.h"
+
+#import <OCMock/OCMock.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingSecureSocket.h"
+#import "FIRMessagingUtilities.h"
+
+static NSString *const kHost = @"localhost";
+static const int kPort = 6234;
+
+@interface FIRMessagingSecureSocket ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+
+@end
+
+@interface FIRMessagingConnection ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;
+
+- (void)setupConnectionSocket;
+- (void)connectToSocket:(FIRMessagingSecureSocket *)socket;
+- (NSTimeInterval)connectionTimeoutInterval;
+- (void)sendHeartbeatPing;
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didReceiveData:(NSData *)data
+ withTag:(int8_t)tag;
+
+@end
+
+@implementation FIRMessagingFakeConnection
+
+- (void)signIn {
+
+ // use this if you don't really want to mock/stub the login behaviour. In case
+ // you want to stub the login behavoiur you should do these things manually in
+ // your test and add custom logic in between as required for your testing.
+ [self setupConnectionSocket];
+
+ id socketMock = OCMPartialMock(self.socket);
+ self.socket = socketMock;
+ [[[socketMock stub]
+ andDo:^(NSInvocation *invocation) {
+ if (self.shouldFakeSuccessLogin) {
+ [self willFakeSuccessfulLoginToFCM];
+ }
+ self.socket.state = kFIRMessagingSecureSocketOpen;
+ [self.socket.delegate secureSocketDidConnect:self.socket];
+ }]
+ connectToHost:kHost
+ port:kPort
+ onRunLoop:[OCMArg any]];
+
+ [self connectToSocket:socketMock];
+}
+
+- (NSTimeInterval)connectionTimeoutInterval {
+ if (self.fakeConnectionTimeout) {
+ return self.fakeConnectionTimeout;
+ } else {
+ return 0.5; // 0.5s
+ }
+}
+
+- (void)mockSocketDisconnect {
+ id mockSocket = self.socket;
+ [[[mockSocket stub] andDo:^(NSInvocation *invocation) {
+ self.socket.state = kFIRMessagingSecureSocketClosed;
+ }] disconnect];
+}
+
+- (void)disconnectNow {
+ [self.socket disconnect];
+ [self.socket.delegate didDisconnectWithSecureSocket:self.socket];
+}
+
++ (NSString *)fakeHost {
+ return @"localhost";
+}
+
++ (int)fakePort {
+ return 6234;
+}
+
+- (void)willFakeSuccessfulLoginToFCM {
+ id mockSocket = self.socket;
+ [[[mockSocket stub]
+ andDo:^(NSInvocation *invocation) {
+ // mock successful login
+
+ GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
+ [response setId_p:@""];
+ [self secureSocket:self.socket
+ didReceiveData:[response data]
+ withTag:kFIRMessagingProtoTagLoginResponse];
+ }]
+ sendData:[OCMArg any]
+ withTag:kFIRMessagingProtoTagLoginRequest
+ rmqId:[OCMArg isNil]];
+}
+
+@end
+
+@implementation FIRMessagingFakeFailConnection
+
+- (void)signIn {
+ self.signInRequests++;
+ [self setupConnectionSocket];
+ id mockSocket = OCMPartialMock(self.socket);
+ self.socket = mockSocket;
+ [[[mockSocket stub]
+ andDo:^(NSInvocation *invocation) {
+ [self mockSocketDisconnect];
+ if (self.signInRequests <= self.failCount) {
+ // do nothing -- should timeout
+ } else {
+ // since we will always fail once we would disconnect the socket before
+ // we ever try again thus mock the disconnect to change the state and
+ // prevent any assertions
+ [self willFakeSuccessfulLoginToFCM];
+ self.socket.state = kFIRMessagingSecureSocketOpen;
+ [self.socket.delegate secureSocketDidConnect:self.socket];
+ }
+ }]
+ connectToHost:kHost
+ port:kPort
+ onRunLoop:[OCMArg any]];
+
+ [self connectToSocket:mockSocket];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingFakeSocket.h b/Example/Messaging/Tests/FIRMessagingFakeSocket.h
new file mode 100644
index 0000000..ecba9dc
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingFakeSocket.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 "FIRMessagingSecureSocket.h"
+
+@interface FIRMessagingFakeSocket : FIRMessagingSecureSocket
+
+/**
+ * Initialize socket with a given buffer size. Designated Initializer.
+ *
+ * @param bufferSize The buffer size used to connect the input and the output stream. Note
+ * when we write data to the output stream it's read in terms of this buffer
+ * size. So for tests using `FIRMessagingFakeSocket` you should use an appropriate
+ * buffer size in terms of what you are writing to the buffer and what should
+ * be read. Since there is no "flush" operation in NSStream we would have to
+ * live with this.
+ *
+ * @see {FIRMessagingSecureSocketTest} for example usage.
+ * @return A fake secure socket.
+ */
+- (instancetype)initWithBufferSize:(uint8_t)bufferSize;
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingFakeSocket.m b/Example/Messaging/Tests/FIRMessagingFakeSocket.m
new file mode 100644
index 0000000..2b0b477
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingFakeSocket.m
@@ -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 "FIRMessagingFakeSocket.h"
+
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import <GoogleToolboxForMac/GTMDefines.h>
+
+@interface FIRMessagingSecureSocket() <NSStreamDelegate>
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+@property(nonatomic, readwrite, strong) NSInputStream *inStream;
+@property(nonatomic, readwrite, strong) NSOutputStream *outStream;
+
+@property(nonatomic, readwrite, assign) BOOL isInStreamOpen;
+@property(nonatomic, readwrite, assign) BOOL isOutStreamOpen;
+
+@property(nonatomic, readwrite, strong) NSRunLoop *runLoop;
+
+@end
+
+@interface FIRMessagingFakeSocket ()
+
+@property(nonatomic, readwrite, assign) int8_t bufferSize;
+
+@end
+
+@implementation FIRMessagingFakeSocket
+
+- (instancetype)initWithBufferSize:(uint8_t)bufferSize {
+ self = [super init];
+ if (self) {
+ _bufferSize = bufferSize;
+ }
+ return self;
+}
+
+- (void)connectToHost:(NSString *)host
+ port:(NSUInteger)port
+ onRunLoop:(NSRunLoop *)runLoop {
+ self.state = kFIRMessagingSecureSocketOpening;
+ self.runLoop = runLoop;
+
+ CFReadStreamRef inputStreamRef = nil;
+ CFWriteStreamRef outputStreamRef = nil;
+
+ CFStreamCreateBoundPair(NULL,
+ &inputStreamRef,
+ &outputStreamRef,
+ self.bufferSize);
+
+ self.inStream = CFBridgingRelease(inputStreamRef);
+ self.outStream = CFBridgingRelease(outputStreamRef);
+ if (!self.inStream || !self.outStream) {
+ FIRMessaging_FAIL(@"cannot create a fake socket");
+ return;
+ }
+
+ self.isInStreamOpen = NO;
+ self.isOutStreamOpen = NO;
+
+ [self openStream:self.outStream];
+ [self openStream:self.inStream];
+}
+
+- (void)openStream:(NSStream *)stream {
+ _GTMDevAssert(stream, @"Cannot open nil stream");
+ if (stream) {
+ stream.delegate = self;
+ [stream scheduleInRunLoop:self.runLoop forMode:NSDefaultRunLoopMode];
+ [stream open];
+ }
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m b/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m
new file mode 100644
index 0000000..94ce530
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m
@@ -0,0 +1,94 @@
+/*
+ * 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;
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingTestNotificationUtilities.h"
+
+@interface FIRMessaging ()
+
+- (instancetype)initWithConfig:(FIRMessagingConfig *)config;
+- (NSURL *)linkURLFromMessage:(NSDictionary *)message;
+
+@end
+
+@interface FIRMessagingLinkHandlingTest : XCTestCase
+
+@property(nonatomic, readonly, strong) FIRMessaging *messaging;
+
+@end
+
+@implementation FIRMessagingLinkHandlingTest
+
+- (void)setUp {
+ [super setUp];
+
+ FIRMessagingConfig *config = [FIRMessagingConfig defaultConfig];
+ _messaging = [[FIRMessaging alloc] initWithConfig:config];
+}
+
+- (void)tearDown {
+ _messaging = nil;
+ [super tearDown];
+}
+
+#pragma mark - Link Handling Testing
+
+- (void)testNonExistentLinkInMessage {
+ NSMutableDictionary *notification =
+ [FIRMessagingTestNotificationUtilities createBasicNotificationWithUniqueMessageID];
+ NSURL *url = [self.messaging linkURLFromMessage:notification];
+ XCTAssertNil(url);
+}
+
+- (void)testEmptyLinkInMessage {
+ NSMutableDictionary *notification =
+ [FIRMessagingTestNotificationUtilities createBasicNotificationWithUniqueMessageID];
+ notification[kFIRMessagingMessageLinkKey] = @"";
+ NSURL *url = [self.messaging linkURLFromMessage:notification];
+ XCTAssertNil(url);
+}
+
+- (void)testNonStringLinkInMessage {
+ NSMutableDictionary *notification =
+ [FIRMessagingTestNotificationUtilities createBasicNotificationWithUniqueMessageID];
+ notification[kFIRMessagingMessageLinkKey] = @(5);
+ NSURL *url = [self.messaging linkURLFromMessage:notification];
+ XCTAssertNil(url);
+}
+
+- (void)testInvalidURLStringLinkInMessage {
+ NSMutableDictionary *notification =
+ [FIRMessagingTestNotificationUtilities createBasicNotificationWithUniqueMessageID];
+ notification[kFIRMessagingMessageLinkKey] = @"This is not a valid url string";
+ NSURL *url = [self.messaging linkURLFromMessage:notification];
+ XCTAssertNil(url);
+}
+
+- (void)testValidURLStringLinkInMessage {
+ NSMutableDictionary *notification =
+ [FIRMessagingTestNotificationUtilities createBasicNotificationWithUniqueMessageID];
+ notification[kFIRMessagingMessageLinkKey] = @"https://www.google.com/";
+ NSURL *url = [self.messaging linkURLFromMessage:notification];
+ XCTAssertTrue([url.absoluteString isEqualToString:@"https://www.google.com/"]);
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingPendingTopicsListTest.m b/Example/Messaging/Tests/FIRMessagingPendingTopicsListTest.m
new file mode 100644
index 0000000..2033cb4
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingPendingTopicsListTest.m
@@ -0,0 +1,263 @@
+/*
+ * 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 <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingPendingTopicsList.h"
+#import "FIRMessagingTopicsCommon.h"
+
+typedef void (^MockDelegateSubscriptionHandler)(NSString *topic,
+ FIRMessagingTopicAction action,
+ FIRMessagingTopicOperationCompletion completion);
+
+/**
+ * This object lets us provide a stub delegate where we can customize the behavior by providing
+ * blocks. We need to use this instead of stubbing a OCMockProtocol because our delegate methods
+ * take primitive values (e.g. action), which is not easy to use from OCMock
+ * @see http://stackoverflow.com/a/6332023
+ */
+@interface MockPendingTopicsListDelegate: NSObject <FIRMessagingPendingTopicsListDelegate>
+
+@property(nonatomic, assign) BOOL isReady;
+@property(nonatomic, copy) MockDelegateSubscriptionHandler subscriptionHandler;
+@property(nonatomic, copy) void(^updateHandler)();
+
+@end
+
+@implementation MockPendingTopicsListDelegate
+
+- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
+ return self.isReady;
+}
+
+- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
+ requestedUpdateForTopic:(NSString *)topic
+ action:(FIRMessagingTopicAction)action
+ completion:(FIRMessagingTopicOperationCompletion)completion {
+ if (self.subscriptionHandler) {
+ self.subscriptionHandler(topic, action, completion);
+ }
+}
+
+- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
+ if (self.updateHandler) {
+ self.updateHandler();
+ }
+}
+
+@end
+
+@interface FIRMessagingPendingTopicsListTest : XCTestCase
+
+/// Using this delegate lets us prevent any topic operations from start, making it easy to measure
+/// our batches
+@property(nonatomic, strong) MockPendingTopicsListDelegate *notReadyDelegate;
+/// Using this delegate will always begin topic operations (which will never return by default).
+/// Useful for overriding with block-methods to handle update requests
+@property(nonatomic, strong) MockPendingTopicsListDelegate *alwaysReadyDelegate;
+
+@end
+
+@implementation FIRMessagingPendingTopicsListTest
+
+- (void)setUp {
+ [super setUp];
+ self.notReadyDelegate = [[MockPendingTopicsListDelegate alloc] init];
+ self.notReadyDelegate.isReady = NO;
+
+ self.alwaysReadyDelegate = [[MockPendingTopicsListDelegate alloc] init];
+ self.alwaysReadyDelegate.isReady = YES;
+}
+
+- (void)tearDown {
+ self.notReadyDelegate = nil;
+ self.alwaysReadyDelegate = nil;
+ [super tearDown];
+}
+
+- (void)testAddSingleTopic {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.notReadyDelegate;
+
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ XCTAssertEqual(pendingTopics.numberOfBatches, 1);
+}
+
+- (void)testAddSameTopicAndActionMultipleTimes {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.notReadyDelegate;
+
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ XCTAssertEqual(pendingTopics.numberOfBatches, 1);
+}
+
+- (void)testAddMultiplePendingTopicsWithSameAction {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.notReadyDelegate;
+
+ for (NSInteger i = 0; i < 10; i++) {
+ NSString *topic = [NSString stringWithFormat:@"/topics/%ld", i];
+ [pendingTopics addOperationForTopic:topic
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ }
+ XCTAssertEqual(pendingTopics.numberOfBatches, 1);
+}
+
+- (void)testAddTopicsWithDifferentActions {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.notReadyDelegate;
+
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/1"
+ withAction:FIRMessagingTopicActionUnsubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/2"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ XCTAssertEqual(pendingTopics.numberOfBatches, 3);
+}
+
+- (void)testBatchSizeReductionAfterSuccessfulTopicUpdate {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.alwaysReadyDelegate;
+
+ XCTestExpectation *batchSizeReductionExpectation =
+ [self expectationWithDescription:@"Batch size was reduced after topic suscription"];
+
+ FIRMessaging_WEAKIFY(self)
+ self.alwaysReadyDelegate.subscriptionHandler =
+ ^(NSString *topic,
+ FIRMessagingTopicAction action,
+ FIRMessagingTopicOperationCompletion completion) {
+ // Simulate that the handler is generally called asynchronously
+ dispatch_async(dispatch_get_main_queue(), ^{
+ FIRMessaging_STRONGIFY(self)
+ if (action == FIRMessagingTopicActionUnsubscribe) {
+ XCTAssertEqual(pendingTopics.numberOfBatches, 1);
+ [batchSizeReductionExpectation fulfill];
+ }
+ completion(FIRMessagingTopicOperationResultSucceeded, nil);
+ });
+ };
+
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/1"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/2"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/1"
+ withAction:FIRMessagingTopicActionUnsubscribe
+ completion:nil];
+
+ [self waitForExpectationsWithTimeout:5.0 handler:nil];
+}
+
+- (void)testCompletionOfTopicUpdatesInSameThread {
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.alwaysReadyDelegate;
+
+ XCTestExpectation *allOperationsSucceededed =
+ [self expectationWithDescription:@"All queued operations succeeded"];
+
+ self.alwaysReadyDelegate.subscriptionHandler =
+ ^(NSString *topic,
+ FIRMessagingTopicAction action,
+ FIRMessagingTopicOperationCompletion completion) {
+ // Typically, our callbacks happen asynchronously, but to ensure resilience,
+ // call back the operation on the same thread it was called in.
+ completion(FIRMessagingTopicOperationResultSucceeded, nil);
+ };
+
+ self.alwaysReadyDelegate.updateHandler = ^{
+ if (pendingTopics.numberOfBatches == 0) {
+ [allOperationsSucceededed fulfill];
+ }
+ };
+
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/1"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ [pendingTopics addOperationForTopic:@"/topics/2"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+
+ [self waitForExpectationsWithTimeout:5.0 handler:nil];
+}
+
+- (void)testAddingTopicToCurrentBatchWhileCurrentBatchTopicsInFlight {
+
+ FIRMessagingPendingTopicsList *pendingTopics = [[FIRMessagingPendingTopicsList alloc] init];
+ pendingTopics.delegate = self.alwaysReadyDelegate;
+
+ NSString *stragglerTopic = @"/topics/straggler";
+ XCTestExpectation *stragglerTopicWasAddedToInFlightOperations =
+ [self expectationWithDescription:@"The topic was added to in-flight operations"];
+
+ self.alwaysReadyDelegate.subscriptionHandler =
+ ^(NSString *topic,
+ FIRMessagingTopicAction action,
+ FIRMessagingTopicOperationCompletion completion) {
+ if ([topic isEqualToString:stragglerTopic]) {
+ [stragglerTopicWasAddedToInFlightOperations fulfill];
+ }
+ // Add a 0.5 second delay to the completion, to give time to add a straggler before the batch
+ // is completed
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(),
+ ^{
+ completion(FIRMessagingTopicOperationResultSucceeded, nil);
+ });
+ };
+
+ // This is a normal topic, which should start fairly soon, but take a while to complete
+ [pendingTopics addOperationForTopic:@"/topics/0"
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ // While waiting for the first topic to complete, we add another topic after a slight delay
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(),
+ ^{
+ [pendingTopics addOperationForTopic:stragglerTopic
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+ });
+
+ [self waitForExpectationsWithTimeout:5.0 handler:nil];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingPubSubTest.m b/Example/Messaging/Tests/FIRMessagingPubSubTest.m
new file mode 100644
index 0000000..2981b54
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingPubSubTest.m
@@ -0,0 +1,81 @@
+/*
+ * 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;
+
+#import "FIRMessagingPubSub.h"
+
+@interface FIRMessagingPubSubTest : XCTestCase
+@end
+
+@implementation FIRMessagingPubSubTest
+
+static NSString *const kTopicName = @"topic-Name";
+
+#pragma mark - topicMatchForSender tests
+
+/// Tests that an empty topic name is an invalid topic.
+- (void)testTopicMatchForEmptyTopicPrefix {
+ XCTAssertFalse([FIRMessagingPubSub isValidTopicWithPrefix:@""]);
+}
+
+/// Tests that a topic with an invalid prefix is not a valid topic name.
+- (void)testTopicMatchWithInvalidTopicPrefix {
+ XCTAssertFalse([FIRMessagingPubSub isValidTopicWithPrefix:@"/topics+abcdef/"]);
+}
+
+/// Tests that a topic with a valid prefix but invalid name is not a valid topic name.
+- (void)testTopicMatchWithValidTopicPrefixButInvalidName {
+ XCTAssertFalse([FIRMessagingPubSub isValidTopicWithPrefix:@"/topics/aaaaaa/topics/lala"]);
+}
+
+/// Tests that multiple backslashes in topics is an invalid topic name.
+- (void)testTopicMatchForInvalidTopicPrefix_multipleBackslash {
+ XCTAssertFalse([FIRMessagingPubSub isValidTopicWithPrefix:@"/topics//abc"]);
+}
+
+/// Tests a topic name with a valid prefix and name.
+- (void)testTopicMatchForValidTopicSender {
+ NSString *topic = [NSString stringWithFormat:@"/topics/%@", kTopicName];
+ XCTAssertTrue([FIRMessagingPubSub isValidTopicWithPrefix:topic]);
+}
+
+/// Tests topic prefix for topics with no prefix.
+- (void)testTopicHasNoTopicPrefix {
+ XCTAssertFalse([FIRMessagingPubSub hasTopicsPrefix:@""]);
+}
+
+/// Tests topic prefix for valid prefix.
+- (void)testTopicHasValidToicsPrefix {
+ XCTAssertTrue([FIRMessagingPubSub hasTopicsPrefix:@"/topics/"]);
+}
+
+/// Tests topic prefix wih no prefix.
+- (void)testAddTopicPrefix_withNoPrefix {
+ NSString *topic = [FIRMessagingPubSub addPrefixToTopic:@""];
+ XCTAssertTrue([FIRMessagingPubSub hasTopicsPrefix:topic]);
+ XCTAssertFalse([FIRMessagingPubSub isValidTopicWithPrefix:topic]);
+}
+
+/// Tests adding the "/topics/" prefix for topic name which already has a prefix.
+- (void)testAddTopicPrefix_withPrefix {
+ NSString *topic = [NSString stringWithFormat:@"/topics/%@", kTopicName];
+ topic = [FIRMessagingPubSub addPrefixToTopic:topic];
+ XCTAssertTrue([FIRMessagingPubSub hasTopicsPrefix:topic]);
+ XCTAssertTrue([FIRMessagingPubSub isValidTopicWithPrefix:topic]);
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingRegistrarTest.m b/Example/Messaging/Tests/FIRMessagingRegistrarTest.m
new file mode 100644
index 0000000..b32851c
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingRegistrarTest.m
@@ -0,0 +1,134 @@
+/*
+ * 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 XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingPubSubRegistrar.h"
+#import "FIRMessagingRegistrar.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+static NSString *const kFIRMessagingUserDefaultsSuite =
+ @"FIRMessagingRegistrarTestUserDefaultsSuite";
+
+static NSString *const kDeviceAuthId = @"12345";
+static NSString *const kSecretToken = @"45657809";
+static NSString *const kVersionInfo = @"1.0";
+static NSString *const kTopicToSubscribeTo = @"/topics/xyz/hello-world";
+static NSString *const kFIRMessagingAppIDToken = @"abcdefgh1234lmno";
+static NSString *const kSubscriptionID = @"sample-subscription-id-xyz";
+
+@interface FIRMessagingRegistrar ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingPubSubRegistrar *pubsubRegistrar;
+@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
+
+@end
+
+@interface FIRMessagingRegistrarTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) FIRMessagingRegistrar *registrar;
+@property(nonatomic, readwrite, strong) id mockRegistrar;
+@property(nonatomic, readwrite, strong) id mockCheckin;
+@property(nonatomic, readwrite, strong) id mockPubsubRegistrar;
+
+@end
+
+@implementation FIRMessagingRegistrarTest
+
+- (void)setUp {
+ [super setUp];
+ _registrar = [[FIRMessagingRegistrar alloc] init];
+ _mockRegistrar = OCMPartialMock(_registrar);
+ _mockCheckin = OCMPartialMock(_registrar.checkinService);
+ _registrar.checkinService = _mockCheckin;
+ _registrar.pubsubRegistrar = OCMClassMock([FIRMessagingPubSubRegistrar class]);
+ _mockPubsubRegistrar = _registrar.pubsubRegistrar;
+}
+
+- (void)testUpdateSubscriptionWithValidCheckinData {
+ [self stubCheckinService];
+
+ [self.registrar updateSubscriptionToTopic:kTopicToSubscribeTo
+ withToken:kFIRMessagingAppIDToken
+ options:nil
+ shouldDelete:NO
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ }];
+
+ OCMVerify([self.mockPubsubRegistrar updateSubscriptionToTopic:[OCMArg isEqual:kTopicToSubscribeTo]
+ withToken:[OCMArg isEqual:kFIRMessagingAppIDToken]
+ options:nil
+ shouldDelete:NO
+ handler:OCMOCK_ANY]);
+}
+
+- (void)testUpdateSubscription {
+ [self stubCheckinService];
+
+ __block FIRMessagingTopicOperationCompletion pubsubCompletion;
+ [[[self.mockPubsubRegistrar stub]
+ andDo:^(NSInvocation *invocation) {
+ pubsubCompletion(FIRMessagingTopicOperationResultSucceeded, nil);
+ }]
+ updateSubscriptionToTopic:kTopicToSubscribeTo
+ withToken:kFIRMessagingAppIDToken
+ options:nil
+ shouldDelete:NO
+ handler:[OCMArg checkWithBlock:^BOOL(id obj) {
+ return (pubsubCompletion = obj) != nil;
+ }]];
+
+ [self.registrar updateSubscriptionToTopic:kTopicToSubscribeTo
+ withToken:kFIRMessagingAppIDToken
+ options:nil
+ shouldDelete:NO
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqual(result, FIRMessagingTopicOperationResultSucceeded);
+ }];
+}
+
+- (void)testFailedUpdateSubscriptionWithNoCheckin {
+ // Mock checkin service to always return NO for hasValidCheckinInfo
+ [[[self.mockCheckin stub] andReturnValue:@NO] hasValidCheckinInfo];
+ // This should not create a network request since we don't have checkin info
+ [self.registrar updateSubscriptionToTopic:kTopicToSubscribeTo
+ withToken:kFIRMessagingAppIDToken
+ options:nil
+ shouldDelete:NO
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqual(result, FIRMessagingTopicOperationResultError);
+ }];
+}
+
+#pragma mark - Private Helpers
+
+- (void)stubCheckinService {
+ [[[self.mockCheckin stub] andReturn:kDeviceAuthId] deviceAuthID];
+ [[[self.mockCheckin stub] andReturn:kSecretToken] secretToken];
+ [[[self.mockCheckin stub] andReturnValue:@YES] hasValidCheckinInfo];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m b/Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m
new file mode 100644
index 0000000..9138c50
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m
@@ -0,0 +1,279 @@
+/*
+ * 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.
+ */
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+@import UserNotifications;
+#endif
+@import XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessagingRemoteNotificationsProxy.h"
+
+#pragma mark - Expose Internal Methods for Testing
+// Expose some internal properties and methods here, in order to test
+@interface FIRMessagingRemoteNotificationsProxy ()
+
+@property(readonly, nonatomic) BOOL didSwizzleMethods;
+@property(readonly, nonatomic) BOOL didSwizzleAppDelegateMethods;
+
+@property(readonly, nonatomic) BOOL hasSwizzledUserNotificationDelegate;
+@property(readonly, nonatomic) BOOL isObservingUserNotificationDelegateChanges;
+
+@property(strong, readonly, nonatomic) id userNotificationCenter;
+@property(strong, readonly, nonatomic) id currentUserNotificationCenterDelegate;
+
++ (instancetype)sharedProxy;
+
+- (BOOL)swizzleAppDelegateMethods:(id<UIApplicationDelegate>)appDelegate;
+- (void)listenForDelegateChangesInUserNotificationCenter:(id)notificationCenter;
+- (void)swizzleUserNotificationCenterDelegate:(id)delegate;
+- (void)unswizzleUserNotificationCenterDelegate:(id)delegate;
+
+void FCM_swizzle_appDidReceiveRemoteNotification(id self,
+ SEL _cmd,
+ UIApplication *app,
+ NSDictionary *userInfo);
+void FCM_swizzle_appDidReceiveRemoteNotificationWithHandler(
+ id self, SEL _cmd, UIApplication *app, NSDictionary *userInfo,
+ void (^handler)(UIBackgroundFetchResult));
+void FCM_swizzle_willPresentNotificationWithHandler(
+ id self, SEL _cmd, id center, id notification, void (^handler)(NSUInteger));
+
+@end
+
+#pragma mark - Incomplete App Delegate
+@interface IncompleteAppDelegate : NSObject <UIApplicationDelegate>
+@end
+@implementation IncompleteAppDelegate
+@end
+
+#pragma mark - Fake AppDelegate
+@interface FakeAppDelegate : NSObject <UIApplicationDelegate>
+@property(nonatomic) BOOL remoteNotificationMethodWasCalled;
+@property(nonatomic) BOOL remoteNotificationWithFetchHandlerWasCalled;
+@end
+@implementation FakeAppDelegate
+- (void)application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)userInfo {
+ self.remoteNotificationMethodWasCalled = YES;
+}
+- (void)application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)userInfo
+ fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
+ self.remoteNotificationWithFetchHandlerWasCalled = YES;
+}
+@end
+
+#pragma mark - Incompete UNUserNotificationCenterDelegate
+@interface IncompleteUserNotificationCenterDelegate : NSObject <UNUserNotificationCenterDelegate>
+@end
+@implementation IncompleteUserNotificationCenterDelegate
+@end
+
+#pragma mark - Fake UNUserNotificationCenterDelegate
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+@interface FakeUserNotificationCenterDelegate : NSObject <UNUserNotificationCenterDelegate>
+@property(nonatomic) BOOL willPresentWasCalled;
+@end
+@implementation FakeUserNotificationCenterDelegate
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center
+ willPresentNotification:(UNNotification *)notification
+ withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))
+ completionHandler {
+ self.willPresentWasCalled = YES;
+}
+@end
+#endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+#pragma mark - Local, Per-Test Properties
+
+@interface FIRMessagingRemoteNotificationsProxyTest : XCTestCase
+
+@property(nonatomic, strong) FIRMessagingRemoteNotificationsProxy *proxy;
+@property(nonatomic, strong) id mockProxy;
+@property(nonatomic, strong) id mockProxyClass;
+@property(nonatomic, strong) id mockMessagingClass;
+
+@end
+
+@implementation FIRMessagingRemoteNotificationsProxyTest
+
+- (void)setUp {
+ [super setUp];
+ _proxy = [[FIRMessagingRemoteNotificationsProxy alloc] init];
+ _mockProxy = OCMPartialMock(_proxy);
+ _mockProxyClass = OCMClassMock([FIRMessagingRemoteNotificationsProxy class]);
+ // Update +sharedProxy to always return our partial mock of FIRMessagingRemoteNotificationsProxy
+ OCMStub([_mockProxyClass sharedProxy]).andReturn(_mockProxy);
+ // Many of our swizzled methods call [FIRMessaging messaging], but we don't need it,
+ // so just stub it to return nil
+ _mockMessagingClass = OCMClassMock([FIRMessaging class]);
+ OCMStub([_mockMessagingClass messaging]).andReturn(nil);
+}
+
+- (void)tearDown {
+ [_mockMessagingClass stopMocking];
+ _mockMessagingClass = nil;
+
+ [_mockProxyClass stopMocking];
+ _mockProxyClass = nil;
+
+ [_mockProxy stopMocking];
+ _mockProxy = nil;
+
+ _proxy = nil;
+ [super tearDown];
+}
+
+#pragma mark - Method Swizzling Tests
+
+- (void)testSwizzlingNonAppDelegate {
+ id randomObject = @"Random Object that is not an App Delegate";
+ [self.proxy swizzleAppDelegateMethods:randomObject];
+ XCTAssertFalse(self.proxy.didSwizzleAppDelegateMethods);
+}
+
+- (void)testSwizzlingAppDelegate {
+ IncompleteAppDelegate *incompleteAppDelegate = [[IncompleteAppDelegate alloc] init];
+ [self.proxy swizzleAppDelegateMethods:incompleteAppDelegate];
+ XCTAssertTrue(self.proxy.didSwizzleAppDelegateMethods);
+}
+
+- (void)testSwizzledIncompleteAppDelegateRemoteNotificationMethod {
+ IncompleteAppDelegate *incompleteAppDelegate = [[IncompleteAppDelegate alloc] init];
+ [self.mockProxy swizzleAppDelegateMethods:incompleteAppDelegate];
+ SEL selector = @selector(application:didReceiveRemoteNotification:);
+ XCTAssertTrue([incompleteAppDelegate respondsToSelector:selector]);
+ [incompleteAppDelegate application:OCMClassMock([UIApplication class])
+ didReceiveRemoteNotification:@{}];
+ // Verify our swizzled method was called
+ OCMVerify(FCM_swizzle_appDidReceiveRemoteNotification);
+}
+
+// If the remote notification with fetch handler is NOT implemented, we will force-implement
+// the backup -application:didReceiveRemoteNotification: method
+- (void)testIncompleteAppDelegateRemoteNotificationWithFetchHandlerMethod {
+ IncompleteAppDelegate *incompleteAppDelegate = [[IncompleteAppDelegate alloc] init];
+ [self.mockProxy swizzleAppDelegateMethods:incompleteAppDelegate];
+ SEL remoteNotificationWithFetchHandler =
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
+ XCTAssertFalse([incompleteAppDelegate respondsToSelector:remoteNotificationWithFetchHandler]);
+
+ SEL remoteNotification = @selector(application:didReceiveRemoteNotification:);
+ XCTAssertTrue([incompleteAppDelegate respondsToSelector:remoteNotification]);
+}
+
+- (void)testSwizzledAppDelegateRemoteNotificationMethods {
+ FakeAppDelegate *appDelegate = [[FakeAppDelegate alloc] init];
+ [self.mockProxy swizzleAppDelegateMethods:appDelegate];
+ [appDelegate application:OCMClassMock([UIApplication class]) didReceiveRemoteNotification:@{}];
+ // Verify our swizzled method was called
+ OCMVerify(FCM_swizzle_appDidReceiveRemoteNotification);
+ // Verify our original method was called
+ XCTAssertTrue(appDelegate.remoteNotificationMethodWasCalled);
+
+ // Now call the remote notification with handler method
+ [appDelegate application:OCMClassMock([UIApplication class])
+ didReceiveRemoteNotification:@{}
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {}];
+ // Verify our swizzled method was called
+ OCMVerify(FCM_swizzle_appDidReceiveRemoteNotificationWithHandler);
+ // Verify our original method was called
+ XCTAssertTrue(appDelegate.remoteNotificationWithFetchHandlerWasCalled);
+}
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+- (void)testListeningForDelegateChangesOnInvalidUserNotificationCenter {
+ id randomObject = @"Random Object that is not a User Notification Center";
+ [self.proxy listenForDelegateChangesInUserNotificationCenter:randomObject];
+ XCTAssertFalse(self.proxy.isObservingUserNotificationDelegateChanges);
+}
+
+- (void)testSwizzlingInvalidUserNotificationCenterDelegate {
+ id randomObject = @"Random Object that is not a User Notification Center Delegate";
+ [self.proxy swizzleUserNotificationCenterDelegate:randomObject];
+ XCTAssertFalse(self.proxy.hasSwizzledUserNotificationDelegate);
+}
+
+- (void)testSwizzlingUserNotificationsCenterDelegate {
+ FakeUserNotificationCenterDelegate *delegate = [[FakeUserNotificationCenterDelegate alloc] init];
+ [self.proxy swizzleUserNotificationCenterDelegate:delegate];
+ XCTAssertTrue(self.proxy.hasSwizzledUserNotificationDelegate);
+}
+
+// Use a fake delegate that doesn't actually implement the needed delegate method.
+// Our swizzled method should still be called.
+
+- (void)testIncompleteUserNotificationCenterDelegateMethod {
+ // Early exit if running on pre iOS 10
+ if (![UNNotification class]) {
+ return;
+ }
+ IncompleteUserNotificationCenterDelegate *delegate =
+ [[IncompleteUserNotificationCenterDelegate alloc] init];
+ [self.mockProxy swizzleUserNotificationCenterDelegate:delegate];
+ SEL selector = @selector(userNotificationCenter:willPresentNotification:withCompletionHandler:);
+ XCTAssertTrue([delegate respondsToSelector:selector]);
+ // Invoking delegate method should also invoke our swizzled method
+ // The swizzled method uses the +sharedProxy, which should be
+ // returning our mocked proxy.
+ // Use non-nil, proper classes, otherwise our SDK bails out.
+ [delegate userNotificationCenter:OCMClassMock([UNUserNotificationCenter class])
+ willPresentNotification:[self generateMockNotification]
+ withCompletionHandler:^(NSUInteger options) {}];
+ // Verify our swizzled method was called
+ OCMVerify(FCM_swizzle_willPresentNotificationWithHandler);
+}
+
+// Use an object that does actually implement the needed method. Both should be called.
+- (void)testSwizzledUserNotificationsCenterDelegate {
+ // Early exit if running on pre iOS 10
+ if (![UNNotification class]) {
+ return;
+ }
+ FakeUserNotificationCenterDelegate *delegate = [[FakeUserNotificationCenterDelegate alloc] init];
+ [self.mockProxy swizzleUserNotificationCenterDelegate:delegate];
+ // Invoking delegate method should also invoke our swizzled method
+ // The swizzled method uses the +sharedProxy, which should be
+ // returning our mocked proxy.
+ // Use non-nil, proper classes, otherwise our SDK bails out.
+ [delegate userNotificationCenter:OCMClassMock([UNUserNotificationCenter class])
+ willPresentNotification:[self generateMockNotification]
+ withCompletionHandler:^(NSUInteger options) {}];
+ // Verify our swizzled method was called
+ OCMVerify(FCM_swizzle_willPresentNotificationWithHandler);
+ // Verify our original method was called
+ XCTAssertTrue(delegate.willPresentWasCalled);
+}
+
+- (id)generateMockNotification {
+ // Stub out: notification.request.content.userInfo
+ id mockNotification = OCMClassMock([UNNotification class]);
+ id mockRequest = OCMClassMock([UNNotificationRequest class]);
+ id mockContent = OCMClassMock([UNNotificationContent class]);
+ OCMStub([mockContent userInfo]).andReturn(@{});
+ OCMStub([mockRequest content]).andReturn(mockContent);
+ OCMStub([mockNotification request]).andReturn(mockRequest);
+ return mockNotification;
+}
+
+#endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingRmqManagerTest.m b/Example/Messaging/Tests/FIRMessagingRmqManagerTest.m
new file mode 100644
index 0000000..88d7c4e
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingRmqManagerTest.m
@@ -0,0 +1,332 @@
+/*
+ * 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/XCTest.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingPersistentSyncMessage.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingUtilities.h"
+
+static NSString *const kRmqDatabaseName = @"rmq-test-db";
+static NSString *const kRmqDataMessageCategory = @"com.google.gcm-rmq-test";
+
+@interface FIRMessagingRmqManagerTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmqManager;
+
+@end
+
+@implementation FIRMessagingRmqManagerTest
+
+- (void)setUp {
+ [super setUp];
+ // Make sure we start off with a clean state each time
+ [FIRMessagingRmqManager removeDatabaseWithName:kRmqDatabaseName];
+ _rmqManager = [[FIRMessagingRmqManager alloc] initWithDatabaseName:kRmqDatabaseName];
+}
+
+- (void)tearDown {
+ [super tearDown];
+ [FIRMessagingRmqManager removeDatabaseWithName:kRmqDatabaseName];
+}
+
+/**
+ * Add s2d messages with different RMQ-ID's to the RMQ. Fetch the messages
+ * and verify that all messages were successfully saved.
+ */
+- (void)testSavingS2dMessages {
+ NSArray *messageIDs = @[ @"message1", @"message2", @"123456" ];
+ for (NSString *messageID in messageIDs) {
+ [self.rmqManager saveS2dMessageWithRmqId:messageID];
+ }
+ NSArray *rmqMessages = [self.rmqManager unackedS2dRmqIds];
+ XCTAssertEqual(messageIDs.count, rmqMessages.count);
+ for (NSString *messageID in rmqMessages) {
+ XCTAssertTrue([messageIDs containsObject:messageID]);
+ }
+}
+
+/**
+ * Add s2d messages with different RMQ-ID's to the RMQ. Delete some of the
+ * messages stored, assuming we received a server ACK for them. The remaining
+ * messages should be fetched successfully.
+ */
+- (void)testDeletingS2dMessages {
+ NSArray *addMessages = @[ @"message1", @"message2", @"message3", @"message4"];
+ for (NSString *messageID in addMessages) {
+ [self.rmqManager saveS2dMessageWithRmqId:messageID];
+ }
+ NSArray *removeMessages = @[ addMessages[1], addMessages[3] ];
+ [self.rmqManager removeS2dIds:removeMessages];
+ NSArray *remainingMessages = [self.rmqManager unackedS2dRmqIds];
+ XCTAssertEqual(2, remainingMessages.count);
+ XCTAssertTrue([remainingMessages containsObject:addMessages[0]]);
+ XCTAssertTrue([remainingMessages containsObject:addMessages[2]]);
+}
+
+/**
+ * Test deleting a s2d message that is not in the persistent store. This shouldn't
+ * crash or alter the valid contents of the RMQ store.
+ */
+- (void)testDeletingInvalidS2dMessage {
+ NSString *validMessageID = @"validMessage123";
+ [self.rmqManager saveS2dMessageWithRmqId:validMessageID];
+ NSString *invalidMessageID = @"invalidMessage123";
+ [self.rmqManager removeS2dIds:@[invalidMessageID]];
+ NSArray *remainingMessages = [self.rmqManager unackedS2dRmqIds];
+ XCTAssertEqual(1, remainingMessages.count);
+ XCTAssertEqualObjects(validMessageID, remainingMessages[0]);
+}
+
+/**
+ * Test loading the RMQ-ID for d2s messages when there are no outgoing messages in the RMQ.
+ */
+- (void)testLoadRmqIDWithNoD2sMessages {
+ [self.rmqManager loadRmqId];
+ XCTAssertEqual(-1, [self maxRmqIDInRmqStoreForD2SMessages]);
+}
+
+/**
+ * Test that outgoing RMQ messages are correctly saved
+ */
+- (void)testOutgoingRmqWithValidMessages {
+ NSString *from = @"rmq-test";
+ [self.rmqManager loadRmqId];
+ GtalkDataMessageStanza *message1 = [self dataMessageWithMessageID:@"message1"
+ from:from
+ data:nil];
+ NSError *error;
+
+ // should successfully save the message to RMQ
+ XCTAssertTrue([self.rmqManager saveRmqMessage:message1 error:&error]);
+ XCTAssertNil(error);
+
+ GtalkDataMessageStanza *message2 = [self dataMessageWithMessageID:@"message2"
+ from:from
+ data:nil];
+
+ // should successfully save the second message to RMQ
+ XCTAssertTrue([self.rmqManager saveRmqMessage:message2 error:&error]);
+ XCTAssertNil(error);
+
+ // message1 should have RMQ-ID = 2, message2 = 3
+ XCTAssertEqual(3, [self maxRmqIDInRmqStoreForD2SMessages]);
+ [self.rmqManager scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ if (rmqId == 2) {
+ XCTAssertEqualObjects(@"message1", stanza.id_p);
+ } else if (rmqId == 3) {
+ XCTAssertEqualObjects(@"message2", stanza.id_p);
+ } else {
+ XCTFail(@"Invalid RmqID %lld for s2d message", rmqId);
+ }
+ }];
+}
+
+/**
+ * Test that an outgoing message with different properties is correctly saved to the RMQ.
+ */
+- (void)testOutgoingDataMessageIsCorrectlySaved {
+ NSString *from = @"rmq-test";
+ NSString *messageID = @"message123";
+ NSString *to = @"to-senderID-123";
+ int32_t ttl = 2400;
+ NSString *registrationToken = @"registration-token";
+ NSDictionary *data = @{
+ @"hello" : @"world",
+ @"count" : @"2",
+ };
+
+ [self.rmqManager loadRmqId];
+ GtalkDataMessageStanza *message = [self dataMessageWithMessageID:messageID
+ from:from
+ data:data];
+ [message setTo:to];
+ [message setTtl:ttl];
+ [message setRegId:registrationToken];
+ NSError *error;
+
+ // should successfully save the message to RMQ
+ XCTAssertTrue([self.rmqManager saveRmqMessage:message error:&error]);
+ XCTAssertNil(error);
+
+ [self.rmqManager scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ XCTAssertEqualObjects(from, stanza.from);
+ XCTAssertEqualObjects(messageID, stanza.id_p);
+ XCTAssertEqualObjects(to, stanza.to);
+ XCTAssertEqualObjects(registrationToken, stanza.regId);
+ XCTAssertEqual(ttl, stanza.ttl);
+ NSMutableDictionary *d = [NSMutableDictionary dictionary];
+ for (GtalkAppData *appData in stanza.appDataArray) {
+ d[appData.key] = appData.value;
+ }
+ XCTAssertTrue([data isEqualToDictionary:d]);
+ }];
+}
+
+/**
+ * Test D2S messages being deleted from RMQ.
+ */
+- (void)testDeletingD2SMessagesFromRMQ {
+ NSString *message1 = @"message123";
+ NSString *ackedMessage = @"message234";
+ NSString *from = @"from-rmq-test";
+ GtalkDataMessageStanza *stanza1 = [self dataMessageWithMessageID:message1 from:from data:nil];
+ GtalkDataMessageStanza *stanza2 = [self dataMessageWithMessageID:ackedMessage
+ from:from
+ data:nil];
+ NSError *error;
+ XCTAssertTrue([self.rmqManager saveRmqMessage:stanza1 error:&error]);
+ XCTAssertNil(error);
+ XCTAssertTrue([self.rmqManager saveRmqMessage:stanza2 error:&error]);
+ XCTAssertNil(error);
+
+ __block int64_t ackedMessageRmqID = -1;
+ [self.rmqManager scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ if ([stanza.id_p isEqualToString:ackedMessage]) {
+ ackedMessageRmqID = rmqId;
+ }
+ }];
+ // should be a valid RMQ ID
+ XCTAssertTrue(ackedMessageRmqID > 0);
+
+ // delete the acked message
+ NSString *rmqIDString = [NSString stringWithFormat:@"%lld", ackedMessageRmqID];
+ XCTAssertEqual(1, [self.rmqManager removeRmqMessagesWithRmqId:rmqIDString]);
+
+ // should only have one message in the d2s RMQ
+ [self.rmqManager scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ // the acked message was queued later so should have
+ // rmqID = ackedMessageRMQID - 1
+ XCTAssertEqual(ackedMessageRmqID - 1, rmqId);
+ XCTAssertEqual(message1, stanza2.id_p);
+ }];
+}
+
+/**
+ * Test saving a sync message to SYNC_RMQ.
+ */
+- (void)testSavingSyncMessage {
+ NSString *rmqID = @"fake-rmq-id-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 1;
+ XCTAssertTrue([self.rmqManager saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:YES
+ mcsReceived:NO
+ error:nil]);
+
+ FIRMessagingPersistentSyncMessage *persistentMessage = [self.rmqManager querySyncMessageWithRmqID:rmqID];
+ XCTAssertEqual(persistentMessage.expirationTime, expirationTime);
+ XCTAssertTrue(persistentMessage.apnsReceived);
+ XCTAssertFalse(persistentMessage.mcsReceived);
+}
+
+/**
+ * Test updating a sync message initially received via MCS, now being received via APNS.
+ */
+- (void)testUpdateMessageReceivedViaAPNS {
+ NSString *rmqID = @"fake-rmq-id-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 1;
+ XCTAssertTrue([self.rmqManager saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:NO
+ mcsReceived:YES
+ error:nil]);
+
+ // Message was now received via APNS
+ XCTAssertTrue([self.rmqManager updateSyncMessageViaAPNSWithRmqID:rmqID error:nil]);
+
+ FIRMessagingPersistentSyncMessage *persistentMessage = [self.rmqManager querySyncMessageWithRmqID:rmqID];
+ XCTAssertTrue(persistentMessage.apnsReceived);
+ XCTAssertTrue(persistentMessage.mcsReceived);
+}
+
+/**
+ * Test updating a sync message initially received via APNS, now being received via MCS.
+ */
+- (void)testUpdateMessageReceivedViaMCS {
+ NSString *rmqID = @"fake-rmq-id-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 1;
+ XCTAssertTrue([self.rmqManager saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:YES
+ mcsReceived:NO
+ error:nil]);
+
+ // Message was now received via APNS
+ XCTAssertTrue([self.rmqManager updateSyncMessageViaMCSWithRmqID:rmqID error:nil]);
+
+ FIRMessagingPersistentSyncMessage *persistentMessage = [self.rmqManager querySyncMessageWithRmqID:rmqID];
+ XCTAssertTrue(persistentMessage.apnsReceived);
+ XCTAssertTrue(persistentMessage.mcsReceived);
+}
+
+/**
+ * Test deleting sync messages from SYNC_RMQ.
+ */
+- (void)testDeleteSyncMessage {
+ NSString *rmqID = @"fake-rmq-id-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 1;
+ XCTAssertTrue([self.rmqManager saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:YES
+ mcsReceived:NO
+ error:nil]);
+ XCTAssertNotNil([self.rmqManager querySyncMessageWithRmqID:rmqID]);
+
+ // should successfully delete the message
+ XCTAssertTrue([self.rmqManager deleteSyncMessageWithRmqID:rmqID]);
+ XCTAssertNil([self.rmqManager querySyncMessageWithRmqID:rmqID]);
+}
+
+#pragma mark - Private Helpers
+
+- (GtalkDataMessageStanza *)dataMessageWithMessageID:(NSString *)messageID
+ from:(NSString *)from
+ data:(NSDictionary *)data {
+ GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
+ [stanza setId_p:messageID];
+ [stanza setFrom:from];
+ [stanza setCategory:kRmqDataMessageCategory];
+
+ for (NSString *key in data) {
+ NSString *val = data[key];
+ GtalkAppData *appData = [[GtalkAppData alloc] init];
+ [appData setKey:key];
+ [appData setValue:val];
+ [[stanza appDataArray] addObject:appData];
+ }
+
+ return stanza;
+}
+
+- (int64_t)maxRmqIDInRmqStoreForD2SMessages {
+ __block int64_t maxRmqID = -1;
+ [self.rmqManager scanWithRmqMessageHandler:^(int64_t rmqId, int8_t tag, NSData *data) {
+ if (rmqId > maxRmqID) {
+ maxRmqID = rmqId;
+ }
+ }
+ dataMessageHandler:nil];
+ return maxRmqID;
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingSecureSocketTest.m b/Example/Messaging/Tests/FIRMessagingSecureSocketTest.m
new file mode 100644
index 0000000..9f6186b
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingSecureSocketTest.m
@@ -0,0 +1,323 @@
+/*
+ * 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;
+
+#import <OCMock/OCMock.h>
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingFakeSocket.h"
+#import "FIRMessagingSecureSocket.h"
+#import "FIRMessagingUtilities.h"
+
+@interface FIRMessagingConnection ()
+
++ (GtalkLoginRequest *)loginRequestWithToken:(NSString *)token authID:(NSString *)authID;
+
+@end
+
+@interface FIRMessagingSecureSocket() <NSStreamDelegate>
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+@property(nonatomic, readwrite, strong) NSInputStream *inStream;
+@property(nonatomic, readwrite, strong) NSOutputStream *outStream;
+
+@property(nonatomic, readwrite, assign) BOOL isVersionSent;
+@property(nonatomic, readwrite, assign) BOOL isVersionReceived;
+@property(nonatomic, readwrite, assign) BOOL isInStreamOpen;
+@property(nonatomic, readwrite, assign) BOOL isOutStreamOpen;
+
+@property(nonatomic, readwrite, strong) NSRunLoop *runLoop;
+
+- (BOOL)performRead;
+
+@end
+
+typedef void(^FIRMessagingTestSocketDisconnectHandler)(void);
+typedef void(^FIRMessagingTestSocketConnectHandler)(void);
+
+@interface FIRMessagingSecureSocketTest : XCTestCase <FIRMessagingSecureSocketDelegate>
+
+@property(nonatomic, readwrite, strong) FIRMessagingFakeSocket *socket;
+@property(nonatomic, readwrite, strong) id mockSocket;
+@property(nonatomic, readwrite, strong) NSError *protoParseError;
+@property(nonatomic, readwrite, strong) GPBMessage *protoReceived;
+@property(nonatomic, readwrite, assign) int8_t protoTagReceived;
+
+@property(nonatomic, readwrite, copy) FIRMessagingTestSocketDisconnectHandler disconnectHandler;
+@property(nonatomic, readwrite, copy) FIRMessagingTestSocketConnectHandler connectHandler;
+
+@end
+
+static BOOL isSafeToDisconnectSocket = NO;
+
+@implementation FIRMessagingSecureSocketTest
+
+- (void)setUp {
+ [super setUp];
+ isSafeToDisconnectSocket = NO;
+ self.protoParseError = nil;
+ self.protoReceived = nil;
+ self.protoTagReceived = 0;
+}
+
+- (void)tearDown {
+ self.disconnectHandler = nil;
+ self.connectHandler = nil;
+ isSafeToDisconnectSocket = YES;
+ [self.socket disconnect];
+ [super tearDown];
+}
+
+#pragma mark - Test Reading
+
+- (void)testSendingVersion {
+ // read as soon as 1 byte is written
+ [self createAndConnectSocketWithBufferSize:1];
+ uint8_t versionByte = 40;
+ [self.socket.outStream write:&versionByte maxLength:1];
+
+ [[[self.mockSocket stub] andDo:^(NSInvocation *invocation) {
+ XCTAssertTrue(isSafeToDisconnectSocket,
+ @"Should not disconnect socket now");
+ }] disconnect];
+ XCTestExpectation *shouldAcceptVersionExpectation =
+ [self expectationWithDescription:@"Socket should accept version"];
+ dispatch_time_t delay =
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
+ dispatch_after(delay, dispatch_get_main_queue(), ^{
+ XCTAssertTrue(self.socket.isVersionReceived);
+ [shouldAcceptVersionExpectation fulfill];
+ });
+
+ [self waitForExpectationsWithTimeout:3.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+}
+
+- (void)testReceivingDataMessage {
+ [self createAndConnectSocketWithBufferSize:61];
+ [self writeVersionToOutStream];
+ GtalkDataMessageStanza *message = [[GtalkDataMessageStanza alloc] init];
+ [message setCategory:@"socket-test-category"];
+ [message setFrom:@"socket-test-from"];
+ FIRMessagingSetLastStreamId(message, 2);
+ FIRMessagingSetRmq2Id(message, @"socket-test-rmq");
+
+ XCTestExpectation *dataExpectation = [self
+ expectationWithDescription:@"FIRMessaging socket should receive data message"];
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ [self.socket sendData:[message data]
+ withTag:kFIRMessagingProtoTagDataMessageStanza
+ rmqId:FIRMessagingGetRmq2Id(message)];
+ });
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ XCTAssertEqual(self.protoTagReceived, kFIRMessagingProtoTagDataMessageStanza);
+ [dataExpectation fulfill];
+ });
+
+ [self waitForExpectationsWithTimeout:5.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+}
+
+#pragma mark - Writing
+
+- (void)testLoginRequest {
+ [self createAndConnectSocketWithBufferSize:99];
+
+ XCTestExpectation *loginExpectation =
+ [self expectationWithDescription:@"Socket send valid login proto"];
+ [self writeVersionToOutStream];
+ GtalkLoginRequest *loginRequest =
+ [FIRMessagingConnection loginRequestWithToken:@"gcmtoken" authID:@"gcmauthid"];
+ FIRMessagingSetLastStreamId(loginRequest, 1);
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ [self.socket sendData:[loginRequest data]
+ withTag:FIRMessagingGetTagForProto(loginRequest)
+ rmqId:FIRMessagingGetRmq2Id(loginRequest)];
+ });
+
+ dispatch_time_t delay =
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
+ dispatch_after(delay, dispatch_get_main_queue(), ^{
+ XCTAssertTrue(self.socket.isVersionReceived);
+ XCTAssertEqual(self.protoTagReceived, kFIRMessagingProtoTagLoginRequest);
+ [loginExpectation fulfill];
+ });
+
+ [self waitForExpectationsWithTimeout:6.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+}
+
+- (void)testSendingImproperData {
+ [self createAndConnectSocketWithBufferSize:124];
+ [self writeVersionToOutStream];
+
+ NSString *randomString = @"some random data string";
+ NSData *randomData = [randomString dataUsingEncoding:NSUTF8StringEncoding];
+
+ XCTestExpectation *parseErrorExpectation =
+ [self expectationWithDescription:@"Sending improper data results in a parse error"];
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ [self.socket sendData:randomData withTag:3 rmqId:@"some-random-rmq-id"];
+ });
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.5 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ if (self.protoParseError != nil) {
+ [parseErrorExpectation fulfill];
+ }
+ });
+
+ [self waitForExpectationsWithTimeout:3.0 handler:nil];
+}
+
+- (void)testSendingDataWithImproperTag {
+ [self createAndConnectSocketWithBufferSize:124];
+ [self writeVersionToOutStream];
+ const char dataString[] = { 0x02, 0x02, 0x11, 0x11, 0x11, 0x11 }; // tag 10, random data
+ NSData *randomData = [NSData dataWithBytes:dataString length:6];
+
+ // Create an expectation for a method which should not be invoked during this test.
+ // This is required to allow us to wait for the socket stream to be read and
+ // processed by FIRMessagingSecureSocket
+ OCMExpect([self.mockSocket disconnect]);
+
+ NSTimeInterval sendDelay = 2.0;
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(sendDelay * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ [self.socket sendData:randomData withTag:10 rmqId:@"some-random-rmq-id"];
+ });
+
+ @try {
+ // While waiting to verify this call, an exception should be thrown
+ // trying to parse the random data in our delegate.
+ // Wait slightly longer than the sendDelay, to allow for the parsing
+ OCMVerifyAllWithDelay(self.mockSocket, sendDelay+0.25);
+ XCTFail(@"Invalid data being read should have thrown an exception.");
+ }
+ @catch (NSException *exception) {
+ XCTAssertNotNil(exception);
+ }
+ @finally { }
+}
+
+- (void)testDisconnect {
+ [self createAndConnectSocketWithBufferSize:1];
+ [self writeVersionToOutStream];
+ // version read and written let's disconnect
+ XCTestExpectation *disconnectExpectation =
+ [self expectationWithDescription:@"socket should disconnect properly"];
+ self.disconnectHandler = ^{
+ [disconnectExpectation fulfill];
+ };
+
+ [self.socket disconnect];
+
+ [self waitForExpectationsWithTimeout:5.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+
+ XCTAssertNil(self.socket.inStream);
+ XCTAssertNil(self.socket.outStream);
+ XCTAssertEqual(self.socket.state, kFIRMessagingSecureSocketClosed);
+}
+
+- (void)testSocketOpening {
+ XCTestExpectation *openSocketExpectation =
+ [self expectationWithDescription:@"Socket should open properly"];
+ self.connectHandler = ^{
+ [openSocketExpectation fulfill];
+ };
+ [self createAndConnectSocketWithBufferSize:1];
+ [self writeVersionToOutStream];
+
+ [self waitForExpectationsWithTimeout:10.0
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+
+ XCTAssertTrue(self.socket.isInStreamOpen);
+ XCTAssertTrue(self.socket.isOutStreamOpen);
+}
+
+#pragma mark - FIRMessagingSecureSocketDelegate protocol
+
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didReceiveData:(NSData *)data
+ withTag:(int8_t)tag {
+ NSError *error;
+ GPBMessage *proto =
+ [FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data
+ error:&error];
+ self.protoParseError = error;
+ self.protoReceived = proto;
+ self.protoTagReceived = tag;
+}
+
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didSendProtoWithTag:(int8_t)tag
+ rmqId:(NSString *)rmqId {
+ // do nothing
+}
+
+- (void)secureSocketDidConnect:(FIRMessagingSecureSocket *)socket {
+ if (self.connectHandler) {
+ self.connectHandler();
+ }
+}
+
+- (void)didDisconnectWithSecureSocket:(FIRMessagingSecureSocket *)socket {
+ if (self.disconnectHandler) {
+ self.disconnectHandler();
+ }
+}
+
+#pragma mark - Private Helpers
+
+- (void)createAndConnectSocketWithBufferSize:(uint8_t)bufferSize {
+ self.socket = [[FIRMessagingFakeSocket alloc] initWithBufferSize:bufferSize];
+ self.mockSocket = OCMPartialMock(self.socket);
+ self.socket.delegate = self;
+
+ [self.socket connectToHost:@"localhost"
+ port:6234
+ onRunLoop:[NSRunLoop mainRunLoop]];
+}
+
+- (void)writeVersionToOutStream {
+ uint8_t versionByte = 40;
+ [self.socket.outStream write:&versionByte maxLength:1];
+ // don't resend the version
+ self.socket.isVersionSent = YES;
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingServiceTest.m b/Example/Messaging/Tests/FIRMessagingServiceTest.m
new file mode 100644
index 0000000..bdbc0c2
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingServiceTest.m
@@ -0,0 +1,288 @@
+/*
+ * 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 XCTest;
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingPubSub.h"
+#import "FIRMessagingTopicsCommon.h"
+#import "InternalHeaders/FIRMessagingInternalUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+@interface FIRMessaging () <FIRMessagingClientDelegate>
+
+@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
+@property(nonatomic, readwrite, strong) FIRMessagingPubSub *pubsub;
+@property(nonatomic, readwrite, strong) NSString *defaultFcmToken;
+
+@end
+
+@interface FIRMessagingPubSub ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
+
+@end
+
+
+@interface FIRMessagingServiceTest : XCTestCase
+
+@end
+
+@implementation FIRMessagingServiceTest
+
+- (void)testSubscribe {
+ id mockClient = OCMClassMock([FIRMessagingClient class]);
+ FIRMessaging *service = [FIRMessaging messaging];
+ [service setClient:mockClient];
+ [service.pubsub setClient:mockClient];
+
+ XCTestExpectation *subscribeExpectation =
+ [self expectationWithDescription:@"Should call subscribe on FIRMessagingClient"];
+ NSString *token = @"abcdefghijklmn";
+ NSString *topic = @"/topics/some-random-topic";
+
+ [[[mockClient stub]
+ andDo:^(NSInvocation *invocation) {
+ [subscribeExpectation fulfill];
+ }]
+ updateSubscriptionWithToken:token
+ topic:topic
+ options:OCMOCK_ANY
+ shouldDelete:NO
+ handler:OCMOCK_ANY];
+
+ [service.pubsub subscribeWithToken:token
+ topic:topic
+ options:nil
+ handler:^(FIRMessagingTopicOperationResult result, NSError *error) {
+ // not a nil block
+ }];
+
+ // should call updateSubscription
+ [self waitForExpectationsWithTimeout:0.1
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ [mockClient verify];
+ }];
+}
+
+- (void)testUnsubscribe {
+ id mockClient = OCMClassMock([FIRMessagingClient class]);
+ FIRMessaging *messaging = [FIRMessaging messaging];
+ [messaging setClient:mockClient];
+ [messaging.pubsub setClient:mockClient];
+
+ XCTestExpectation *subscribeExpectation =
+ [self expectationWithDescription:@"Should call unsubscribe on FIRMessagingClient"];
+
+ NSString *token = @"abcdefghijklmn";
+ NSString *topic = @"/topics/some-random-topic";
+
+ [[[mockClient stub] andDo:^(NSInvocation *invocation) {
+ [subscribeExpectation fulfill];
+ }]
+ updateSubscriptionWithToken:[OCMArg isEqual:token]
+ topic:[OCMArg isEqual:topic]
+ options:[OCMArg checkWithBlock:^BOOL(id obj) {
+ if ([obj isKindOfClass:[NSDictionary class]]) {
+ return [(NSDictionary *)obj count] == 0;
+ }
+ return NO;
+ }]
+ shouldDelete:YES
+ handler:OCMOCK_ANY];
+
+ [messaging.pubsub unsubscribeWithToken:token
+ topic:topic
+ options:nil
+ handler:^(FIRMessagingTopicOperationResult result, NSError *error){
+
+ }];
+
+ // should call updateSubscription
+ [self waitForExpectationsWithTimeout:0.1
+ handler:^(NSError *error) {
+ XCTAssertNil(error);
+ [mockClient verify];
+ }];
+}
+
+/**
+ * Test using PubSub without explicitly starting FIRMessagingService.
+ */
+- (void)testSubscribeWithoutStart {
+ [[[FIRMessaging messaging] pubsub] subscribeWithToken:@"abcdef1234"
+ topic:@"/topics/hello-world"
+ options:nil
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqual(kFIRMessagingErrorCodePubSubFIRMessagingNotSetup,
+ error.code);
+ }];
+}
+
+- (void)testSubscribeWithInvalidToken {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+
+ XCTestExpectation *exceptionExpectation =
+ [self expectationWithDescription:@"Should throw exception for invalid token"];
+ @try {
+ [messaging.pubsub subscribeWithToken:@""
+ topic:@"/topics/hello-world"
+ options:nil
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ XCTFail(@"Should not invoke the handler");
+ }];
+ }
+ @catch (NSException *exception) {
+ [exceptionExpectation fulfill];
+ }
+ @finally {
+ [self waitForExpectationsWithTimeout:0.1 handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+ }
+}
+
+- (void)testUnsubscribeWithInvalidTopic {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+
+ XCTestExpectation *exceptionExpectation =
+ [self expectationWithDescription:@"Should throw exception for invalid token"];
+ @try {
+ [messaging.pubsub unsubscribeWithToken:@"abcdef1234"
+ topic:nil
+ options:nil
+ handler:
+ ^(FIRMessagingTopicOperationResult result, NSError *error) {
+ XCTFail(@"Should not invoke the handler");
+ }];
+ }
+ @catch (NSException *exception) {
+ [exceptionExpectation fulfill];
+ }
+ @finally {
+ [self waitForExpectationsWithTimeout:0.1 handler:^(NSError *error) {
+ XCTAssertNil(error);
+ }];
+ }
+}
+
+- (void)testSubscribeWithNoTopicPrefix {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+ FIRMessagingPubSub *pubSub = messaging.pubsub;
+ id mockPubSub = OCMClassMock([FIRMessagingPubSub class]);
+
+ NSString *topicName = @"topicWithoutPrefix";
+ NSString *topicNameWithPrefix = [FIRMessagingPubSub addPrefixToTopic:topicName];
+ messaging.pubsub = mockPubSub;
+ messaging.defaultFcmToken = @"fake-default-token";
+ OCMExpect([messaging.pubsub subscribeToTopic:[OCMArg isEqual:topicNameWithPrefix]]);
+ [messaging subscribeToTopic:topicName];
+ OCMVerifyAll(mockPubSub);
+ // Need to swap back since it's a singleton and hence will live beyond the scope of this test.
+ messaging.pubsub = pubSub;
+}
+
+- (void)testSubscribeWithTopicPrefix {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+ FIRMessagingPubSub *pubSub = messaging.pubsub;
+ id mockPubSub = OCMClassMock([FIRMessagingPubSub class]);
+
+ NSString *topicName = @"/topics/topicWithoutPrefix";
+ messaging.pubsub = mockPubSub;
+ messaging.defaultFcmToken = @"fake-default-token";
+ OCMExpect([messaging.pubsub subscribeToTopic:[OCMArg isEqual:topicName]]);
+ [messaging subscribeToTopic:topicName];
+ OCMVerifyAll(mockPubSub);
+ // Need to swap back since it's a singleton and hence will live beyond the scope of this test.
+ messaging.pubsub = pubSub;
+}
+
+- (void)testUnsubscribeWithNoTopicPrefix {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+ FIRMessagingPubSub *pubSub = messaging.pubsub;
+ id mockPubSub = OCMClassMock([FIRMessagingPubSub class]);
+
+ NSString *topicName = @"topicWithoutPrefix";
+ NSString *topicNameWithPrefix = [FIRMessagingPubSub addPrefixToTopic:topicName];
+ messaging.pubsub = mockPubSub;
+ messaging.defaultFcmToken = @"fake-default-token";
+ OCMExpect([messaging.pubsub unsubscribeFromTopic:[OCMArg isEqual:topicNameWithPrefix]]);
+ [messaging unsubscribeFromTopic:topicName];
+ OCMVerifyAll(mockPubSub);
+ // Need to swap back since it's a singleton and hence will live beyond the scope of this test.
+ messaging.pubsub = pubSub;
+}
+
+- (void)testUnsubscribeWithTopicPrefix {
+ FIRMessaging *messaging = [FIRMessaging messaging];
+ FIRMessagingPubSub *pubSub = messaging.pubsub;
+ id mockPubSub = OCMClassMock([FIRMessagingPubSub class]);
+
+ NSString *topicName = @"/topics/topicWithPrefix";
+ messaging.pubsub = mockPubSub;
+ messaging.defaultFcmToken = @"fake-default-token";
+ OCMExpect([messaging.pubsub unsubscribeFromTopic:[OCMArg isEqual:topicName]]);
+ [messaging unsubscribeFromTopic:topicName];
+ OCMVerifyAll(mockPubSub);
+ // Need to swap back since it's a singleton and hence will live beyond the scope of this test.
+ messaging.pubsub = pubSub;
+}
+
+- (void)testFIRMessagingSDKVersionInFIRMessagingService {
+ Class versionClass = NSClassFromString(kFIRMessagingSDKClassString);
+ SEL versionSelector = NSSelectorFromString(kFIRMessagingSDKVersionSelectorString);
+ if ([versionClass respondsToSelector:versionSelector]) {
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ id versionString = [versionClass performSelector:versionSelector];
+#pragma clang diagnostic pop
+
+ XCTAssertTrue([versionString isKindOfClass:[NSString class]]);
+ } else {
+ XCTFail("%@ does not respond to selector %@",
+ kFIRMessagingSDKClassString, kFIRMessagingSDKVersionSelectorString);
+ }
+}
+
+- (void)testFIRMessagingSDKLocaleInFIRMessagingService {
+ Class klass = NSClassFromString(kFIRMessagingSDKClassString);
+ SEL localeSelector = NSSelectorFromString(kFIRMessagingSDKLocaleSelectorString);
+ if ([klass respondsToSelector:localeSelector]) {
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ id locale = [klass performSelector:localeSelector];
+#pragma clang diagnostic pop
+
+ XCTAssertTrue([locale isKindOfClass:[NSString class]]);
+ XCTAssertNotNil(locale);
+ } else {
+ XCTFail("%@ does not respond to selector %@",
+ kFIRMessagingSDKClassString, kFIRMessagingSDKLocaleSelectorString);
+ }
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingSyncMessageManagerTest.m b/Example/Messaging/Tests/FIRMessagingSyncMessageManagerTest.m
new file mode 100644
index 0000000..e40e1f2
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingSyncMessageManagerTest.m
@@ -0,0 +1,256 @@
+/*
+ * 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/XCTest.h>
+
+#import "FIRMessagingPersistentSyncMessage.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSyncMessageManager.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRMessagingConstants.h"
+
+static NSString *const kRmqSqliteFilename = @"rmq-sync-manager-test";
+
+@interface FIRMessagingSyncMessageManagerTest : XCTestCase
+
+@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmqManager;
+@property(nonatomic, readwrite, strong) FIRMessagingSyncMessageManager *syncMessageManager;
+
+@end
+
+@implementation FIRMessagingSyncMessageManagerTest
+
+- (void)setUp {
+ [super setUp];
+ // Make sure the db state is clean before we begin.
+ [FIRMessagingRmqManager removeDatabaseWithName:kRmqSqliteFilename];
+ self.rmqManager = [[FIRMessagingRmqManager alloc] initWithDatabaseName:kRmqSqliteFilename];
+ self.syncMessageManager = [[FIRMessagingSyncMessageManager alloc] initWithRmqManager:self.rmqManager];
+}
+
+- (void)tearDown {
+ [[self.rmqManager class] removeDatabaseWithName:kRmqSqliteFilename];
+ [super tearDown];
+}
+
+/**
+ * Test receiving a new sync message via APNS should be added to SYNC_RMQ.
+ */
+- (void)testNewAPNSMessage {
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 86400; // 1 day in future
+
+ NSDictionary *oldMessage = @{
+ kFIRMessagingMessageIDKey : @"fake-rmq-1",
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:oldMessage]);
+
+ NSDictionary *newMessage = @{
+ kFIRMessagingMessageIDKey : @"fake-rmq-2",
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:newMessage]);
+}
+
+/**
+ * Test receiving a new sync message via MCS should be added to SYNC_RMQ.
+ */
+- (void)testNewMCSMessage {
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 86400; // 1 day in future
+ NSDictionary *oldMessage = @{
+ kFIRMessagingMessageIDKey : @"fake-rmq-1",
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveMCSSyncMessage:oldMessage]);
+
+ NSDictionary *newMessage = @{
+ kFIRMessagingMessageIDKey : @"fake-rmq-2",
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:newMessage]);
+}
+
+/**
+ * Test receiving a duplicate message via APNS.
+ */
+- (void)testDuplicateAPNSMessage {
+ NSString *messageID = @"fake-rmq-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 86400; // 1 day in future
+ NSDictionary *newMessage = @{
+ kFIRMessagingMessageIDKey : messageID,
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:newMessage]);
+
+ // The message is a duplicate
+ XCTAssertTrue([self.syncMessageManager didReceiveAPNSSyncMessage:newMessage]);
+
+ FIRMessagingPersistentSyncMessage *persistentMessage =
+ [self.rmqManager querySyncMessageWithRmqID:messageID];
+ XCTAssertTrue(persistentMessage.apnsReceived);
+ XCTAssertFalse(persistentMessage.mcsReceived);
+}
+
+/**
+ * Test receiving a duplicate message via MCS.
+ */
+- (void)testDuplicateMCSMessage {
+ NSString *messageID = @"fake-rmq-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 86400; // 1 day in future
+ NSDictionary *newMessage = @{
+ kFIRMessagingMessageIDKey : messageID,
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+
+ XCTAssertFalse([self.syncMessageManager didReceiveMCSSyncMessage:newMessage]);
+
+ // The message is a duplicate
+ XCTAssertTrue([self.syncMessageManager didReceiveMCSSyncMessage:newMessage]);
+
+ FIRMessagingPersistentSyncMessage *persistentMessage =
+ [self.rmqManager querySyncMessageWithRmqID:messageID];
+ XCTAssertFalse(persistentMessage.apnsReceived);
+ XCTAssertTrue(persistentMessage.mcsReceived);
+}
+
+/**
+ * Test receiving a sync message both via APNS and MCS.
+ */
+- (void)testMessageReceivedBothViaAPNSAndMCS {
+ NSString *messageID = @"fake-rmq-1";
+ int64_t expirationTime = FIRMessagingCurrentTimestampInSeconds() + 86400; // 1 day in future
+ NSDictionary *newMessage = @{
+ kFIRMessagingMessageIDKey : messageID,
+ kFIRMessagingMessageSyncViaMCSKey : @(expirationTime),
+ @"hello" : @"world",
+ };
+
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:newMessage]);
+ // Duplicate of the above received APNS message
+ XCTAssertTrue([self.syncMessageManager didReceiveMCSSyncMessage:newMessage]);
+
+ // Since we've received both APNS and MCS messages we should have deleted them from SYNC_RMQ
+ FIRMessagingPersistentSyncMessage *persistentMessage =
+ [self.rmqManager querySyncMessageWithRmqID:messageID];
+ XCTAssertNil(persistentMessage);
+}
+
+- (void)testDeletingExpiredMessages {
+ NSString *unexpiredMessageID = @"fake-not-expired-rmqID";
+ int64_t futureExpirationTime = 86400; // 1 day in future
+ NSDictionary *unexpiredMessage = @{
+ kFIRMessagingMessageIDKey : unexpiredMessageID,
+ kFIRMessagingMessageSyncMessageTTLKey : @(futureExpirationTime),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:unexpiredMessage]);
+
+ NSString *expiredMessageID = @"fake-expired-rmqID";
+ int64_t past = -86400; // 1 day in past
+ NSDictionary *expiredMessage = @{
+ kFIRMessagingMessageIDKey : expiredMessageID,
+ kFIRMessagingMessageSyncMessageTTLKey : @(past),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:expiredMessage]);
+
+ NSString *noTTLMessageID = @"no-ttl-rmqID"; // no TTL specified should be 4 weeks
+ NSDictionary *noTTLMessage = @{
+ kFIRMessagingMessageIDKey : noTTLMessageID,
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:noTTLMessage]);
+
+ [self.syncMessageManager removeExpiredSyncMessages];
+
+ XCTAssertNotNil([self.rmqManager querySyncMessageWithRmqID:unexpiredMessageID]);
+ XCTAssertNil([self.rmqManager querySyncMessageWithRmqID:expiredMessageID]);
+ XCTAssertNotNil([self.rmqManager querySyncMessageWithRmqID:noTTLMessageID]);
+}
+
+- (void)testDeleteFinishedMessages {
+ NSString *unexpiredMessageID = @"fake-not-expired-rmqID";
+ int64_t futureExpirationTime = 86400; // 1 day in future
+ NSDictionary *unexpiredMessage = @{
+ kFIRMessagingMessageIDKey : unexpiredMessageID,
+ kFIRMessagingMessageSyncMessageTTLKey : @(futureExpirationTime),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:unexpiredMessage]);
+
+ NSString *noTTLMessageID = @"no-ttl-rmqID"; // no TTL specified should be 4 weeks
+ NSDictionary *noTTLMessage = @{
+ kFIRMessagingMessageIDKey : noTTLMessageID,
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:noTTLMessage]);
+
+ // Mark the no-TTL message as received via MCS too
+ XCTAssertTrue([self.rmqManager updateSyncMessageViaMCSWithRmqID:noTTLMessageID error:nil]);
+
+ [self.syncMessageManager removeExpiredSyncMessages];
+
+ XCTAssertNotNil([self.rmqManager querySyncMessageWithRmqID:unexpiredMessageID]);
+ XCTAssertNil([self.rmqManager querySyncMessageWithRmqID:noTTLMessageID]);
+}
+
+- (void)testDeleteFinishedAndExpiredMessages {
+ NSString *unexpiredMessageID = @"fake-not-expired-rmqID";
+ int64_t futureExpirationTime = 86400; // 1 day in future
+ NSDictionary *unexpiredMessage = @{
+ kFIRMessagingMessageIDKey : unexpiredMessageID,
+ kFIRMessagingMessageSyncMessageTTLKey : @(futureExpirationTime),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:unexpiredMessage]);
+
+ NSString *expiredMessageID = @"fake-expired-rmqID";
+ int64_t past = -86400; // 1 day in past
+ NSDictionary *expiredMessage = @{
+ kFIRMessagingMessageIDKey : expiredMessageID,
+ kFIRMessagingMessageSyncMessageTTLKey : @(past),
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:expiredMessage]);
+
+ NSString *noTTLMessageID = @"no-ttl-rmqID"; // no TTL specified should be 4 weeks
+ NSDictionary *noTTLMessage = @{
+ kFIRMessagingMessageIDKey : noTTLMessageID,
+ @"hello" : @"world",
+ };
+ XCTAssertFalse([self.syncMessageManager didReceiveAPNSSyncMessage:noTTLMessage]);
+
+ // Mark the no-TTL message as received via MCS too
+ XCTAssertTrue([self.rmqManager updateSyncMessageViaMCSWithRmqID:noTTLMessageID error:nil]);
+
+ // Remove expired or finished sync messages.
+ [self.syncMessageManager removeExpiredSyncMessages];
+
+ XCTAssertNotNil([self.rmqManager querySyncMessageWithRmqID:unexpiredMessageID]);
+ XCTAssertNil([self.rmqManager querySyncMessageWithRmqID:expiredMessageID]);
+ XCTAssertNil([self.rmqManager querySyncMessageWithRmqID:noTTLMessageID]);
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingTest.m b/Example/Messaging/Tests/FIRMessagingTest.m
new file mode 100644
index 0000000..3d7c95f
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingTest.m
@@ -0,0 +1,214 @@
+/*
+ * 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;
+
+#import <OCMock/OCMock.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingInstanceIDProxy.h"
+
+extern NSString *const kFIRMessagingFCMTokenFetchAPNSOption;
+
+@interface FIRMessaging ()
+
+@property(nonatomic, readwrite, strong) NSString *defaultFcmToken;
+@property(nonatomic, readwrite, strong) NSData *apnsTokenData;
+@property(nonatomic, readwrite, strong) FIRMessagingInstanceIDProxy *instanceIDProxy;
+
+- (instancetype)initWithConfig:(FIRMessagingConfig *)config;
+// Direct Channel Methods
+- (void)updateAutomaticClientConnection;
+- (BOOL)shouldBeConnectedAutomatically;
+
+@end
+
+@interface FIRMessagingTest : XCTestCase
+
+@property(nonatomic, readonly, strong) FIRMessaging *messaging;
+@property(nonatomic, readwrite, strong) id mockMessaging;
+@property(nonatomic, readwrite, strong) id mockInstanceIDProxy;
+
+@end
+
+@implementation FIRMessagingTest
+
+- (void)setUp {
+ [super setUp];
+ FIRMessagingConfig *config = [FIRMessagingConfig defaultConfig];
+ _messaging = [[FIRMessaging alloc] initWithConfig:config];
+ _mockMessaging = OCMPartialMock(self.messaging);
+ _mockInstanceIDProxy = OCMPartialMock(self.messaging.instanceIDProxy);
+ self.messaging.instanceIDProxy = _mockInstanceIDProxy;
+}
+
+- (void)tearDown {
+ _messaging = nil;
+ _mockMessaging = nil;
+ [super tearDown];
+}
+
+#pragma mark - Direct Channel Establishment Testing
+
+// Should connect with valid token and application in foreground
+- (void)testDoesAutomaticallyConnectIfTokenAvailableAndForegrounded {
+ // Disable actually attempting a connection
+ [[[_mockMessaging stub] andDo:^(NSInvocation *invocation) {
+ // Doing nothing on purpose, when -updateAutomaticClientConnection is called
+ }] updateAutomaticClientConnection];
+ // Set direct channel to be established after disabling connection attempt
+ self.messaging.shouldEstablishDirectChannel = YES;
+ // Set a "valid" token (i.e. not nil or empty)
+ self.messaging.defaultFcmToken = @"1234567";
+ // Swizzle application state to return UIApplicationStateActive
+ UIApplication *app = [UIApplication sharedApplication];
+ id mockApp = OCMPartialMock(app);
+ [[[mockApp stub] andReturnValue:@(UIApplicationStateActive)] applicationState];
+ BOOL shouldBeConnected = [_mockMessaging shouldBeConnectedAutomatically];
+ XCTAssertTrue(shouldBeConnected);
+}
+
+// Should not connect if application is active, but token is empty
+- (void)testDoesNotAutomaticallyConnectIfTokenIsEmpty {
+ // Disable actually attempting a connection
+ [[[_mockMessaging stub] andDo:^(NSInvocation *invocation) {
+ // Doing nothing on purpose, when -updateAutomaticClientConnection is called
+ }] updateAutomaticClientConnection];
+ // Set direct channel to be established after disabling connection attempt
+ self.messaging.shouldEstablishDirectChannel = YES;
+ // By default, there should be no fcmToken
+ // Swizzle application state to return UIApplicationStateActive
+ UIApplication *app = [UIApplication sharedApplication];
+ id mockApp = OCMPartialMock(app);
+ [[[mockApp stub] andReturnValue:@(UIApplicationStateActive)] applicationState];
+ BOOL shouldBeConnected = [_mockMessaging shouldBeConnectedAutomatically];
+ XCTAssertFalse(shouldBeConnected);
+}
+
+// Should not connect if token valid but application isn't active
+- (void)testDoesNotAutomaticallyConnectIfApplicationNotActive {
+ // Disable actually attempting a connection
+ [[[_mockMessaging stub] andDo:^(NSInvocation *invocation) {
+ // Doing nothing on purpose, when -updateAutomaticClientConnection is called
+ }] updateAutomaticClientConnection];
+ // Set direct channel to be established after disabling connection attempt
+ self.messaging.shouldEstablishDirectChannel = YES;
+ // Set a "valid" token (i.e. not nil or empty)
+ self.messaging.defaultFcmToken = @"abcd1234";
+ // Swizzle application state to return UIApplicationStateActive
+ UIApplication *app = [UIApplication sharedApplication];
+ id mockApp = OCMPartialMock(app);
+ [[[mockApp stub] andReturnValue:@(UIApplicationStateBackground)] applicationState];
+ BOOL shouldBeConnected = [_mockMessaging shouldBeConnectedAutomatically];
+ XCTAssertFalse(shouldBeConnected);
+}
+
+#pragma mark - FCM Token Fetching and Deleting
+
+#ifdef NEED_WORKAROUND_FOR_PRIVATE_OCMOCK_getArgumentAtIndexAsObject
+- (void)testAPNSTokenIncludedInOptionsIfAvailableDuringTokenFetch {
+ self.messaging.apnsTokenData =
+ [@"PRETENDING_TO_BE_A_DEVICE_TOKEN" dataUsingEncoding:NSUTF8StringEncoding];
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Included APNS Token data in options dict."];
+ // Inspect the 'options' dictionary to tell whether our expectation was fulfilled
+ [[[self.mockInstanceIDProxy stub] andDo:^(NSInvocation *invocation) {
+ // Calling getArgument:atIndex: directly leads to an EXC_BAD_ACCESS; use OCMock's wrapper.
+ NSDictionary *options = [invocation getArgumentAtIndexAsObject:4];
+ if (options[@"apns_token"] != nil) {
+ [expectation fulfill];
+ }
+ }] tokenWithAuthorizedEntity:OCMOCK_ANY scope:OCMOCK_ANY options:OCMOCK_ANY handler:OCMOCK_ANY];
+ [self.messaging retrieveFCMTokenForSenderID:@"123456"
+ completion:^(NSString * _Nullable FCMToken,
+ NSError * _Nullable error) {}];
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+
+- (void)testAPNSTokenNotIncludedIfUnavailableDuringTokenFetch {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Included APNS Token data not included in options dict."];
+ // Inspect the 'options' dictionary to tell whether our expectation was fulfilled
+ [[[self.mockInstanceIDProxy stub] andDo:^(NSInvocation *invocation) {
+ // Calling getArgument:atIndex: directly leads to an EXC_BAD_ACCESS; use OCMock's wrapper.
+ NSDictionary *options = [invocation getArgumentAtIndexAsObject:4];
+ if (options[@"apns_token"] == nil) {
+ [expectation fulfill];
+ }
+ }] tokenWithAuthorizedEntity:OCMOCK_ANY scope:OCMOCK_ANY options:OCMOCK_ANY handler:OCMOCK_ANY];
+ [self.messaging retrieveFCMTokenForSenderID:@"123456"
+ completion:^(NSString * _Nullable FCMToken,
+ NSError * _Nullable error) {}];
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+#endif
+
+- (void)testReturnsErrorWhenFetchingTokenWithoutSenderID {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Returned an error fetching token without Sender ID"];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+ [self.messaging retrieveFCMTokenForSenderID:nil
+ completion:
+ ^(NSString * _Nullable FCMToken, NSError * _Nullable error) {
+ if (error != nil) {
+ [expectation fulfill];
+ }
+ }];
+#pragma clang diagnostic pop
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+
+- (void)testReturnsErrorWhenFetchingTokenWithEmptySenderID {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Returned an error fetching token with empty Sender ID"];
+ [self.messaging retrieveFCMTokenForSenderID:@""
+ completion:
+ ^(NSString * _Nullable FCMToken, NSError * _Nullable error) {
+ if (error != nil) {
+ [expectation fulfill];
+ }
+ }];
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+
+- (void)testReturnsErrorWhenDeletingTokenWithoutSenderID {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Returned an error deleting token without Sender ID"];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+ [self.messaging deleteFCMTokenForSenderID:nil completion:^(NSError * _Nullable error) {
+ if (error != nil) {
+ [expectation fulfill];
+ }
+ }];
+#pragma clang diagnostic pop
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+
+- (void)testReturnsErrorWhenDeletingTokenWithEmptySenderID {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Returned an error deleting token with empty Sender ID"];
+ [self.messaging deleteFCMTokenForSenderID:@"" completion:^(NSError * _Nullable error) {
+ if (error != nil) {
+ [expectation fulfill];
+ }
+ }];
+ [self waitForExpectationsWithTimeout:0.1 handler:nil];
+}
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.h b/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.h
new file mode 100644
index 0000000..0bc6010
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.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 <Foundation/Foundation.h>
+
+@interface FIRMessagingTestNotificationUtilities : NSObject
+
++ (NSMutableDictionary *)createBasicNotificationWithUniqueMessageID;
+
+@end
diff --git a/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.m b/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.m
new file mode 100644
index 0000000..43842ee
--- /dev/null
+++ b/Example/Messaging/Tests/FIRMessagingTestNotificationUtilities.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 "FIRMessagingTestNotificationUtilities.h"
+
+#import "FIRMessagingConstants.h"
+
+@implementation FIRMessagingTestNotificationUtilities
+
++ (NSMutableDictionary *)createBasicNotificationWithUniqueMessageID {
+ NSMutableDictionary *notification = [NSMutableDictionary dictionary];
+ // Always generate a unique message id
+ notification[kFIRMessagingMessageIDKey] =
+ [NSString stringWithFormat:@"%@", @([NSDate date].timeIntervalSince1970)];
+ return notification;
+}
+
+@end
diff --git a/Example/Messaging/Tests/Info.plist b/Example/Messaging/Tests/Info.plist
new file mode 100644
index 0000000..ba72822
--- /dev/null
+++ b/Example/Messaging/Tests/Info.plist
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Example/Podfile b/Example/Podfile
new file mode 100644
index 0000000..50bc37e
--- /dev/null
+++ b/Example/Podfile
@@ -0,0 +1,61 @@
+
+use_frameworks!
+platform :ios, '8.0'
+
+target 'Core_Example' do
+ pod 'FirebaseDev/Core', :path => '../'
+
+ target 'Core_Tests' do
+ inherit! :search_paths
+ pod 'OCMock'
+ end
+end
+
+target 'Auth_Example' do
+ pod 'FirebaseDev/Auth', :path => '../'
+
+ target 'Auth_Tests' do
+ inherit! :search_paths
+ pod 'OCMock'
+ end
+end
+
+target 'Database_Example' do
+ pod 'FirebaseDev/Database', :path => '../'
+
+ target 'Database_Tests' do
+ inherit! :search_paths
+ pod 'OCMock'
+ end
+
+ target 'Database_IntegrationTests' do
+ inherit! :search_paths
+ pod 'OCMock'
+ end
+end
+
+target 'Messaging_Example' do
+ pod 'FirebaseDev/Messaging', :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' do
+ inherit! :search_paths
+ pod 'OCMock'
+ end
+end
+
+target 'Storage_Example' do
+ pod 'FirebaseDev/Storage', :path => '../'
+
+ target 'Storage_Tests' do
+ inherit! :search_paths
+ pod 'OCMock'
+ end
+
+ target 'Storage_IntegrationTests' do
+ inherit! :search_paths
+ pod 'OCMock'
+ end
+end
diff --git a/Example/Shared/FIRSampleAppUtilities.h b/Example/Shared/FIRSampleAppUtilities.h
new file mode 100644
index 0000000..891c2b6
--- /dev/null
+++ b/Example/Shared/FIRSampleAppUtilities.h
@@ -0,0 +1,26 @@
+/*
+ * 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/UIKit.h>
+
+NS_SWIFT_NAME(SampleAppUtilities)
+@interface FIRSampleAppUtilities : NSObject
+
++ (BOOL)appContainsRealServiceInfoPlist;
++ (void)presentAlertForInvalidServiceInfoPlistFromViewController:(UIViewController *)
+ viewController NS_SWIFT_NAME(presentAlertForInvalidServiceInfoPlistFrom(_:));
+
+@end
diff --git a/Example/Shared/FIRSampleAppUtilities.m b/Example/Shared/FIRSampleAppUtilities.m
new file mode 100644
index 0000000..a0a4794
--- /dev/null
+++ b/Example/Shared/FIRSampleAppUtilities.m
@@ -0,0 +1,113 @@
+/*
+ * 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 "FIRSampleAppUtilities.h"
+
+#import <SafariServices/SafariServices.h>
+
+// Plist file name.
+NSString *const kServiceInfoFileName = @"GoogleService-Info";
+// Plist file type.
+NSString *const kServiceInfoFileType = @"plist";
+// GOOGLE_APP_ID key
+NSString *const kGoogleAppIDPlistKey = @"GOOGLE_APP_ID";
+// Dummy plist GOOGLE_APP_ID
+NSString *const kDummyGoogleAppID = @"1:123:ios:123abc";
+// Github Repo URL String
+NSString *const kGithubRepoURLString = @"https://github.com/firebase/firebase-ios-sdk/";
+// Alert contents
+NSString *const kInvalidPlistAlertTitle = @"GoogleService-Info.plist";
+NSString *const kInvalidPlistAlertMessage = @"This sample app needs to be updated with a valid "
+ @"GoogleService-Info.plist file in order to configure "
+ @"Firebase.\n\n"
+ @"Please update the app with a valid plist file, "
+ @"following the instructions in the Firebase Github "
+ @"repository at: %@";
+
+@implementation FIRSampleAppUtilities
+
++ (BOOL)appContainsRealServiceInfoPlist {
+ static BOOL containsRealServiceInfoPlist = NO;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSBundle *bundle = [NSBundle mainBundle];
+ containsRealServiceInfoPlist = [self containsRealServiceInfoPlistInBundle:bundle];
+ });
+ return containsRealServiceInfoPlist;
+}
+
++ (BOOL)containsRealServiceInfoPlistInBundle:(NSBundle *)bundle {
+ NSString *bundlePath = bundle.bundlePath;
+ if (!bundlePath.length) {
+ return NO;
+ }
+
+ NSString *plistFilePath = [bundle pathForResource:kServiceInfoFileName
+ ofType:kServiceInfoFileType];
+ if (!plistFilePath.length) {
+ return NO;
+ }
+
+ NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:plistFilePath];
+ if (!plist) {
+ return NO;
+ }
+
+ // Perform a very naive validation by checking to see if the plist has the dummy google app id
+ NSString *googleAppID = plist[kGoogleAppIDPlistKey];
+ if (!googleAppID.length) {
+ return NO;
+ }
+ if ([googleAppID isEqualToString:kDummyGoogleAppID]) {
+ return NO;
+ }
+
+ return YES;
+}
+
++ (void)presentAlertForInvalidServiceInfoPlistFromViewController:(UIViewController *)
+ viewController {
+ NSString *message = [NSString stringWithFormat:kInvalidPlistAlertMessage, kGithubRepoURLString];
+ UIAlertController *alertController =
+ [UIAlertController alertControllerWithTitle:kInvalidPlistAlertTitle
+ message:message
+ preferredStyle:UIAlertControllerStyleAlert];
+ UIAlertAction *viewReadmeAction =
+ [UIAlertAction actionWithTitle:@"View Github"
+ style:UIAlertActionStyleDefault
+ handler:^(UIAlertAction * _Nonnull action) {
+ NSURL *githubURL = [NSURL URLWithString:kGithubRepoURLString];
+ [FIRSampleAppUtilities navigateToURL:githubURL fromViewController:viewController];
+
+ }];
+ [alertController addAction:viewReadmeAction];
+
+ UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleCancel handler:nil];
+ [alertController addAction:cancelAction];
+
+ [viewController presentViewController:alertController animated:YES completion:nil];
+}
+
++ (void)navigateToURL:(NSURL *)url fromViewController:(UIViewController *)viewController {
+ if ([SFSafariViewController class]) {
+ SFSafariViewController *safariController = [[SFSafariViewController alloc] initWithURL:url];
+ [viewController showDetailViewController:safariController sender:nil];
+ } else {
+ [[UIApplication sharedApplication] openURL:url];
+ }
+}
+
+@end
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Contents.json b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..f659eef
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,113 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-Phone-Notification@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-Phone-Notification@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-Phone-Settings@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-Phone-Settings@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-Phone-Spotlight@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-Phone-Spotlight@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-Phone-60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-Phone-60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-Pad-Notification.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-Pad-Notification@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-Pad-Settings.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-Pad-Settings@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-Pad-Spotlight.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-Pad-Spotlight@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-Pad-76.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-Pad-76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-Pad-83.5@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "pre-rendered" : true
+ }
+} \ No newline at end of file
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-76.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-76.png
new file mode 100644
index 0000000..edc9400
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-76.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-76@2x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-76@2x.png
new file mode 100644
index 0000000..a8be9e3
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-76@2x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-83.5@2x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-83.5@2x.png
new file mode 100644
index 0000000..e56fdc8
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-83.5@2x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Notification.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Notification.png
new file mode 100644
index 0000000..bd7df97
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Notification.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Notification@2x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Notification@2x.png
new file mode 100644
index 0000000..8334506
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Notification@2x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Settings.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Settings.png
new file mode 100644
index 0000000..6fe0542
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Settings.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Settings@2x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Settings@2x.png
new file mode 100644
index 0000000..2906484
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Settings@2x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Spotlight.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Spotlight.png
new file mode 100644
index 0000000..8334506
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Spotlight.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Spotlight@2x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Spotlight@2x.png
new file mode 100644
index 0000000..bb34c9a
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Pad-Spotlight@2x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-60@2x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-60@2x.png
new file mode 100644
index 0000000..dbf3007
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-60@2x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-60@3x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-60@3x.png
new file mode 100644
index 0000000..4e23f57
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-60@3x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Notification@2x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Notification@2x.png
new file mode 100644
index 0000000..8334506
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Notification@2x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Notification@3x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Notification@3x.png
new file mode 100644
index 0000000..13eb060
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Notification@3x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Settings@2x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Settings@2x.png
new file mode 100644
index 0000000..2906484
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Settings@2x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Settings@3x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Settings@3x.png
new file mode 100644
index 0000000..a415097
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Settings@3x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Spotlight@2x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Spotlight@2x.png
new file mode 100644
index 0000000..bb34c9a
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Spotlight@2x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Spotlight@3x.png b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Spotlight@3x.png
new file mode 100644
index 0000000..dbf3007
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/AppIcon.appiconset/Icon-Phone-Spotlight@3x.png
Binary files differ
diff --git a/Example/Shared/Shared.xcassets/Contents.json b/Example/Shared/Shared.xcassets/Contents.json
new file mode 100644
index 0000000..da4a164
--- /dev/null
+++ b/Example/Shared/Shared.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/Example/Storage/App/1mb.dat b/Example/Storage/App/1mb.dat
new file mode 100644
index 0000000..9e0f96a
--- /dev/null
+++ b/Example/Storage/App/1mb.dat
Binary files differ
diff --git a/Example/Storage/App/Base.lproj/LaunchScreen.storyboard b/Example/Storage/App/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..66a7681
--- /dev/null
+++ b/Example/Storage/App/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Storage/App/Base.lproj/Main.storyboard b/Example/Storage/App/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..d164a23
--- /dev/null
+++ b/Example/Storage/App/Base.lproj/Main.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="whP-gf-Uak">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="wQg-tq-qST">
+ <objects>
+ <viewController id="whP-gf-Uak" customClass="FIRViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="uEw-UM-LJ8"/>
+ <viewControllerLayoutGuide type="bottom" id="Mvr-aV-6Um"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="TpU-gO-2f1">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="tc2-Qw-aMS" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="305" y="433"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Storage/App/FIRAppDelegate.h b/Example/Storage/App/FIRAppDelegate.h
new file mode 100644
index 0000000..e3fba8f
--- /dev/null
+++ b/Example/Storage/App/FIRAppDelegate.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 UIKit;
+
+@interface FIRAppDelegate : UIResponder <UIApplicationDelegate>
+
+@property (strong, nonatomic) UIWindow *window;
+
+@end
diff --git a/Example/Storage/App/FIRAppDelegate.m b/Example/Storage/App/FIRAppDelegate.m
new file mode 100644
index 0000000..0ecfdea
--- /dev/null
+++ b/Example/Storage/App/FIRAppDelegate.m
@@ -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 "FIRAppDelegate.h"
+
+@implementation FIRAppDelegate
+
+- (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 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/Storage/App/FIRViewController.h b/Example/Storage/App/FIRViewController.h
new file mode 100644
index 0000000..64b4b74
--- /dev/null
+++ b/Example/Storage/App/FIRViewController.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 UIKit;
+
+@interface FIRViewController : UIViewController
+
+@end
diff --git a/Example/Storage/App/FIRViewController.m b/Example/Storage/App/FIRViewController.m
new file mode 100644
index 0000000..901accf
--- /dev/null
+++ b/Example/Storage/App/FIRViewController.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 "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/Storage/App/Storage-Info.plist b/Example/Storage/App/Storage-Info.plist
new file mode 100644
index 0000000..7576a0d
--- /dev/null
+++ b/Example/Storage/App/Storage-Info.plist
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Storage/App/main.m b/Example/Storage/App/main.m
new file mode 100644
index 0000000..03b5c12
--- /dev/null
+++ b/Example/Storage/App/main.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 UIKit;
+#import "FIRAppDelegate.h"
+
+int main(int argc, char * argv[])
+{
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class]));
+ }
+}
diff --git a/Example/Storage/Tests/Integration/FIRStorageIntegrationTests.m b/Example/Storage/Tests/Integration/FIRStorageIntegrationTests.m
new file mode 100644
index 0000000..60a2496
--- /dev/null
+++ b/Example/Storage/Tests/Integration/FIRStorageIntegrationTests.m
@@ -0,0 +1,520 @@
+// 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 <math.h>
+#import <XCTest/XCTest.h>
+
+#import "FirebaseStorage.h"
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+
+NSTimeInterval kFIRStorageIntegrationTestTimeout = 30;
+
+/**
+ * Firebase Storage Integration tests
+ *
+ * To run these tests, you need to define the following access rights for your Firebase App:
+ * - unauthentication read/write access to /ios/public
+ * - authentication read/write access to /ios/private
+ *
+ * A sample configuration may look like:
+ *
+ * service firebase.storage {
+ * match /b/{YOUR_PROJECT_ID}.appspot.com/o {
+ * ...
+ * match /ios {
+ * match /public/{allPaths=**} {
+ * allow read, write;
+ * }
+ * match /private/{allPaths=**} {
+ * allow none;
+ * }
+ * }
+ * }
+ * }
+ *
+ * You can define these access rights in the Firebase Console of your project.
+ */
+@interface FIRStorageIntegrationTests : XCTestCase
+
+@property(strong, nonatomic) FIRApp *app;
+@property(strong, nonatomic) FIRStorage *storage;
+
+@end
+
+@implementation FIRStorageIntegrationTests
+
+
++ (void)setUp {
+ [FIRApp configure];
+}
+
+- (void)setUp {
+ [super setUp];
+
+ self.app = [FIRApp defaultApp];
+ self.storage = [FIRStorage storageForApp:self.app];
+
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ XCTestExpectation *expectation = [self expectationWithDescription:@"setup"];
+
+ FIRStorageReference *ref = [[FIRStorage storage].reference child:@"ios/public/1mb"];
+ NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"1mb" ofType:@"dat"]];
+ XCTAssertNotNil(data, "Could not load bundled file");
+ [ref putData:data metadata:nil completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+ });
+}
+
+- (void)tearDown {
+ self.app = nil;
+ self.storage = nil;
+
+ [super tearDown];
+}
+
+- (void)testName {
+ NSString *aGSURI = [NSString stringWithFormat:@"gs://%@.appspot.com/path/to", [[FIRApp defaultApp] options].projectID];
+ FIRStorageReference *ref = [self.storage referenceForURL:aGSURI];
+ XCTAssertEqualObjects(ref.description, aGSURI);
+}
+
+- (void)testUnauthenticatedGetMetadata {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedGetMetadata"];
+ FIRStorageReference *ref = [self.storage.reference child:@"ios/public/1mb"];
+
+ [ref metadataWithCompletion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNotNil(metadata, "Metadata should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedUpdateMetadata {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedUpdateMetadata"];
+
+ FIRStorageReference *ref = [self.storage referenceWithPath:@"ios/public/1mb"];
+
+ FIRStorageMetadata *meta = [[FIRStorageMetadata alloc] init];
+ [meta setContentType:@"lol/custom"];
+ [meta setCustomMetadata:@{
+ @"lol" : @"custom metadata is neat",
+ @"ちかてつ" : @"🚇",
+ @"shinkansen" : @"新幹線"
+ }];
+
+ [ref updateMetadata:meta
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertEqualObjects(meta.contentType, metadata.contentType);
+ XCTAssertEqualObjects(meta.customMetadata[@"lol"],
+ metadata.customMetadata[@"lol"]);
+ XCTAssertEqualObjects(meta.customMetadata[@"ちかてつ"],
+ metadata.customMetadata[@"ちかてつ"]);
+ XCTAssertEqualObjects(meta.customMetadata[@"shinkansen"],
+ metadata.customMetadata[@"shinkansen"]);
+ XCTAssertNil(error, "Error should be nil");
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedDelete {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUnauthenticatedDelete"];
+
+ FIRStorageReference *ref = [self.storage referenceWithPath:@"ios/public/fileToDelete"];
+
+ NSData *data = [@"Delete me!!!!!!!" dataUsingEncoding:NSUTF8StringEncoding];
+
+ [ref putData:data
+ metadata:nil
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNotNil(metadata, "Metadata should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ [ref deleteWithCompletion:^(NSError *error) {
+ XCTAssertNil(error, "Error should be nil");
+ [expectation fulfill];
+ }];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testDeleteWithNilCompletion {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testDeleteWithNilCompletion"];
+
+ FIRStorageReference *ref = [self.storage referenceWithPath:@"ios/public/fileToDelete"];
+
+ NSData *data = [@"Delete me!!!!!!!" dataUsingEncoding:NSUTF8StringEncoding];
+
+ [ref putData:data
+ metadata:nil
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNotNil(metadata, "Metadata should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ [ref deleteWithCompletion:nil];
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedSimplePutData {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedSimplePutData"];
+ FIRStorageReference *ref = [self.storage referenceWithPath:@"ios/public/testBytesUpload"];
+
+ NSData *data = [@"Hello World" dataUsingEncoding:NSUTF8StringEncoding];
+
+ [ref putData:data
+ metadata:nil
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNotNil(metadata, "Metadata should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedSimplePutEmptyData {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedSimplePutEmptyData"];
+
+ FIRStorageReference *ref =
+ [self.storage referenceWithPath:@"ios/public/testUnauthenticatedSimplePutEmptyData"];
+
+ NSData *data = [[NSData alloc] init];
+
+ [ref putData:data
+ metadata:nil
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNotNil(metadata, "Metadata should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedSimplePutDataUnauthorized {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedSimplePutDataUnauthorized"];
+ FIRStorageReference *ref = [self.storage referenceWithPath:@"ios/private/secretfile.txt"];
+
+ NSData *data = [@"Hello World" dataUsingEncoding:NSUTF8StringEncoding];
+
+ [ref putData:data
+ metadata:nil
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNil(metadata, "Metadata should be nil");
+ XCTAssertNotNil(error, "Error should not be nil");
+ XCTAssertEqual(error.code, FIRStorageErrorCodeUnauthorized);
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedSimplePutFile {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedSimplePutFile"];
+
+ FIRStorageReference *ref =
+ [self.storage referenceWithPath:@"ios/public/testUnauthenticatedSimplePutFile"];
+
+ NSData *data = [@"Hello World" dataUsingEncoding:NSUTF8StringEncoding];
+ NSURL *tmpDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
+ NSURL *fileURL =
+ [[tmpDirURL URLByAppendingPathComponent:@"hello"] URLByAppendingPathExtension:@"txt"];
+ [data writeToURL:fileURL atomically:YES];
+
+ FIRStorageUploadTask *task = [ref putFile:fileURL
+ metadata:nil
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNotNil(metadata, "Metadata should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ }];
+
+ __block long uploadedBytes = -1;
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *snapshot) {
+ XCTAssertEqualObjects([snapshot description], @"<State: Success>");
+ [expectation fulfill];
+ }];
+
+ [task observeStatus:FIRStorageTaskStatusProgress
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ XCTAssertTrue([[snapshot description] containsString:@"State: Progress"] ||
+ [[snapshot description] containsString:@"State: Resume"]);
+ NSProgress *progress = snapshot.progress;
+ XCTAssertGreaterThanOrEqual(progress.completedUnitCount, uploadedBytes);
+ uploadedBytes = progress.completedUnitCount;
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testPutFileWithSpecialCharacters {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testPutFileWithSpecialCharacters"];
+
+ NSString *fileName = @"hello&+@_ .txt";
+ FIRStorageReference *ref =
+ [self.storage referenceWithPath:[@"ios/public/" stringByAppendingString:fileName]];
+
+ NSData *data = [@"Hello World" dataUsingEncoding:NSUTF8StringEncoding];
+ NSURL *tmpDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
+ NSURL *fileURL = [tmpDirURL URLByAppendingPathComponent:fileName];
+ [data writeToURL:fileURL atomically:YES];
+
+ [ref putFile:fileURL
+ metadata:nil
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNotNil(metadata, "Metadata should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ XCTAssertEqualObjects(fileName, metadata.name);
+ FIRStorageReference *download =
+ [self.storage referenceWithPath:[@"ios/public/" stringByAppendingString:fileName]];
+ [download metadataWithCompletion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNotNil(metadata, "Metadata should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ XCTAssertEqualObjects(fileName, metadata.name);
+ [expectation fulfill];
+ }];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedSimplePutDataNoMetadata {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedSimplePutDataNoMetadata"];
+
+ FIRStorageReference *ref =
+ [self.storage referenceWithPath:@"ios/public/testUnauthenticatedSimplePutDataNoMetadata"];
+
+ NSData *data = [@"Hello World" dataUsingEncoding:NSUTF8StringEncoding];
+
+ [ref putData:data
+ metadata:nil
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNotNil(metadata, "Metadata should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedSimplePutFileNoMetadata {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedSimplePutFileNoMetadata"];
+
+ FIRStorageReference *ref =
+ [self.storage referenceWithPath:@"ios/public/testUnauthenticatedSimplePutFileNoMetadata"];
+
+ NSData *data = [@"Hello World" dataUsingEncoding:NSUTF8StringEncoding];
+ NSURL *tmpDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
+ NSURL *fileURL =
+ [[tmpDirURL URLByAppendingPathComponent:@"hello"] URLByAppendingPathExtension:@"txt"];
+ [data writeToURL:fileURL atomically:YES];
+
+ [ref putFile:fileURL
+ metadata:nil
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertNotNil(metadata, "Metadata should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedSimpleGetData {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedSimpleGetData"];
+
+ FIRStorageReference *ref = [self.storage referenceWithPath:@"ios/public/1mb"];
+
+ [ref dataWithMaxSize:1 * 1024 * 1024
+ completion:^(NSData *data, NSError *error) {
+ XCTAssertNotNil(data, "Data should not be nil");
+ XCTAssertNil(error, "Error should be nil");
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedSimpleGetDataTooSmall {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedSimpleGetDataTooSmall"];
+
+ FIRStorageReference *ref = [self.storage referenceWithPath:@"ios/public/1mb"];
+
+ /// Only allow 1kB size, which is smaller than our file
+ [ref dataWithMaxSize:1 * 1024
+ completion:^(NSData *data, NSError *error) {
+ XCTAssertEqual(data, nil);
+ XCTAssertEqual(error.code, FIRStorageErrorCodeDownloadSizeExceeded);
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedSimpleGetFile {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedSimpleGetData"];
+
+ FIRStorageReference *ref = [self.storage referenceWithPath:@"ios/public/helloworld"];
+
+ NSURL *tmpDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
+ NSURL *fileURL =
+ [[tmpDirURL URLByAppendingPathComponent:@"hello"] URLByAppendingPathExtension:@"txt"];
+
+ [ref putData:[@"Hello World" dataUsingEncoding:NSUTF8StringEncoding] metadata:nil
+ completion:^(FIRStorageMetadata *metadata, NSError *error)
+ {
+ FIRStorageDownloadTask *task = [ref writeToFile:fileURL];
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *snapshot) {
+ NSString *data = [NSString stringWithContentsOfURL:fileURL
+ encoding:NSUTF8StringEncoding
+ error:NULL];
+ NSString *expectedData = @"Hello World";
+ XCTAssertEqualObjects(data, expectedData);
+ XCTAssertEqualObjects([snapshot description], @"<State: Success>");
+ [expectation fulfill];
+ }];
+
+ [task observeStatus:FIRStorageTaskStatusProgress
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ NSProgress *progress = snapshot.progress;
+ NSLog(@"%lld of %lld", progress.completedUnitCount, progress.totalUnitCount);
+ }];
+
+ [task observeStatus:FIRStorageTaskStatusFailure
+ handler:^(FIRStorageTaskSnapshot *snapshot) {
+ XCTAssertNil(snapshot.error);
+ [expectation fulfill];
+ }];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testCancelDownload {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testCancelDownload"];
+
+ FIRStorageReference *ref = [self.storage referenceWithPath:@"ios/public/1mb"];
+
+ NSURL *tmpDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
+ NSURL *fileURL =
+ [[tmpDirURL URLByAppendingPathComponent:@"hello"] URLByAppendingPathExtension:@"dat"];
+
+ FIRStorageDownloadTask *task = [ref writeToFile:fileURL];
+
+ [task observeStatus:FIRStorageTaskStatusFailure
+ handler:^(FIRStorageTaskSnapshot *snapshot) {
+ XCTAssertTrue([[snapshot description] containsString:@"State: Failed"]);
+ [expectation fulfill];
+ }];
+
+ [task observeStatus:FIRStorageTaskStatusProgress
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ [task cancel];
+ }];
+
+ [self waitForExpectations];
+}
+
+- (void)testUnauthenticatedResumeGetFile {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnauthenticatedResumeGetFile"];
+
+ FIRStorageReference *ref = [self.storage referenceWithPath:@"ios/public/1mb"];
+
+ NSURL *tmpDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
+ NSURL *fileURL =
+ [[tmpDirURL URLByAppendingPathComponent:@"hello"] URLByAppendingPathExtension:@"txt"];
+
+ __block long resumeAtBytes = 256 * 1024;
+ __block long downloadedBytes = 0;
+ __block double computationResult = 0;
+
+ FIRStorageDownloadTask *task = [ref writeToFile:fileURL];
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *snapshot) {
+ XCTAssertEqualObjects([snapshot description], @"<State: Success>");
+ [expectation fulfill];
+ }];
+
+ [task observeStatus:FIRStorageTaskStatusProgress
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ XCTAssertTrue([[snapshot description] containsString:@"State: Progress"] ||
+ [[snapshot description] containsString:@"State: Resume"]);
+ NSProgress *progress = snapshot.progress;
+ XCTAssertGreaterThanOrEqual(progress.completedUnitCount, downloadedBytes);
+ downloadedBytes = progress.completedUnitCount;
+ if (progress.completedUnitCount > resumeAtBytes) {
+ // Making sure the main run loop is busy.
+ for (int i = 0; i < 500; ++i) {
+ dispatch_async(dispatch_get_main_queue(), ^ {
+ computationResult = sqrt(INT_MAX - i);
+ });
+ }
+ NSLog(@"Pausing");
+ [task pause];
+ resumeAtBytes = INT_MAX;
+ }
+ }];
+
+ [task observeStatus:FIRStorageTaskStatusPause
+ handler:^(FIRStorageTaskSnapshot *snapshot) {
+ XCTAssertEqualObjects([snapshot description], @"<State: Paused>");
+ NSLog(@"Resuming");
+ [task resume];
+ }];
+
+ [self waitForExpectations];
+ XCTAssertEqual(INT_MAX, resumeAtBytes);
+ XCTAssertEqualWithAccuracy(sqrt(INT_MAX - 499), computationResult, 0.1);
+}
+
+- (void)waitForExpectations {
+ [self waitForExpectationsWithTimeout:kFIRStorageIntegrationTestTimeout
+ handler:^(NSError *_Nullable error) {
+ if (error) {
+ NSLog(@"%@", error);
+ }
+ }];
+}
+
+@end
diff --git a/Example/Storage/Tests/Tests-Info.plist b/Example/Storage/Tests/Tests-Info.plist
new file mode 100644
index 0000000..169b6f7
--- /dev/null
+++ b/Example/Storage/Tests/Tests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Example/Storage/Tests/Unit/FIRStorageDeleteTests.m b/Example/Storage/Tests/Unit/FIRStorageDeleteTests.m
new file mode 100644
index 0000000..42a3b1a
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStorageDeleteTests.m
@@ -0,0 +1,164 @@
+// 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 "FIRStorageDeleteTask.h"
+#import "FIRStorageTestHelpers.h"
+
+@interface FIRStorageDeleteTests : XCTestCase
+
+@property(strong, nonatomic) GTMSessionFetcherService *fetcherService;
+@property(strong, nonatomic) FIRStorageMetadata *metadata;
+@property(strong, nonatomic) FIRStorage *storage;
+@property(strong, nonatomic) id mockApp;
+
+@end
+
+@implementation FIRStorageDeleteTests
+
+- (void)setUp {
+ [super setUp];
+
+ NSDictionary *metadataDict = @{ @"bucket" : @"bucket", @"name" : @"path/to/object" };
+ self.metadata = [[FIRStorageMetadata alloc] initWithDictionary:metadataDict];
+
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"bucket.appspot.com");
+
+ self.mockApp = OCMClassMock([FIRApp class]);
+ OCMStub([self.mockApp name]).andReturn(kFIRStorageAppName);
+ OCMStub([(FIRApp *)self.mockApp options]).andReturn(mockOptions);
+
+ self.fetcherService = [[GTMSessionFetcherService alloc] init];
+ self.fetcherService.authorizer =
+ [[FIRStorageTokenAuthorizer alloc] initWithApp:self.mockApp
+ fetcherService:self.fetcherService];
+
+ self.storage = [FIRStorage storageForApp:self.mockApp];
+}
+
+- (void)tearDown {
+ self.fetcherService = nil;
+ self.storage = nil;
+ self.mockApp = nil;
+ [super tearDown];
+}
+
+- (void)testFetcherConfiguration {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testSuccessfulFetch"];
+
+ self.fetcherService.testBlock =
+ ^(GTMSessionFetcher *fetcher, GTMSessionFetcherTestResponse response) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-retain-cycles"
+ XCTAssertEqualObjects(fetcher.request.URL, [FIRStorageTestHelpers objectURL]);
+#pragma clang diagnostic pop
+ XCTAssertEqualObjects(fetcher.request.HTTPMethod, @"DELETE");
+ NSHTTPURLResponse *httpResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:fetcher.request.URL
+ statusCode:200
+ HTTPVersion:kHTTPVersion
+ headerFields:nil];
+ response(httpResponse, nil, nil);
+ };
+
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageDeleteTask *task = [[FIRStorageDeleteTask alloc] initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(NSError *error) {
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testSuccessfulFetch {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testSuccessfulFetch"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers successBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageDeleteTask *task = [[FIRStorageDeleteTask alloc] initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(NSError *error) {
+ XCTAssertEqual(error, nil);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchUnauthenticated {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchUnauthenticated"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers unauthenticatedBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageDeleteTask *task =
+ [[FIRStorageDeleteTask alloc] initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(NSError *error) {
+ XCTAssertEqual(error.code,
+ FIRStorageErrorCodeUnauthenticated);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchUnauthorized {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchUnauthorized"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers unauthorizedBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageDeleteTask *task =
+ [[FIRStorageDeleteTask alloc] initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(NSError *error) {
+ XCTAssertEqual(error.code,
+ FIRStorageErrorCodeUnauthorized);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchObjectDoesntExist {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchObjectDoesntExist"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers notFoundBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers notFoundPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageDeleteTask *task =
+ [[FIRStorageDeleteTask alloc] initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(NSError *error) {
+ XCTAssertEqual(error.code,
+ FIRStorageErrorCodeObjectNotFound);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+@end
diff --git a/Example/Storage/Tests/Unit/FIRStorageGetMetadataTests.m b/Example/Storage/Tests/Unit/FIRStorageGetMetadataTests.m
new file mode 100644
index 0000000..72a057d
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStorageGetMetadataTests.m
@@ -0,0 +1,188 @@
+// 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 "FIRStorageGetMetadataTask.h"
+#import "FIRStorageTestHelpers.h"
+
+@interface FIRStorageGetMetadataTests : XCTestCase
+
+@property(strong, nonatomic) GTMSessionFetcherService *fetcherService;
+@property(strong, nonatomic) FIRStorageMetadata *metadata;
+@property(strong, nonatomic) FIRStorage *storage;
+@property(strong, nonatomic) id mockApp;
+
+@end
+
+@implementation FIRStorageGetMetadataTests
+
+- (void)setUp {
+ [super setUp];
+
+ NSDictionary *metadataDict = @{ @"bucket" : @"bucket", @"name" : @"path/to/object" };
+ self.metadata = [[FIRStorageMetadata alloc] initWithDictionary:metadataDict];
+
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"bucket.appspot.com");
+
+ self.mockApp = OCMClassMock([FIRApp class]);
+ OCMStub([self.mockApp name]).andReturn(kFIRStorageAppName);
+ OCMStub([(FIRApp *)self.mockApp options]).andReturn(mockOptions);
+
+ self.fetcherService = [[GTMSessionFetcherService alloc] init];
+ self.fetcherService.authorizer =
+ [[FIRStorageTokenAuthorizer alloc] initWithApp:self.mockApp
+ fetcherService:self.fetcherService];
+
+ self.storage = [FIRStorage storageForApp:self.mockApp];
+}
+
+- (void)tearDown {
+ self.fetcherService = nil;
+ self.storage = nil;
+ self.mockApp = nil;
+ [super tearDown];
+}
+
+- (void)testFetcherConfiguration {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testSuccessfulFetch"];
+
+ self.fetcherService.testBlock =
+ ^(GTMSessionFetcher *fetcher, GTMSessionFetcherTestResponse response) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-retain-cycles"
+ XCTAssertEqualObjects(fetcher.request.URL, [FIRStorageTestHelpers objectURL]);
+#pragma clang diagnostic pop
+ XCTAssertEqualObjects(fetcher.request.HTTPMethod, @"GET");
+ NSHTTPURLResponse *httpResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:fetcher.request.URL
+ statusCode:200
+ HTTPVersion:kHTTPVersion
+ headerFields:nil];
+ response(httpResponse, nil, nil);
+ };
+
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageGetMetadataTask *task = [[FIRStorageGetMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testSuccessfulFetch {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testSuccessfulFetch"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers successBlockWithMetadata:self.metadata];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageGetMetadataTask *task = [[FIRStorageGetMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertEqualObjects(self.metadata.bucket, metadata.bucket);
+ XCTAssertEqualObjects(self.metadata.name, metadata.name);
+ XCTAssertEqual(error, nil);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchUnauthenticated {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchUnauthenticated"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers unauthenticatedBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageGetMetadataTask *task = [[FIRStorageGetMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertEqual(metadata, nil);
+ XCTAssertEqual(error.code, FIRStorageErrorCodeUnauthenticated);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchUnauthorized {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchUnauthorized"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers unauthorizedBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageGetMetadataTask *task = [[FIRStorageGetMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertEqual(metadata, nil);
+ XCTAssertEqual(error.code, FIRStorageErrorCodeUnauthorized);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchObjectDoesntExist {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchObjectDoesntExist"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers notFoundBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers notFoundPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageGetMetadataTask *task = [[FIRStorageGetMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertEqual(metadata, nil);
+ XCTAssertEqual(error.code, FIRStorageErrorCodeObjectNotFound);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchBadJSON {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchBadJSON"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers invalidJSONBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageGetMetadataTask *task = [[FIRStorageGetMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ completion:^(FIRStorageMetadata *metadata, NSError *error) {
+ XCTAssertEqual(metadata, nil);
+ XCTAssertEqual(error.code, FIRStorageErrorCodeUnknown);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+@end
diff --git a/Example/Storage/Tests/Unit/FIRStorageMetadataTests.m b/Example/Storage/Tests/Unit/FIRStorageMetadataTests.m
new file mode 100644
index 0000000..f5fb3b3
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStorageMetadataTests.m
@@ -0,0 +1,282 @@
+// 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/XCTest.h>
+
+#import "FIRStorageMetadata.h"
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageUtils.h"
+
+@interface FIRStorageMetadataTests : XCTestCase
+
+@end
+
+@implementation FIRStorageMetadataTests
+
+- (void)testInitialzeNoMetadata {
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:@{}];
+ XCTAssertNotNil(metadata);
+}
+
+- (void)testInitialzeFullMetadata {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataCacheControl : @"max-age=3600, no-cache",
+ kFIRStorageMetadataContentDisposition : @"inline",
+ kFIRStorageMetadataContentEncoding : @"gzip",
+ kFIRStorageMetadataContentLanguage : @"en-us",
+ kFIRStorageMetadataContentType : @"application/octet-stream",
+ kFIRStorageMetadataCustomMetadata : @{@"foo" : @{@"bar" : @"baz"}},
+ kFIRStorageMetadataDownloadTokens : @"1234567890",
+ kFIRStorageMetadataGeneration : @"12345",
+ kFIRStorageMetadataMetageneration : @"67890",
+ kFIRStorageMetadataName : @"path/to/object",
+ kFIRStorageMetadataTimeCreated : @"1992-08-07T17:22:53.108Z",
+ kFIRStorageMetadataUpdated : @"2016-03-01T20:16:01.673Z"
+ };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNotNil(metadata);
+ XCTAssertEqualObjects(metadata.bucket, metaDict[kFIRStorageMetadataBucket]);
+ XCTAssertEqualObjects(metadata.cacheControl, metaDict[kFIRStorageMetadataCacheControl]);
+ XCTAssertEqualObjects(metadata.contentDisposition,
+ metaDict[kFIRStorageMetadataContentDisposition]);
+ XCTAssertEqualObjects(metadata.contentEncoding, metaDict[kFIRStorageMetadataContentEncoding], );
+ XCTAssertEqualObjects(metadata.contentType, metaDict[kFIRStorageMetadataContentType]);
+ XCTAssertEqualObjects(metadata.customMetadata, metaDict[kFIRStorageMetadataCustomMetadata]);
+ NSString *URLFormat = @"https://firebasestorage.googleapis.com/v0/b/%@/o/%@?alt=media&token=%@";
+ NSString *URLString = [NSString
+ stringWithFormat:URLFormat, metaDict[kFIRStorageMetadataBucket],
+ [FIRStorageUtils GCSEscapedString:metaDict[kFIRStorageMetadataName]],
+ metaDict[kFIRStorageMetadataDownloadTokens]];
+ XCTAssertEqualObjects([metadata.downloadURL description], URLString);
+ NSString *generation = [NSString stringWithFormat:@"%lld", metadata.generation];
+ XCTAssertEqualObjects(generation, metaDict[kFIRStorageMetadataGeneration]);
+ NSString *metageneration = [NSString stringWithFormat:@"%lld", metadata.metageneration];
+ XCTAssertEqualObjects(metageneration, metaDict[kFIRStorageMetadataMetageneration]);
+ XCTAssertEqualObjects(metadata.path, metaDict[kFIRStorageMetadataName]);
+ XCTAssertEqualObjects([metadata RFC3339StringFromDate:metadata.timeCreated],
+ metaDict[kFIRStorageMetadataTimeCreated]);
+ XCTAssertEqualObjects([metadata RFC3339StringFromDate:metadata.updated],
+ metaDict[kFIRStorageMetadataUpdated]);
+}
+
+- (void)testDictionaryRepresentation {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataCacheControl : @"max-age=3600, no-cache",
+ kFIRStorageMetadataContentDisposition : @"inline",
+ kFIRStorageMetadataContentEncoding : @"gzip",
+ kFIRStorageMetadataContentLanguage : @"en-us",
+ kFIRStorageMetadataContentType : @"application/octet-stream",
+ kFIRStorageMetadataCustomMetadata : @{@"foo" : @{@"bar" : @"baz"}},
+ kFIRStorageMetadataDownloadTokens : @"1234567890",
+ kFIRStorageMetadataGeneration : @"12345",
+ kFIRStorageMetadataMetageneration : @"67890",
+ kFIRStorageMetadataName : @"path/to/object",
+ kFIRStorageMetadataTimeCreated : @"1992-08-07T17:22:53.108Z",
+ kFIRStorageMetadataUpdated : @"2016-03-01T20:16:01.673Z"
+ };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ NSDictionary *dictRepresentation = [metadata dictionaryRepresentation];
+ XCTAssertNotEqual(dictRepresentation, nil);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataBucket],
+ metaDict[kFIRStorageMetadataBucket]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataCacheControl],
+ metaDict[kFIRStorageMetadataCacheControl]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataContentDisposition],
+ metaDict[kFIRStorageMetadataContentDisposition]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataContentEncoding],
+ metaDict[kFIRStorageMetadataContentEncoding]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataContentLanguage],
+ metaDict[kFIRStorageMetadataContentLanguage]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataContentType],
+ metaDict[kFIRStorageMetadataContentType]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataCustomMetadata],
+ metaDict[kFIRStorageMetadataCustomMetadata]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataDownloadTokens],
+ metaDict[kFIRStorageMetadataDownloadTokens]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataGeneration],
+ metaDict[kFIRStorageMetadataGeneration]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataMetageneration],
+ metaDict[kFIRStorageMetadataMetageneration]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataName],
+ metaDict[kFIRStorageMetadataName]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataTimeCreated],
+ metaDict[kFIRStorageMetadataTimeCreated]);
+ XCTAssertEqualObjects(dictRepresentation[kFIRStorageMetadataUpdated],
+ metaDict[kFIRStorageMetadataUpdated]);
+}
+
+- (void)testInitialzeNoDownloadTokensGetToken {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataName : @"path/to/object",
+ };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNotNil(metadata);
+ XCTAssertEqual(metadata.downloadURL, nil);
+ XCTAssertEqual(metadata.downloadURLs, nil);
+}
+
+- (void)testInitialzeMultipleDownloadTokensGetToken {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataDownloadTokens : @"12345,67890",
+ kFIRStorageMetadataName : @"path/to/object",
+ };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNotNil(metadata);
+ NSString *URLformat = @"https://firebasestorage.googleapis.com/v0/b/%@/o/%@?alt=media&token=%@";
+ NSString *URLString0 = [NSString
+ stringWithFormat:URLformat, metaDict[kFIRStorageMetadataBucket],
+ [FIRStorageUtils GCSEscapedString:metaDict[kFIRStorageMetadataName]],
+ @"12345"];
+ NSString *URLString1 = [NSString
+ stringWithFormat:URLformat, metaDict[kFIRStorageMetadataBucket],
+ [FIRStorageUtils GCSEscapedString:metaDict[kFIRStorageMetadataName]],
+ @"67890"];
+ XCTAssertEqualObjects([metadata.downloadURL absoluteString], URLString0);
+ XCTAssertEqualObjects([metadata.downloadURLs[0] absoluteString], URLString0);
+ XCTAssertEqualObjects([metadata.downloadURLs[1] absoluteString], URLString1);
+}
+
+- (void)testMultipleDownloadURLsGetToken {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataName : @"path/to/object",
+ };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ NSString *URLformat = @"https://firebasestorage.googleapis.com/v0/b/%@/o/%@?alt=media&token=%@";
+ NSString *URLString0 = [NSString
+ stringWithFormat:URLformat, metaDict[kFIRStorageMetadataBucket],
+ [FIRStorageUtils GCSEscapedString:metaDict[kFIRStorageMetadataName]],
+ @"12345"];
+ NSString *URLString1 = [NSString
+ stringWithFormat:URLformat, metaDict[kFIRStorageMetadataBucket],
+ [FIRStorageUtils GCSEscapedString:metaDict[kFIRStorageMetadataName]],
+ @"67890"];
+ NSURL *URL0 = [NSURL URLWithString:URLString0];
+ NSURL *URL1 = [NSURL URLWithString:URLString1];
+ NSArray *downloadURLs = @[ URL0, URL1 ];
+ [metadata setValue:downloadURLs forKey:@"downloadURLs"];
+ NSDictionary *newMetaDict = metadata.dictionaryRepresentation;
+ XCTAssertEqualObjects(newMetaDict[kFIRStorageMetadataDownloadTokens], @"12345,67890");
+}
+
+- (void)testInitialzeMetadataWithFile {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataName : @"path/to/file",
+ };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ [metadata setType:FIRStorageMetadataTypeFile];
+ XCTAssertEqual(metadata.isFile, YES);
+ XCTAssertEqual(metadata.isFolder, NO);
+}
+
+- (void)testInitialzeMetadataWithFolder {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataName : @"path/to/folder/",
+ };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ [metadata setType:FIRStorageMetadataTypeFolder];
+ XCTAssertEqual(metadata.isFolder, YES);
+ XCTAssertEqual(metadata.isFile, NO);
+}
+
+- (void)testReflexiveMetadataEquality {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataName : @"path/to/object",
+ };
+ FIRStorageMetadata *metadata0 = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ FIRStorageMetadata *metadata1 = metadata0;
+ XCTAssertEqual(metadata0, metadata1);
+ XCTAssertEqualObjects(metadata0, metadata1);
+}
+
+- (void)testNonsenseMetadataEquality {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataName : @"path/to/object",
+ };
+ FIRStorageMetadata *metadata0 = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNotEqualObjects(metadata0, @"I'm not object metadata!");
+}
+
+- (void)testMetadataEquality {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataName : @"path/to/object",
+ };
+ FIRStorageMetadata *metadata0 = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ FIRStorageMetadata *metadata1 = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNotEqual(metadata0, metadata1);
+ XCTAssertEqualObjects(metadata0, metadata1);
+}
+
+- (void)testMetadataCopy {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataName : @"path/to/object",
+ };
+ FIRStorageMetadata *metadata0 = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ FIRStorageMetadata *metadata1 = [metadata0 copy];
+ XCTAssertNotEqual(metadata0, metadata1);
+ XCTAssertEqualObjects(metadata0, metadata1);
+}
+
+- (void)testMetadataHashEquality {
+ NSDictionary *metaDict = @{
+ kFIRStorageMetadataBucket : @"bucket",
+ kFIRStorageMetadataName : @"path/to/object",
+ };
+ FIRStorageMetadata *metadata0 = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ FIRStorageMetadata *metadata1 = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNotEqual(metadata0, metadata1);
+ XCTAssertEqual([metadata0 hash], [metadata1 hash]);
+}
+
+- (void)testZuluTimeOffset {
+ NSDictionary *metaDict = @{ kFIRStorageMetadataTimeCreated : @"1992-08-07T17:22:53.108Z" };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNotNil(metadata.timeCreated);
+}
+
+- (void)testZuluZeroTimeOffset {
+ NSDictionary *metaDict = @{ kFIRStorageMetadataTimeCreated : @"1992-08-07T17:22:53.108+0000" };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNotNil(metadata.timeCreated);
+}
+
+- (void)testGoogleStandardTimeOffset {
+ NSDictionary *metaDict = @{ kFIRStorageMetadataTimeCreated : @"1992-08-07T17:22:53.108-0700" };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNotNil(metadata.timeCreated);
+}
+
+- (void)testUnspecifiedTimeOffset {
+ NSDictionary *metaDict = @{ kFIRStorageMetadataTimeCreated : @"1992-08-07T17:22:53.108-0000" };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNotNil(metadata.timeCreated);
+}
+
+- (void)testNoTimeOffset {
+ NSDictionary *metaDict = @{ kFIRStorageMetadataTimeCreated : @"1992-08-07T17:22:53.108" };
+ FIRStorageMetadata *metadata = [[FIRStorageMetadata alloc] initWithDictionary:metaDict];
+ XCTAssertNil(metadata.timeCreated);
+}
+
+@end
diff --git a/Example/Storage/Tests/Unit/FIRStoragePathTests.m b/Example/Storage/Tests/Unit/FIRStoragePathTests.m
new file mode 100644
index 0000000..017f41d
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStoragePathTests.m
@@ -0,0 +1,234 @@
+// 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/XCTest.h>
+
+#import "FIRStoragePath.h"
+
+@interface FIRStoragePathTests : XCTestCase
+
+@end
+
+@implementation FIRStoragePathTests
+
+- (void)testGSURI {
+ FIRStoragePath *path = [FIRStoragePath pathFromString:@"gs://bucket/path/to/object"];
+ XCTAssertEqualObjects(path.bucket, @"bucket");
+ XCTAssertEqualObjects(path.object, @"path/to/object");
+}
+
+- (void)testHTTPURL {
+ NSString *httpURL =
+ @"http://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object?token=signed_url_params";
+ FIRStoragePath *path = [FIRStoragePath pathFromString:httpURL];
+ XCTAssertEqualObjects(path.bucket, @"bucket");
+ XCTAssertEqualObjects(path.object, @"path/to/object");
+}
+
+- (void)testGSURINoPath {
+ FIRStoragePath *path = [FIRStoragePath pathFromString:@"gs://bucket/"];
+ XCTAssertEqualObjects(path.bucket, @"bucket");
+ XCTAssertNil(path.object);
+}
+
+- (void)testHTTPURLNoPath {
+ FIRStoragePath *path =
+ [FIRStoragePath pathFromString:@"http://firebasestorage.googleapis.com/v0/b/bucket/"];
+ XCTAssertEqualObjects(path.bucket, @"bucket");
+ XCTAssertNil(path.object);
+}
+
+- (void)testGSURINoTrailingSlash {
+ FIRStoragePath *path = [FIRStoragePath pathFromString:@"gs://bucket"];
+ XCTAssertEqualObjects(path.bucket, @"bucket");
+ XCTAssertNil(path.object);
+}
+
+- (void)testHTTPURLNoTrailingSlash {
+ FIRStoragePath *path =
+ [FIRStoragePath pathFromString:@"http://firebasestorage.googleapis.com/v0/b/bucket"];
+ XCTAssertEqualObjects(path.bucket, @"bucket");
+ XCTAssertNil(path.object);
+}
+
+- (void)testGSURIPercentEncoding {
+ FIRStoragePath *path = [FIRStoragePath pathFromString:@"gs://bucket/?/%/#"];
+ XCTAssertEqualObjects(path.bucket, @"bucket");
+ XCTAssertEqualObjects(path.object, @"?/%/#");
+}
+
+- (void)testHTTPURLPercentEncoding {
+ NSString *httpURL =
+ @"http://firebasestorage.googleapis.com/v0/b/bucket/o/%3F/%25/%23?token=signed_url_params";
+ FIRStoragePath *path = [FIRStoragePath pathFromString:httpURL];
+ XCTAssertEqualObjects(path.bucket, @"bucket");
+ XCTAssertEqualObjects(path.object, @"?/%/#");
+}
+
+- (void)testHTTPURLNoToken {
+ NSString *httpURL = @"http://firebasestorage.googleapis.com/v0/b/bucket/o/%23hashtag/no/token";
+ FIRStoragePath *path = [FIRStoragePath pathFromString:httpURL];
+ XCTAssertEqualObjects(path.bucket, @"bucket");
+ XCTAssertEqualObjects(path.object, @"#hashtag/no/token");
+}
+
+- (void)testGSURIThrowsOnNoBucket {
+ XCTAssertThrows([FIRStoragePath pathFromString:@"gs://"]);
+}
+
+- (void)testHTTPURLThrowsOnNoBucket {
+ XCTAssertThrows([FIRStoragePath pathFromString:@"http://firebasestorage.googleapis.com/"]);
+}
+
+- (void)testThrowsOnInvalidScheme {
+ NSString *ftpURL = @"ftp://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object";
+ XCTAssertThrows([FIRStoragePath pathFromString:ftpURL]);
+}
+
+- (void)testHTTPURLNilIncorrectHost {
+ NSString *httpURL = @"http://foo.google.com/v0/b/bucket/o/%3F/%25/%23?token=signed_url_params";
+ XCTAssertThrows([FIRStoragePath pathFromString:httpURL]);
+}
+
+- (void)testchildToRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *childPath = [path child:@"object"];
+ XCTAssertEqualObjects([childPath stringValue], @"gs://bucket/object");
+}
+
+- (void)testChildByAppendingNilToRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *childPath = [path child:nil];
+ XCTAssertEqualObjects([childPath stringValue], @"gs://bucket/");
+}
+
+- (void)testChildByAppendingNoPathToRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *childPath = [path child:@""];
+ XCTAssertEqualObjects([childPath stringValue], @"gs://bucket/");
+}
+
+- (void)testChildByAppendingLeadingSlashChildToRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *childPath = [path child:@"/object"];
+ XCTAssertEqualObjects([childPath stringValue], @"gs://bucket/object");
+}
+
+- (void)testChildByAppendingTrailingSlashChildToRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *childPath = [path child:@"object/"];
+ XCTAssertEqualObjects([childPath stringValue], @"gs://bucket/object");
+}
+
+- (void)testChildByAppendingLeadingAndTrailingSlashChildToRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *childPath = [path child:@"/object/"];
+ XCTAssertEqualObjects([childPath stringValue], @"gs://bucket/object");
+}
+
+- (void)testChildByAppendingMultipleChildrenToRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *childPath = [path child:@"path/to/object"];
+ XCTAssertEqualObjects([childPath stringValue], @"gs://bucket/path/to/object");
+}
+
+- (void)testChildByAppendingMultipleChildrenWithMultipleSlashesToRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *childPath = [path child:@"/path//to///object////"];
+ XCTAssertEqualObjects([childPath stringValue], @"gs://bucket/path/to/object");
+}
+
+- (void)testChildByAppendingOnlySlashesToRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *childPath = [path child:@"//////////"];
+ XCTAssertEqualObjects([childPath stringValue], @"gs://bucket/");
+}
+
+- (void)testParentAtRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *parent = [path parent];
+ XCTAssertNil(parent);
+}
+
+- (void)testParentChildPath {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"path/to/object"];
+ FIRStoragePath *parent = [path parent];
+ XCTAssertEqualObjects([parent stringValue], @"gs://bucket/path/to");
+}
+
+- (void)testParentChildPathSlashes {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"/path//to///"];
+ FIRStoragePath *parent = [path parent];
+ XCTAssertEqualObjects([parent stringValue], @"gs://bucket/path");
+}
+
+- (void)testParentChildPathOnlySlashs {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"/////"];
+ FIRStoragePath *parent = [path parent];
+ XCTAssertNil(parent);
+}
+
+- (void)testRootAtRoot {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *root = [path root];
+ XCTAssertEqualObjects([root stringValue], @"gs://bucket/");
+}
+
+- (void)testRootAtChildPath {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"path/to/object"];
+ FIRStoragePath *root = [path root];
+ XCTAssertEqualObjects([root stringValue], @"gs://bucket/");
+}
+
+- (void)testRootAtSlashPath {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"//////////"];
+ FIRStoragePath *root = [path root];
+ XCTAssertEqualObjects([root stringValue], @"gs://bucket/");
+}
+
+- (void)testCopy {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"object"];
+ FIRStoragePath *copiedPath = [path copy];
+ XCTAssertNotEqual(copiedPath, path);
+ XCTAssertEqualObjects(copiedPath, path);
+}
+
+- (void)testCopyNoBucket {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:nil object:@"object"];
+#pragma clang diagnostic pop
+ FIRStoragePath *copiedPath = [path copy];
+ XCTAssertNotEqual(copiedPath, path);
+ XCTAssertEqualObjects(copiedPath, path);
+}
+
+- (void)testCopyNoObject {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStoragePath *copiedPath = [path copy];
+ XCTAssertNotEqual(copiedPath, path);
+ XCTAssertEqualObjects(copiedPath, path);
+}
+
+- (void)testCopyNothing {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:nil object:nil];
+#pragma clang diagnostic pop
+ FIRStoragePath *copiedPath = [path copy];
+ XCTAssertNotEqual(copiedPath, path);
+ XCTAssertEqualObjects(copiedPath, path);
+}
+
+@end
diff --git a/Example/Storage/Tests/Unit/FIRStorageReferenceTests.m b/Example/Storage/Tests/Unit/FIRStorageReferenceTests.m
new file mode 100644
index 0000000..e54896c
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStorageReferenceTests.m
@@ -0,0 +1,163 @@
+// 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 "FirebaseStorage.h"
+
+#import "FIRStorageReference_Private.h"
+#import "FIRStorageTestHelpers.h"
+
+@interface FIRStorageReferenceTests : XCTestCase
+
+@property(strong, nonatomic) FIRStorage *storage;
+
+@end
+
+@implementation FIRStorageReferenceTests
+
+- (void)setUp {
+ [super setUp];
+
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"bucket");
+
+ id mockApp = OCMClassMock([FIRApp class]);
+ OCMStub([mockApp name]).andReturn(kFIRStorageAppName);
+ OCMStub([(FIRApp *)mockApp options]).andReturn(mockOptions);
+ self.storage = [FIRStorage storageForApp:mockApp];
+}
+
+- (void)tearDown {
+ self.storage = nil;
+ [super tearDown];
+}
+
+- (void)testRoot {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/path/to/object"];
+ XCTAssertEqualObjects([ref.root stringValue], @"gs://bucket/");
+}
+
+- (void)testRootWithNoPath {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/"];
+ XCTAssertEqualObjects([ref.root stringValue], @"gs://bucket/");
+}
+
+- (void)testSingleChild {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/"];
+ FIRStorageReference *childRef = [ref child:@"path"];
+ XCTAssertEqualObjects([childRef stringValue], @"gs://bucket/path");
+}
+
+- (void)testMultipleChildrenSingleString {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/"];
+ FIRStorageReference *childRef = [ref child:@"path/to/object"];
+ XCTAssertEqualObjects([childRef stringValue], @"gs://bucket/path/to/object");
+}
+
+- (void)testMultipleChildrenMultipleStrings {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/"];
+ FIRStorageReference *childRef = [ref child:@"path"];
+ childRef = [childRef child:@"to"];
+ childRef = [childRef child:@"object"];
+ XCTAssertEqualObjects([childRef stringValue], @"gs://bucket/path/to/object");
+}
+
+- (void)testSameChildDifferentRef {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/"];
+ FIRStorageReference *firstRef = [ref child:@"1"];
+ FIRStorageReference *secondRef = [ref child:@"1"];
+ XCTAssertEqualObjects([ref stringValue], @"gs://bucket/");
+ XCTAssertEqualObjects(firstRef, secondRef);
+ XCTAssertNotEqual(firstRef, secondRef);
+}
+
+- (void)testDifferentChildDifferentRef {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/"];
+ FIRStorageReference *firstRef = [ref child:@"1"];
+ FIRStorageReference *secondRef = [ref child:@"2"];
+ XCTAssertEqualObjects([ref stringValue], @"gs://bucket/");
+ XCTAssertNotEqual(firstRef, secondRef);
+}
+
+- (void)testChildWithTrailingSlash {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/path/to/object/"];
+ XCTAssertEqualObjects([ref stringValue], @"gs://bucket/path/to/object");
+}
+
+- (void)testChildWithLeadingSlash {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket//path/to/object/"];
+ XCTAssertEqualObjects([ref stringValue], @"gs://bucket/path/to/object");
+}
+
+- (void)testChildCompressSlashes {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket//path///to////object////"];
+ XCTAssertEqualObjects([ref stringValue], @"gs://bucket/path/to/object");
+}
+
+- (void)testParent {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/path/to/object"];
+ FIRStorageReference *parentRef = [ref parent];
+ XCTAssertEqualObjects([parentRef stringValue], @"gs://bucket/path/to");
+}
+
+- (void)testParentToRoot {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/path"];
+ FIRStorageReference *parentRef = [ref parent];
+ XCTAssertEqualObjects([parentRef stringValue], @"gs://bucket/");
+}
+
+- (void)testParentToRootTrailingSlash {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/path/"];
+ FIRStorageReference *parentRef = [ref parent];
+ XCTAssertEqualObjects([parentRef stringValue], @"gs://bucket/");
+}
+
+- (void)testParentAtRoot {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/"];
+ FIRStorageReference *parentRef = [ref parent];
+ XCTAssertNil(parentRef);
+}
+
+- (void)testBucket {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/path/to/object"];
+ XCTAssertEqualObjects(ref.bucket, @"bucket");
+}
+
+- (void)testName {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/path/to/object"];
+ XCTAssertEqualObjects(ref.name, @"object");
+}
+
+- (void)testNameNoObject {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/"];
+ XCTAssertEqualObjects(ref.name, @"");
+}
+
+- (void)testFullPath {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/path/to/object"];
+ XCTAssertEqualObjects(ref.fullPath, @"path/to/object");
+}
+
+- (void)testFullPathNoObject {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/"];
+ XCTAssertEqualObjects(ref.fullPath, @"");
+}
+
+- (void)testCopy {
+ FIRStorageReference *ref = [self.storage referenceForURL:@"gs://bucket/"];
+ FIRStorageReference *copiedRef = [ref copy];
+ XCTAssertEqualObjects(ref, copiedRef);
+ XCTAssertNotEqual(ref, copiedRef);
+}
+
+@end
diff --git a/Example/Storage/Tests/Unit/FIRStorageTestHelpers.h b/Example/Storage/Tests/Unit/FIRStorageTestHelpers.h
new file mode 100644
index 0000000..4489c07
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStorageTestHelpers.h
@@ -0,0 +1,127 @@
+/*
+ * 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 "FIRAppInternal.h"
+
+#import <Foundation/Foundation.h>
+#import <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageErrors.h"
+#import "FIRStorageMetadata.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageReference_Private.h"
+#import "FIRStorageTask.h"
+#import "FIRStorageTask_Private.h"
+#import "FIRStorageTokenAuthorizer.h"
+#import "FIRStorageUtils.h"
+
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+FOUNDATION_EXPORT NSString *const kGoogleHTTPErrorDomain;
+FOUNDATION_EXPORT NSString *const kHTTPVersion;
+FOUNDATION_EXPORT NSString *const kUnauthenticatedResponseString;
+FOUNDATION_EXPORT NSString *const kUnauthorizedResponseString;
+FOUNDATION_EXPORT NSString *const kNotFoundResponseString;
+FOUNDATION_EXPORT NSString *const kInvalidJSONResponseString;
+FOUNDATION_EXPORT NSString *const kFIRStorageValidURL;
+FOUNDATION_EXPORT NSString *const kFIRStorageNotFoundURL;
+FOUNDATION_EXPORT NSString *const kFIRStorageTestAuthToken;
+FOUNDATION_EXPORT NSString *const kFIRStorageAppName;
+
+/**
+ * Standard timeout for all async tests.
+ */
+static NSTimeInterval kExpectationTimeoutSeconds = 10;
+
+@interface FIRStorageTestHelpers : NSObject
+
+/**
+ * Returns a valid URL for an object stored.
+ */
++ (NSURL *)objectURL;
+
+/**
+ * Returns a valid URL for a bucket.
+ */
++ (NSURL *)bucketURL;
+
+/**
+ * Returns a valid URL for an object not found in the current storage bucket.
+ */
++ (NSURL *)notFoundURL;
+
+/**
+ * Returns a valid FIRStoragePath for an object stored.
+ */
++ (FIRStoragePath *)objectPath;
+
+/**
+ * Returns a valid FIRStoragePath for a bucket (no object).
+ */
++ (FIRStoragePath *)bucketPath;
+
+/**
+ * Returns a valid FIRStoragePath for an object not found in the current storage bucket.
+ */
++ (FIRStoragePath *)notFoundPath;
+
+/**
+ * Returns a successful response block.
+ */
++ (GTMSessionFetcherTestBlock)successBlock;
+
+/**
+ * Returns a successful response block containing object metadata.
+ * @param metadata Metadata returned in the request.
+ */
++ (GTMSessionFetcherTestBlock)successBlockWithMetadata:(nullable FIRStorageMetadata *)metadata;
+
+/**
+ * Returns a unsuccessful response block due to improper authentication.
+ */
++ (GTMSessionFetcherTestBlock)unauthenticatedBlock;
+
+/**
+ * Returns a unsuccessful response block due to improper authorization.
+ */
++ (GTMSessionFetcherTestBlock)unauthorizedBlock;
+
+/**
+ * Returns a unsuccessful response block due the object not being found.
+ */
++ (GTMSessionFetcherTestBlock)notFoundBlock;
+
+/**
+ * Returns a unsuccessful response block due invalid JSON returned by the server.
+ */
++ (GTMSessionFetcherTestBlock)invalidJSONBlock;
+
+/**
+ * Waits for the given test case to time out by wrapping -waitForExpectation.
+ */
++ (void)waitForExpectation:(id)test;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Storage/Tests/Unit/FIRStorageTestHelpers.m b/Example/Storage/Tests/Unit/FIRStorageTestHelpers.m
new file mode 100644
index 0000000..fef67ef
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStorageTestHelpers.m
@@ -0,0 +1,128 @@
+// 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 "FIRStorageTestHelpers.h"
+
+NSString *const kGoogleHTTPErrorDomain = @"com.google.HTTPStatus";
+NSString *const kHTTPVersion = @"HTTP/1.1";
+NSString *const kUnauthenticatedResponseString =
+ @"<html><body><p>User not authenticated. Authentication via Authorization header required. "
+ @"Authorization Header does not match expected format of 'Authorization: Firebase "
+ @"<JWT>'.</p></body></html>";
+NSString *const kUnauthorizedResponseString =
+ @"<html><body><p>User not authorized. Authentication via Authorization header required. "
+ @"Authorization Header does not match expected format of 'Authorization: Firebase "
+ @"<JWT>'.</p></body></html>";
+NSString *const kNotFoundResponseString = @"<html><body><p>Object not found.</p></body></html>";
+NSString *const kInvalidJSONResponseString = @"This is not a JSON object";
+NSString *const kFIRStorageObjectURL =
+ @"https://firebasestorage.googleapis.com/v0/b/bucket/o/object";
+NSString *const kFIRStorageBucketURL = @"https://firebasestorage.googleapis.com/v0/b/bucket/o";
+NSString *const kFIRStorageNotFoundURL =
+ @"https://firebasestorage.googleapis.com/v0/b/bucket/o/i/dont/exist";
+NSString *const kFIRStorageTestAuthToken = @"1234-5678-9012-3456-7890";
+NSString *const kFIRStorageAppName = @"app";
+
+@implementation FIRStorageTestHelpers
+
++ (NSURL *)objectURL {
+ return [NSURL URLWithString:kFIRStorageObjectURL];
+}
+
++ (NSURL *)bucketURL {
+ return [NSURL URLWithString:kFIRStorageBucketURL];
+}
+
++ (NSURL *)notFoundURL {
+ return [NSURL URLWithString:kFIRStorageNotFoundURL];
+}
+
++ (FIRStoragePath *)objectPath {
+ return [FIRStoragePath pathFromString:kFIRStorageObjectURL];
+}
+
++ (FIRStoragePath *)bucketPath {
+ return [FIRStoragePath pathFromString:kFIRStorageBucketURL];
+}
+
++ (FIRStoragePath *)notFoundPath {
+ return [FIRStoragePath pathFromString:kFIRStorageNotFoundURL];
+}
+
++ (GTMSessionFetcherTestBlock)successBlock {
+ return [FIRStorageTestHelpers successBlockWithMetadata:nil];
+}
+
++ (GTMSessionFetcherTestBlock)successBlockWithMetadata:(nullable FIRStorageMetadata *)metadata {
+ NSData *data;
+ if (metadata) {
+ data = [NSData frs_dataFromJSONDictionary:[metadata dictionaryRepresentation]];
+ }
+ return [FIRStorageTestHelpers blockForData:data statusCode:200];
+}
+
++ (GTMSessionFetcherTestBlock)unauthenticatedBlock {
+ NSData *data = [kUnauthenticatedResponseString dataUsingEncoding:NSUTF8StringEncoding];
+ return [FIRStorageTestHelpers blockForData:data statusCode:401];
+}
+
++ (GTMSessionFetcherTestBlock)unauthorizedBlock {
+ NSData *data = [kUnauthorizedResponseString dataUsingEncoding:NSUTF8StringEncoding];
+ return [FIRStorageTestHelpers blockForData:data statusCode:403];
+}
+
++ (GTMSessionFetcherTestBlock)notFoundBlock {
+ NSData *data = [kNotFoundResponseString dataUsingEncoding:NSUTF8StringEncoding];
+ return [FIRStorageTestHelpers blockForData:data statusCode:404];
+}
+
++ (GTMSessionFetcherTestBlock)invalidJSONBlock {
+ NSData *data = [kInvalidJSONResponseString dataUsingEncoding:NSUTF8StringEncoding];
+ return [FIRStorageTestHelpers blockForData:data statusCode:200];
+}
+
+#pragma mark - Private methods
+
++ (GTMSessionFetcherTestBlock)blockForData:(nullable NSData *)data statusCode:(NSInteger)code {
+ GTMSessionFetcherTestBlock block =
+ ^(GTMSessionFetcher *fetcher, GTMSessionFetcherTestResponse response) {
+ NSHTTPURLResponse *httpResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:fetcher.request.URL
+ statusCode:code
+ HTTPVersion:kHTTPVersion
+ headerFields:nil];
+ NSError *error;
+ if (code >= 400) {
+ NSDictionary *userInfo;
+ if (data) {
+ userInfo = @{ @"data" : data };
+ }
+ error = [NSError errorWithDomain:kGoogleHTTPErrorDomain code:code userInfo:userInfo];
+ }
+
+ response(httpResponse, data, error);
+ };
+ return block;
+}
+
++ (void)waitForExpectation:(id)test {
+ [test waitForExpectationsWithTimeout:kExpectationTimeoutSeconds
+ handler:^(NSError *_Nullable error) {
+ if (error) {
+ NSLog(@"Error: %@", error);
+ }
+ }];
+}
+
+@end
diff --git a/Example/Storage/Tests/Unit/FIRStorageTests.m b/Example/Storage/Tests/Unit/FIRStorageTests.m
new file mode 100644
index 0000000..1b45295
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStorageTests.m
@@ -0,0 +1,214 @@
+// 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 "FIRStorageTestHelpers.h"
+
+#import "FIRStorageReference.h"
+#import "FIRStorageReference_Private.h"
+#import "FIRStorage_Private.h"
+
+@interface FIRStorageTests : XCTestCase
+
+@property(strong, nonatomic) id app;
+
+@end
+
+@implementation FIRStorageTests
+
+- (void)setUp {
+ [super setUp];
+
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"bucket");
+
+ self.app = OCMClassMock([FIRApp class]);
+ OCMStub([self.app name]).andReturn(kFIRStorageAppName);
+ OCMStub([(FIRApp *)self.app options]).andReturn(mockOptions);
+}
+
+- (void)tearDown {
+ self.app = nil;
+ [super tearDown];
+}
+
+- (void)testBucketNotEnforced {
+ FIROptions * mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"");
+ FIRApp *app = OCMClassMock([FIRApp class]);
+ OCMStub([app name]).andReturn(kFIRStorageAppName);
+ OCMStub([(FIRApp *)app options]).andReturn(mockOptions);
+
+ FIRStorage *storage = [FIRStorage storageForApp:app];
+ [storage referenceForURL:@"gs://benwu-test1.storage.firebase.com/child"];
+ [storage referenceForURL:@"gs://benwu-test2.storage.firebase.com/child"];
+}
+
+- (void)testBucketEnforced {
+ FIRStorage *storage = [FIRStorage storageForApp:self.app
+ URL:@"gs://benwu-test1.storage.firebase.com"];
+ [storage referenceForURL:@"gs://benwu-test1.storage.firebase.com/child"];
+ storage = [FIRStorage storageForApp:self.app URL:@"gs://benwu-test1.storage.firebase.com/"];
+ [storage referenceForURL:@"gs://benwu-test1.storage.firebase.com/child"];
+ XCTAssertThrows([storage referenceForURL:@"gs://benwu-test2.storage.firebase.com/child"]);
+}
+
+- (void) testInitWithCustomUrl {
+ FIRStorage *storage = [FIRStorage storageForApp:self.app URL:@"gs://foo-bar.appspot.com"];
+ XCTAssertEqualObjects(@"gs://foo-bar.appspot.com/", [[storage reference] description]);
+ storage = [FIRStorage storageForApp:self.app URL:@"gs://foo-bar.appspot.com/"];
+ XCTAssertEqualObjects(@"gs://foo-bar.appspot.com/", [[storage reference] description]);
+}
+
+- (void) testInitWithWrongScheme {
+ XCTAssertThrows([FIRStorage storageForApp:self.app URL:@"http://foo-bar.appspot.com"]);
+}
+
+- (void) testInitWithNoScheme {
+ XCTAssertThrows([FIRStorage storageForApp:self.app URL:@"foo-bar.appspot.com"]);
+}
+
+- (void) testInitWithNilURL {
+ XCTAssertThrows([FIRStorage storageForApp:self.app URL:nil]);
+}
+
+- (void) testInitWithPath {
+ XCTAssertThrows([FIRStorage storageForApp:self.app URL:@"gs://foo-bar.appspot.com/child"]);
+}
+
+- (void) testInitWithDefaultAndCustomUrl {
+ FIRStorage *customInstance = [FIRStorage storageForApp:self.app
+ URL:@"gs://foo-bar.appspot.com"];
+ FIRStorage *defaultInstance = [FIRStorage storageForApp:self.app];
+ XCTAssertEqualObjects(@"gs://foo-bar.appspot.com/", [[customInstance reference] description]);
+ XCTAssertEqualObjects(@"gs://bucket/", [[defaultInstance reference] description]);
+}
+
+- (void)testStorageDefaultApp {
+ FIRStorage *storage = [FIRStorage storageForApp:self.app];
+ XCTAssertEqualObjects(storage.app.name, ((FIRApp *)self.app).name);
+ XCTAssertNotNil(storage.fetcherServiceForApp);
+}
+
+- (void)testStorageCustomApp {
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"bucket");
+ id secondApp = OCMClassMock([FIRApp class]);
+ OCMStub([secondApp name]).andReturn(@"secondApp");
+ OCMStub([(FIRApp *)secondApp options]).andReturn(mockOptions);
+ FIRStorage *storage = [FIRStorage storageForApp:secondApp];
+ XCTAssertNotEqual(storage.app.name, ((FIRApp *)self.app).name);
+ XCTAssertNotNil(storage.fetcherServiceForApp);
+ XCTAssertNotEqualObjects(storage.fetcherServiceForApp,
+ [FIRStorage storageForApp:self.app].fetcherServiceForApp);
+}
+
+- (void)testStorageNoBucketInConfig {
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(nil);
+ id secondApp = OCMClassMock([FIRApp class]);
+ OCMStub([secondApp name]).andReturn(@"secondApp");
+ OCMStub([(FIRApp *)secondApp options]).andReturn(mockOptions);
+ XCTAssertThrows([FIRStorage storageForApp:secondApp]);
+}
+
+- (void)testStorageEmptyBucketInConfig {
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"");
+ id secondApp = OCMClassMock([FIRApp class]);
+ OCMStub([secondApp name]).andReturn(@"secondApp");
+ OCMStub([(FIRApp *)secondApp options]).andReturn(mockOptions);
+ FIRStorage *storage = [FIRStorage storageForApp:secondApp];
+ FIRStorageReference *storageRef = [storage referenceForURL:@"gs://bucket/path/to/object"];
+ XCTAssertEqualObjects(storageRef.bucket, @"bucket");
+}
+
+- (void)testStorageWrongBucketInConfig {
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"notMyBucket");
+ id secondApp = OCMClassMock([FIRApp class]);
+ OCMStub([secondApp name]).andReturn(@"secondApp");
+ OCMStub([(FIRApp *)secondApp options]).andReturn(mockOptions);
+ FIRStorage *storage = [FIRStorage storageForApp:secondApp];
+ XCTAssertEqualObjects([(FIRApp *)secondApp options].storageBucket, @"notMyBucket");
+ XCTAssertThrows([storage referenceForURL:@"gs://bucket/path/to/object"]);
+}
+
+- (void)testRefDefaultApp {
+ FIRStorageReference *convenienceRef =
+ [[FIRStorage storageForApp:self.app] referenceForURL:@"gs://bucket/path/to/object"];
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"path/to/object"];
+ FIRStorageReference *builtRef =
+ [[FIRStorageReference alloc] initWithStorage:[FIRStorage storageForApp:self.app] path:path];
+ XCTAssertEqualObjects([convenienceRef description], [builtRef description]);
+ XCTAssertEqualObjects(convenienceRef.storage.app, builtRef.storage.app);
+}
+
+- (void)testRefCustomApp {
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"bucket");
+ id secondApp = OCMClassMock([FIRApp class]);
+ OCMStub([secondApp name]).andReturn(@"secondApp");
+ OCMStub([(FIRApp *)secondApp options]).andReturn(mockOptions);
+ FIRStorageReference *convenienceRef =
+ [[FIRStorage storageForApp:secondApp] referenceForURL:@"gs://bucket/path/to/object"];
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"path/to/object"];
+ FIRStorageReference *builtRef =
+ [[FIRStorageReference alloc] initWithStorage:[FIRStorage storageForApp:secondApp] path:path];
+ XCTAssertEqualObjects([convenienceRef description], [builtRef description]);
+ XCTAssertEqualObjects(convenienceRef.storage.app, builtRef.storage.app);
+}
+
+- (void)testRootRefDefaultApp {
+ FIRStorageReference *convenienceRef = [[FIRStorage storageForApp:self.app] reference];
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ FIRStorageReference *builtRef =
+ [[FIRStorageReference alloc] initWithStorage:[FIRStorage storageForApp:self.app] path:path];
+ XCTAssertEqualObjects([convenienceRef description], [builtRef description]);
+ XCTAssertEqualObjects(convenienceRef.storage.app, builtRef.storage.app);
+}
+
+- (void)testRefWithPathDefaultApp {
+ FIRStorageReference *convenienceRef =
+ [[FIRStorage storageForApp:self.app] referenceWithPath:@"path/to/object"];
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"path/to/object"];
+ FIRStorageReference *builtRef =
+ [[FIRStorageReference alloc] initWithStorage:[FIRStorage storageForApp:self.app] path:path];
+ XCTAssertEqualObjects([convenienceRef description], [builtRef description]);
+ XCTAssertEqualObjects(convenienceRef.storage.app, builtRef.storage.app);
+}
+
+- (void)testEqual {
+ FIRStorage *storage = [FIRStorage storageForApp:self.app];
+ FIRStorage *copy = [storage copy];
+ XCTAssertEqualObjects(storage.app.name, copy.app.name);
+}
+
+- (void)testNotEqual {
+ FIRStorage *storage = [FIRStorage storageForApp:self.app];
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"bucket");
+ id secondApp = OCMClassMock([FIRApp class]);
+ OCMStub([secondApp name]).andReturn(@"secondApp");
+ OCMStub([(FIRApp *)secondApp options]).andReturn(mockOptions);
+ FIRStorage *secondStorage = [FIRStorage storageForApp:secondApp];
+ XCTAssertNotEqualObjects(storage, secondStorage);
+}
+
+- (void)testHash {
+ FIRStorage *storage = [FIRStorage storageForApp:self.app];
+ FIRStorage *copy = [storage copy];
+ XCTAssertEqual([storage hash], [copy hash]);
+}
+
+@end
diff --git a/Example/Storage/Tests/Unit/FIRStorageTokenAuthorizerTests.m b/Example/Storage/Tests/Unit/FIRStorageTokenAuthorizerTests.m
new file mode 100644
index 0000000..e98ffbb
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStorageTokenAuthorizerTests.m
@@ -0,0 +1,226 @@
+// 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 "FIRStorageTestHelpers.h"
+
+@interface FIRStorageTokenAuthorizerTests : XCTestCase
+
+@property(strong, nonatomic) GTMSessionFetcher *fetcher;
+@property(strong, nonatomic) id mockApp;
+
+@end
+
+@implementation FIRStorageTokenAuthorizerTests
+
+- (void)setUp {
+ [super setUp];
+ NSURLRequest *fetchRequest = [NSURLRequest requestWithURL:[FIRStorageTestHelpers objectURL]];
+ self.fetcher = [GTMSessionFetcher fetcherWithRequest:fetchRequest];
+
+ self.mockApp = OCMClassMock([FIRApp class]);
+ OCMStub([self.mockApp getTokenImplementation])
+ .andReturn(^{
+ });
+ FIRTokenCallback mockCallback =
+ [OCMArg invokeBlockWithArgs:kFIRStorageTestAuthToken, [NSNull null], nil];
+ OCMStub([self.mockApp getTokenForcingRefresh:NO withCallback:mockCallback]);
+ GTMSessionFetcherService *fetcherService = [[GTMSessionFetcherService alloc] init];
+ self.fetcher.authorizer =
+ [[FIRStorageTokenAuthorizer alloc] initWithApp:self.mockApp fetcherService:fetcherService];
+}
+
+- (void)tearDown {
+ self.fetcher = nil;
+ self.mockApp = nil;
+ [super tearDown];
+}
+
+- (void)testSuccessfulAuth {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testSuccessfulAuth"];
+
+ self.fetcher.testBlock = ^(GTMSessionFetcher *fetcher, GTMSessionFetcherTestResponse response) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-retain-cycles"
+ XCTAssertTrue([self.fetcher.authorizer isAuthorizedRequest:fetcher.request]);
+#pragma clang diagnostic pop
+ NSHTTPURLResponse *httpResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:fetcher.request.URL
+ statusCode:200
+ HTTPVersion:kHTTPVersion
+ headerFields:nil];
+ response(httpResponse, nil, nil);
+ };
+
+ [self.fetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data,
+ NSError *_Nullable error) {
+ NSDictionary<NSString *, NSString *> *headers = self.fetcher.request.allHTTPHeaderFields;
+ NSString *authHeader = [headers objectForKey:@"Authorization"];
+ NSString *firebaseToken =
+ [NSString stringWithFormat:kFIRStorageAuthTokenFormat, kFIRStorageTestAuthToken];
+ XCTAssertEqualObjects(authHeader, firebaseToken);
+ [expectation fulfill];
+ }];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulAuth {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUnsuccessfulAuth"];
+
+ NSError *authError = [NSError errorWithDomain:FIRStorageErrorDomain
+ code:FIRStorageErrorCodeUnauthenticated
+ userInfo:nil];
+ id unsuccessfulApp = OCMClassMock([FIRApp class]);
+ OCMStub([unsuccessfulApp getTokenImplementation])
+ .andReturn(^{
+ });
+ FIRTokenCallback mockCallback = [OCMArg invokeBlockWithArgs:[NSNull null], authError, nil];
+ OCMStub([unsuccessfulApp getTokenForcingRefresh:NO withCallback:mockCallback]);
+ GTMSessionFetcherService *fetcherService = [[GTMSessionFetcherService alloc] init];
+ self.fetcher.authorizer =
+ [[FIRStorageTokenAuthorizer alloc] initWithApp:unsuccessfulApp fetcherService:fetcherService];
+
+ self.fetcher.testBlock = ^(GTMSessionFetcher *fetcher, GTMSessionFetcherTestResponse response) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-retain-cycles"
+ XCTAssertEqual([self.fetcher.authorizer isAuthorizedRequest:fetcher.request], NO);
+#pragma cland diagnostic pop
+ NSHTTPURLResponse *httpResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:fetcher.request.URL
+ statusCode:401
+ HTTPVersion:kHTTPVersion
+ headerFields:nil];
+ response(httpResponse, nil, authError);
+ };
+
+ [self.fetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data,
+ NSError *_Nullable error) {
+ NSDictionary<NSString *, NSString *> *headers = self.fetcher.request.allHTTPHeaderFields;
+ NSString *authHeader = [headers objectForKey:@"Authorization"];
+ XCTAssertNil(authHeader);
+ XCTAssertEqualObjects(error.domain, FIRStorageErrorDomain);
+ XCTAssertEqual(error.code, FIRStorageErrorCodeUnauthenticated);
+ [expectation fulfill];
+ }];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testSuccessfulUnauthenticatedAuth {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testSuccessfulUnauthenticatedAuth"];
+
+ // Note that self.mockApp is left with null properties--this simulates no token present
+ self.mockApp = OCMClassMock([FIRApp class]);
+ GTMSessionFetcherService *fetcherService = [[GTMSessionFetcherService alloc] init];
+ self.fetcher.authorizer =
+ [[FIRStorageTokenAuthorizer alloc] initWithApp:self.mockApp fetcherService:fetcherService];
+
+ self.fetcher.testBlock = ^(GTMSessionFetcher *fetcher, GTMSessionFetcherTestResponse response) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-retain-cycles"
+ XCTAssertFalse([self.fetcher.authorizer isAuthorizedRequest:fetcher.request]);
+#pragma cland diagnostic pop
+ NSHTTPURLResponse *httpResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:fetcher.request.URL
+ statusCode:200
+ HTTPVersion:kHTTPVersion
+ headerFields:nil];
+ response(httpResponse, nil, nil);
+ };
+
+ [self.fetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data,
+ NSError *_Nullable error) {
+ NSDictionary<NSString *, NSString *> *headers = self.fetcher.request.allHTTPHeaderFields;
+ NSString *authHeader = [headers objectForKey:@"Authorization"];
+ XCTAssertNil(authHeader);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testIsAuthorizing {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testIsAuthorizing"];
+
+ self.fetcher.testBlock = ^(GTMSessionFetcher *fetcher, GTMSessionFetcherTestResponse response) {
+ XCTAssertFalse([fetcher.authorizer isAuthorizingRequest:fetcher.request]);
+ NSHTTPURLResponse *httpResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:fetcher.request.URL
+ statusCode:200
+ HTTPVersion:kHTTPVersion
+ headerFields:nil];
+ response(httpResponse, nil, nil);
+ };
+
+ [self.fetcher
+ beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testStopAuthorizingNoop {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testStopAuthorizingNoop"];
+
+ self.fetcher.testBlock = ^(GTMSessionFetcher *fetcher, GTMSessionFetcherTestResponse response) {
+ // Since both of these are noops, we expect that invoking them
+ // will still result in successful authentication
+ [fetcher.authorizer stopAuthorization];
+ [fetcher.authorizer stopAuthorizationForRequest:fetcher.request];
+ NSHTTPURLResponse *httpResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:fetcher.request.URL
+ statusCode:200
+ HTTPVersion:kHTTPVersion
+ headerFields:nil];
+ response(httpResponse, nil, nil);
+ };
+
+ [self.fetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data,
+ NSError *_Nullable error) {
+ NSDictionary<NSString *, NSString *> *headers = self.fetcher.request.allHTTPHeaderFields;
+ NSString *authHeader = [headers objectForKey:@"Authorization"];
+ NSString *firebaseToken =
+ [NSString stringWithFormat:kFIRStorageAuthTokenFormat, kFIRStorageTestAuthToken];
+ XCTAssertEqualObjects(authHeader, firebaseToken);
+ [expectation fulfill];
+ }];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testEmail {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testEmail"];
+
+ self.fetcher.testBlock = ^(GTMSessionFetcher *fetcher, GTMSessionFetcherTestResponse response) {
+ XCTAssertNil([fetcher.authorizer userEmail]);
+ NSHTTPURLResponse *httpResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:fetcher.request.URL
+ statusCode:200
+ HTTPVersion:kHTTPVersion
+ headerFields:nil];
+ response(httpResponse, nil, nil);
+ };
+
+ [self.fetcher
+ beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+@end
diff --git a/Example/Storage/Tests/Unit/FIRStorageUpdateMetadataTests.m b/Example/Storage/Tests/Unit/FIRStorageUpdateMetadataTests.m
new file mode 100644
index 0000000..a85d181
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStorageUpdateMetadataTests.m
@@ -0,0 +1,199 @@
+// 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 "FIRStorageTestHelpers.h"
+#import "FIRStorageUpdateMetadataTask.h"
+
+@interface FIRStorageUpdateMetadataTests : XCTestCase
+
+@property(strong, nonatomic) GTMSessionFetcherService *fetcherService;
+@property(strong, nonatomic) FIRStorageMetadata *metadata;
+@property(strong, nonatomic) FIRStorage *storage;
+@property(strong, nonatomic) id mockApp;
+
+@end
+
+@implementation FIRStorageUpdateMetadataTests
+
+- (void)setUp {
+ [super setUp];
+
+ NSDictionary *metadataDict = @{ @"bucket" : @"bucket", @"name" : @"path/to/object" };
+ self.metadata = [[FIRStorageMetadata alloc] initWithDictionary:metadataDict];
+
+ id mockOptions = OCMClassMock([FIROptions class]);
+ OCMStub([mockOptions storageBucket]).andReturn(@"bucket.appspot.com");
+
+ self.mockApp = OCMClassMock([FIRApp class]);
+ OCMStub([self.mockApp name]).andReturn(kFIRStorageAppName);
+ OCMStub([(FIRApp *)self.mockApp options]).andReturn(mockOptions);
+
+ self.fetcherService = [[GTMSessionFetcherService alloc] init];
+ self.fetcherService.authorizer =
+ [[FIRStorageTokenAuthorizer alloc] initWithApp:self.mockApp
+ fetcherService:self.fetcherService];
+
+ self.storage = [FIRStorage storageForApp:self.mockApp];
+}
+
+- (void)tearDown {
+ self.fetcherService = nil;
+ self.storage = nil;
+ self.mockApp = nil;
+ [super tearDown];
+}
+
+- (void)testFetcherConfiguration {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testSuccessfulFetch"];
+
+ self.fetcherService.testBlock = ^(GTMSessionFetcher *fetcher,
+ GTMSessionFetcherTestResponse response) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-retain-cycles"
+ XCTAssertEqualObjects(fetcher.request.URL, [FIRStorageTestHelpers objectURL]);
+ XCTAssertEqualObjects(fetcher.request.HTTPMethod, @"PATCH");
+ NSData *bodyData = [NSData frs_dataFromJSONDictionary:[self.metadata dictionaryRepresentation]];
+ XCTAssertEqualObjects(fetcher.request.HTTPBody, bodyData);
+ NSDictionary *HTTPHeaders = fetcher.request.allHTTPHeaderFields;
+ XCTAssertEqualObjects(HTTPHeaders[@"Content-Type"], @"application/json; charset=UTF-8");
+ XCTAssertEqualObjects(HTTPHeaders[@"Content-Length"], [@(bodyData.length) stringValue]);
+#pragma clang diagnostic pop
+ NSHTTPURLResponse *httpResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:fetcher.request.URL
+ statusCode:200
+ HTTPVersion:kHTTPVersion
+ headerFields:nil];
+ response(httpResponse, nil, nil);
+ };
+
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageUpdateMetadataTask *task = [[FIRStorageUpdateMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ metadata:self.metadata
+ completion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testSuccessfulFetch {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testSuccessfulFetch"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers successBlockWithMetadata:self.metadata];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageUpdateMetadataTask *task = [[FIRStorageUpdateMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ metadata:self.metadata
+ completion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) {
+ XCTAssertEqualObjects(self.metadata.bucket, metadata.bucket);
+ XCTAssertEqualObjects(self.metadata.name, metadata.name);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchUnauthenticated {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchUnauthenticated"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers unauthenticatedBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageUpdateMetadataTask *task = [[FIRStorageUpdateMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ metadata:self.metadata
+ completion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) {
+ XCTAssertNil(metadata);
+ XCTAssertEqual(error.code, FIRStorageErrorCodeUnauthenticated);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchUnauthorized {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchUnauthorized"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers unauthorizedBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageUpdateMetadataTask *task = [[FIRStorageUpdateMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ metadata:self.metadata
+ completion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) {
+ XCTAssertNil(metadata);
+ XCTAssertEqual(error.code, FIRStorageErrorCodeUnauthorized);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchObjectDoesntExist {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchObjectDoesntExist"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers notFoundBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers notFoundPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageUpdateMetadataTask *task = [[FIRStorageUpdateMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ metadata:self.metadata
+ completion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) {
+ XCTAssertNil(metadata);
+ XCTAssertEqual(error.code, FIRStorageErrorCodeObjectNotFound);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+- (void)testUnsuccessfulFetchBadJSON {
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUnsuccessfulFetchBadJSON"];
+
+ self.fetcherService.testBlock = [FIRStorageTestHelpers invalidJSONBlock];
+ FIRStoragePath *path = [FIRStorageTestHelpers objectPath];
+ FIRStorageReference *ref = [[FIRStorageReference alloc] initWithStorage:self.storage path:path];
+ FIRStorageUpdateMetadataTask *task = [[FIRStorageUpdateMetadataTask alloc]
+ initWithReference:ref
+ fetcherService:self.fetcherService
+ metadata:self.metadata
+ completion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) {
+ XCTAssertNil(metadata);
+ XCTAssertEqual(error.code, FIRStorageErrorCodeUnknown);
+ [expectation fulfill];
+ }];
+ [task enqueue];
+
+ [FIRStorageTestHelpers waitForExpectation:self];
+}
+
+@end
diff --git a/Example/Storage/Tests/Unit/FIRStorageUtilsTests.m b/Example/Storage/Tests/Unit/FIRStorageUtilsTests.m
new file mode 100644
index 0000000..8bf4c7e
--- /dev/null
+++ b/Example/Storage/Tests/Unit/FIRStorageUtilsTests.m
@@ -0,0 +1,125 @@
+// 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/XCTest.h>
+
+#import "FIRStorageUtils.h"
+
+#import "FIRStoragePath.h"
+
+@interface FIRStorageUtilsTests : XCTestCase
+
+@end
+
+@implementation FIRStorageUtilsTests
+
+- (void)testCommonExtensionToMIMEType {
+ NSDictionary<NSString *, NSString *> *extensionToMIMEType = @{
+ @"txt" : @"text/plain",
+ @"png" : @"image/png",
+ @"mp3" : @"audio/mpeg",
+ @"mov" : @"video/quicktime",
+ @"gif" : @"image/gif"
+ };
+ [extensionToMIMEType
+ enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull extension, NSString *_Nonnull MIMEType,
+ BOOL *_Nonnull stop) {
+ XCTAssertEqualObjects([FIRStorageUtils MIMETypeForExtension:extension], MIMEType);
+ }];
+}
+
+- (void)testParseGoodDataToDict {
+ NSString *JSONString = @"{\"hello\" : \"world\"}";
+ NSData *JSONData = [JSONString dataUsingEncoding:NSUTF8StringEncoding];
+ NSDictionary *JSONDictionary = [NSDictionary frs_dictionaryFromJSONData:JSONData];
+ NSDictionary *expectedDictionary = @{ @"hello" : @"world" };
+ XCTAssertEqualObjects(JSONDictionary, expectedDictionary);
+}
+
+- (void)testParseBadDataToDict {
+ NSString *JSONString = @"Invalid JSON Object";
+ NSData *JSONData = [JSONString dataUsingEncoding:NSUTF8StringEncoding];
+ NSDictionary *JSONDictionary = [NSDictionary frs_dictionaryFromJSONData:JSONData];
+ XCTAssertNil(JSONDictionary);
+}
+
+- (void)testParseNilToDict {
+ NSDictionary *JSONDictionary = [NSDictionary frs_dictionaryFromJSONData:nil];
+ XCTAssertNil(JSONDictionary);
+}
+
+- (void)testParseGoodDictToData {
+ NSDictionary *JSONDictionary = @{ @"hello" : @"world" };
+ NSData *expectedData = [NSData frs_dataFromJSONDictionary:JSONDictionary];
+ NSString *JSONString = [[NSString alloc] initWithData:expectedData encoding:NSUTF8StringEncoding];
+ NSString *expectedString = @"{\"hello\":\"world\"}";
+ XCTAssertEqualObjects(JSONString, expectedString);
+}
+
+- (void)testParseNilToData {
+ NSData *JSONData = [NSData frs_dataFromJSONDictionary:nil];
+ XCTAssertNil(JSONData);
+}
+
+- (void)testNilDictToQueryString {
+ NSDictionary *params;
+ NSString *queryString = [FIRStorageUtils queryStringForDictionary:params];
+ XCTAssertEqualObjects(queryString, @"");
+}
+
+- (void)testEmptyDictToQueryString {
+ NSDictionary *params = @{};
+ NSString *queryString = [FIRStorageUtils queryStringForDictionary:params];
+ XCTAssertEqualObjects(queryString, @"");
+}
+
+- (void)testSingleItemToQueryString {
+ NSDictionary *params = @{ @"foo" : @"bar" };
+ NSString *queryString = [FIRStorageUtils queryStringForDictionary:params];
+ XCTAssertEqualObjects(queryString, @"foo=bar");
+}
+
+- (void)testMultiItemDictToQueryString {
+ NSDictionary *params = @{ @"foo" : @"bar", @"baz" : @"qux" };
+ NSString *queryString = [FIRStorageUtils queryStringForDictionary:params];
+ XCTAssertEqualObjects(queryString, @"foo=bar&baz=qux");
+}
+
+- (void)testDefaultRequestForFullPath {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"path/to/object"];
+ NSURLRequest *request = [FIRStorageUtils defaultRequestForPath:path];
+ XCTAssertEqualObjects([request.URL absoluteString],
+ @"https://firebasestorage.googleapis.com/v0/b/bucket/o/path%2Fto%2Fobject");
+}
+
+- (void)testDefaultRequestForNoPath {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ NSURLRequest *request = [FIRStorageUtils defaultRequestForPath:path];
+ XCTAssertEqualObjects([request.URL absoluteString],
+ @"https://firebasestorage.googleapis.com/v0/b/bucket/o");
+}
+
+- (void)testEncodedURLForFullPath {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:@"path/to/object"];
+ NSString *encodedURL = [FIRStorageUtils encodedURLForPath:path];
+ XCTAssertEqualObjects(encodedURL, @"/v0/b/bucket/o/path%2Fto%2Fobject");
+}
+
+- (void)testEncodedURLForNoPath {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:@"bucket" object:nil];
+ NSString *encodedURL = [FIRStorageUtils encodedURLForPath:path];
+ XCTAssertEqualObjects(encodedURL, @"/v0/b/bucket/o");
+}
+
+@end
diff --git a/Firebase/Auth/CHANGELOG.md b/Firebase/Auth/CHANGELOG.md
new file mode 100644
index 0000000..d2eb535
--- /dev/null
+++ b/Firebase/Auth/CHANGELOG.md
@@ -0,0 +1,62 @@
+# 2017-05-17 -- v4.0.0
+- Adds Phone Number Authentication.
+- Adds support for generic OAuth2 identity providers.
+- Adds methods that return additional user data from identity providers if
+ available when authenticating users.
+- Improves session management by automatically refreshing tokens if possible
+ and signing out users if the session is detected invalidated, for example,
+ after the user changed password or deleted account from another device.
+- Fixes an issue that reauthentication creates new user account if the user
+ credential is valid but does not match the currently signed in user.
+- Fixes an issue that the "password" provider is not immediately listed on the
+ client side after adding a password to an account.
+- Changes factory methods to return non-null FIRAuth instances or raises an
+ exception, instead of returning nullable instances.
+- Changes auth state change listener to only be triggered when the user changes.
+- Adds a new listener which is triggered whenever the ID token is changed.
+- Switches ERROR_EMAIL_ALREADY_IN_USE to
+ ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL when the email used in the
+ signInWithCredential: call is already in use by another account.
+- Deprecates FIREmailPasswordAuthProvider in favor of FIREmailAuthProvider.
+- Deprecates getTokenWithCompletion in favor of getIDTokenWithCompletion on
+ FIRUser.
+- Changes Swift API names to better align with Swift convention.
+
+# 2017-02-06 -- v3.1.1
+- Allows handling of additional errors when sending OOB action emails. The
+ server can respond with the following new error messages:
+ INVALID_MESSAGE_PAYLOAD,INVALID_SENDER and INVALID_RECIPIENT_EMAIL.
+- Removes incorrect reference to FIRAuthErrorCodeCredentialTooOld in FIRUser.h.
+- Provides additional error information from server if available.
+
+# 2016-12-13 -- v3.1.0
+- Adds FIRAuth methods that enable the app to follow up with user actions
+ delivered by email, such as verifying email address or reset password.
+- No longer applies the keychain workaround introduced in v3.0.5 on iOS 10.2
+ simulator or above since the issue has been fixed.
+- Fixes nullability compilation warnings when used in Swift.
+- Better reports missing password error.
+
+# 2016-10-24 -- v3.0.6
+- Switches to depend on open sourced GoogleToolboxForMac and GTMSessionFetcher.
+- Improves logging of keychain error when initializing.
+
+# 2016-09-14 -- v3.0.5
+- Works around a keychain issue in iOS 10 simulator.
+- Reports the correct error for invalid email when signing in with email and
+ password.
+
+# 2016-07-18 -- v3.0.4
+- Fixes a race condition bug that could crash the app with an exception from
+ NSURLSession on iOS 9.
+
+# 2016-06-20 -- v3.0.3
+- Adds documentation for all possible errors returned by each method.
+- Improves error handling and messages for a variety of error conditions.
+- Whether or not an user is considered anonymous is now consistent with other
+ platforms.
+- A saved signed in user is now siloed between different Firebase projects
+ within the same app.
+
+# 2016-05-18 -- v3.0.2
+- Initial public release.
diff --git a/Firebase/Auth/Docs/threading.md b/Firebase/Auth/Docs/threading.md
new file mode 100644
index 0000000..2f5b782
--- /dev/null
+++ b/Firebase/Auth/Docs/threading.md
@@ -0,0 +1,119 @@
+# Firebase Auth Thread Safety
+
+This document describes how Firebase Auth maintains thread-safety. The Firebase
+Auth library (not including Firebase Auth UI and Auth Provider UIs for now)
+must be thread-safe, meaning deveopers are free to call any method in any
+thread at any time. Thus, all code that may take part in race conditions must
+be protected in some way.
+
+## Local Synchronization
+
+When contested data and accessing code is limited in scope, for example,
+a mutable array accessed only by two methods, a `@synchronized` directive is
+probably the simplest solution. Make sure the object to be locked on is not
+`nil`, e.g., `self`.
+
+## Global Work Queue
+
+A more scalable solution used throughout the current code base is to execute
+all potentially conflicting code in the same serial dispatch queue, which is
+referred as "the auth global work queue", or in some other serial queue that
+has its target queue set to this auth global work queue. This way we don't
+have to think about which variables may be contested. We only need to make
+sure all public APIs that may have thread-safety issues make the dispatch.
+The auth global work queue is defined in
+[FIRAuthGlobalWorkQueue.h](../Source/Private/FIRAuthGlobalWorkQueue.h)
+and any serial task queue created by
+[FIRAuthSerialTaskQueue.h](../Source/Private/FIRAuthSerialTaskQueue.h)
+already has its target set properly.
+
+In following sub-sections, we divided methods into three categories, according
+to the two criteria below:
+
+1. Whether the method is public or private:
+ * A public method can be directly called by developers.
+ * A private method can only be called by our own code.
+2. Whether the method is synchronous or asynchronous.
+ * A synchronous method returns some value or object in the calling
+ thread immediately.
+ * An asynchronous method returns nothing but calls the callback provided
+ by the caller at some point in future.
+
+### Public Asynchronous Methods
+
+Unless it's a simple wrapper of another public asynchronous method, a public
+asynchronous method shall
+
+* Dispatch asynchronously to the auth global work queue immediately.
+* Dispatch asynchronously to the main queue before calling the callback.
+ This is to make developers' life easier so they don't have to manage
+ thread-safety.
+
+The code would look like:
+
+```objectivec
+- (void)doSomethingWithCompletion:(nullable CompletionBlock)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ // Do things...
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(args);
+ });
+ }
+ });
+}
+```
+
+### Public Synchronous Methods
+
+A public synchronous method that needs protection shall dispatch
+*synchronously* to the auth global work queue for its work. The code would
+look like:
+
+```objectivec
+- (ReturnType)something {
+ __block ReturnType result;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ // Compute result.
+ result = computedResult;
+ });
+ return result;
+}
+```
+
+**But don't call methods protected this way from private methods, or a
+deadlock would occur.** This is because you are not supposed to
+`dispatch_sync` to the queue you're already in. This can be easily worked
+around by creating an equivalent private synchronous method to be called by
+both public and private methods and making the public synchronous method a
+wrapper of that. For example,
+
+```objectivec
+- (ReturnType)somethingInternal {
+ // Compute result.
+ return computedResult;
+}
+
+- (ReturnType)something {
+ __block ReturnType result;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ result = [self somethingInternal];
+ });
+ return result;
+}
+```
+
+### Private Methods
+
+Generally speaking there is nothing special needed to be done for private
+methods:
+
+* The calling code should already be in the auth global work queue.
+* The callback, if any, is provided by our own code, so it expects to called
+ in the auth global work queue as well. This is usually already the case,
+ unless the method pass the callback to some other asychronous methods
+ outside our library, in which case we need to manually make the callback
+ called in the auth global work queue.
+
+Just beware you can't call public synchronous methods protected by the auth
+global work queue from private methods as stated in the preceding sub-section.
diff --git a/Firebase/Auth/FirebaseAuth.podspec b/Firebase/Auth/FirebaseAuth.podspec
new file mode 100644
index 0000000..74aa07c
--- /dev/null
+++ b/Firebase/Auth/FirebaseAuth.podspec
@@ -0,0 +1,56 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseAuth'
+ s.version = '4.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]'
+ s.public_header_files =
+ 'Source/FirebaseAuth.h',
+ 'Source/FirebaseAuthVersion.h',
+ 'Source/FIRAdditionalUserInfo.h',
+ 'Source/FIRAuth.h',
+ 'Source/FIRAuthAPNSTokenType.h',
+ 'Source/FIRAuthCredential.h',
+ 'Source/FIRAuthDataResult.h',
+ 'Source/FIRAuthErrors.h',
+ 'Source/FIRAuthSwiftNameSupport.h',
+ 'Source/AuthProviders/EmailPassword/FIREmailAuthProvider.h',
+ 'Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h',
+ 'Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h',
+ 'Source/AuthProviders/Google/FIRGoogleAuthProvider.h',
+ 'Source/AuthProviders/OAuth/FIROAuthProvider.h',
+ 'Source/AuthProviders/Phone/FIRPhoneAuthCredential.h',
+ 'Source/AuthProviders/Phone/FIRPhoneAuthProvider.h',
+ 'Source/AuthProviders/Twitter/FIRTwitterAuthProvider.h',
+ 'Source/FIRUser.h',
+ 'Source/FIRUserInfo.h'
+ s.preserve_paths =
+ 'README.md',
+ 'CHANGELOG.md'
+ s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' + 'FIRAuth_VERSION=' + s.version.to_s +
+ ' FIRAuth_MINOR_VERSION=' + s.version.to_s.split(".")[0] + "." + s.version.to_s.split(".")[1]
+ }
+ s.framework = 'Security'
+# s.dependency 'FirebaseDev/Core'
+ s.dependency 'GTMSessionFetcher/Core', '~> 1.1'
+ s.dependency 'GoogleToolboxForMac/NSDictionary+URLArguments', '~> 2.1'
+end
diff --git a/Firebase/Auth/README.md b/Firebase/Auth/README.md
new file mode 100644
index 0000000..e766949
--- /dev/null
+++ b/Firebase/Auth/README.md
@@ -0,0 +1,8 @@
+# Firebase Auth for iOS
+
+Firebase Auth enables apps to easily support multiple authentication options
+for their end users.
+
+Please visit [our developer site](https://developers.google.com/) for
+integration instructions, documentation, support information, and terms of
+service.
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.h b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.h
new file mode 100644
index 0000000..4fb5ea0
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the email & password identity provider.
+ */
+extern NSString *const FIREmailAuthProviderID FIR_SWIFT_NAME(EmailAuthProviderID);
+
+/**
+ @brief please use @c FIREmailAuthProviderID instead.
+ */
+extern NSString *const FIREmailPasswordAuthProviderID __attribute__((deprecated));
+
+/** @class FIREmailAuthProvider
+ @brief A concrete implementation of @c FIRAuthProvider for Email & Password Sign In.
+ */
+FIR_SWIFT_NAME(EmailAuthProvider)
+@interface FIREmailAuthProvider : NSObject
+
+/** @typedef FIREmailPasswordAuthProvider
+ @brief Please use @c FIREmailAuthProvider instead.
+ */
+typedef FIREmailAuthProvider FIREmailPasswordAuthProvider __attribute__((deprecated));
+
+
+/** @fn credentialWithEmail:password:
+ @brief Creates an @c FIRAuthCredential for an email & password sign in.
+
+ @param email The user's email address.
+ @param password The user's password.
+ @return A FIRAuthCredential containing the email & password credential.
+ */
++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password;
+
+/** @fn init
+ @brief This class is not meant to be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m
new file mode 100644
index 0000000..d27611e
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.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 "FIREmailAuthProvider.h"
+
+#import "FIREmailPasswordAuthCredential.h"
+
+// FIREmailPasswordAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIREmailAuthProvider
+
+- (instancetype)init {
+ @throw [NSException exceptionWithName:@"Attempt to call unavailable initializer."
+ reason:@"This class is not meant to be initialized."
+ userInfo:nil];
+}
+
++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password {
+ return [[FIREmailPasswordAuthCredential alloc] initWithEmail:email password:password];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h
new file mode 100644
index 0000000..004716c
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h
@@ -0,0 +1,48 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIREmailPasswordAuthCredential
+ @brief Internal implementation of FIRAuthCredential for Email/Password credentials.
+ */
+@interface FIREmailPasswordAuthCredential : FIRAuthCredential
+
+/** @property email
+ @brief The user's email address.
+ */
+@property(nonatomic, readonly) NSString *email;
+
+/** @property password
+ @brief The user's password.
+ */
+@property(nonatomic, readonly) NSString *password;
+
+/** @fn initWithEmail:password:
+ @brief Designated initializer.
+ @param email The user's email address.
+ @param password The user's password.
+ */
+- (nullable instancetype)initWithEmail:(NSString *)email password:(NSString *)password
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m
new file mode 100644
index 0000000..4361366
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.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 "FIREmailPasswordAuthCredential.h"
+
+#import "FIREmailAuthProvider.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+@interface FIREmailPasswordAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIREmailPasswordAuthCredential
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (nullable instancetype)initWithEmail:(NSString *)email password:(NSString *)password {
+ self = [super initWithProvider:FIREmailAuthProviderID];
+ if (self) {
+ _email = [email copy];
+ _password = [password copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Attempt to call prepareVerifyAssertionRequest: on a FIREmailPasswordAuthCredential."];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.h b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.h
new file mode 100644
index 0000000..bf7db21
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.h
@@ -0,0 +1,49 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the email & password identity provider.
+ */
+extern NSString *const FIREmailPasswordAuthProviderID;
+
+/** @class FIREmailPasswordAuthProvider
+ @brief A concrete implementation of @c FIRAuthProvider for Email & Password Sign In.
+ */
+@interface FIREmailPasswordAuthProvider : NSObject
+
+/** @fn credentialWithEmail:password:
+ @brief Creates an @c FIRAuthCredential for an email & password sign in.
+
+ @param email The user's email address.
+ @param password The user's password.
+ @return A FIRAuthCredential containing the email & password credential.
+ */
++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password;
+
+/** @fn init
+ @brief This class is not meant to be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.m b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.m
new file mode 100644
index 0000000..84c3787
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthProvider.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 "FIREmailPasswordAuthProvider.h"
+
+#import "FIREmailPasswordAuthCredential.h"
+
+// FIREmailPasswordAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIREmailPasswordAuthProvider
+
+- (instancetype)init {
+ @throw [NSException exceptionWithName:@"Attempt to call unavailable initializer."
+ reason:@"This class is not meant to be initialized."
+ userInfo:nil];
+}
+
++ (FIRAuthCredential *)credentialWithEmail:(NSString *)email password:(NSString *)password {
+ return [[FIREmailPasswordAuthCredential alloc] initWithEmail:email password:password];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.h b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.h
new file mode 100644
index 0000000..1c03573
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.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 <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRFacebookAuthCredential
+ @brief Internal implementation of FIRAuthCredential for the Facebook IdP.
+ */
+@interface FIRFacebookAuthCredential : FIRAuthCredential
+
+/** @fn initWithAccessToken:
+ @brief Designated initializer.
+ @param accessToken The Access Token obtained from Facebook.
+ */
+- (nullable instancetype)initWithAccessToken:(NSString *)accessToken NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m
new file mode 100644
index 0000000..1c3576a
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.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 "FIRFacebookAuthCredential.h"
+
+#import "FIRFacebookAuthProvider.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+@interface FIRFacebookAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIRFacebookAuthCredential {
+ NSString *_accessToken;
+}
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (nullable instancetype)initWithAccessToken:(NSString *)accessToken {
+ self = [super initWithProvider:FIRFacebookAuthProviderID];
+ if (self) {
+ _accessToken = [accessToken copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ request.providerAccessToken = _accessToken;
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h
new file mode 100644
index 0000000..2307b08
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the Facebook identity provider.
+ */
+extern NSString *const FIRFacebookAuthProviderID FIR_SWIFT_NAME(FacebookAuthProviderID);
+
+/** @class FIRFacebookAuthProvider
+ @brief Utility class for constructing Facebook credentials.
+ */
+FIR_SWIFT_NAME(FacebookAuthProvider)
+@interface FIRFacebookAuthProvider : NSObject
+
+/** @fn credentialWithAccessToken:
+ @brief Creates an @c FIRAuthCredential for a Facebook sign in.
+
+ @param accessToken The Access Token from Facebook.
+ @return A FIRAuthCredential containing the Facebook credentials.
+ */
++ (FIRAuthCredential *)credentialWithAccessToken:(NSString *)accessToken;
+
+/** @fn init
+ @brief This class should not be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m
new file mode 100644
index 0000000..d2759ae
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m
@@ -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 "FIRFacebookAuthProvider.h"
+
+#import "FIRFacebookAuthCredential.h"
+#import "FIRAuthExceptionUtils.h"
+
+// FIRFacebookAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIRFacebookAuthProvider
+
+- (instancetype)init {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"This class is not meant to be initialized."];
+ return nil;
+}
+
++ (FIRAuthCredential *)credentialWithAccessToken:(NSString *)accessToken {
+ return [[FIRFacebookAuthCredential alloc] initWithAccessToken:accessToken];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.h b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.h
new file mode 100644
index 0000000..c43fb52
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.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 <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRGitHubAuthCredential
+ @brief Internal implementation of FIRAuthCredential for GitHub credentials.
+ */
+@interface FIRGitHubAuthCredential : FIRAuthCredential
+
+/** @property token
+ @brief The GitHub OAuth access token.
+ */
+@property(nonatomic, readonly) NSString *token;
+
+/** @fn initWithToken:
+ @brief Designated initializer.
+ @param token The GitHub OAuth access token.
+ */
+- (nullable instancetype)initWithToken:(NSString *)token NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m
new file mode 100644
index 0000000..a0185eb
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m
@@ -0,0 +1,49 @@
+/*
+ * 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 "FIRGitHubAuthCredential.h"
+
+#import "FIRGitHubAuthProvider.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+@interface FIRGitHubAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIRGitHubAuthCredential
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (nullable instancetype)initWithToken:(NSString *)token {
+ self = [super initWithProvider:FIRGitHubAuthProviderID];
+ if (self) {
+ _token = [token copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ request.providerAccessToken = _token;
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h
new file mode 100644
index 0000000..ab5c0ef
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the GitHub identity provider.
+ */
+extern NSString *const FIRGitHubAuthProviderID FIR_SWIFT_NAME(GitHubAuthProviderID);
+
+/** @class FIRGitHubAuthProvider
+ @brief Utility class for constructing GitHub credentials.
+ */
+FIR_SWIFT_NAME(GitHubAuthProvider)
+@interface FIRGitHubAuthProvider : NSObject
+
+/** @fn credentialWithToken:
+ @brief Creates an @c FIRAuthCredential for a GitHub sign in.
+
+ @param token The GitHub OAuth access token.
+ @return A FIRAuthCredential containing the GitHub credential.
+ */
++ (FIRAuthCredential *)credentialWithToken:(NSString *)token;
+
+/** @fn init
+ @brief This class is not meant to be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m
new file mode 100644
index 0000000..8e0ff76
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m
@@ -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 "FIRGitHubAuthProvider.h"
+
+#import "FIRGitHubAuthCredential.h"
+#import "FIRAuthExceptionUtils.h"
+
+// FIRGitHubAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIRGitHubAuthProvider
+
+- (instancetype)init {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"This class is not meant to be initialized."];
+ return nil;
+}
+
++ (FIRAuthCredential *)credentialWithToken:(NSString *)token {
+ return [[FIRGitHubAuthCredential alloc] initWithToken:token];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.h b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.h
new file mode 100644
index 0000000..ae98fbc
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.h
@@ -0,0 +1,38 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRGoogleAuthCredential
+ @brief Internal implementation of FIRAuthCredential for the Google IdP.
+ */
+@interface FIRGoogleAuthCredential : FIRAuthCredential
+
+/** @fn initWithIDToken:accessToken:
+ @brief Designated initializer.
+ @param IDToken The ID Token obtained from Google.
+ @param accessToken The Access Token obtained from Google.
+ */
+- (nullable instancetype)initWithIDToken:(NSString *)IDToken accessToken:(NSString *)accessToken
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m
new file mode 100644
index 0000000..d66b2e2
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m
@@ -0,0 +1,54 @@
+/*
+ * 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 "FIRGoogleAuthCredential.h"
+
+#import "FIRGoogleAuthProvider.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+@interface FIRGoogleAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIRGoogleAuthCredential {
+ NSString *_IDToken;
+ NSString *_accessToken;
+}
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (nullable instancetype)initWithIDToken:(NSString *)IDToken accessToken:(NSString *)accessToken {
+ self = [super initWithProvider:FIRGoogleAuthProviderID];
+ if (self) {
+ _IDToken = [IDToken copy];
+ _accessToken = [accessToken copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ request.providerIDToken = _IDToken;
+ request.providerAccessToken = _accessToken;
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.h b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.h
new file mode 100644
index 0000000..92f0db2
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the Google identity provider.
+ */
+extern NSString *const FIRGoogleAuthProviderID FIR_SWIFT_NAME(GoogleAuthProviderID);
+
+/** @class FIRGoogleAuthProvider
+ @brief Utility class for constructing Google Sign In credentials.
+ */
+FIR_SWIFT_NAME(GoogleAuthProvider)
+@interface FIRGoogleAuthProvider : NSObject
+
+/** @fn credentialWithIDToken:accessToken:
+ @brief Creates an @c FIRAuthCredential for a Google sign in.
+
+ @param IDToken The ID Token from Google.
+ @param accessToken The Access Token from Google.
+ @return A FIRAuthCredential containing the Google credentials.
+ */
++ (FIRAuthCredential *)credentialWithIDToken:(NSString *)IDToken
+ accessToken:(NSString *)accessToken;
+
+/** @fn init
+ @brief This class should not be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m
new file mode 100644
index 0000000..a2f4c79
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m
@@ -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 "FIRGoogleAuthProvider.h"
+
+#import "FIRGoogleAuthCredential.h"
+#import "FIRAuthExceptionUtils.h"
+
+// FIRGoogleAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIRGoogleAuthProvider
+
+- (instancetype)init {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"This class is not meant to be initialized."];
+ return nil;
+}
+
++ (FIRAuthCredential *)credentialWithIDToken:(NSString *)IDToken
+ accessToken:(NSString *)accessToken {
+ return [[FIRGoogleAuthCredential alloc] initWithIDToken:IDToken accessToken:accessToken];
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.h b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.h
new file mode 100644
index 0000000..744df33
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.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 <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIROAuthCredential
+ @brief Internal implementation of FIRAuthCredential for generic credentials.
+ */
+@interface FIROAuthCredential : FIRAuthCredential
+
+/** @property providerID
+ @brief The provider ID associated with this credential.
+ */
+@property(nonatomic, readonly) NSString *providerID;
+
+/** @property IDToken
+ @brief The ID Token associated with this credential.
+ */
+@property(nonatomic, readonly) NSString *IDToken;
+
+/** @property accessToken
+ @brief The access token associated with this credential.
+ */
+@property(nonatomic, readonly) 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.
+ */
+- (nullable instancetype)initWithProvierID:(NSString *)providerID
+ IDToken:(nullable NSString*)IDToken
+ accessToken:(nullable NSString *)accessToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m
new file mode 100644
index 0000000..28712a6
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m
@@ -0,0 +1,50 @@
+/*
+ * 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 "FIROAuthCredential.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIROAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIROAuthCredential
+
+- (nullable instancetype)initWithProvierID:(NSString *)providerID
+ IDToken:(nullable NSString *)IDToken
+ accessToken:(nullable NSString *)accessToken {
+ self = [super initWithProvider:providerID];
+ if (self) {
+ _providerID = providerID;
+ _IDToken = IDToken;
+ _accessToken = accessToken;
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ request.providerIDToken = _IDToken;
+ request.providerAccessToken = _accessToken;
+}
+
+NS_ASSUME_NONNULL_END
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.h b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.h
new file mode 100644
index 0000000..e059b22
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIROAuthProvider
+ @brief A concrete implementation of @c FIRAuthProvider for generic OAuth Providers.
+ */
+FIR_SWIFT_NAME(OAuthProvider)
+@interface FIROAuthProvider : NSObject
+
+/** @fn credentialWithProviderID:IDToken:accessToken:
+ @brief Creates an @c 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.
+ */
++ (FIRAuthCredential *)credentialWithProviderID:(NSString *)providerID
+ IDToken:(NSString *)IDToken
+ accessToken:(nullable NSString *)accessToken;
+
+
+/** @fn credentialWithProviderID:accessToken:
+ @brief Creates an @c FIRAuthCredential for that OAuth 2 provider identified by providerID using
+ an ID token.
+
+ @param providerID The provider ID associated with the Auth credential being created.
+ @param accessToken The accessstoken associated with the Auth credential be created
+ @return A FIRAuthCredential.
+ */
++ (FIRAuthCredential *)credentialWithProviderID:(NSString *)providerID
+ accessToken:(NSString *)accessToken;
+
+/** @fn init
+ @brief This class is not meant to be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m
new file mode 100644
index 0000000..e810680
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m
@@ -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 "FIROAuthProvider.h"
+
+#import "FIROAuthCredential.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIROAuthProvider
+
++ (FIRAuthCredential *)credentialWithProviderID:(NSString *)providerID
+ IDToken:(NSString *)IDToken
+ accessToken:(nullable NSString *)accessToken {
+ return [[FIROAuthCredential alloc] initWithProvierID:providerID
+ IDToken:IDToken
+ accessToken:accessToken];
+}
+
++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID
+ accessToken:(NSString *)accessToken {
+ return [[FIROAuthCredential alloc] initWithProvierID:providerID
+ IDToken:nil
+ accessToken:accessToken];
+}
+
+NS_ASSUME_NONNULL_END
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.h b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.h
new file mode 100644
index 0000000..d951564
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.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 <Foundation/Foundation.h>
+
+#import "FIRAuthCredential.h"
+#import "FIRAuthSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRPhoneAuthCredential
+ @brief Implementation of FIRAuthCredential for Phone Auth credentials.
+ */
+FIR_SWIFT_NAME(PhoneAuthCredential)
+@interface FIRPhoneAuthCredential : FIRAuthCredential
+
+/** @fn init
+ @brief This class is not supposed to be instantiated directly.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.m
new file mode 100644
index 0000000..b9bf577
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.m
@@ -0,0 +1,65 @@
+/*
+ * 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 "FIRPhoneAuthCredential.h"
+
+#import "FIRPhoneAuthProvider.h"
+#import "FIRPhoneAuthCredential_Internal.h"
+#import "FIRAuthCredential_Internal.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRPhoneAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIRPhoneAuthCredential
+
+- (instancetype)initWithTemporaryProof:(NSString *)temporaryProof
+ phoneNumber:(NSString *)phoneNumber
+ providerID:(NSString *)providerID {
+ self = [super initWithProvider:providerID];
+ if (self) {
+ _temporaryProof = [temporaryProof copy];
+ _phoneNumber = [phoneNumber copy];
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (instancetype)initWithProviderID:(NSString *)providerID
+ verificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode {
+ self = [super initWithProvider:providerID];
+ if (self) {
+ _verificationID = [verificationID copy];
+ _verificationCode = [verificationCode copy];
+ }
+ return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h
new file mode 100644
index 0000000..f260b89
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRPhoneAuthCredential.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @extension FIRPhoneAuthCredential
+ @brief Internal implementation of FIRAuthCredential for Phone Auth credentials.
+ */
+@interface FIRPhoneAuthCredential ()
+
+/** @var verificationID
+ @brief The verification ID obtained from invoking @c verifyPhoneNumber:completion:
+ */
+@property(nonatomic, readonly, nonnull) NSString *verificationID;
+
+/** @var verificationCode
+ @brief The verification code provided by the user.
+ */
+@property(nonatomic, readonly, nonnull) NSString *verificationCode;
+
+/** @var temporaryProof
+ @brief The a temporary proof code perftaining to this credential, returned from the backend.
+ */
+@property(nonatomic, readonly, nonnull) NSString *temporaryProof;
+
+/** @var phoneNumber
+ @brief The a phone number pertaining to this credential, returned from the backend.
+ */
+@property(nonatomic, readonly, nonnull) NSString *phoneNumber;
+
+/** @var initWithTemporaryProof:phoneNumber:
+ @brief Designated Initializer.
+ @param providerID The provider ID associated with the phone auth credential being created.
+ */
+- (instancetype)initWithTemporaryProof:(NSString *)temporaryProof
+ phoneNumber:(NSString *)phoneNumber
+ providerID:(NSString *)providerID NS_DESIGNATED_INITIALIZER;
+
+/** @var initWithProviderID:verificationID:verificationCode:
+ @brief Designated Initializer.
+ @param providerID The provider ID associated with the phone auth credential being created.
+ @param verificationID The verification ID associated witht Phone Auth credential being created.
+ @param verificationCode The verification code associated witht Phone Auth credential being
+ created.
+ */
+- (instancetype)initWithProviderID:(NSString *)providerID
+ verificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.h b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.h
new file mode 100644
index 0000000..bc12b43
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.h
@@ -0,0 +1,90 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuth.h"
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRPhoneAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var FIRPhoneAuthProviderID
+ @brief A string constant identifying the phone identity provider.
+ */
+extern NSString *const FIRPhoneAuthProviderID FIR_SWIFT_NAME(PhoneAuthProviderID);
+
+/** @typedef FIRVerificationResultCallback
+ @brief The type of block invoked when a request to send a verification code has finished.
+
+ @param verificationID On success, the verification ID provided, nil otherwise.
+ @param error On error, the error that occured, nil otherwise.
+ */
+typedef void (^FIRVerificationResultCallback)(NSString *_Nullable verificationID,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(VerificationResultCallback);
+
+/** @class FIRPhoneNumberProvider
+ @brief A concrete implementation of @c FIRAuthProvider for Phone Auth Providers.
+ */
+FIR_SWIFT_NAME(PhoneAuthProvider)
+@interface FIRPhoneAuthProvider : NSObject
+
+/** @fn provider
+ @brief Returns an instance of @c FIRPhoneAuthProvider for the default @c FIRAuth object.
+ */
++ (instancetype)provider FIR_SWIFT_NAME(provider());
+
+/** @fn providerWithAuth:
+ @brief Returns an instance of @c FIRPhoneAuthProvider for the provided @c FIRAuth object.
+
+ @param auth The auth object to associate with the @c PhoneauthProvider instance.
+ */
++ (instancetype)providerWithAuth:(FIRAuth *)auth FIR_SWIFT_NAME(provider(auth:));
+
+/** @fn verifyPhoneNumber:completion:
+ @brief Starts the phone number authentication flow by sending a verifcation code to the
+ specified phone number.
+
+ @param phoneNumber The phone number to be verified.
+ @param completion The callback to be invoked when the verification flow is finished.
+ */
+- (void)verifyPhoneNumber:(NSString *)phoneNumber
+ completion:(nullable FIRVerificationResultCallback)completion;
+
+/** @fn credentialWithVerificationID:verificationCode:
+ @brief Creates an @c FIRAuthCredential for the phone number provider identified by the
+ verification ID and verification code.
+
+ @param verificationID The verification ID obtained from invoking @c
+ verifyPhoneNumber:completion:
+ @param verificationCode The verification code obtained from the user.
+ @return The corresponding @c FIRAuthCredential for the verification ID and verification code
+ provided.
+ */
+- (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode;
+
+/** @fn init
+ @brief Please use the @c provider or @providerWithAuth: methods to obtain an instance of @c
+ FIRPhoneAuthProvider.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m
new file mode 100644
index 0000000..423b2b6
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m
@@ -0,0 +1,213 @@
+/*
+ * 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 "FIRPhoneAuthProvider.h"
+
+#import "FIRLogger.h"
+#import "FIRPhoneAuthCredential_Internal.h"
+#import "NSString+FIRAuth.h"
+#import "../../Private/FIRAuthAPNSToken.h"
+#import "../../Private/FIRAuthAPNSTokenManager.h"
+#import "../../Private/FIRAuthAppCredential.h"
+#import "../../Private/FIRAuthAppCredentialManager.h"
+#import "../../Private/FIRAuthGlobalWorkQueue.h"
+#import "../../Private/FIRAuth_Internal.h"
+#import "../../Private/FIRAuthNotificationManager.h"
+#import "../../Private/FIRAuthErrorUtils.h"
+#import "FIRAuthBackend.h"
+#import "FIRSendVerificationCodeRequest.h"
+#import "FIRSendVerificationCodeResponse.h"
+#import "FIRVerifyClientRequest.h"
+#import "FIRVerifyClientResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRVerifyClientCallback
+ @brief The callback invoked at the end of a client verification flow.
+ @param appCredential credential that proves the identity of the app during a phone
+ authentication flow.
+ @param error The error that occured while verifying the app, if any.
+ */
+typedef void (^FIRVerifyClientCallback)(FIRAuthAppCredential *_Nullable appCredential,
+ NSError *_Nullable error);
+
+@implementation FIRPhoneAuthProvider {
+
+ /** @var _auth
+ @brief The auth instance used to for verifying the phone number.
+ */
+ FIRAuth *_auth;
+}
+
+/** @fn initWithAuth:
+ @brief returns an instance of @c FIRPhoneAuthProvider assocaited with the provided auth
+ instance.
+ @return An Instance of @c FIRPhoneAuthProvider.
+ */
+- (nullable instancetype)initWithAuth:(FIRAuth *)auth {
+ self = [super init];
+ if (self) {
+ _auth = auth;
+ }
+ return self;
+}
+
+- (void)verifyPhoneNumber:(NSString *)phoneNumber
+ completion:(nullable FIRVerificationResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRVerificationResultCallback callBackOnMainThread = ^(NSString *_Nullable verificationID,
+ NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(verificationID, error);
+ });
+ }
+ };
+
+ if (!phoneNumber.length) {
+ callBackOnMainThread(nil,
+ [FIRAuthErrorUtils missingPhoneNumberErrorWithMessage:nil]);
+ return;
+ }
+ [_auth.notificationManager checkNotificationForwardingWithCallback:
+ ^(BOOL isNotificationBeingForwarded) {
+ if (!isNotificationBeingForwarded) {
+ callBackOnMainThread(nil, [FIRAuthErrorUtils notificationNotForwardedError]);
+ return;
+ }
+ [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
+ retryOnInvalidAppCredential:YES
+ callback:callBackOnMainThread];
+ }];
+ });
+}
+
+- (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode {
+ return [[FIRPhoneAuthCredential alloc] initWithProviderID:FIRPhoneAuthProviderID
+ verificationID:verificationID
+ verificationCode:verificationCode];
+}
+
++ (instancetype)provider {
+ return [[self alloc]initWithAuth:[FIRAuth auth]];
+}
+
++ (instancetype)providerWithAuth:(FIRAuth *)auth {
+ return [[self alloc]initWithAuth:auth];
+}
+
+#pragma mark - Internal Methods
+
+/** @fn verifyClientAndSendVerificationCodeToPhoneNumber:retryOnInvalidAppCredential:callback:
+ @brief Starts the flow to verify the client via silent push notification.
+ @param retryOnInvalidAppCredential Whether of not the flow should be retried if an
+ FIRAuthErrorCodeInvalidAppCredential error is returned from the backend.
+ @param phoneNumber The phone number to be verified.
+ @param callback The callback to be invoked on the global work queue when the flow is
+ finished.
+ */
+- (void)verifyClientAndSendVerificationCodeToPhoneNumber:(NSString *)phoneNumber
+ retryOnInvalidAppCredential:(BOOL)retryOnInvalidAppCredential
+ callback:(FIRVerificationResultCallback)callback {
+ [self verifyClientWithCompletion:^(FIRAuthAppCredential *_Nullable appCredential,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ FIRSendVerificationCodeRequest *request =
+ [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
+ appCredential:appCredential
+ APIKey:_auth.APIKey];
+ [FIRAuthBackend sendVerificationCode:request
+ callback:^(FIRSendVerificationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ if (error.code == FIRAuthErrorCodeInvalidAppCredential) {
+ if (retryOnInvalidAppCredential) {
+ [_auth.appCredentialManager clearCredential];
+ [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
+ retryOnInvalidAppCredential:NO
+ callback:callback];
+ return;
+ }
+ callback(nil, [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:nil
+ underlyingError:error]);
+ return;
+ }
+ callback(nil, error);
+ return;
+ }
+ // Associate the phone number with the verification ID.
+ response.verificationID.fir_authPhoneNumber = phoneNumber;
+ callback(response.verificationID, nil);
+ }];
+ }];
+}
+
+/** @fn verifyClientWithCompletion:completion:
+ @brief Continues the flow to verify the client via silent push notification.
+ @param completion The callback to be invoked when the client verification flow is finished.
+ */
+- (void)verifyClientWithCompletion:(FIRVerifyClientCallback)completion {
+ if (_auth.appCredentialManager.credential) {
+ completion(_auth.appCredentialManager.credential, nil);
+ return;
+ }
+ [_auth.tokenManager getTokenWithCallback:^(FIRAuthAPNSToken * _Nullable token) {
+ if (!token) {
+ completion(nil, [FIRAuthErrorUtils missingAppTokenError]);
+ return;
+ }
+
+ // Convert token data to hex string.
+ NSUInteger capacity = token.data.length * 2;
+ NSMutableString *tokenString = [NSMutableString stringWithCapacity:capacity];
+ const unsigned char *tokenData = token.data.bytes;
+ for (int idx = 0; idx < token.data.length; ++idx) {
+ [tokenString appendFormat:@"%02X", (int)tokenData[idx]];
+ }
+
+ FIRVerifyClientRequest *request =
+ [[FIRVerifyClientRequest alloc] initWithAppToken:tokenString
+ isSandbox:token.type == FIRAuthAPNSTokenTypeSandbox
+ APIKey:_auth.APIKey];
+ [FIRAuthBackend verifyClient:request callback:^(FIRVerifyClientResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ completion(nil, error);
+ return;
+ }
+ NSTimeInterval timeout = [response.suggestedTimeOutDate timeIntervalSinceNow];
+ [_auth.appCredentialManager
+ didStartVerificationWithReceipt:response.receipt
+ timeout:timeout
+ callback:^(FIRAuthAppCredential *credential) {
+ if (!credential.secret) {
+ FIRLogError(kFIRLoggerAuth, @"I-AUT000014",
+ @"Failed to receive remote notification to verify app identity within "
+ @"%.0f second(s)", timeout);
+ }
+ completion(credential, nil);
+ }];
+ }];
+ }];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.h b/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.h
new file mode 100644
index 0000000..ba123fa
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @category NSString(FIRAuth)
+ @brief A FIRAuth category for extending the functionality of NSString for specific Firebase Auth
+ use cases.
+ */
+@interface NSString (FIRAuth)
+
+/** @property fir_authPhoneNumber
+ @brief A phone number associated with the verification ID (NSString instance).
+ @remarks Allows an instance on NSString to be associated with a phone number in order to link
+ phone number with the verificationID returned from verifyPhoneNumber:completion:
+ */
+@property(nonatomic, strong) NSString *fir_authPhoneNumber;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.m b/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.m
new file mode 100644
index 0000000..87f3b1c
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Phone/NSString+FIRAuth.m
@@ -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 "NSString+FIRAuth.h"
+
+#import <objc/runtime.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation NSString (FIRAuth)
+
+- (void)setFir_authPhoneNumber:(NSString *)phoneNumber {
+ objc_setAssociatedObject(self, @selector(fir_authPhoneNumber), phoneNumber,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (NSString *)fir_authPhoneNumber {
+ return objc_getAssociatedObject(self, @selector(fir_authPhoneNumber));
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.h b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.h
new file mode 100644
index 0000000..5fab4e2
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.h
@@ -0,0 +1,48 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "../../Private/FIRAuthCredential_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRTwitterAuthCredential
+ @brief Internal implementation of FIRAuthCredential for Twitter credentials.
+ */
+@interface FIRTwitterAuthCredential : FIRAuthCredential
+
+/** @property token
+ @brief The Twitter OAuth token.
+ */
+@property(nonatomic, readonly) NSString *token;
+
+/** @property secret
+ @brief The Twitter OAuth secret.
+ */
+@property(nonatomic, readonly) NSString *secret;
+
+/** @fn initWithToken:secret:
+ @brief Designated initializer.
+ @param token The Twitter OAuth token.
+ @param secret The Twitter OAuth secret.
+ */
+- (nullable instancetype)initWithToken:(NSString *)token secret:(NSString *)secret
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m
new file mode 100644
index 0000000..6772d6f
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.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 "FIRTwitterAuthCredential.h"
+
+#import "FIRTwitterAuthProvider.h"
+#import "FIRAuthExceptionUtils.h"
+#import "FIRVerifyAssertionRequest.h"
+
+@interface FIRTwitterAuthCredential ()
+
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE;
+
+@end
+
+@implementation FIRTwitterAuthCredential
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"Please call the designated initializer."];
+ return nil;
+}
+
+- (nullable instancetype)initWithToken:(NSString *)token secret:(NSString *)secret {
+ self = [super initWithProvider:FIRTwitterAuthProviderID];
+ if (self) {
+ _token = [token copy];
+ _secret = [secret copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ request.providerAccessToken = _token;
+ request.providerOAuthTokenSecret = _secret;
+}
+
+@end
diff --git a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.h b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.h
new file mode 100644
index 0000000..d8f647d
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief A string constant identifying the Twitter identity provider.
+ */
+extern NSString *const FIRTwitterAuthProviderID FIR_SWIFT_NAME(TwitterAuthProviderID);
+
+/** @class FIRTwitterAuthProvider
+ @brief Utility class for constructing Twitter credentials.
+ */
+FIR_SWIFT_NAME(TwitterAuthProvider)
+@interface FIRTwitterAuthProvider : NSObject
+
+/** @fn credentialWithToken:secret:
+ @brief Creates an @c FIRAuthCredential for a Twitter sign in.
+
+ @param token The Twitter OAuth token.
+ @param secret The Twitter OAuth secret.
+ @return A FIRAuthCredential containing the Twitter credential.
+ */
++ (FIRAuthCredential *)credentialWithToken:(NSString *)token secret:(NSString *)secret;
+
+/** @fn init
+ @brief This class is not meant to be initialized.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m
new file mode 100644
index 0000000..5d738ce
--- /dev/null
+++ b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m
@@ -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 "FIRTwitterAuthProvider.h"
+
+#import "FIRTwitterAuthCredential.h"
+#import "FIRAuthExceptionUtils.h"
+
+// FIRTwitterAuthProviderID is defined in FIRAuthProvider.m.
+
+@implementation FIRTwitterAuthProvider
+
+- (instancetype)init {
+ [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason:
+ @"This class is not meant to be initialized."];
+ return nil;
+}
+
++ (FIRAuthCredential *)credentialWithToken:(NSString *)token secret:(NSString *)secret {
+ return [[FIRTwitterAuthCredential alloc] initWithToken:token secret:secret];
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRActionCodeSettings.m b/Firebase/Auth/Source/FIRActionCodeSettings.m
new file mode 100644
index 0000000..26d7538
--- /dev/null
+++ b/Firebase/Auth/Source/FIRActionCodeSettings.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 "Private/FIRActionCodeSettings.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRActionCodeSettings
+
+- (void)setIOSBundleID:(NSString *)iOSBundleID
+ appStoreID:(nullable NSString *)appStoreID {
+ _iOSBundleID = [iOSBundleID copy];
+ _iOSAppStoreID = [appStoreID copy];
+}
+
+- (void)setAndroidPackageName:(NSString *)androidPackageName
+ installIfNotAvailable:(BOOL)installIfNotAvailable
+ minimumVersion:(nullable NSString *)minimumVersion {
+ _androidPackageName = [androidPackageName copy];
+ _androidInstallIfNotAvailable = installIfNotAvailable;
+ _androidMinimumVersion = [minimumVersion copy];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAdditionalUserInfo.h b/Firebase/Auth/Source/FIRAdditionalUserInfo.h
new file mode 100644
index 0000000..70e9e57
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAdditionalUserInfo.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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRVerifyAssertionResponse;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAdditionalUserInfo
+ @brief Represents additional user data returned from an identity provider.
+ */
+FIR_SWIFT_NAME(AdditionalUserInfo)
+@interface FIRAdditionalUserInfo : NSObject
+
+/** @fn init
+ @brief This class should not be initialized manually. @c FIRAdditionalUserInfo can be retrieved
+ from @c FIRAuthDataResult .
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @property providerID
+ @brief The provider identifier.
+ */
+@property(nonatomic, readonly) NSString *providerID;
+
+/** @property profile
+ @brief profile Dictionary containing the additional IdP specific information.
+ */
+@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSObject *> *profile;
+
+/** @property username
+ @brief username The name of the user.
+ */
+@property(nonatomic, readonly, nullable) NSString *username;
+
+/** @property newUser
+ @brief Indicates whether or not the current user was signed in for the first time.
+ */
+@property(nonatomic, readonly, getter=isNewUser) BOOL newUser;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAdditionalUserInfo.m b/Firebase/Auth/Source/FIRAdditionalUserInfo.m
new file mode 100644
index 0000000..e00347d
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAdditionalUserInfo.m
@@ -0,0 +1,98 @@
+/*
+ * 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 "Private/FIRAdditionalUserInfo_Internal.h"
+
+#import "FIRVerifyAssertionResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAdditionalUserInfo
+
+/** @var kProviderIDCodingKey
+ @brief The key used to encode the providerID property for NSSecureCoding.
+ */
+static NSString *const kProviderIDCodingKey = @"providerID";
+
+/** @var kProfileCodingKey
+ @brief The key used to encode the profile property for NSSecureCoding.
+ */
+static NSString *const kProfileCodingKey = @"profile";
+
+/** @var kUsernameCodingKey
+ @brief The key used to encode the username property for NSSecureCoding.
+ */
+static NSString *const kUsernameCodingKey = @"username";
+
+/** @var kNewUserKey
+ @brief The key used to encode the newUser property for NSSecureCoding.
+ */
+static NSString *const kNewUserKey = @"newUser";
+
++ (nullable instancetype)userInfoWithVerifyAssertionResponse:
+ (FIRVerifyAssertionResponse *)verifyAssertionResponse {
+ return [[self alloc] initWithProviderID:verifyAssertionResponse.providerID
+ profile:verifyAssertionResponse.profile
+ username:verifyAssertionResponse.username
+ isNewUser:verifyAssertionResponse.isNewUser];
+}
+
+- (nullable instancetype)initWithProviderID:(NSString *)providerID
+ profile:(nullable NSDictionary<NSString *, NSObject *> *)profile
+ username:(nullable NSString *)username
+ isNewUser:(BOOL)isNewUser {
+ self = [super init];
+ if (self) {
+ _providerID = [providerID copy];
+ if (profile) {
+ _profile = [[NSDictionary alloc] initWithDictionary:profile copyItems:YES];
+ }
+ _username = [username copy];
+ _newUser = isNewUser;
+ }
+ return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ NSString *providerID =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kProviderIDCodingKey];
+ NSDictionary<NSString *, NSObject *> *profile =
+ [aDecoder decodeObjectOfClass:[NSDictionary class] forKey:kProfileCodingKey];
+ NSString *username = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUsernameCodingKey];
+ NSNumber *isNewUser = [aDecoder decodeObjectOfClass:[NSNumber class] forKey:kNewUserKey];
+
+ return [self initWithProviderID:providerID
+ profile:profile
+ username:username
+ isNewUser:isNewUser.boolValue];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_providerID forKey:kProviderIDCodingKey];
+ [aCoder encodeObject:_profile forKey:kProfileCodingKey];
+ [aCoder encodeObject:_username forKey:kUsernameCodingKey];
+ [aCoder encodeObject:[NSNumber numberWithBool:_newUser] forKey:kNewUserKey];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuth.h b/Firebase/Auth/Source/FIRAuth.h
new file mode 100644
index 0000000..b913380
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuth.h
@@ -0,0 +1,612 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthAPNSTokenType.h"
+#import "FIRAuthErrors.h"
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRApp;
+@class FIRAuth;
+@class FIRAuthCredential;
+@class FIRAuthDataResult;
+@class FIRUser;
+@protocol FIRAuthStateListener;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthStateDidChangeListenerHandle
+ @brief The type of handle returned by @c FIRAuth.addAuthStateDidChangeListener:.
+ */
+typedef id<NSObject> FIRAuthStateDidChangeListenerHandle
+ FIR_SWIFT_NAME(AuthStateDidChangeListenerHandle);
+
+/** @typedef FIRAuthStateDidChangeListenerBlock
+ @brief The type of block which can be registered as a listener for auth state did change events.
+
+ @param auth The FIRAuth object on which state changes occurred.
+ @param user Optionally; the current signed in user, if any.
+ */
+typedef void(^FIRAuthStateDidChangeListenerBlock)(FIRAuth *auth, FIRUser *_Nullable user)
+ FIR_SWIFT_NAME(AuthStateDidChangeListenerBlock);
+
+/** @typedef FIRIDTokenDidChangeListenerHandle
+ @brief The type of handle returned by @c FIRAuth.addIDTokenDidChangeListener:.
+ */
+typedef id<NSObject> FIRIDTokenDidChangeListenerHandle
+ FIR_SWIFT_NAME(IDTokenDidChangeListenerHandle);
+
+/** @typedef FIRIDTokenDidChangeListenerBlock
+ @brief The type of block which can be registered as a listener for ID token did change events.
+
+ @param auth The FIRAuth object on which ID token changes occurred.
+ @param user Optionally; the current signed in user, if any.
+ */
+typedef void(^FIRIDTokenDidChangeListenerBlock)(FIRAuth *auth, FIRUser *_Nullable user)
+ FIR_SWIFT_NAME(IDTokenDidChangeListenerBlock);
+
+/** @typedef FIRAuthDataResultCallback
+ @brief The type of block invoked when sign-in related events complete.
+
+ @param authResult Optionally; Result of sign-in request containing @c FIRUser and
+ @c FIRAdditionalUserInfo.
+ @param error Optionally; the error which occurred - or nil if the request was successful.
+ */
+typedef void (^FIRAuthDataResultCallback)(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(AuthDataResultCallback);
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+/**
+ @brief The name of the @c NSNotificationCenter notification which is posted when the auth state
+ changes (for example, a new token has been produced, a user signs in or signs out). The
+ object parameter of the notification is the sender @c FIRAuth instance.
+ */
+extern const NSNotificationName FIRAuthStateDidChangeNotification
+ FIR_SWIFT_NAME(AuthStateDidChange);
+#else
+/**
+ @brief The name of the @c NSNotificationCenter notification which is posted when the auth state
+ changes (for example, a new token has been produced, a user signs in or signs out). The
+ object parameter of the notification is the sender @c FIRAuth instance.
+ */
+extern NSString *const FIRAuthStateDidChangeNotification
+ FIR_SWIFT_NAME(AuthStateDidChangeNotification);
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+/** @typedef FIRAuthResultCallback
+ @brief The type of block invoked when sign-in related events complete.
+
+ @param user Optionally; the signed in user, if any.
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRAuthResultCallback)(FIRUser *_Nullable user, NSError *_Nullable error)
+ FIR_SWIFT_NAME(AuthResultCallback);
+
+/** @typedef FIRProviderQueryCallback
+ @brief The type of block invoked when a list of identity providers for a given email address is
+ requested.
+
+ @param providers Optionally; a list of provider identifiers, if any.
+ @see FIRGoogleAuthProviderID etc.
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRProviderQueryCallback)(NSArray<NSString *> *_Nullable providers,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(ProviderQueryCallback);
+
+/** @typedef FIRSendPasswordResetCallback
+ @brief The type of block invoked when sending a password reset email.
+
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRSendPasswordResetCallback)(NSError *_Nullable error)
+ FIR_SWIFT_NAME(SendPasswordResetCallback);
+
+/** @typedef FIRConfirmPasswordResetCallback
+ @brief The type of block invoked when performing a password reset.
+
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRConfirmPasswordResetCallback)(NSError *_Nullable error)
+ FIR_SWIFT_NAME(ConfirmPasswordResetCallback);
+
+/** @typedef FIRVerifyPasswordResetCodeCallback
+ @brief The type of block invoked when verifying that an out of band code should be used to
+ perform password reset.
+
+ @param email Optionally; the email address of the user for which the out of band code applies.
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRVerifyPasswordResetCodeCallback)(NSString *_Nullable email,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(VerifyPasswordResetCodeCallback);
+
+/** @typedef FIRApplyActionCodeCallback
+ @brief The type of block invoked when applying an action code.
+
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRApplyActionCodeCallback)(NSError *_Nullable error)
+ FIR_SWIFT_NAME(ApplyActionCodeCallback);
+
+/**
+ @brief Keys used to retrieve operation data from a @c FIRActionCodeInfo object by the @c
+ dataForKey method.
+ */
+typedef NS_ENUM(NSInteger, FIRActionDataKey) {
+ /**
+ * The email address to which the code was sent.
+ * For FIRActionCodeOperationRecoverEmail, the new email address for the account.
+ */
+ FIRActionCodeEmailKey = 0,
+
+ /** For FIRActionCodeOperationRecoverEmail, the current email address for the account. */
+ FIRActionCodeFromEmailKey = 1
+} FIR_SWIFT_NAME(ActionDataKey);
+
+/** @class FIRActionCodeInfo
+ @brief Manages information regarding action codes.
+ */
+FIR_SWIFT_NAME(ActionCodeInfo)
+@interface FIRActionCodeInfo : NSObject
+
+/**
+ @brief Operations which can be performed with action codes.
+ */
+typedef NS_ENUM(NSInteger, FIRActionCodeOperation) {
+ /** Action code for unknown operation. */
+ FIRActionCodeOperationUnknown = 0,
+
+ /** Action code for password reset operation. */
+ FIRActionCodeOperationPasswordReset = 1,
+
+ /** Action code for verify email operation. */
+ FIRActionCodeOperationVerifyEmail = 2
+} FIR_SWIFT_NAME(ActionCodeOperation);
+
+/**
+ @brief The operation being performed.
+ */
+@property(nonatomic, readonly) FIRActionCodeOperation operation;
+
+/** @fn dataForKey:
+ @brief The operation being performed.
+
+ @param key The FIRActionDataKey value used to retrieve the operation data.
+
+ @return The operation data pertaining to the provided action code key.
+ */
+- (NSString *)dataForKey:(FIRActionDataKey)key;
+
+/** @fn init
+ @brief please use initWithOperation: instead.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+/** @typedef FIRCheckActionCodeCallBack
+ @brief The type of block invoked when performing a check action code operation.
+
+ @param info Metadata corresponding to the action code.
+ @param error Optionally; if an error occurs, this is the NSError object that describes the
+ problem. Set to nil otherwise.
+ */
+typedef void (^FIRCheckActionCodeCallBack)(FIRActionCodeInfo *_Nullable info,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(CheckActionCodeCallback);
+
+/** @class FIRAuth
+ @brief Manages authentication for Firebase apps.
+ @remarks This class is thread-safe.
+ */
+FIR_SWIFT_NAME(Auth)
+@interface FIRAuth : NSObject
+
+/** @fn auth
+ @brief Gets the auth object for the default Firebase app.
+ @remarks The default Firebase app must have already been configured or an exception will be
+ raised.
+ */
++ (FIRAuth *)auth FIR_SWIFT_NAME(auth());
+
+/** @fn authWithApp:
+ @brief Gets the auth object for a @c FIRApp.
+
+ @param app The FIRApp for which to retrieve the associated FIRAuth instance.
+ @return The FIRAuth instance associated with the given FIRApp.
+ */
++ (FIRAuth *)authWithApp:(FIRApp *)app FIR_SWIFT_NAME(auth(app:));
+
+/** @property app
+ @brief Gets the @c FIRApp object that this auth object is connected to.
+ */
+@property(nonatomic, weak, readonly, nullable) FIRApp *app;
+
+/** @property currentUser
+ @brief Synchronously gets the cached current user, or null if there is none.
+ */
+@property(nonatomic, strong, readonly, nullable) FIRUser *currentUser;
+
+/** @property APNSToken
+ @brief The APNs token used for phone number authentication. The type of the token (production
+ or sandbox) will be attempted to be automatcially detected.
+ @remarks If swizzling is disabled, the APNs Token must be set for phone number auth to work,
+ by either setting this property or by calling @c setAPNSToken:type:
+ */
+@property(nonatomic, strong, nullable) NSData *APNSToken;
+
+/** @fn init
+ @brief Please access auth instances using @c FIRAuth.auth and @c FIRAuth.authForApp:.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn fetchProvidersForEmail:completion:
+ @brief Fetches the list of IdPs that can be used for signing in with the provided email address.
+ Useful for an "identifier-first" sign-in flow.
+
+ @param email The email address for which to obtain a list of identity providers.
+ @param completion Optionally; a block which is invoked when the list of providers for the
+ specified email address is ready or an error was encountered. Invoked asynchronously on the
+ main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.</li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)fetchProvidersForEmail:(NSString *)email
+ completion:(nullable FIRProviderQueryCallback)completion;
+
+/** @fn signInWithEmail:password:completion:
+ @brief Signs in using an email address and password.
+
+ @param email The user's email address.
+ @param password The user's password.
+ @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:
+
+ <ul>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that email and password
+ accounts are not enabled. Enable them in the Auth section of the
+ Firebase console.
+ </li>
+ <li>@c FIRAuthErrorCodeUserDisabled - Indicates the user's account is disabled.
+ </li>
+ <li>@c FIRAuthErrorCodeWrongPassword - Indicates the user attempted
+ sign in with an incorrect password.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)signInWithEmail:(NSString *)email
+ password:(NSString *)password
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn signInWithCredential:completion:
+ @brief Convenience method for @c signInAndRetrieveDataWithCredential:completion: This method
+ doesn't return additional identity provider data.
+ */
+- (void)signInWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthResultCallback)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
+ identity provider data.
+
+ @param credential The credential supplied by the IdP.
+ @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:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidCredential - Indicates the supplied credential is invalid.
+ This could happen if it has expired or it is malformed.
+ </li>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that accounts
+ with the identity provider represented by the credential are not enabled.
+ Enable them in the Auth section of the Firebase console.
+ </li>
+ <li>@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.
+ </li>
+ <li>@c FIRAuthErrorCodeUserDisabled - Indicates the user's account is disabled.
+ </li>
+ <li>@c FIRAuthErrorCodeWrongPassword - Indicates the user attempted sign in with an
+ incorrect password, if credential is of the type EmailPasswordAuthCredential.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)signInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthDataResultCallback)completion;
+
+/** @fn signInAnonymouslyWithCompletion:
+ @brief Asynchronously creates and becomes an anonymous user.
+ @param completion Optionally; a block which is invoked when the sign in finishes, or is
+ canceled. Invoked asynchronously on the main thread in the future.
+
+ @remarks If there is already an anonymous user signed in, that user will be returned instead.
+ If there is any other existing user signed in, that user will be signed out.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that anonymous accounts are
+ not enabled. Enable them in the Auth section of the Firebase console.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)signInAnonymouslyWithCompletion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn signInWithCustomToken:completion:
+ @brief Asynchronously signs in to Firebase with the given Auth token.
+
+ @param token A self-signed custom auth token.
+ @param completion Optionally; a block which is invoked when the sign in finishes, or is
+ canceled. Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidCustomToken - Indicates a validation error with
+ the custom token.
+ </li>
+ <li>@c FIRAuthErrorCodeCustomTokenMismatch - Indicates the service account and the API key
+ belong to different projects.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)signInWithCustomToken:(NSString *)token
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn createUserWithEmail:password:completion:
+ @brief Creates and, on success, signs in a user with the given email address and password.
+
+ @param email The user's email address.
+ @param password The user's desired password.
+ @param completion Optionally; a block which is invoked when the sign up flow finishes, or is
+ canceled. Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.
+ </li>
+ <li>@c FIRAuthErrorCodeEmailAlreadyInUse - Indicates the email used to attempt sign up
+ already exists. Call fetchProvidersForEmail to check which sign-in mechanisms the user
+ used, and prompt the user to sign in with one of those.
+ </li>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that email and password accounts
+ are not enabled. Enable them in the Auth section of the Firebase console.
+ </li>
+ <li>@c FIRAuthErrorCodeWeakPassword - Indicates an attempt to set a password that is
+ considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo
+ dictionary object will contain more detailed explanation that can be shown to the user.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)createUserWithEmail:(NSString *)email
+ password:(NSString *)password
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn confirmPasswordResetWithCode:newPassword:completion:
+ @brief Resets the password given a code sent to the user outside of the app and a new password
+ for the user.
+
+ @param newPassword The new password.
+ @param completion Optionally; a block which is invoked when the request finishes. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeWeakPassword - Indicates an attempt to set a password that is
+ considered too weak.
+ </li>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates the administrator disabled sign
+ in with the specified identity provider.
+ </li>
+ <li>@c FIRAuthErrorCodeExpiredActionCode - Indicates the OOB code is expired.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidActionCode - Indicates the OOB code is invalid.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)confirmPasswordResetWithCode:(NSString *)code
+ newPassword:(NSString *)newPassword
+ completion:(FIRConfirmPasswordResetCallback)completion;
+
+/** @fn checkActionCode:completion:
+ @brief Checks the validity of an out of band code.
+
+ @param code The out of band code to check validity.
+ @param completion Optionally; a block which is invoked when the request finishes. Invoked
+ asynchronously on the main thread in the future.
+ */
+- (void)checkActionCode:(NSString *)code completion:(FIRCheckActionCodeCallBack)completion;
+
+/** @fn verifyPasswordResetCode:completion:
+ @brief Checks the validity of a verify password reset code.
+
+ @param code The password reset code to be verified.
+ @param completion Optionally; a block which is invoked when the request finishes. Invoked
+ asynchronously on the main thread in the future.
+ */
+- (void)verifyPasswordResetCode:(NSString *)code
+ completion:(FIRVerifyPasswordResetCodeCallback)completion;
+
+/** @fn applyActionCode:completion:
+ @brief Applies out of band code.
+
+ @param code The out of band code to be applied.
+ @param completion Optionally; a block which is invoked when the request finishes. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks This method will not work for out of band codes which require an additional parameter,
+ such as password reset code.
+ */
+- (void)applyActionCode:(NSString *)code
+ completion:(FIRApplyActionCodeCallback)completion;
+
+/** @fn sendPasswordResetWithEmail:completion:
+ @brief Initiates a password reset for the given email address.
+
+ @param email The email address of the user.
+ @param completion Optionally; a block which is invoked when the request finishes. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidRecipientEmail - Indicates an invalid recipient email was
+ sent in the request.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidSender - Indicates an invalid sender email is set in
+ the console for this action.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidMessagePayload - Indicates an invalid email template for
+ sending update email.
+ </li>
+ </ul>
+ */
+- (void)sendPasswordResetWithEmail:(NSString *)email
+ completion:(nullable FIRSendPasswordResetCallback)completion;
+
+/** @fn signOut:
+ @brief Signs out the current user.
+
+ @param error Optionally; if an error occurs, upon return contains an NSError object that
+ describes the problem; is nil otherwise.
+ @return @YES when the sign out request was successful. @NO otherwise.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeKeychainError - Indicates an error occurred when accessing the
+ keychain. The @c NSLocalizedFailureReasonErrorKey field in the @c NSError.userInfo
+ dictionary will contain more information about the error encountered.
+ </li>
+ </ul>
+
+ */
+- (BOOL)signOut:(NSError *_Nullable *_Nullable)error;
+
+/** @fn addAuthStateDidChangeListener:
+ @brief Registers a block as an "auth state did change" listener. To be invoked when:
+
+ + The block is registered as a listener,
+ + A user with a different UID from the current user has signed in, or
+ + The current user has signed out.
+
+ @param listener The block to be invoked. The block is always invoked asynchronously on the main
+ thread, even for it's initial invocation after having been added as a listener.
+
+ @remarks The block is invoked immediately after adding it according to it's standard invocation
+ semantics, asynchronously on the main thread. Users should pay special attention to
+ making sure the block does not inadvertently retain objects which should not be retained by
+ the long-lived block. The block itself will be retained by @c FIRAuth until it is
+ unregistered or until the @c FIRAuth instance is otherwise deallocated.
+
+ @return A handle useful for manually unregistering the block as a listener.
+ */
+- (FIRAuthStateDidChangeListenerHandle)addAuthStateDidChangeListener:
+ (FIRAuthStateDidChangeListenerBlock)listener;
+
+/** @fn removeAuthStateDidChangeListener:
+ @brief Unregisters a block as an "auth state did change" listener.
+
+ @param listenerHandle The handle for the listener.
+ */
+- (void)removeAuthStateDidChangeListener:(FIRAuthStateDidChangeListenerHandle)listenerHandle;
+
+/** @fn addIDTokenDidChangeListener:
+ @brief Registers a block as an "ID token did change" listener. To be invoked when:
+
+ + The block is registered as a listener,
+ + A user with a different UID from the current user has signed in,
+ + The ID token of the current user has been refreshed, or
+ + The current user has signed out.
+
+ @param listener The block to be invoked. The block is always invoked asynchronously on the main
+ thread, even for it's initial invocation after having been added as a listener.
+
+ @remarks The block is invoked immediately after adding it according to it's standard invocation
+ semantics, asynchronously on the main thread. Users should pay special attention to
+ making sure the block does not inadvertently retain objects which should not be retained by
+ the long-lived block. The block itself will be retained by @c FIRAuth until it is
+ unregistered or until the @c FIRAuth instance is otherwise deallocated.
+
+ @return A handle useful for manually unregistering the block as a listener.
+ */
+- (FIRIDTokenDidChangeListenerHandle)addIDTokenDidChangeListener:
+ (FIRIDTokenDidChangeListenerBlock)listener;
+
+/** @fn removeIDTokenDidChangeListener:
+ @brief Unregisters a block as an "ID token did change" listener.
+
+ @param listenerHandle The handle for the listener.
+ */
+- (void)removeIDTokenDidChangeListener:(FIRIDTokenDidChangeListenerHandle)listenerHandle;
+
+/** @fn setAPNSToken:type:
+ @brief Sets the APNs token along with its type.
+ @remarks If swizzling is disabled, the APNs Token must be set for phone number auth to work,
+ by either setting calling this method or by setting the @c APNSToken property.
+ */
+- (void)setAPNSToken:(NSData *)token type:(FIRAuthAPNSTokenType)type;
+
+/** @fn canHandleNotification:
+ @brief Whether the specific remote notification is handled by @c FIRAuth .
+ @param userInfo A dictionary that contains information related to the
+ notification in question.
+ @return Whether or the notification is handled. @c YES means the notification is for @c FIRAuth
+ so the caller should ignore the notification from further processing, and @c NO means the
+ the notification is for the app (or another libaray) so the caller should continue handling
+ this notification as usual.
+ @remarks If swizzling is disabled, related remote notifications must be forwarded to this method
+ for phone number auth to work.
+ */
+- (BOOL)canHandleNotification:(NSDictionary *)userInfo;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuth.m b/Firebase/Auth/Source/FIRAuth.m
new file mode 100644
index 0000000..d1beae6
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuth.m
@@ -0,0 +1,1252 @@
+/*
+ * 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 "Private/FIRAuth_Internal.h"
+
+#import "FIRAppAssociationRegistration.h"
+#import "FIRAppInternal.h"
+#import "FIROptions.h"
+#import "FIRLogger.h"
+#import "AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h"
+#import "AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h"
+#import "Private/FIRAdditionalUserInfo_Internal.h"
+#import "Private/FIRAuthAPNSToken.h"
+#import "Private/FIRAuthAPNSTokenManager.h"
+#import "Private/FIRAuthAppCredentialManager.h"
+#import "Private/FIRAuthAppDelegateProxy.h"
+#import "Private/FIRAuthCredential_Internal.h"
+#import "Private/FIRAuthDataResult_Internal.h"
+#import "Private/FIRAuthDispatcher.h"
+#import "Private/FIRAuthErrorUtils.h"
+#import "FIRAuthExceptionUtils.h"
+#import "Private/FIRAuthGlobalWorkQueue.h"
+#import "Private/FIRAuthKeychain.h"
+#import "Private/FIRAuthNotificationManager.h"
+#import "Private/FIRUser_Internal.h"
+#import "FirebaseAuth.h"
+#import "FIRAuthBackend.h"
+#import "FIRCreateAuthURIRequest.h"
+#import "FIRCreateAuthURIResponse.h"
+#import "FIRGetOOBConfirmationCodeRequest.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRResetPasswordRequest.h"
+#import "FIRResetPasswordResponse.h"
+#import "FIRSendVerificationCodeRequest.h"
+#import "FIRSendVerificationCodeResponse.h"
+#import "FIRSetAccountInfoRequest.h"
+#import "FIRSetAccountInfoResponse.h"
+#import "FIRSignUpNewUserRequest.h"
+#import "FIRSignUpNewUserResponse.h"
+#import "FIRVerifyAssertionRequest.h"
+#import "FIRVerifyAssertionResponse.h"
+#import "FIRVerifyCustomTokenRequest.h"
+#import "FIRVerifyCustomTokenResponse.h"
+#import "FIRVerifyPasswordRequest.h"
+#import "FIRVerifyPasswordResponse.h"
+#import "FIRVerifyPhoneNumberRequest.h"
+#import "FIRVerifyPhoneNumberResponse.h"
+
+#pragma mark - Constants
+
+NSString *const FIRAuthStateDidChangeInternalNotification =
+ @"FIRAuthStateDidChangeInternalNotification";
+NSString *const FIRAuthStateDidChangeInternalNotificationTokenKey =
+ @"FIRAuthStateDidChangeInternalNotificationTokenKey";
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+const NSNotificationName FIRAuthStateDidChangeNotification = @"FIRAuthStateDidChangeNotification";
+#else
+NSString *const FIRAuthStateDidChangeNotification = @"FIRAuthStateDidChangeNotification";
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+/** @var kMaxWaitTimeForBackoff
+ @brief The maximum wait time before attempting to retry auto refreshing tokens after a failed
+ attempt.
+ @remarks This is the upper limit (in seconds) of the exponential backoff used for retrying
+ token refresh.
+ */
+static NSTimeInterval kMaxWaitTimeForBackoff = 16 * 60;
+
+/** @var kTokenRefreshHeadStart
+ @brief The amount of time before the token expires that proactive refresh should be attempted.
+ */
+NSTimeInterval kTokenRefreshHeadStart = 5 * 60;
+
+/** @var kUserKey
+ @brief Key of user stored in the keychain. Prefixed with a Firebase app name.
+ */
+static NSString *const kUserKey = @"%@_firebase_user";
+
+/** @var kMissingEmailInvalidParameterExceptionReason
+ @brief The key of missing email key @c invalidParameterException.
+ */
+static NSString *const kEmailInvalidParameterReason = @"The email used to initiate user password "
+ "cannot be nil";
+
+static NSString *const kPasswordResetRequestType = @"PASSWORD_RESET";
+
+static NSString *const kVerifyEmailRequestType = @"VERIFY_EMAIL";
+
+/** @var kMissingPasswordReason
+ @brief The reason why the @c FIRAuthErrorCodeWeakPassword error is thrown.
+ @remarks This error message will be localized in the future.
+ */
+static NSString *const kMissingPasswordReason = @"Missing Password";
+
+/** @var gKeychainServiceNameForAppName
+ @brief A map from Firebase app name to keychain service names.
+ @remarks This map is needed for looking up the keychain service name after the FIRApp instance
+ is deleted, to remove the associated keychain item. Accessing should occur within a
+ @syncronized([FIRAuth class]) context.
+ */
+static NSMutableDictionary *gKeychainServiceNameForAppName;
+
+#pragma mark - FIRActionCodeInfo
+
+@implementation FIRActionCodeInfo {
+ /** @var _email
+ @brief The email address to which the code was sent. The new email address in the case of
+ FIRActionCodeOperationRecoverEmail.
+ */
+ NSString *_email;
+
+ /** @var _fromEmail
+ @brief The current email address in the case of FIRActionCodeOperationRecoverEmail.
+ */
+ NSString *_fromEmail;
+}
+
+- (NSString *)dataForKey:(FIRActionDataKey)key{
+ switch (key) {
+ case FIRActionCodeEmailKey:
+ return _email;
+ case FIRActionCodeFromEmailKey:
+ return _fromEmail;
+ }
+}
+
+- (instancetype)initWithOperation:(FIRActionCodeOperation)operation
+ email:(NSString *)email
+ newEmail:(nullable NSString *)newEmail {
+ self = [super init];
+ if (self) {
+ _operation = operation;
+ if (newEmail) {
+ _email = [newEmail copy];
+ _fromEmail = [email copy];
+ } else {
+ _email = [email copy];
+ }
+ }
+ return self;
+}
+
+/** @fn actionCodeOperationForRequestType:
+ @brief Returns the corresponding operation type per provided request type string.
+ @param requestType Request type returned in in the server response.
+ @return The corresponding FIRActionCodeOperation for the supplied request type.
+ */
++ (FIRActionCodeOperation)actionCodeOperationForRequestType:(NSString *)requestType {
+ if ([requestType isEqualToString:kPasswordResetRequestType]) {
+ return FIRActionCodeOperationPasswordReset;
+ }
+ if ([requestType isEqualToString:kVerifyEmailRequestType]) {
+ return FIRActionCodeOperationVerifyEmail;
+ }
+ return FIRActionCodeOperationUnknown;
+}
+
+@end
+
+#pragma mark - FIRAuth
+
+@interface FIRAuth () <FIRAuthAppDelegateHandler>
+
+/** @property firebaseAppId
+ @brief The Firebase app ID.
+ */
+@property(nonatomic, copy, readonly) NSString *firebaseAppId;
+
+/** @fn initWithApp:
+ @brief Creates a @c FIRAuth instance associated with the provided @c FIRApp instance.
+ @param app The application to associate the auth instance with.
+ */
+- (instancetype)initWithApp:(FIRApp *)app;
+
+@end
+
+@implementation FIRAuth {
+ /** @var _firebaseAppName
+ @brief The Firebase app name.
+ */
+ NSString *_firebaseAppName;
+
+ /** @var _listenerHandles
+ @brief Handles returned from @c NSNotificationCenter for blocks which are "auth state did
+ change" notification listeners.
+ @remarks Mutations should occur within a @syncronized(self) context.
+ */
+ NSMutableArray<FIRAuthStateDidChangeListenerHandle> *_listenerHandles;
+
+ /** @var _keychain
+ @brief The keychain service.
+ */
+ FIRAuthKeychain *_keychain;
+
+ /** @var _autoRefreshTokens
+ @brief This flag denotes whether or not tokens should be automatically refreshed.
+ @remarks Will only be set to @YES if the another Firebase service is included (additionally to
+ Firebase Auth).
+ */
+ BOOL _autoRefreshTokens;
+
+ /** @var _autoRefreshScheduled
+ @brief Whether or not token auto-refresh is currently scheduled.
+ */
+ BOOL _autoRefreshScheduled;
+
+ /** @var _isAppInBackground
+ @brief A flag that is set to YES if the app is put in the background and no when the app is
+ returned to the foreground.
+ */
+ BOOL _isAppInBackground;
+
+ /** @var _applicationDidBecomeActiveObserver
+ @brief An opaque object to act as the observer for UIApplicationDidBecomeActiveNotification.
+ */
+ id<NSObject> _applicationDidBecomeActiveObserver;
+
+ /** @var _applicationDidBecomeActiveObserver
+ @brief An opaque object to act as the observer for
+ UIApplicationDidEnterBackgroundNotification.
+ */
+ id<NSObject> _applicationDidEnterBackgroundObserver;
+}
+
++ (void)load {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ gKeychainServiceNameForAppName = [[NSMutableDictionary alloc] init];
+
+ NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
+
+ // Ensures the @c FIRAuth instance for a given app gets loaded as soon as the app is ready.
+ [defaultCenter addObserverForName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ queue:nil
+ usingBlock:^(NSNotification *notification) {
+ [FIRAuth authWithApp:[FIRApp appNamed:notification.userInfo[kFIRAppNameKey]]];
+ }];
+ // Ensures the saved user is cleared when the app is deleted.
+ [defaultCenter addObserverForName:kFIRAppDeleteNotification
+ object:[FIRApp class]
+ queue:nil
+ usingBlock:^(NSNotification *notification) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ // This doesn't stop any request already issued, see b/27704535 .
+ NSString *appName = notification.userInfo[kFIRAppNameKey];
+ NSString *keychainServiceName = [FIRAuth keychainServiceNameForAppName:appName];
+ if (keychainServiceName) {
+ [self deleteKeychainServiceNameForAppName:appName];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:keychainServiceName];
+ NSString *userKey = [NSString stringWithFormat:kUserKey, appName];
+ [keychain removeDataForKey:userKey error:NULL];
+ }
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:FIRAuthStateDidChangeNotification
+ object:nil];
+ });
+ });
+ }];
+ });
+}
+
++ (FIRAuth *)auth {
+ FIRApp *defaultApp = [FIRApp defaultApp];
+ if (!defaultApp) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"The default FIRApp instance must be configured before the default FIRAuth"
+ @"instance can be initialized. One way to ensure that is to call "
+ @"`[FIRApp configure];` is called in "
+ @"`application:didFinishLaunchingWithOptions:`."];
+ }
+ return [self authWithApp:defaultApp];
+}
+
++ (FIRAuth *)authWithApp:(FIRApp *)app {
+ return [FIRAppAssociationRegistration registeredObjectWithHost:app
+ key:NSStringFromClass(self)
+ creationBlock:^FIRAuth *_Nullable() {
+ return [[FIRAuth alloc] initWithApp:app];
+ }];
+}
+
+- (instancetype)initWithApp:(FIRApp *)app {
+ [FIRAuth setKeychainServiceNameForApp:app];
+ self = [self initWithAPIKey:app.options.APIKey appName:app.name];
+ if (self) {
+ _app = app;
+ __weak FIRAuth *weakSelf = self;
+ app.getTokenImplementation = ^(BOOL forceRefresh, FIRTokenCallback callback) {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuth *strongSelf = weakSelf;
+ if (strongSelf && !strongSelf->_autoRefreshTokens) {
+ FIRLogInfo(kFIRLoggerAuth, @"I-AUT000002", @"Token auto-refresh enabled.");
+ strongSelf->_autoRefreshTokens = YES;
+ strongSelf->_applicationDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter]
+ addObserverForName:UIApplicationDidBecomeActiveNotification
+ object:nil
+ queue:nil
+ usingBlock:^(NSNotification *notification) {
+ FIRAuth *strongSelf = weakSelf;
+ if (strongSelf) {
+ strongSelf->_isAppInBackground = NO;
+ if (!strongSelf->_autoRefreshScheduled) {
+ [weakSelf scheduleAutoTokenRefresh];
+ }
+ }
+ }];
+ strongSelf->_applicationDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter]
+ addObserverForName:UIApplicationDidEnterBackgroundNotification
+ object:nil
+ queue:nil
+ usingBlock:^(NSNotification *notification) {
+ FIRAuth *strongSelf = weakSelf;
+ if (strongSelf) {
+ strongSelf->_isAppInBackground = YES;
+ }
+ }];
+ }
+ if (!strongSelf.currentUser) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(nil, nil);
+ });
+ return;
+ }
+ [strongSelf.currentUser internalGetTokenForcingRefresh:forceRefresh
+ callback:^(NSString *_Nullable token,
+ NSError *_Nullable error) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(token, error);
+ });
+ }];
+ });
+ };
+ app.getUIDImplementation = ^NSString *_Nullable() {
+ __block NSString *uid;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ uid = [weakSelf getUID];
+ });
+ return uid;
+ };
+ }
+ return self;
+}
+
+- (instancetype)initWithAPIKey:(NSString *)APIKey appName:(NSString *)appName {
+ self = [super init];
+ if (self) {
+ _listenerHandles = [NSMutableArray array];
+ _APIKey = [APIKey copy];
+ _firebaseAppName = [appName copy];
+ NSString *keychainServiceName = [FIRAuth keychainServiceNameForAppName:appName];
+ if (keychainServiceName) {
+ _keychain = [[FIRAuthKeychain alloc] initWithService:keychainServiceName];
+ }
+ // Load current user from keychain.
+ FIRUser *user;
+ NSError *error;
+ if ([self getUser:&user error:&error]) {
+ [self updateCurrentUser:user byForce:NO savingToDisk:NO error:&error];
+ } else {
+ FIRLogError(kFIRLoggerAuth, @"I-AUT000001",
+ @"Error loading saved user when starting up: %@", error);
+ }
+ // Initialize for phone number auth.
+ _tokenManager =
+ [[FIRAuthAPNSTokenManager alloc] initWithApplication:[UIApplication sharedApplication]];
+ _appCredentialManager = [[FIRAuthAppCredentialManager alloc] initWithKeychain:_keychain];
+ _notificationManager =
+ [[FIRAuthNotificationManager alloc] initWithApplication:[UIApplication sharedApplication]
+ appCredentialManager:_appCredentialManager];
+ [[FIRAuthAppDelegateProxy sharedInstance] addHandler:self];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ @synchronized (self) {
+ NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
+ while (_listenerHandles.count != 0) {
+ FIRAuthStateDidChangeListenerHandle handleToRemove = _listenerHandles.lastObject;
+ [defaultCenter removeObserver:handleToRemove];
+ [_listenerHandles removeLastObject];
+ }
+ [defaultCenter removeObserver:_applicationDidBecomeActiveObserver
+ name:UIApplicationDidBecomeActiveNotification
+ object:nil];
+ [defaultCenter removeObserver:_applicationDidEnterBackgroundObserver
+ name:UIApplicationDidEnterBackgroundNotification
+ object:nil];
+ }
+}
+
+#pragma mark - Public API
+
+- (void)fetchProvidersForEmail:(NSString *)email
+ completion:(FIRProviderQueryCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRCreateAuthURIRequest *request =
+ [[FIRCreateAuthURIRequest alloc] initWithIdentifier:email
+ continueURI:@"http://www.google.com/"
+ APIKey:_APIKey];
+ [FIRAuthBackend createAuthURI:request callback:^(FIRCreateAuthURIResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(response.allProviders, error);
+ });
+ }
+ }];
+ });
+}
+
+- (void)signInWithEmail:(NSString *)email
+ password:(NSString *)password
+ completion:(FIRAuthResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self signInWithEmail:email
+ password:password
+ callback:[self signInFlowAuthResultCallbackByDecoratingCallback:completion]];
+ });
+}
+
+/** @fn signInWithEmail:password:callback:
+ @brief Signs in using an email address and password.
+ @param email The user's email address.
+ @param password The user's password.
+ @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.
+ @remarks This is the internal counterpart of this method, which uses a callback that does not
+ update the current user.
+ */
+- (void)signInWithEmail:(NSString *)email
+ password:(NSString *)password
+ callback:(FIRAuthResultCallback)callback {
+ FIRVerifyPasswordRequest *request =
+ [[FIRVerifyPasswordRequest alloc] initWithEmail:email password:password APIKey:_APIKey];
+
+ if (![request.password length]) {
+ callback(nil, [FIRAuthErrorUtils wrongPasswordErrorWithMessage:nil]);
+ return;
+ }
+ [FIRAuthBackend verifyPassword:request
+ callback:^(FIRVerifyPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:NO
+ callback:callback];
+ }];
+}
+
+- (void)signInWithCredential:(FIRAuthCredential *)credential
+ completion:(FIRAuthResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuthResultCallback callback =
+ [self signInFlowAuthResultCallbackByDecoratingCallback:completion];
+ [self internalSignInWithCredential:credential callback:callback];
+ });
+}
+
+- (void)signInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthDataResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuthDataResultCallback callback =
+ [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion];
+ [self internalSignInAndRetrieveDataWithCredential:credential
+ isReauthentication:NO
+ callback:callback];
+ });
+}
+
+- (void)internalSignInWithCredential:(FIRAuthCredential *)credential
+ callback:(FIRAuthResultCallback)callback {
+ [self internalSignInAndRetrieveDataWithCredential:credential
+ isReauthentication:NO
+ callback:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ callback(authResult.user, error);
+ }];
+}
+
+- (void)internalSignInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
+ isReauthentication:(BOOL)isReauthentication
+ callback:(nullable FIRAuthDataResultCallback)callback {
+ if ([credential isKindOfClass:[FIREmailPasswordAuthCredential class]]) {
+ // Special case for email/password credentials:
+ FIREmailPasswordAuthCredential *emailPasswordCredential =
+ (FIREmailPasswordAuthCredential *)credential;
+ [self signInWithEmail:emailPasswordCredential.email
+ password:emailPasswordCredential.password
+ callback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (callback) {
+ FIRAuthDataResult *result = user ?
+ [[FIRAuthDataResult alloc] initWithUser:user additionalUserInfo:nil] : nil;
+ callback(result, error);
+ }
+ }];
+ return;
+ }
+
+ if ([credential isKindOfClass:[FIRPhoneAuthCredential class]]) {
+ // Special case for phone auth credential
+ FIRPhoneAuthCredential *phoneCredential = (FIRPhoneAuthCredential *)credential;
+ [self signInWithPhoneCredential:phoneCredential callback:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (callback) {
+ FIRAuthDataResult *result = user ?
+ [[FIRAuthDataResult alloc] initWithUser:user additionalUserInfo:nil] : nil;
+ callback(result, error);
+ }
+ }];
+ return;
+ }
+
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:_APIKey providerID:credential.provider];
+ request.autoCreate = !isReauthentication;
+ [credential prepareVerifyAssertionRequest:request];
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse *response, NSError *error) {
+ if (error) {
+ if (callback) {
+ callback(nil, error);
+ }
+ return;
+ }
+
+ if (response.needConfirmation) {
+ if (callback) {
+ NSString *email = response.email;
+ callback(nil, [FIRAuthErrorUtils accountExistsWithDifferentCredentialErrorWithEmail:email]);
+ }
+ return;
+ }
+
+ if (!response.providerID.length) {
+ if (callback) {
+ callback(nil, [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:response]);
+ }
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:NO
+ callback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (callback) {
+ FIRAdditionalUserInfo *additionalUserInfo =
+ [FIRAdditionalUserInfo userInfoWithVerifyAssertionResponse:response];
+ FIRAuthDataResult *result = user ?
+ [[FIRAuthDataResult alloc] initWithUser:user
+ additionalUserInfo:additionalUserInfo] : nil;
+ callback(result, error);
+ }
+ }];
+ }];
+}
+
+- (void)signInWithCredential:(FIRAuthCredential *)credential
+ callback:(FIRAuthResultCallback)callback {
+ [self signInAndRetrieveDataWithCredential:credential
+ completion:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ callback(authResult.user, error);
+ }];
+}
+
+- (void)signInAnonymouslyWithCompletion:(FIRAuthResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuthResultCallback decoratedCallback =
+ [self signInFlowAuthResultCallbackByDecoratingCallback:completion];
+ if (_currentUser.anonymous) {
+ decoratedCallback(_currentUser, nil);
+ return;
+ }
+ FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:_APIKey];
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ decoratedCallback(nil, error);
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:YES
+ callback:decoratedCallback];
+ }];
+ });
+}
+
+- (void)signInWithCustomToken:(NSString *)token
+ completion:(nullable FIRAuthResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuthResultCallback decoratedCallback =
+ [self signInFlowAuthResultCallbackByDecoratingCallback:completion];
+ FIRVerifyCustomTokenRequest *request =
+ [[FIRVerifyCustomTokenRequest alloc] initWithToken:token APIKey:_APIKey];
+ [FIRAuthBackend verifyCustomToken:request
+ callback:^(FIRVerifyCustomTokenResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ decoratedCallback(nil, error);
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:NO
+ callback:decoratedCallback];
+ }];
+ });
+}
+
+- (void)createUserWithEmail:(NSString *)email
+ password:(NSString *)password
+ completion:(nullable FIRAuthResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRAuthResultCallback decoratedCallback =
+ [self signInFlowAuthResultCallbackByDecoratingCallback:completion];
+ FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:_APIKey
+ email:email
+ password:password
+ displayName:nil];
+ if (![request.password length]) {
+ decoratedCallback(nil, [FIRAuthErrorUtils
+ weakPasswordErrorWithServerResponseReason:kMissingPasswordReason]);
+ return;
+ }
+ [FIRAuthBackend signUpNewUser:request
+ callback:^(FIRSignUpNewUserResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ decoratedCallback(nil, error);
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:NO
+ callback:decoratedCallback];
+ }];
+ });
+}
+
+- (void)confirmPasswordResetWithCode:(NSString *)code
+ newPassword:(NSString *)newPassword
+ completion:(FIRConfirmPasswordResetCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:_APIKey
+ oobCode:code
+ newPassword:newPassword];
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (error) {
+ completion(error);
+ return;
+ }
+ completion(nil);
+ });
+ }
+ }];
+ });
+}
+
+- (void)checkActionCode:(NSString *)code completion:(FIRCheckActionCodeCallBack)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^ {
+ FIRResetPasswordRequest *request =
+ [[FIRResetPasswordRequest alloc] initWithAPIKey:_APIKey
+ oobCode:code
+ newPassword:nil];
+ [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (completion) {
+ if (error) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(nil, error);
+ });
+ return;
+ }
+ FIRActionCodeOperation operation =
+ [FIRActionCodeInfo actionCodeOperationForRequestType:response.requestType];
+ FIRActionCodeInfo *actionCodeInfo =
+ [[FIRActionCodeInfo alloc] initWithOperation:operation
+ email:response.email
+ newEmail:response.verifiedEmail];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(actionCodeInfo, nil);
+ });
+ }
+ }];
+ });
+}
+
+- (void)verifyPasswordResetCode:(NSString *)code
+ completion:(FIRVerifyPasswordResetCodeCallback)completion {
+ [self checkActionCode:code completion:^(FIRActionCodeInfo *_Nullable info,
+ NSError *_Nullable error) {
+ if (completion) {
+ if (error) {
+ completion(nil, error);
+ return;
+ }
+ completion([info dataForKey:FIRActionCodeEmailKey], nil);
+ }
+ }];
+}
+
+- (void)applyActionCode:(NSString *)code completion:(FIRApplyActionCodeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^ {
+ FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc]initWithAPIKey:_APIKey];
+ request.OOBCode = code;
+ [FIRAuthBackend setAccountInfo:request callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(error);
+ });
+ }
+ }];
+ });
+}
+
+- (void)sendPasswordResetWithEmail:(NSString *)email
+ completion:(nullable FIRSendPasswordResetCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ if (!email) {
+ [FIRAuthExceptionUtils raiseInvalidParameterExceptionWithReason:kEmailInvalidParameterReason];
+ }
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:email APIKey:_APIKey];
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(error);
+ });
+ }
+ }];
+ });
+}
+
+- (BOOL)signOut:(NSError *_Nullable *_Nullable)error {
+ __block BOOL result = YES;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ if (!_currentUser) {
+ return;
+ }
+ result = [self updateCurrentUser:nil byForce:NO savingToDisk:YES error:error];
+ });
+ return result;
+}
+
+- (BOOL)signOutByForceWithUserID:(NSString *)userID error:(NSError *_Nullable *_Nullable)error {
+ if (_currentUser.uid != userID) {
+ return YES;
+ }
+ return [self updateCurrentUser:nil byForce:YES savingToDisk:YES error:error];
+}
+
+- (FIRAuthStateDidChangeListenerHandle)addAuthStateDidChangeListener:
+ (FIRAuthStateDidChangeListenerBlock)listener {
+ __block BOOL firstInvocation = YES;
+ __block NSString *previousUserID;
+ return [self addIDTokenDidChangeListener:^(FIRAuth *_Nonnull auth, FIRUser *_Nullable user) {
+ BOOL shouldCallListener = firstInvocation ||
+ !(previousUserID == user.uid || [previousUserID isEqualToString:user.uid]);
+ firstInvocation = NO;
+ previousUserID = [user.uid copy];
+ if (shouldCallListener) {
+ listener(auth, user);
+ }
+ }];
+}
+
+- (void)removeAuthStateDidChangeListener:(FIRAuthStateDidChangeListenerHandle)listenerHandle {
+ [self removeIDTokenDidChangeListener:listenerHandle];
+}
+
+- (FIRIDTokenDidChangeListenerHandle)addIDTokenDidChangeListener:
+ (FIRIDTokenDidChangeListenerBlock)listener {
+ if (!listener) {
+ [NSException raise:NSInvalidArgumentException format:@"listener must not be nil."];
+ return nil;
+ }
+ FIRAuthStateDidChangeListenerHandle handle;
+ NSNotificationCenter *notifications = [NSNotificationCenter defaultCenter];
+ handle = [notifications addObserverForName:FIRAuthStateDidChangeNotification
+ object:self
+ queue:[NSOperationQueue mainQueue]
+ usingBlock:^(NSNotification *_Nonnull notification) {
+ FIRAuth *auth = notification.object;
+ listener(auth, auth.currentUser);
+ }];
+ @synchronized (self) {
+ [_listenerHandles addObject:handle];
+ }
+ dispatch_async(dispatch_get_main_queue(), ^{
+ listener(self, self.currentUser);
+ });
+ return handle;
+}
+
+- (void)removeIDTokenDidChangeListener:(FIRIDTokenDidChangeListenerHandle)listenerHandle {
+ [[NSNotificationCenter defaultCenter] removeObserver:listenerHandle];
+ @synchronized (self) {
+ [_listenerHandles removeObject:listenerHandle];
+ }
+}
+
+- (NSData *)APNStoken {
+ __block NSData *result = nil;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ result = _tokenManager.token.data;
+ });
+ return result;
+}
+
+- (void)setAPNSToken:(NSData *)APNSToken {
+ [self setAPNSToken:APNSToken type:FIRAuthAPNSTokenTypeUnknown];
+}
+
+- (void)setAPNSToken:(NSData *)token type:(FIRAuthAPNSTokenType)type {
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ _tokenManager.token = [[FIRAuthAPNSToken alloc] initWithData:token type:type];
+ });
+}
+
+- (BOOL)canHandleNotification:(NSDictionary *)userInfo {
+ __block BOOL result = NO;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ result = [_notificationManager canHandleNotification:userInfo];
+ });
+ return result;
+}
+
+#pragma mark - Internal Methods
+
+/** @fn signInWithPhoneCredential:callback:
+ @brief Signs in using a phone credential.
+ @param credential The Phone Auth credential used to sign in.
+ @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)signInWithPhoneCredential:(FIRPhoneAuthCredential *)credential
+ callback:(FIRAuthResultCallback)callback {
+ if (credential.temporaryProof.length && credential.phoneNumber.length) {
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc] initWithTemporaryProof:credential.temporaryProof
+ phoneNumber:credential.phoneNumber
+ APIKey:_APIKey];
+ [self phoneNumberSignInWithRequest:request callback:callback];
+ return;
+ }
+
+ if (!credential.verificationID.length) {
+ callback(nil, [FIRAuthErrorUtils missingVerificationIDErrorWithMessage:nil]);
+ return;
+ }
+ if (!credential.verificationCode.length) {
+ callback(nil, [FIRAuthErrorUtils missingVerificationCodeErrorWithMessage:nil]);
+ return;
+ }
+ FIRVerifyPhoneNumberRequest *request =
+ [[FIRVerifyPhoneNumberRequest alloc]initWithVerificationID:credential.verificationID
+ verificationCode:credential.verificationCode
+ APIKey:_APIKey];
+ [self phoneNumberSignInWithRequest:request callback:callback];
+}
+
+
+/** @fn phoneNumberSignInWithVerificationID:pasverificationCodesword:callback:
+ @brief Signs in using a FIRVerifyPhoneNumberRequest object.
+ @param request THe FIRVerifyPhoneNumberRequest request object.
+ @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)phoneNumberSignInWithRequest:(FIRVerifyPhoneNumberRequest *)request
+ callback:(FIRAuthResultCallback)callback {
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ [self completeSignInWithAccessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken
+ anonymous:NO
+ callback:callback];
+ }];
+}
+
+- (void)notifyListenersOfAuthStateChangeWithUser:(FIRUser *)user token:(NSString *)token {
+ if (user && _autoRefreshTokens) {
+ // Shedule new refresh task after successful attempt.
+ [self scheduleAutoTokenRefresh];
+ }
+ if (user == _currentUser) {
+ NSMutableDictionary *internalNotificationParameters = [NSMutableDictionary dictionary];
+ if (token.length) {
+ internalNotificationParameters[FIRAuthStateDidChangeInternalNotificationTokenKey] = token;
+ }
+ NSNotificationCenter *notifications = [NSNotificationCenter defaultCenter];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [notifications postNotificationName:FIRAuthStateDidChangeInternalNotification
+ object:self
+ userInfo:internalNotificationParameters];
+ [notifications postNotificationName:FIRAuthStateDidChangeNotification
+ object:self];
+ });
+ }
+}
+
+- (BOOL)updateKeychainWithUser:(FIRUser *)user error:(NSError *_Nullable *_Nullable)error {
+ if (user == _currentUser) {
+ return [self saveUser:user error:error];
+ }
+ // No-op if the user is no longer signed in. This is not considered an error as we don't check
+ // whether the user is still current on other callbacks of user operations either.
+ return YES;
+}
+
+/** @fn setKeychainServiceNameForApp
+ @brief Sets the keychain service name global data for the particular app.
+ @param app The Firebase app to set keychain service name for.
+ */
++ (void)setKeychainServiceNameForApp:(FIRApp *)app {
+ @synchronized (self) {
+ gKeychainServiceNameForAppName[app.name] =
+ [@"firebase_auth_" stringByAppendingString:app.options.googleAppID];
+ }
+}
+
+/** @fn keychainServiceNameForAppName:
+ @brief Gets the keychain service name global data for the particular app by name.
+ @param appName The name of the Firebase app to get keychain service name for.
+ */
++ (NSString *)keychainServiceNameForAppName:(NSString *)appName {
+ @synchronized (self) {
+ return gKeychainServiceNameForAppName[appName];
+ }
+}
+
+/** @fn deleteKeychainServiceNameForAppName:
+ @brief Deletes the keychain service name global data for the particular app by name.
+ @param appName The name of the Firebase app to delete keychain service name for.
+ */
++ (void)deleteKeychainServiceNameForAppName:(NSString *)appName {
+ @synchronized (self) {
+ [gKeychainServiceNameForAppName removeObjectForKey:appName];
+ }
+}
+
+/** @fn scheduleAutoTokenRefreshWithDelay:
+ @brief Schedules a task to automatically refresh tokens on the current user. The token refresh
+ is scheduled 5 minutes before the scheduled expiration time.
+ @remarks If the token expires in less than 5 minutes, schedule the token refresh immediately.
+ */
+- (void)scheduleAutoTokenRefresh {
+ NSTimeInterval tokenExpirationInterval =
+ [_currentUser.accessTokenExpirationDate timeIntervalSinceNow] - kTokenRefreshHeadStart;
+ [self scheduleAutoTokenRefreshWithDelay:MAX(tokenExpirationInterval, 0) retry:NO];
+}
+
+/** @fn scheduleAutoTokenRefreshWithDelay:
+ @brief Schedules a task to automatically refresh tokens on the current user.
+ @param delay The delay in seconds after which the token refresh task should be scheduled to be
+ executed.
+ @param retry Flag to determine whether the invocation is a retry attempt or not.
+ */
+- (void)scheduleAutoTokenRefreshWithDelay:(NSTimeInterval)delay retry:(BOOL)retry {
+ NSString *accessToken = _currentUser.rawAccessToken;
+ if (!accessToken) {
+ return;
+ }
+ if (retry) {
+ FIRLogNotice(kFIRLoggerAuth, @"I-AUT000003",
+ @"Token auto-refresh re-scheduled in %02d:%02d "
+ @"because of error on previous refresh attempt.",
+ (int)ceil(delay) / 60, (int)ceil(delay) % 60);
+ } else {
+ FIRLogInfo(kFIRLoggerAuth, @"I-AUT000004",
+ @"Token auto-refresh scheduled in %02d:%02d for the new token.",
+ (int)ceil(delay) / 60, (int)ceil(delay) % 60);
+ }
+ _autoRefreshScheduled = YES;
+ __weak FIRAuth *weakSelf = self;
+ [[FIRAuthDispatcher sharedInstance] dispatchAfterDelay:delay
+ queue:FIRAuthGlobalWorkQueue()
+ task:^(void) {
+ FIRAuth *strongSelf = weakSelf;
+ if (!strongSelf) {
+ return;
+ }
+ if (![strongSelf->_currentUser.rawAccessToken isEqualToString:accessToken]) {
+ // Another auto refresh must have been scheduled, so keep _autoRefreshScheduled unchanged.
+ return;
+ }
+ strongSelf->_autoRefreshScheduled = NO;
+ if (strongSelf->_isAppInBackground) {
+ return;
+ }
+ NSString *uid = strongSelf->_currentUser.uid;
+ [strongSelf->_currentUser internalGetTokenForcingRefresh:YES
+ callback:^(NSString *_Nullable token,
+ NSError *_Nullable error) {
+ if (![strongSelf->_currentUser.uid isEqualToString:uid]) {
+ return;
+ }
+ // If the error is an invalid token, sign the user out.
+ if (error.code == FIRAuthErrorCodeInvalidUserToken) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000005",
+ @"Invalid refresh token detected, user is automatically signed out.");
+ [strongSelf signOutByForceWithUserID:uid error:nil];
+ return;
+ }
+ if (error) {
+ // Kicks off exponential back off logic to retry failed attempt. Starts with one minute
+ // delay (60 seconds) if this is the first failed attempt.
+ NSTimeInterval rescheduleDelay;
+ if (retry) {
+ rescheduleDelay = MIN(delay * 2, kMaxWaitTimeForBackoff);
+ } else {
+ rescheduleDelay = 60;
+ }
+ [strongSelf scheduleAutoTokenRefreshWithDelay:rescheduleDelay retry:YES];
+ }
+ }];
+ }];
+}
+
+#pragma mark -
+
+/** @fn completeSignInWithTokenService:callback:
+ @brief Completes a sign-in flow once we have access and refresh tokens for the user.
+ @param accessToken The STS access token.
+ @param accessTokenExpirationDate The approximate expiration date of the access token.
+ @param refreshToken The STS refresh token.
+ @param anonymous Whether or not the user is anonymous.
+ @param callback Called when the user has been signed in or when an error occurred. Invoked
+ asynchronously on the global auth work queue in the future.
+ */
+- (void)completeSignInWithAccessToken:(NSString *)accessToken
+ accessTokenExpirationDate:(NSDate *)accessTokenExpirationDate
+ refreshToken:(NSString *)refreshToken
+ anonymous:(BOOL)anonymous
+ callback:(FIRAuthResultCallback)callback {
+ [FIRUser retrieveUserWithAPIKey:_APIKey
+ accessToken:accessToken
+ accessTokenExpirationDate:accessTokenExpirationDate
+ refreshToken:refreshToken
+ anonymous:anonymous
+ callback:callback];
+}
+
+/** @fn signInFlowAuthResultCallbackByDecoratingCallback:
+ @brief Creates a FIRAuthResultCallback block which wraps another FIRAuthResultCallback; trying
+ to update the current user before forwarding it's invocations along to a subject block
+ @param callback Called when the user has been updated or when an error has occurred. Invoked
+ asynchronously on the main thread in the future.
+ @return Returns a block that updates the current user.
+ @remarks Typically invoked as part of the complete sign-in flow. For any other uses please
+ consider alternative ways of updating the current user.
+*/
+- (FIRAuthResultCallback)signInFlowAuthResultCallbackByDecoratingCallback:
+ (nullable FIRAuthResultCallback)callback {
+ return ^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(nil, error);
+ });
+ }
+ return;
+ }
+ if (![self updateCurrentUser:user byForce:NO savingToDisk:YES error:&error]) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(nil, error);
+ });
+ }
+ return;
+ }
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(user, nil);
+ });
+ }
+ };
+}
+
+/** @fn signInFlowAuthDataResultCallbackByDecoratingCallback:
+ @brief Creates a FIRAuthDataResultCallback block which wraps another FIRAuthDataResultCallback;
+ trying to update the current user before forwarding it's invocations along to a subject
+ block.
+ @param callback Called when the user has been updated or when an error has occurred. Invoked
+ asynchronously on the main thread in the future.
+ @return Returns a block that updates the current user.
+ @remarks Typically invoked as part of the complete sign-in flow. For any other uses please
+ consider alternative ways of updating the current user.
+*/
+- (FIRAuthDataResultCallback)signInFlowAuthDataResultCallbackByDecoratingCallback:
+ (nullable FIRAuthDataResultCallback)callback {
+ return ^(FIRAuthDataResult *_Nullable authResult, NSError *_Nullable error) {
+ if (error) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(nil, error);
+ });
+ }
+ return;
+ }
+ if (![self updateCurrentUser:authResult.user byForce:NO savingToDisk:YES error:&error]) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(nil, error);
+ });
+ }
+ return;
+ }
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(authResult, nil);
+ });
+ }
+ };
+}
+
+#pragma mark - User-Related Methods
+
+/** @fn updateCurrentUser:savingToDisk:
+ @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
+ initialization time. The only time the saveToDisk parameter should be set to NO is during
+ class initialization time because the user was just read from disk.
+ @param user The user to use as the current user (including nil, which is passed at sign out
+ time.)
+ @param saveToDisk Indicates the method should persist the user data to disk.
+ */
+- (BOOL)updateCurrentUser:(FIRUser *)user
+ byForce:(BOOL)force
+ savingToDisk:(BOOL)saveToDisk
+ error:(NSError *_Nullable *_Nullable)error {
+ if (user == _currentUser) {
+ return YES;
+ }
+ BOOL success = YES;
+ if (saveToDisk) {
+ success = [self saveUser:user error:error];
+ }
+ if (success || force) {
+ FIRUser *previousUser = _currentUser;
+ previousUser.auth = nil;
+ _currentUser = user;
+ _currentUser.auth = self;
+ [self notifyListenersOfAuthStateChangeWithUser:user token:user.rawAccessToken];
+ }
+ return success;
+}
+
+/** @fn saveUser:error:
+ @brief Persists user.
+ @param user The user to save.
+ @param error Return value for any error which occurs.
+ @return @YES on success, @NO otherwise.
+ */
+- (BOOL)saveUser:(FIRUser *)user
+ error:(NSError *_Nullable *_Nullable)error {
+ BOOL success;
+ NSString *userKey = [NSString stringWithFormat:kUserKey, _firebaseAppName];
+
+ if (!user) {
+ success = [_keychain removeDataForKey:userKey error:error];
+ } else {
+ // Encode the user object.
+ NSMutableData *archiveData = [NSMutableData data];
+ NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:archiveData];
+ [archiver encodeObject:user forKey:userKey];
+ [archiver finishEncoding];
+
+ // Save the user object's encoded value.
+ success = [_keychain setData:archiveData forKey:userKey error:error];
+ }
+ return success;
+}
+
+/** @fn getUser:error:
+ @brief Retrieves the saved user associated, if one exists, from the keychain.
+ @param outUser An out parameter which is populated with the saved user, if one exists.
+ @param error Return value for any error which occurs.
+ @return YES if the operation was a success (irrespective of whether or not a saved user existed
+ for the given @c firebaseAppId,) NO if an error occurred.
+ */
+- (BOOL)getUser:(FIRUser *_Nullable *)outUser
+ error:(NSError *_Nullable *_Nullable)error {
+ NSString *userKey = [NSString stringWithFormat:kUserKey, _firebaseAppName];
+
+ NSError *keychainError;
+ NSData *encodedUserData = [_keychain dataForKey:userKey error:&keychainError];
+ if (keychainError) {
+ if (error) {
+ *error = keychainError;
+ }
+ return NO;
+ }
+ if (!encodedUserData) {
+ *outUser = nil;
+ return YES;
+ }
+ NSKeyedUnarchiver *unarchiver =
+ [[NSKeyedUnarchiver alloc] initForReadingWithData:encodedUserData];
+ *outUser = [unarchiver decodeObjectOfClass:[FIRUser class] forKey:userKey];
+ return YES;
+}
+
+/** @fn getUID
+ @brief Gets the identifier of the current user, if any.
+ @return The identifier of the current user, or nil if there is no current user.
+ */
+- (nullable NSString *)getUID {
+ return _currentUser.uid;
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthAPNSToken.m b/Firebase/Auth/Source/FIRAuthAPNSToken.m
new file mode 100644
index 0000000..fc5ee2d
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAPNSToken.m
@@ -0,0 +1,34 @@
+/*
+ * 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 "Private/FIRAuthAPNSToken.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAuthAPNSToken
+
+- (instancetype)initWithData:(NSData *)data type:(FIRAuthAPNSTokenType)type {
+ self = [super init];
+ if (self) {
+ _data = [data copy];
+ _type = type;
+ }
+ return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthAPNSTokenManager.m b/Firebase/Auth/Source/FIRAuthAPNSTokenManager.m
new file mode 100644
index 0000000..9609d86
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAPNSTokenManager.m
@@ -0,0 +1,255 @@
+/*
+ * 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 "Private/FIRAuthAPNSTokenManager.h"
+
+#import "FIRLogger.h"
+#import "Private/FIRAuthAPNSToken.h"
+#import "FIRAuthGlobalWorkQueue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kRegistrationTimeout
+ @brief Timeout for registration for remote notification.
+ @remarks Once we start to handle `application:didFailToRegisterForRemoteNotificationsWithError:`
+ we probably don't have to use timeout at all.
+ */
+static const NSTimeInterval kRegistrationTimeout = 5;
+
+/** @var kLegacyRegistrationTimeout
+ @brief Timeout for registration for remote notification on iOS 7.
+ */
+static const NSTimeInterval kLegacyRegistrationTimeout = 30;
+
+@implementation FIRAuthAPNSTokenManager {
+ /** @var _application
+ @brief The @c UIApplication to request the token from.
+ */
+ UIApplication *_application;
+
+ /** @var _pendingCallbacks
+ @brief The list of all pending callbacks for the APNs token.
+ */
+ NSMutableArray<FIRAuthAPNSTokenCallback> *_pendingCallbacks;
+}
+
+- (instancetype)initWithApplication:(UIApplication *)application {
+ self = [super init];
+ if (self) {
+ _application = application;
+ _timeout = [_application respondsToSelector:@selector(registerForRemoteNotifications)] ?
+ kRegistrationTimeout : kLegacyRegistrationTimeout;
+ }
+ return self;
+}
+
+- (void)getTokenWithCallback:(FIRAuthAPNSTokenCallback)callback {
+ if (_token) {
+ callback(_token);
+ return;
+ }
+ if (_pendingCallbacks) {
+ [_pendingCallbacks addObject:callback];
+ return;
+ }
+ _pendingCallbacks =
+ [[NSMutableArray<FIRAuthAPNSTokenCallback> alloc] initWithObjects:callback, nil];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if ([_application respondsToSelector:@selector(registerForRemoteNotifications)]) {
+ [_application registerForRemoteNotifications];
+ } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [_application registerForRemoteNotificationTypes:UIRemoteNotificationTypeAlert];
+#pragma clang diagnostic pop
+ }
+ });
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_timeout * NSEC_PER_SEC)),
+ FIRAuthGlobalWorkQueue(), ^{
+ [self callBack];
+ });
+}
+
+- (void)setToken:(nullable FIRAuthAPNSToken *)token {
+ if (!token) {
+ _token = nil;
+ return;
+ }
+ if (token.type == FIRAuthAPNSTokenTypeUnknown) {
+ static FIRAuthAPNSTokenType detectedTokenType = FIRAuthAPNSTokenTypeUnknown;
+ if (detectedTokenType == FIRAuthAPNSTokenTypeUnknown) {
+ detectedTokenType =
+ [[self class] isProductionApp] ? FIRAuthAPNSTokenTypeProd : FIRAuthAPNSTokenTypeSandbox;
+ }
+ token = [[FIRAuthAPNSToken alloc] initWithData:token.data type:detectedTokenType];
+ }
+ _token = token;
+ [self callBack];
+}
+
+#pragma mark - Internal methods
+
+/** @fn callBack
+ @brief Calls back all pending callbacks with the current APNs token, if one is available.
+ */
+- (void)callBack {
+ if (!_pendingCallbacks) {
+ return;
+ }
+ NSArray<FIRAuthAPNSTokenCallback> *allCallbacks = _pendingCallbacks;
+ _pendingCallbacks = nil;
+ for (FIRAuthAPNSTokenCallback callback in allCallbacks) {
+ callback(_token);
+ }
+};
+
+/** @fn isProductionApp
+ @brief Whether or not the app has production (versus sandbox) provisioning profile.
+ @remarks This method is adapted from @c FIRInstanceID .
+ */
++ (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
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000006",
+ @"Assuming prod APNs token type on simulator.");
+ 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) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000007",
+ @"Error while reading embedded mobileprovision %@", error);
+ 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) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000008",
+ @"Error while reading embedded mobileprovision %@", error);
+ return defaultAppTypeProd;
+ }
+
+ NSScanner *scanner = [NSScanner scannerWithString:embeddedProfile];
+ NSString *plistContents;
+ if ([scanner scanUpToString:@"<plist" intoString:nil]) {
+ if ([scanner scanUpToString:@"</plist>" intoString:&plistContents]) {
+ plistContents = [plistContents stringByAppendingString:@"</plist>"];
+ }
+ }
+
+ if (!plistContents.length) {
+ return defaultAppTypeProd;
+ }
+
+ NSData *data = [plistContents dataUsingEncoding:NSUTF8StringEncoding];
+ if (!data.length) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000009",
+ @"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]]) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000010",
+ @"Error while converting assumed plist to dict %@",
+ plistMapError.localizedDescription);
+ return defaultAppTypeProd;
+ }
+ NSDictionary *plistMap = (NSDictionary *)plistData;
+
+ if ([plistMap valueForKeyPath:@"ProvisionedDevices"]) {
+ FIRLogInfo(kFIRLoggerAuth, @"I-AUT000011",
+ @"Provisioning profile has specifically provisioned devices, "
+ @"most likely a Dev profile.");
+ }
+
+ NSString *apsEnvironment = [plistMap valueForKeyPath:@"Entitlements.aps-environment"];
+ FIRLogDebug(kFIRLoggerAuth, @"I-AUT000012",
+ @"APNS Environment in profile: %@", apsEnvironment);
+
+ // No aps-environment in the profile.
+ if (!apsEnvironment.length) {
+ FIRLogWarning(kFIRLoggerAuth, @"I-AUT000013",
+ @"No aps-environment set. If testing on a device APNS is not "
+ @"correctly configured. Please recheck your provisioning profiles.");
+ return defaultAppTypeProd;
+ }
+
+ if ([apsEnvironment isEqualToString:@"development"]) {
+ return NO;
+ }
+
+ return defaultAppTypeProd;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthAPNSTokenType.h b/Firebase/Auth/Source/FIRAuthAPNSTokenType.h
new file mode 100644
index 0000000..87df574
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAPNSTokenType.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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * @brief The APNs token type for the app.
+ */
+typedef NS_ENUM(NSInteger, FIRAuthAPNSTokenType) {
+
+ /** Unknown token type.
+ The actual token type will be detected from the provisioning profile in the app's bundle.
+ */
+ FIRAuthAPNSTokenTypeUnknown,
+
+ /** Sandbox token type.
+ */
+ FIRAuthAPNSTokenTypeSandbox,
+
+ /** Production token type.
+ */
+ FIRAuthAPNSTokenTypeProd,
+} FIR_SWIFT_NAME(AuthAPNSTokenType);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthAppCredential.m b/Firebase/Auth/Source/FIRAuthAppCredential.m
new file mode 100644
index 0000000..ad12741
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAppCredential.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 "Private/FIRAuthAppCredential.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kReceiptKey
+ @brief The key used to encode the receipt property for NSSecureCoding.
+ */
+static NSString *const kReceiptKey = @"receipt";
+
+/** @var kSecretKey
+ @brief The key used to encode the secret property for NSSecureCoding.
+ */
+static NSString *const kSecretKey = @"secret";
+
+@implementation FIRAuthAppCredential
+
+- (instancetype)initWithReceipt:(NSString *)receipt secret:(nullable NSString *)secret {
+ self = [super init];
+ if (self) {
+ _receipt = [receipt copy];
+ _secret = [secret copy];
+ }
+ return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ NSString *receipt = [aDecoder decodeObjectOfClass:[NSString class] forKey:kReceiptKey];
+ if (!receipt) {
+ return nil;
+ }
+ NSString *secret = [aDecoder decodeObjectOfClass:[NSString class] forKey:kSecretKey];
+ return [self initWithReceipt:receipt secret:secret];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_receipt forKey:kReceiptKey];
+ [aCoder encodeObject:_secret forKey:kSecretKey];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthAppCredentialManager.m b/Firebase/Auth/Source/FIRAuthAppCredentialManager.m
new file mode 100644
index 0000000..2a0d1c7
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAppCredentialManager.m
@@ -0,0 +1,164 @@
+/*
+ * 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 "Private/FIRAuthAppCredentialManager.h"
+
+#import "Private/FIRAuthAppCredential.h"
+#import "FIRAuthGlobalWorkQueue.h"
+#import "FIRAuthKeychain.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kKeychainDataKey
+ @brief The keychain key for the data.
+ */
+static NSString *const kKeychainDataKey = @"app_credentials";
+
+/** @var kFullCredentialKey
+ @brief The data key for the full app credential.
+ */
+static NSString *const kFullCredentialKey = @"full_credential";
+
+/** @var kPendingReceiptsKey
+ @brief The data key for the array of pending receipts.
+ */
+static NSString *const kPendingReceiptsKey = @"pending_receipts";
+
+/** @var kMaximumNumberOfPendingReceipts
+ @brief The maximum number of partial credentials kept by this class.
+ */
+static const NSUInteger kMaximumNumberOfPendingReceipts = 32;
+
+@implementation FIRAuthAppCredentialManager {
+ /** @var _keychain
+ @brief The keychain for app credentials to load from and to save to.
+ */
+ FIRAuthKeychain *_keychain;
+
+ /** @var _pendingReceipts
+ @brief A list of pending receipts sorted in the order they were recorded.
+ */
+ NSMutableArray<NSString *> *_pendingReceipts;
+
+ /** @var _callbacksByReceipt
+ @brief A map from pending receipts to callbacks.
+ */
+ NSMutableDictionary<NSString *, FIRAuthAppCredentialCallback> *_callbacksByReceipt;
+}
+
+- (instancetype)initWithKeychain:(FIRAuthKeychain *)keychain {
+ self = [super init];
+ if (self) {
+ _keychain = keychain;
+ // Load the credentials from keychain if possible.
+ NSError *error;
+ NSData *encodedData = [_keychain dataForKey:kKeychainDataKey error:&error];
+ if (!error && encodedData) {
+ NSKeyedUnarchiver *unarchiver =
+ [[NSKeyedUnarchiver alloc] initForReadingWithData:encodedData];
+ FIRAuthAppCredential *credential =
+ [unarchiver decodeObjectOfClass:[FIRAuthAppCredential class]
+ forKey:kFullCredentialKey];
+ if ([credential isKindOfClass:[FIRAuthAppCredential class]]) {
+ _credential = credential;
+ }
+ NSSet<Class> *allowedClasses =
+ [NSSet<Class> setWithObjects:[NSArray class], [NSString class], nil];
+ NSArray<NSString *> *pendingReceipts =
+ [unarchiver decodeObjectOfClasses:allowedClasses forKey:kPendingReceiptsKey];
+ if ([pendingReceipts isKindOfClass:[NSArray class]]) {
+ _pendingReceipts = [pendingReceipts mutableCopy];
+ }
+ }
+ if (!_pendingReceipts) {
+ _pendingReceipts = [[NSMutableArray<NSString *> alloc] init];
+ }
+ _callbacksByReceipt =
+ [[NSMutableDictionary<NSString *, FIRAuthAppCredentialCallback> alloc] init];
+ }
+ return self;
+}
+
+- (NSUInteger)maximumNumberOfPendingReceipts {
+ return kMaximumNumberOfPendingReceipts;
+}
+
+- (void)didStartVerificationWithReceipt:(NSString *)receipt
+ timeout:(NSTimeInterval)timeout
+ callback:(FIRAuthAppCredentialCallback)callback {
+ [_pendingReceipts removeObject:receipt];
+ if (_pendingReceipts.count >= kMaximumNumberOfPendingReceipts) {
+ [_pendingReceipts removeObjectAtIndex:0];
+ }
+ [_pendingReceipts addObject:receipt];
+ _callbacksByReceipt[receipt] = callback;
+ [self saveData];
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)),
+ FIRAuthGlobalWorkQueue(), ^{
+ [self callBackWithReceipt:receipt];
+ });
+}
+
+- (BOOL)canFinishVerificationWithReceipt:(NSString *)receipt secret:(NSString *)secret {
+ if (![_pendingReceipts containsObject:receipt]) {
+ return NO;
+ }
+ [_pendingReceipts removeObject:receipt];
+ _credential = [[FIRAuthAppCredential alloc] initWithReceipt:receipt secret:secret];
+ [self saveData];
+ [self callBackWithReceipt:receipt];
+ return YES;
+}
+
+- (void)clearCredential {
+ _credential = nil;
+ [self saveData];
+}
+
+#pragma mark - Internal methods
+
+/** @fn saveData
+ @brief Save the data in memory to the keychain ignoring any errors.
+ */
+- (void)saveData {
+ NSMutableData *archiveData = [NSMutableData data];
+ NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:archiveData];
+ [archiver encodeObject:_credential forKey:kFullCredentialKey];
+ [archiver encodeObject:_pendingReceipts forKey:kPendingReceiptsKey];
+ [archiver finishEncoding];
+ [_keychain setData:archiveData forKey:kKeychainDataKey error:NULL];
+}
+
+/** @fn callBackWithReceipt:
+ @brief Calls the saved callback for the specifc receipt.
+ @param receipt The receipt associated with the callback.
+ */
+- (void)callBackWithReceipt:(NSString *)receipt {
+ FIRAuthAppCredentialCallback callback = _callbacksByReceipt[receipt];
+ if (!callback) {
+ return;
+ }
+ [_callbacksByReceipt removeObjectForKey:receipt];
+ if (_credential) {
+ callback(_credential);
+ } else {
+ callback([[FIRAuthAppCredential alloc] initWithReceipt:receipt secret:nil]);
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthAppDelegateProxy.m b/Firebase/Auth/Source/FIRAuthAppDelegateProxy.m
new file mode 100644
index 0000000..ca88a4c
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthAppDelegateProxy.m
@@ -0,0 +1,245 @@
+/*
+ * 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 "Private/FIRAuthAppDelegateProxy.h"
+
+#import <objc/runtime.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kProxyEnabledBundleKey
+ @brief The key in application's bundle plist for whether or not proxy should be enabled.
+ @remarks This key is a shared constant with Analytics and FCM.
+ */
+static NSString *const kProxyEnabledBundleKey = @"FirebaseAppDelegateProxyEnabled";
+
+/** @fn noop
+ @brief A function that does nothing.
+ @remarks This is used as the placeholder for unimplemented UApplicationDelegate methods,
+ because once we added a method there is no way to remove it from the class.
+ */
+#if !OBJC_OLD_DISPATCH_PROTOTYPES
+static void noop(void) {
+}
+#else
+static id noop(id object, SEL cmd, ...) {
+ return nil;
+}
+#endif
+
+@implementation FIRAuthAppDelegateProxy {
+ /** @var _appDelegate
+ @brief The application delegate whose method is being swizzled.
+ */
+ id<UIApplicationDelegate> _appDelegate;
+
+ /** @var _orginalImplementationsBySelector
+ @brief A map from selectors to original implementations that have been swizzled.
+ */
+ NSMutableDictionary<NSValue *, NSValue *> *_originalImplementationsBySelector;
+
+ /** @var _handlers
+ @brief The array of weak pointers of `id<FIRAuthAppDelegateHandler>`.
+ */
+ NSPointerArray *_handlers;
+}
+
+- (nullable instancetype)initWithApplication:(nullable UIApplication *)application {
+ self = [super init];
+ if (self) {
+ id proxyEnabled = [[NSBundle mainBundle] objectForInfoDictionaryKey:kProxyEnabledBundleKey];
+ if ([proxyEnabled isKindOfClass:[NSNumber class]] && !((NSNumber *)proxyEnabled).boolValue) {
+ return nil;
+ }
+ _appDelegate = application.delegate;
+ if (![_appDelegate conformsToProtocol:@protocol(UIApplicationDelegate)]) {
+ return nil;
+ }
+ _originalImplementationsBySelector = [[NSMutableDictionary<NSValue *, NSValue *> alloc] init];
+ _handlers = [[NSPointerArray alloc] initWithOptions:NSPointerFunctionsWeakMemory];
+
+ // Swizzle the methods.
+ __weak FIRAuthAppDelegateProxy *weakSelf = self;
+ SEL registerDeviceTokenSelector =
+ @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
+ [self replaceSelector:registerDeviceTokenSelector
+ withBlock:^(id object, UIApplication* application, NSData *deviceToken) {
+ [weakSelf object:object
+ selector:registerDeviceTokenSelector
+ application:application
+ didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
+ }];
+ SEL receiveNotificationSelector = @selector(application:didReceiveRemoteNotification:);
+ SEL receiveNotificationWithHandlerSelector =
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
+ if ([_appDelegate respondsToSelector:receiveNotificationWithHandlerSelector] ||
+ ![_appDelegate respondsToSelector:receiveNotificationSelector]) {
+ // Replace the modern selector which is available on iOS 7 and above.
+ [self replaceSelector:receiveNotificationWithHandlerSelector
+ withBlock:^(id object, UIApplication *application, NSDictionary *notification,
+ void (^completionHandler)(UIBackgroundFetchResult)) {
+ [weakSelf object:object
+ selector:receiveNotificationWithHandlerSelector
+ application:application
+ didReceiveRemoteNotification:notification
+ fetchCompletionHandler:completionHandler];
+ }];
+ } else {
+ // Replace the deprecated selector because this is the only one that the client app uses.
+ [self replaceSelector:receiveNotificationSelector
+ withBlock:^(id object, UIApplication *application, NSDictionary *notification) {
+ [weakSelf object:object
+ selector:receiveNotificationSelector
+ application:application
+ didReceiveRemoteNotification:notification];
+ }];
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ for (NSValue *selector in _originalImplementationsBySelector) {
+ IMP implementation = _originalImplementationsBySelector[selector].pointerValue;
+ Method method = class_getInstanceMethod([_appDelegate class], selector.pointerValue);
+ imp_removeBlock(method_setImplementation(method, implementation));
+ }
+}
+
+- (void)addHandler:(__weak id<FIRAuthAppDelegateHandler>)handler {
+ @synchronized (_handlers) {
+ [_handlers addPointer:(__bridge void *)handler];
+ }
+}
+
++ (nullable instancetype)sharedInstance {
+ static dispatch_once_t onceToken;
+ static FIRAuthAppDelegateProxy *_Nullable sharedInstance;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[self alloc] initWithApplication:[UIApplication sharedApplication]];
+ });
+ return sharedInstance;
+}
+
+#pragma mark - UIApplicationDelegate proxy methods.
+
+- (void)object:(id)object
+ selector:(SEL)selector
+ application:(UIApplication *)application
+ didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
+ if (object == _appDelegate) {
+ for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
+ [handler setAPNSToken:deviceToken];
+ }
+ }
+ IMP originalImplementation = [self originalImplementationForSelector:selector];
+ if (originalImplementation) {
+ typedef void (*Implmentation)(id, SEL, UIApplication*, NSData *);
+ ((Implmentation)originalImplementation)(object, selector, application, deviceToken);
+ }
+}
+
+- (void)object:(id)object
+ selector:(SEL)selector
+ application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)notification
+ fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
+ if (object == _appDelegate) {
+ for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
+ if ([handler canHandleNotification:notification]) {
+ completionHandler(UIBackgroundFetchResultNoData);
+ return;
+ };
+ }
+ }
+ IMP originalImplementation = [self originalImplementationForSelector:selector];
+ typedef void (*Implmentation)(id, SEL, UIApplication*, NSDictionary *,
+ void (^)(UIBackgroundFetchResult));
+ ((Implmentation)originalImplementation)(object, selector, application, notification,
+ completionHandler);
+}
+
+- (void)object:(id)object
+ selector:(SEL)selector
+ application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)notification {
+ if (object == _appDelegate) {
+ for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
+ if ([handler canHandleNotification:notification]) {
+ return;
+ };
+ }
+ }
+ IMP originalImplementation = [self originalImplementationForSelector:selector];
+ typedef void (*Implmentation)(id, SEL, UIApplication*, NSDictionary *);
+ ((Implmentation)originalImplementation)(object, selector, application, notification);
+}
+
+#pragma mark - Internal Methods
+
+/** @fn handlers
+ @brief Gets the list of handlers from `_handlers` safely.
+ */
+- (NSArray<id<FIRAuthAppDelegateHandler>> *)handlers {
+ @synchronized (_handlers) {
+ NSMutableArray<id<FIRAuthAppDelegateHandler>> *liveHandlers =
+ [[NSMutableArray<id<FIRAuthAppDelegateHandler>> alloc] initWithCapacity:_handlers.count];
+ for (__weak id<FIRAuthAppDelegateHandler> handler in _handlers) {
+ if (handler) {
+ [liveHandlers addObject:handler];
+ }
+ }
+ if (liveHandlers.count < _handlers.count) {
+ [_handlers compact];
+ }
+ return liveHandlers;
+ }
+}
+
+/** @fn replaceSelector:withBlock:
+ @brief replaces the implementation for a method of `_appDelegate` specified by a selector.
+ @param selector The selector for the method.
+ @param block The block as the new implementation of the method.
+ */
+- (void)replaceSelector:(SEL)selector withBlock:(id)block {
+ Method originalMethod = class_getInstanceMethod([_appDelegate class], selector);
+ IMP newImplementation = imp_implementationWithBlock(block);
+ IMP originalImplementation;
+ if (originalMethod) {
+ originalImplementation = method_setImplementation(originalMethod, newImplementation) ?: &noop;
+ } else {
+ // The original method was not implemented in the class, add it with the new implementation.
+ struct objc_method_description methodDescription =
+ protocol_getMethodDescription(@protocol(UIApplicationDelegate), selector, NO, YES);
+ class_addMethod([_appDelegate class], selector, newImplementation, methodDescription.types);
+ originalImplementation = &noop;
+ }
+ _originalImplementationsBySelector[[NSValue valueWithPointer:selector]] =
+ [NSValue valueWithPointer:originalImplementation];
+}
+
+/** @fn originalImplementationForSelector:
+ @brief Gets the original implementation for the given selector.
+ @param selector The selector for the method that has been replaced.
+ @return The original implementation if there was one.
+ */
+- (IMP)originalImplementationForSelector:(SEL)selector {
+ return _originalImplementationsBySelector[[NSValue valueWithPointer:selector]].pointerValue;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthCredential.h b/Firebase/Auth/Source/FIRAuthCredential.h
new file mode 100644
index 0000000..ce28854
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthCredential.h
@@ -0,0 +1,43 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthCredential
+ @brief Represents a credential.
+ */
+FIR_SWIFT_NAME(AuthCredential)
+@interface FIRAuthCredential : NSObject
+
+/** @property provider
+ @brief Gets the name of the identity provider for the credential.
+ */
+@property(nonatomic, copy, readonly) NSString *provider;
+
+/** @fn init
+ @brief This is an abstract base class. Concrete instances should be created via factory
+ methods available in the various authentication provider libraries (like the Facebook
+ provider or the Google provider libraries.)
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthCredential.m b/Firebase/Auth/Source/FIRAuthCredential.m
new file mode 100644
index 0000000..d476d6d
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthCredential.m
@@ -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 "Private/FIRAuthCredential_Internal.h"
+
+@implementation FIRAuthCredential
+
+- (instancetype)init {
+ @throw [NSException exceptionWithName:@"Attempt to call unavailable initializer."
+ reason:@"This class is an abstract base class. It's init method "
+ "should not be called directly."
+ userInfo:nil];
+}
+
+- (nullable instancetype)initWithProvider:(NSString *)provider {
+ self = [super init];
+ if (self) {
+ _provider = [provider copy];
+ }
+ return self;
+}
+
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request {
+ @throw [NSException exceptionWithName:@"Attempt to call virtual method."
+ reason:@"This method must be overridden by a subclass."
+ userInfo:nil];
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthDataResult.h b/Firebase/Auth/Source/FIRAuthDataResult.h
new file mode 100644
index 0000000..e72adf2
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthDataResult.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+@class FIRAdditionalUserInfo;
+@class FIRUser;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthDataResult
+ @brief Helper object that contains the result of a successful sign-in, link and reauthenticate.
+ It contains a reference to a @c FIRUser and @c FIRAdditionalUserInfo.
+ */
+FIR_SWIFT_NAME(AuthDataResult)
+@interface FIRAuthDataResult : NSObject
+
+/** @fn init
+ @brief This class should not be initialized manually. @c FIRAuthDataResult instance is
+ returned as part of @c FIRAuthDataResultCallback .
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @property user
+ @brief The signed in user.
+ */
+@property(nonatomic, readonly) FIRUser *user;
+
+/** @property additionalUserInfo
+ @brief If available contains the additional IdP specific information about signed in user.
+ */
+@property(nonatomic, readonly, nullable) FIRAdditionalUserInfo *additionalUserInfo;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthDataResult.m b/Firebase/Auth/Source/FIRAuthDataResult.m
new file mode 100644
index 0000000..fd2daa7
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthDataResult.m
@@ -0,0 +1,69 @@
+/*
+ * 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 "Private/FIRAuthDataResult_Internal.h"
+
+#import "FIRAdditionalUserInfo.h"
+#import "FIRUser.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAuthDataResult
+
+/** @var kAdditionalUserInfoCodingKey
+ @brief The key used to encode the additionalUserInfo property for NSSecureCoding.
+ */
+static NSString *const kAdditionalUserInfoCodingKey = @"additionalUserInfo";
+
+/** @var kUserCodingKey
+ @brief The key used to encode the user property for NSSecureCoding.
+ */
+static NSString *const kUserCodingKey = @"user";
+
+- (nullable instancetype)initWithUser:(FIRUser *)user
+ additionalUserInfo:(nullable FIRAdditionalUserInfo *)additionalUserInfo {
+ self = [super init];
+ if (self) {
+ _additionalUserInfo = additionalUserInfo;
+ _user = user;
+ }
+ return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ FIRUser *user =
+ [aDecoder decodeObjectOfClass:[FIRUser class] forKey:kUserCodingKey];
+ FIRAdditionalUserInfo *additionalUserInfo =
+ [aDecoder decodeObjectOfClass:[FIRAdditionalUserInfo class]
+ forKey:kAdditionalUserInfoCodingKey];
+
+ return [self initWithUser:user additionalUserInfo:additionalUserInfo];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_user forKey:kUserCodingKey];
+ [aCoder encodeObject:_additionalUserInfo forKey:kAdditionalUserInfoCodingKey];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthDispatcher.m b/Firebase/Auth/Source/FIRAuthDispatcher.m
new file mode 100644
index 0000000..98eb50a
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthDispatcher.m
@@ -0,0 +1,46 @@
+/*
+ * 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 "Private/FIRAuthDispatcher.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAuthDispatcher
+
+@synthesize dispatchAfterImplementation = _dispatchAfterImplementation;
+
++ (instancetype)sharedInstance {
+ static dispatch_once_t onceToken;
+ static FIRAuthDispatcher *sharedInstance;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[self alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (void)dispatchAfterDelay:(NSTimeInterval)delay
+ queue:(dispatch_queue_t)queue
+ task:(void (^)(void))task {
+ if (_dispatchAfterImplementation) {
+ _dispatchAfterImplementation(delay, queue, task);
+ return;
+ }
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), queue, task);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthErrorUtils.m b/Firebase/Auth/Source/FIRAuthErrorUtils.m
new file mode 100644
index 0000000..8179d02
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthErrorUtils.m
@@ -0,0 +1,794 @@
+/*
+ * 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 "Private/FIRAuthErrorUtils.h"
+
+#import "FIRAuthCredential.h"
+#import "Private/FIRAuthInternalErrors.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+NSString *const FIRAuthErrorDomain = @"FIRAuthErrorDomain";
+
+NSString *const FIRAuthInternalErrorDomain = @"FIRAuthInternalErrorDomain";
+
+NSString *const FIRAuthErrorUserInfoDeserializedResponseKey =
+ @"FIRAuthErrorUserInfoDeserializedResponseKey";
+
+NSString *const FIRAuthErrorUserInfoDataKey = @"FIRAuthErrorUserInfoDataKey";
+
+NSString *const FIRAuthErrorUserInfoEmailKey = @"FIRAuthErrorUserInfoEmailKey";
+
+NSString *const FIRAuthErrorNameKey = @"error_name";
+
+NSString *const FIRAuthUpdatedCredentialKey = @"FIRAuthUpdatedCredentialKey";
+
+/** @var kServerErrorDetailMarker
+ @brief This marker indicates that the server error message contains a detail error message which
+ should be used instead of the hardcoded client error message.
+ */
+static NSString *const kServerErrorDetailMarker = @" : ";
+
+#pragma mark - Standard Error Messages
+
+/** @var kFIRAuthErrorMessageInvalidCustomToken
+ @brief Message for @c FIRAuthErrorCodeInvalidCustomToken error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidCustomToken = @"The custom token format is "
+ "incorrect. Please check the documentation.";
+
+/** @var kFIRAuthErrorMessageCustomTokenMismatch
+ @brief Message for @c FIRAuthErrorCodeCustomTokenMismatch error code.
+ */
+static NSString *const kFIRAuthErrorMessageCustomTokenMismatch = @"The custom token corresponds to "
+ "a different audience.";
+
+/** @var kFIRAuthErrorMessageInvalidEmail
+ @brief Message for @c FIRAuthErrorCodeInvalidEmail error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidEmail = @"The email address is badly formatted.";
+
+/** @var kFIRAuthErrorMessageInvalidCredential
+ @brief Message for @c FIRAuthErrorCodeInvalidCredential error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidCredential = @"The supplied auth credential is "
+ "malformed or has expired.";
+
+/** @var kFIRAuthErrorMessageUserDisabled
+ @brief Message for @c FIRAuthErrorCodeUserDisabled error code.
+ */
+static NSString *const kFIRAuthErrorMessageUserDisabled = @"The user account has been disabled by "
+ "an administrator.";
+
+/** @var kFIRAuthErrorMessageEmailAlreadyInUse
+ @brief Message for @c FIRAuthErrorCodeEmailAlreadyInUse error code.
+ */
+static NSString *const kFIRAuthErrorMessageEmailAlreadyInUse = @"The email address is already in "
+ "use by another account.";
+
+/** @var kFIRAuthErrorMessageWrongPassword
+ @brief Message for @c FIRAuthErrorCodeWrongPassword error code.
+ */
+static NSString *const kFIRAuthErrorMessageWrongPassword = @"The password is invalid or the user "
+ "does not have a password.";
+
+/** @var kFIRAuthErrorMessageTooManyRequests
+ @brief Message for @c FIRAuthErrorCodeTooManyRequests error code.
+ */
+static NSString *const kFIRAuthErrorMessageTooManyRequests = @"We have blocked all requests from "
+ "this device due to unusual activity. Try again later.";
+
+/** @var kFIRAuthErrorMessageAccountExistsWithDifferentCredential
+ @brief Message for @c FIRAuthErrorCodeAccountLinkNeeded error code.
+ */
+static NSString *const kFIRAuthErrorMessageAccountExistsWithDifferentCredential = @"An account "
+ "already exists with the same email address but different sign-in credentials. Sign in using a "
+ "provider associated with this email address.";
+
+/** @var kFIRAuthErrorMessageRequiresRecentLogin
+ @brief Message for @c FIRAuthErrorCodeRequiresRecentLogin error code.
+ */
+static NSString *const kFIRAuthErrorMessageRequiresRecentLogin= @"This operation is sensitive and "
+ "requires recent authentication. Log in again before retrying this request.";
+
+/** @var kFIRAuthErrorMessageProviderAlreadyLinked
+ @brief Message for @c FIRAuthErrorCodeProviderAlreadyExists error code.
+ */
+static NSString *const kFIRAuthErrorMessageProviderAlreadyLinked =
+ @"[ERROR_PROVIDER_ALREADY_LINKED] - User can only be linked to one identity for the given "
+ "provider.";
+
+/** @var kFIRAuthErrorMessageNoSuchProvider
+ @brief Message for @c FIRAuthErrorCodeNoSuchProvider error code.
+ */
+static NSString *const kFIRAuthErrorMessageNoSuchProvider = @"User was not linked to an account "
+ "with the given provider.";
+
+/** @var kFIRAuthErrorMessageInvalidUserToken
+ @brief Message for @c FIRAuthErrorCodeInvalidUserToken error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidUserToken = @"The user's credential is no longer "
+ "valid. The user must sign in again.";
+
+/** @var kFIRAuthErrorMessageNetworkError
+ @brief Message for @c FIRAuthErrorCodeNetworkError error code.
+ */
+static NSString *const kFIRAuthErrorMessageNetworkError = @"Network error (such as timeout, "
+ "interrupted connection or unreachable host) has occurred.";
+
+/** @var kFIRAuthErrorMessageKeychainError
+ @brief Message for @c FIRAuthErrorCodeKeychainError error code.
+ */
+static NSString *const kFIRAuthErrorMessageKeychainError = @"An error occurred when accessing the "
+ "keychain. The @c NSLocalizedFailureReasonErrorKey field in the @c NSError.userInfo dictionary "
+ "will contain more information about the error encountered";
+
+/** @var kFIRAuthErrorMessageUserTokenExpired
+ @brief Message for @c FIRAuthErrorCodeTokenExpired error code.
+ */
+static NSString *const kFIRAuthErrorMessageUserTokenExpired = @"The user's credential is no longer "
+ "valid. The user must sign in again.";
+
+/** @var kFIRAuthErrorMessageUserNotFound
+ @brief Message for @c FIRAuthErrorCodeUserNotFound error code.
+ */
+static NSString *const kFIRAuthErrorMessageUserNotFound = @"There is no user record corresponding "
+ "to this identifier. The user may have been deleted.";
+
+/** @var kFIRAuthErrorMessageInvalidAPIKey
+ @brief Message for @c FIRAuthErrorCodeInvalidAPIKey error code.
+ @remarks This error is not thrown by the server.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidAPIKey = @"An invalid API Key was supplied in "
+ "the request.";
+
+/** @var kFIRAuthErrorMessageUserMismatch.
+ @brief Message for @c FIRAuthErrorCodeInvalidAPIKey error code.
+ */
+static NSString *const FIRAuthErrorMessageUserMismatch = @"The supplied credentials do not "
+ "correspond to the previously signed in user.";
+
+/** @var kFIRAuthErrorMessageCredentialAlreadyInUse
+ @brief Message for @c FIRAuthErrorCodeCredentialAlreadyInUse error code.
+ */
+static NSString *const kFIRAuthErrorMessageCredentialAlreadyInUse = @"This credential is already "
+ "associated with a different user account.";
+
+/** @var kFIRAuthErrorMessageOperationNotAllowed
+ @brief Message for @c FIRAuthErrorCodeOperationNotAllowed error code.
+ */
+static NSString *const kFIRAuthErrorMessageOperationNotAllowed = @"The given sign-in provider is "
+ "disabled for this Firebase project. Enable it in the Firebase console, under the sign-in "
+ "method tab of the Auth section.";
+
+/** @var kFIRAuthErrorMessageWeakPassword
+ @brief Message for @c FIRAuthErrorCodeWeakPassword error code.
+ */
+static NSString *const kFIRAuthErrorMessageWeakPassword = @"The password must be 6 characters long "
+ "or more.";
+
+/** @var kFIRAuthErrorMessageAppNotAuthorized
+ @brief Message for @c FIRAuthErrorCodeAppNotAuthorized error code.
+ */
+static NSString *const kFIRAuthErrorMessageAppNotAuthorized = @"This app is not authorized to use "
+ "Firebase Authentication with the provided API key. Review your key configuration in the "
+ "Google API console and ensure that it accepts requests from your app's bundle ID.";
+
+/** @var kFIRAuthErrorMessageExpiredActionCode
+ @brief Message for @c FIRAuthErrorCodeExpiredActionCode error code.
+ */
+static NSString *const kFIRAuthErrorMessageExpiredActionCode = @"The action code has expired.";
+
+/** @var kFIRAuthErrorMessageInvalidActionCode
+ @brief Message for @c FIRAuthErrorCodeInvalidActionCode error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidActionCode = @"The action code is invalid. This "
+ "can happen if the code is malformed, expired, or has already been used.";
+
+/** @var kFIRAuthErrorMessageInvalidMessagePayload
+ @brief Message for @c FIRAuthErrorCodeInvalidMessagePayload error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidMessagePayload = @"The action code is invalid. "
+ "This can happen if the code is malformed, expired, or has already been used.";
+
+/** @var kFIRAuthErrorMessageInvalidSender
+ @brief Message for @c FIRAuthErrorCodeInvalidSender error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidSender = @"The email template corresponding to "
+ "this action contains invalid characters in its message. Please fix by going to the Auth email "
+ "templates section in the Firebase Console.";
+
+/** @var kFIRAuthErrorMessageInvalidRecipientEmail
+ @brief Message for @c FIRAuthErrorCodeInvalidRecipient error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidRecipientEmail = @"The action code is invalid. "
+ "This can happen if the code is malformed, expired, or has already been used.";
+
+/** @var kFIRAuthErrorMessageMissingContinueURI
+ @brief Message for @c FIRAuthErrorCodeMissingContinueURI error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingContinueURI =
+ @"A continue URL must be provided in the request.";
+
+/** @var kFIRAuthErrorMessageMissingPhoneNumber
+ @brief Message for @c FIRAuthErrorCodeMissingPhoneNumber error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingPhoneNumber =
+ @"To send verification codes, provide a phone number for the recipient.";
+
+/** @var kFIRAuthErrorMessageInvalidPhoneNumber
+ @brief Message for @c FIRAuthErrorCodeMissingPhoneNumber error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidPhoneNumber =
+ @"The format of the phone number provided is incorrect. Please enter the phone number in a "
+ "format that can be parsed into E.164 format. E.164 phone numbers are written in the format "
+ "[+][country code][subscriber number including area code].";
+
+/** @var kFIRAuthErrorMessageMissingVerificationCode
+ @brief Message for @c FIRAuthErrorCodeMissingVerificationCode error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingVerificationCode =
+ @"The Phone Auth Credential was created with an empty SMS verification Code.";
+
+/** @var kFIRAuthErrorMessageInvalidVerificationCode
+ @brief Message for @c FIRAuthErrorCodeInvalidVerificationCode error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidVerificationCode =
+ @"The SMS verification code used to create the phone auth credential is invalid. Please resend "
+ "the verification code sms and be sure use the verification code provided by the user.";
+
+/** @var kFIRAuthErrorMessageMissingVerificationID
+ @brief Message for @c FIRAuthErrorCodeInvalidVerificationID error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingVerificationID =
+ @"The Phone Auth Credential was created with an empty verification ID.";
+
+/** @var kFIRAuthErrorMessageInvalidVerificationID
+ @brief Message for @c FIRAuthErrorCodeInvalidVerificationID error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidVerificationID =
+ @"The verification ID used to create the phone auth credential is invalid.";
+
+/** @var kFIRAuthErrorMessageSessionExpired
+ @brief Message for @c FIRAuthErrorCodeSessionExpired error code.
+ */
+static NSString *const kFIRAuthErrorMessageSessionExpired = @"The SMS code has expired. Please "
+ @"re-send the verification code to try again.";
+
+/** @var kFIRAuthErrorMessageMissingAppCredential
+ @brief Message for @c FIRAuthErrorCodeMissingAppCredential error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingAppCredential = @"The phone verification request "
+ "is missing an APNs Device token. Firebase Auth automatically detects APNs Device Tokens, "
+ "however, if method swizzling is disabled, the APNs token must be set via the APNSToken "
+ "property on FIRAuth or by calling setAPNSToken:type on FIRAuth.";
+
+/** @var kFIRAuthErrorMessageInvalidAppCredential
+ @brief Message for @c FIRAuthErrorCodeInvalidAppCredential error code.
+ */
+static NSString *const kFIRAuthErrorMessageInvalidAppCredential = @"The APNs device token provided "
+ "may be incorrect or does not match the private certificate uploaded to the Firebase Console.";
+
+/** @var kFIRAuthErrorMessageQuotaExceeded
+ @brief Message for @c FIRAuthErrorCodeQuotaExceeded error code.
+ */
+static NSString *const kFIRAuthErrorMessageQuotaExceeded = @"The SMS quota for this project has "
+ "been exceeded.";
+
+/** @var kFIRAuthErrorMessageMissingAppToken
+ @brief Message for @c FIRAuthErrorCodeMissingAppToken error code.
+ */
+static NSString *const kFIRAuthErrorMessageMissingAppToken = @"Remote notification and background "
+ "fetching need to be set up for the app. If app delegate swizzling is disabled, the APNs "
+ "device token received by UIApplicationDelegate needs to be forwarded to FIRAuth's APNSToken "
+ "property.";
+
+/** @var kFIRAuthErrorMessageMissingAppToken
+ @brief Message for @c FIRAuthErrorCodeMissingAppToken error code.
+ */
+static NSString *const kFIRAuthErrorMessageNotificationNotForwarded = @"If app delegate swizzling "
+ "is disabled, remote notifications received by UIApplicationDelegate need to be forwarded to "
+ "FIRAuth's canHandleNotificaton: method.";
+
+/** @var kFIRAuthErrorMessageAppNotVerified
+ @brief Message for @c FIRAuthErrorCodeMissingAppToken error code.
+ */
+static NSString *const kFIRAuthErrorMessageAppNotVerified = @"Firebase could not retrieve the "
+ "silent push notification and therefore could not verify your app. Ensure that you configured "
+ "your app correctly to recieve push notifications.";
+
+/** @var kFIRAuthErrorMessageInternalError
+ @brief Message for @c FIRAuthErrorCodeInternalError error code.
+ */
+static NSString *const kFIRAuthErrorMessageInternalError = @"An internal error has occurred, "
+ "print and inspect the error details for more information.";
+
+/** @var FIRAuthErrorDescription
+ @brief The error descrioption, based on the error code.
+ @remarks No default case so that we get a compiler warning if a new value was added to the enum.
+ */
+static NSString *FIRAuthErrorDescription(FIRAuthErrorCode code) {
+ switch (code) {
+ case FIRAuthErrorCodeInvalidCustomToken:
+ return kFIRAuthErrorMessageInvalidCustomToken;
+ case FIRAuthErrorCodeCustomTokenMismatch:
+ return kFIRAuthErrorMessageCustomTokenMismatch;
+ case FIRAuthErrorCodeInvalidEmail:
+ return kFIRAuthErrorMessageInvalidEmail;
+ case FIRAuthErrorCodeInvalidCredential:
+ return kFIRAuthErrorMessageInvalidCredential;
+ case FIRAuthErrorCodeUserDisabled:
+ return kFIRAuthErrorMessageUserDisabled;
+ case FIRAuthErrorCodeEmailAlreadyInUse:
+ return kFIRAuthErrorMessageEmailAlreadyInUse;
+ case FIRAuthErrorCodeWrongPassword:
+ return kFIRAuthErrorMessageWrongPassword;
+ case FIRAuthErrorCodeTooManyRequests:
+ return kFIRAuthErrorMessageTooManyRequests;
+ case FIRAuthErrorCodeAccountExistsWithDifferentCredential:
+ return kFIRAuthErrorMessageAccountExistsWithDifferentCredential;
+ case FIRAuthErrorCodeRequiresRecentLogin:
+ return kFIRAuthErrorMessageRequiresRecentLogin;
+ case FIRAuthErrorCodeProviderAlreadyLinked:
+ return kFIRAuthErrorMessageProviderAlreadyLinked;
+ case FIRAuthErrorCodeNoSuchProvider:
+ return kFIRAuthErrorMessageNoSuchProvider;
+ case FIRAuthErrorCodeInvalidUserToken:
+ return kFIRAuthErrorMessageInvalidUserToken;
+ case FIRAuthErrorCodeNetworkError:
+ return kFIRAuthErrorMessageNetworkError;
+ case FIRAuthErrorCodeKeychainError:
+ return kFIRAuthErrorMessageKeychainError;
+ case FIRAuthErrorCodeUserTokenExpired:
+ return kFIRAuthErrorMessageUserTokenExpired;
+ case FIRAuthErrorCodeUserNotFound:
+ return kFIRAuthErrorMessageUserNotFound;
+ case FIRAuthErrorCodeInvalidAPIKey:
+ return kFIRAuthErrorMessageInvalidAPIKey;
+ case FIRAuthErrorCodeCredentialAlreadyInUse:
+ return kFIRAuthErrorMessageCredentialAlreadyInUse;
+ case FIRAuthErrorCodeInternalError:
+ return kFIRAuthErrorMessageInternalError;
+ case FIRAuthErrorCodeUserMismatch:
+ return FIRAuthErrorMessageUserMismatch;
+ case FIRAuthErrorCodeOperationNotAllowed:
+ return kFIRAuthErrorMessageOperationNotAllowed;
+ case FIRAuthErrorCodeWeakPassword:
+ return kFIRAuthErrorMessageWeakPassword;
+ case FIRAuthErrorCodeAppNotAuthorized:
+ return kFIRAuthErrorMessageAppNotAuthorized;
+ case FIRAuthErrorCodeExpiredActionCode:
+ return kFIRAuthErrorMessageExpiredActionCode;
+ case FIRAuthErrorCodeInvalidActionCode:
+ return kFIRAuthErrorMessageInvalidActionCode;
+ case FIRAuthErrorCodeInvalidSender:
+ return kFIRAuthErrorMessageInvalidSender;
+ case FIRAuthErrorCodeInvalidMessagePayload:
+ return kFIRAuthErrorMessageInvalidMessagePayload;
+ case FIRAuthErrorCodeInvalidRecipientEmail:
+ return kFIRAuthErrorMessageInvalidRecipientEmail;
+ case FIRAuthErrorCodeMissingPhoneNumber:
+ return kFIRAuthErrorMessageMissingPhoneNumber;
+ case FIRAuthErrorCodeInvalidPhoneNumber:
+ return kFIRAuthErrorMessageInvalidPhoneNumber;
+ case FIRAuthErrorCodeMissingVerificationCode:
+ return kFIRAuthErrorMessageMissingVerificationCode;
+ case FIRAuthErrorCodeInvalidVerificationCode:
+ return kFIRAuthErrorMessageInvalidVerificationCode;
+ case FIRAuthErrorCodeMissingVerificationID:
+ return kFIRAuthErrorMessageMissingVerificationID;
+ case FIRAuthErrorCodeInvalidVerificationID:
+ return kFIRAuthErrorMessageInvalidVerificationID;
+ case FIRAuthErrorCodeSessionExpired:
+ return kFIRAuthErrorMessageSessionExpired;
+ case FIRAuthErrorCodeMissingAppCredential:
+ return kFIRAuthErrorMessageMissingAppCredential;
+ case FIRAuthErrorCodeInvalidAppCredential:
+ return kFIRAuthErrorMessageInvalidAppCredential;
+ case FIRAuthErrorCodeQuotaExceeded:
+ return kFIRAuthErrorMessageQuotaExceeded;
+ case FIRAuthErrorCodeMissingAppToken:
+ return kFIRAuthErrorMessageMissingAppToken;
+ case FIRAuthErrorCodeNotificationNotForwarded:
+ return kFIRAuthErrorMessageNotificationNotForwarded;
+ case FIRAuthErrorCodeAppNotVerified:
+ return kFIRAuthErrorMessageAppNotVerified;
+ }
+}
+
+/** @var FIRAuthErrorCodeString
+ @brief The the error short string, based on the error code.
+ @remarks No default case so that we get a compiler warning if a new value was added to the enum.
+ */
+static NSString *const FIRAuthErrorCodeString(FIRAuthErrorCode code) {
+ switch (code) {
+ case FIRAuthErrorCodeInvalidCustomToken:
+ return @"ERROR_INVALID_CUSTOM_TOKEN";
+ case FIRAuthErrorCodeCustomTokenMismatch:
+ return @"ERROR_CUSTOM_TOKEN_MISMATCH";
+ case FIRAuthErrorCodeInvalidEmail:
+ return @"ERROR_INVALID_EMAIL";
+ case FIRAuthErrorCodeInvalidCredential:
+ return @"ERROR_INVALID_CREDENTIAL";
+ case FIRAuthErrorCodeUserDisabled:
+ return @"ERROR_USER_DISABLED";
+ case FIRAuthErrorCodeEmailAlreadyInUse:
+ return @"ERROR_EMAIL_ALREADY_IN_USE";
+ case FIRAuthErrorCodeWrongPassword:
+ return @"ERROR_WRONG_PASSWORD";
+ case FIRAuthErrorCodeTooManyRequests:
+ return @"ERROR_TOO_MANY_REQUESTS";
+ case FIRAuthErrorCodeAccountExistsWithDifferentCredential:
+ return @"ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL";
+ case FIRAuthErrorCodeRequiresRecentLogin:
+ return @"ERROR_REQUIRES_RECENT_LOGIN";
+ case FIRAuthErrorCodeProviderAlreadyLinked:
+ return @"ERROR_PROVIDER_ALREADY_LINKED";
+ case FIRAuthErrorCodeNoSuchProvider:
+ return @"ERROR_NO_SUCH_PROVIDER";
+ case FIRAuthErrorCodeInvalidUserToken:
+ return @"ERROR_INVALID_USER_TOKEN";
+ case FIRAuthErrorCodeNetworkError:
+ return @"ERROR_NETWORK_REQUEST_FAILED";
+ case FIRAuthErrorCodeKeychainError:
+ return @"ERROR_KEYCHAIN_ERROR";
+ case FIRAuthErrorCodeUserTokenExpired:
+ return @"ERROR_USER_TOKEN_EXPIRED";
+ case FIRAuthErrorCodeUserNotFound:
+ return @"ERROR_USER_NOT_FOUND";
+ case FIRAuthErrorCodeInvalidAPIKey:
+ return @"ERROR_INVALID_API_KEY";
+ case FIRAuthErrorCodeCredentialAlreadyInUse:
+ return @"ERROR_CREDENTIAL_ALREADY_IN_USE";
+ case FIRAuthErrorCodeInternalError:
+ return @"ERROR_INTERNAL_ERROR";
+ case FIRAuthErrorCodeUserMismatch:
+ return @"ERROR_USER_MISMATCH";
+ case FIRAuthErrorCodeOperationNotAllowed:
+ return @"ERROR_OPERATION_NOT_ALLOWED";
+ case FIRAuthErrorCodeWeakPassword:
+ return @"ERROR_WEAK_PASSWORD";
+ case FIRAuthErrorCodeAppNotAuthorized:
+ return @"ERROR_APP_NOT_AUTHORIZED";
+ case FIRAuthErrorCodeExpiredActionCode:
+ return @"ERROR_EXPIRED_ACTION_CODE";
+ case FIRAuthErrorCodeInvalidActionCode:
+ return @"ERROR_INVALID_ACTION_CODE";
+ case FIRAuthErrorCodeInvalidMessagePayload:
+ return @"ERROR_INVALID_MESSAGE_PAYLOAD";
+ case FIRAuthErrorCodeInvalidSender:
+ return @"ERROR_INVALID_SENDER";
+ case FIRAuthErrorCodeInvalidRecipientEmail:
+ return @"ERROR_INVALID_RECIPIENT_EMAIL";
+ case FIRAuthErrorCodeMissingPhoneNumber:
+ return @"ERROR_MISSING_PHONE_NUMBER";
+ case FIRAuthErrorCodeInvalidPhoneNumber:
+ return @"ERROR_INVALID_PHONE_NUMBER";
+ case FIRAuthErrorCodeMissingVerificationCode:
+ return @"ERROR_MISSING_VERIFICATION_CODE";
+ case FIRAuthErrorCodeInvalidVerificationCode:
+ return @"ERROR_INVALID_VERIFICATION_CODE";
+ case FIRAuthErrorCodeMissingVerificationID:
+ return @"ERROR_MISSING_VERIFICATION_ID";
+ case FIRAuthErrorCodeInvalidVerificationID:
+ return @"ERROR_INVALID_VERIFICATION_ID";
+ case FIRAuthErrorCodeSessionExpired:
+ return @"ERROR_SESSION_EXPIRED";
+ case FIRAuthErrorCodeMissingAppCredential:
+ return @"MISSING_APP_CREDENTIAL";
+ case FIRAuthErrorCodeInvalidAppCredential:
+ return @"INVALID_APP_CREDENTIAL";
+ case FIRAuthErrorCodeQuotaExceeded:
+ return @"ERROR_QUOTA_EXCEEDED";
+ case FIRAuthErrorCodeMissingAppToken:
+ return @"ERROR_MISSING_APP_TOKEN";
+ case FIRAuthErrorCodeNotificationNotForwarded:
+ return @"ERROR_NOTIFICATION_NOT_FORWARDED";
+ case FIRAuthErrorCodeAppNotVerified:
+ return @"ERROR_APP_NOT_VERIFIED";
+ }
+}
+
+@implementation FIRAuthErrorUtils
+
++ (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code {
+ return [self errorWithCode:code message:nil];
+}
+
++ (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code
+ message:(nullable NSString *)message {
+ NSDictionary *userInfo = nil;
+ if (message.length) {
+ userInfo = @{
+ NSLocalizedDescriptionKey : message
+ };
+ }
+ return [self errorWithCode:code userInfo:userInfo];
+}
+
++ (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code
+ underlyingError:(nullable NSError *)underlyingError {
+ NSDictionary *errorUserInfo = nil;
+ if (underlyingError) {
+ errorUserInfo = @{
+ NSUnderlyingErrorKey : underlyingError
+ };
+ }
+ return [self errorWithCode:code userInfo:errorUserInfo];
+}
+
++ (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code userInfo:(NSDictionary *)userInfo {
+ BOOL isPublic = (code & FIRAuthPublicErrorCodeFlag) == FIRAuthPublicErrorCodeFlag;
+ if (isPublic) {
+ // This is a public error. Return it as a public error and add a description.
+ NSInteger errorCode = code & ~FIRAuthPublicErrorCodeFlag;
+ NSMutableDictionary *errorUserInfo = [NSMutableDictionary dictionaryWithDictionary:userInfo];
+ if (!errorUserInfo[NSLocalizedDescriptionKey]) {
+ errorUserInfo[NSLocalizedDescriptionKey] = FIRAuthErrorDescription(errorCode);
+ }
+ errorUserInfo[FIRAuthErrorNameKey] = FIRAuthErrorCodeString(errorCode);
+ return [NSError errorWithDomain:FIRAuthErrorDomain code:errorCode userInfo:errorUserInfo];
+ } else {
+ // This is an internal error. Wrap it in an internal error.
+ NSError *error =
+ [NSError errorWithDomain:FIRAuthInternalErrorDomain code:code userInfo:userInfo];
+ return [self errorWithCode:FIRAuthInternalErrorCodeInternalError underlyingError:error];
+ }
+}
+
++ (NSError *)RPCRequestEncodingErrorWithUnderlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeRPCRequestEncodingError
+ underlyingError:underlyingError];
+}
+
++ (NSError *)JSONSerializationErrorForUnencodableType {
+ return [self errorWithCode:FIRAuthInternalErrorCodeJSONSerializationError];
+}
+
++ (NSError *)JSONSerializationErrorWithUnderlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeJSONSerializationError
+ underlyingError:underlyingError];
+}
+
++ (NSError *)networkErrorWithUnderlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeNetworkError
+ underlyingError:underlyingError];
+}
+
++ (NSError *)unexpectedErrorResponseWithData:(NSData *)data
+ underlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedErrorResponse userInfo:@{
+ FIRAuthErrorUserInfoDataKey : data,
+ NSUnderlyingErrorKey : underlyingError
+ }];
+}
+
++ (NSError *)unexpectedErrorResponseWithDeserializedResponse:(id)deserializedResponse {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedErrorResponse userInfo:@{
+ FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse
+ }];
+}
+
++ (NSError *)unexpectedResponseWithData:(NSData *)data
+ underlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:@{
+ FIRAuthErrorUserInfoDataKey : data,
+ NSUnderlyingErrorKey : underlyingError
+ }];
+}
+
++ (NSError *)unexpectedResponseWithDeserializedResponse:(id)deserializedResponse {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:@{
+ FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse
+ }];
+}
+
++ (NSError *)unexpectedResponseWithDeserializedResponse:(nullable id)deserializedResponse
+ underlyingError:(NSError *)underlyingError {
+ NSMutableDictionary *userInfo =
+ [NSMutableDictionary dictionaryWithDictionary:@{ NSUnderlyingErrorKey : underlyingError }];
+ if (deserializedResponse) {
+ userInfo[FIRAuthErrorUserInfoDeserializedResponseKey] = deserializedResponse;
+ }
+ return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:userInfo];
+}
+
++ (NSError *)RPCResponseDecodingErrorWithDeserializedResponse:(id)deserializedResponse
+ underlyingError:(NSError *)underlyingError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeRPCResponseDecodingError userInfo:@{
+ FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse,
+ NSUnderlyingErrorKey : underlyingError
+ }];
+}
+
++ (NSError *)emailAlreadyInUseErrorWithEmail:(nullable NSString *)email {
+ NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
+ if (email.length) {
+ userInfo[FIRAuthErrorUserInfoEmailKey] = email;
+ }
+ return [self errorWithCode:FIRAuthInternalErrorCodeEmailAlreadyInUse userInfo:userInfo];
+}
+
++ (NSError *)userDisabledErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUserDisabled message:message];
+}
+
++ (NSError *)wrongPasswordErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeWrongPassword message:message];
+}
+
++ (NSError *)tooManyRequestsErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeTooManyRequests message:message];
+}
+
++ (NSError *)invalidCustomTokenErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidCustomToken message:message];
+}
+
++ (NSError *)customTokenMistmatchErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeCustomTokenMismatch message:message];
+}
+
++ (NSError *)invalidCredentialErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidCredential message:message];
+}
+
++ (NSError *)requiresRecentLoginErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeRequiresRecentLogin message:message];
+}
+
++ (NSError *)invalidUserTokenErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidUserToken message:message];
+}
+
++ (NSError *)invalidEmailErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidEmail message:message];
+}
+
++ (NSError *)accountExistsWithDifferentCredentialErrorWithEmail:(nullable NSString *)email {
+ return [self errorWithCode:FIRAuthInternalErrorCodeAccountExistsWithDifferentCredential
+ userInfo:@{ FIRAuthErrorUserInfoEmailKey : email }];
+}
+
++ (NSError *)providerAlreadyLinkedError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeProviderAlreadyLinked];
+}
+
++ (NSError *)noSuchProviderError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeNoSuchProvider];
+}
+
++ (NSError *)userTokenExpiredErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUserTokenExpired message:message];
+}
+
++ (NSError *)userNotFoundErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUserNotFound message:message];
+}
+
++ (NSError *)invalidAPIKeyError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidAPIKey];
+}
+
++ (NSError *)userMismatchError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeUserMismatch];
+}
+
++ (NSError *)credentialAlreadyInUseErrorWithMessage:(nullable NSString *)message
+ credential:(nullable FIRPhoneAuthCredential *)credential {
+ if (credential) {
+ return [self errorWithCode:FIRAuthInternalErrorCodeCredentialAlreadyInUse
+ userInfo:@{ FIRAuthUpdatedCredentialKey : credential }];
+ }
+ return [self errorWithCode:FIRAuthInternalErrorCodeCredentialAlreadyInUse message:message];
+}
+
++ (NSError *)operationNotAllowedErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeOperationNotAllowed message:message];
+}
+
++ (NSError *)weakPasswordErrorWithServerResponseReason:(NSString *)reason {
+ return [self errorWithCode:FIRAuthInternalErrorCodeWeakPassword userInfo:@{
+ NSLocalizedFailureReasonErrorKey : reason
+ }];
+}
+
++ (NSError *)appNotAuthorizedError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeAppNotAuthorized];
+}
+
++ (NSError *)expiredActionCodeErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeExpiredActionCode message:message];
+}
+
++ (NSError *)invalidActionCodeErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidActionCode message:message];
+}
+
++ (NSError *)invalidMessagePayloadErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidMessagePayload message:message];
+}
+
++ (NSError *)invalidSenderErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidSender message:message];
+}
+
++ (NSError *)invalidRecipientEmailErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidRecipientEmail message:message];
+}
+
++ (NSError *)missingPhoneNumberErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeMissingPhoneNumber message:message];
+}
+
++ (NSError *)invalidPhoneNumberErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidPhoneNumber message:message];
+}
+
++ (NSError *)missingVerificationCodeErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeMissingVerificationCode message:message];
+}
+
++ (NSError *)invalidVerificationCodeErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidVerificationCode message:message];
+}
+
++ (NSError *)missingVerificationIDErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeMissingVerificationID message:message];
+}
+
++ (NSError *)invalidVerificationIDErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidVerificationID message:message];
+}
+
++ (NSError *)sessionExpiredErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeSessionExpired message:message];
+}
+
++ (NSError *)missingAppCredentialWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeMissingAppCredential message:message];
+}
+
++ (NSError *)invalidAppCredentialWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeInvalidAppCredential message:message];
+}
+
++ (NSError *)quotaExceededErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeQuotaExceeded message:message];
+}
+
++ (NSError *)missingAppTokenError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeMissingAppToken];
+}
+
++ (NSError *)notificationNotForwardedError {
+ return [self errorWithCode:FIRAuthInternalErrorCodeNotificationNotForwarded];
+}
+
++ (NSError *)appNotVerifiedErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeAppNotVerified message:message];
+}
+
++ (NSError *)keychainErrorWithFunction:(NSString *)keychainFunction status:(OSStatus)status {
+ NSString *failureReason = [NSString stringWithFormat:@"%@ (%li)", keychainFunction, (long)status];
+ return [self errorWithCode:FIRAuthInternalErrorCodeKeychainError userInfo:@{
+ NSLocalizedFailureReasonErrorKey : failureReason,
+ }];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthErrors.h b/Firebase/Auth/Source/FIRAuthErrors.h
new file mode 100644
index 0000000..c1e7900
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthErrors.h
@@ -0,0 +1,258 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+/** @class FIRAuthErrors
+ @remarks Error Codes common to all API Methods:
+ <ul>
+ <li>@c FIRAuthErrorCodeNetworkError</li>
+ <li>@c FIRAuthErrorCodeUserNotFound</li>
+ <li>@c FIRAuthErrorCodeUserTokenExpired</li>
+ <li>@c FIRAuthErrorCodeTooManyRequests</li>
+ <li>@c FIRAuthErrorCodeInvalidAPIKey</li>
+ <li>@c FIRAuthErrorCodeAppNotAuthorized</li>
+ <li>@c FIRAuthErrorCodeKeychainError</li>
+ <li>@c FIRAuthErrorCodeInternalError</li>
+ </ul>
+ @remarks Common error codes for @c FIRUser operations:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidUserToken</li>
+ <li>@c FIRAuthErrorCodeUserDisabled</li>
+ </ul>
+ */
+FIR_SWIFT_NAME(AuthErrors)
+@interface FIRAuthErrors
+
+/**
+ @brief The Firebase Auth error domain.
+ */
+extern NSString *const FIRAuthErrorDomain FIR_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.
+ */
+extern NSString *const FIRAuthUpdatedCredentialKey FIR_SWIFT_NAME(AuthUpdatedCredentialKey);
+
+/**
+ @brief The name of the key for the "error_name" string in the NSError userinfo dictionary.
+ */
+extern NSString *const FIRAuthErrorNameKey FIR_SWIFT_NAME(AuthErrorNameKey);
+
+/** @var FIRAuthErrorUserInfoEmailKey
+ @brief Errors with the code @c FIRAuthErrorCodeEmailAlreadyInUse may contains an
+ @c NSError.userInfo dictinary which contains this key. The value associated with this key is
+ an NSString of the email address that already exists.
+ */
+extern NSString *const FIRAuthErrorUserInfoEmailKey FIR_SWIFT_NAME(AuthErrorUserInfoEmailKey);
+
+/**
+ @brief Error codes used by Firebase Auth.
+ */
+typedef NS_ENUM(NSInteger, FIRAuthErrorCode) {
+ /** Indicates a validation error with the custom token.
+ */
+ FIRAuthErrorCodeInvalidCustomToken = 17000,
+
+ /** Indicates the service account and the API key belong to different projects.
+ */
+ FIRAuthErrorCodeCustomTokenMismatch = 17002,
+
+ /** Indicates the IDP token or requestUri is invalid.
+ */
+ FIRAuthErrorCodeInvalidCredential = 17004,
+
+ /** Indicates the user's account is disabled on the server.
+ */
+ FIRAuthErrorCodeUserDisabled = 17005,
+
+ /** Indicates the administrator disabled sign in with the specified identity provider.
+ */
+ FIRAuthErrorCodeOperationNotAllowed = 17006,
+
+ /** Indicates the email used to attempt a sign up is already in use.
+ */
+ FIRAuthErrorCodeEmailAlreadyInUse = 17007,
+
+ /** Indicates the email is invalid.
+ */
+ FIRAuthErrorCodeInvalidEmail = 17008,
+
+ /** Indicates the user attempted sign in with a wrong password.
+ */
+ FIRAuthErrorCodeWrongPassword = 17009,
+
+ /** Indicates that too many requests were made to a server method.
+ */
+ FIRAuthErrorCodeTooManyRequests = 17010,
+
+ /** Indicates the user account was not found.
+ */
+ FIRAuthErrorCodeUserNotFound = 17011,
+
+ /** Indicates account linking is required.
+ */
+ FIRAuthErrorCodeAccountExistsWithDifferentCredential = 17012,
+
+ /** Indicates the user has attemped to change email or password more than 5 minutes after
+ signing in.
+ */
+ FIRAuthErrorCodeRequiresRecentLogin = 17014,
+
+ /** Indicates an attempt to link a provider to which the account is already linked.
+ */
+ FIRAuthErrorCodeProviderAlreadyLinked = 17015,
+
+ /** Indicates an attempt to unlink a provider that is not linked.
+ */
+ FIRAuthErrorCodeNoSuchProvider = 17016,
+
+ /** Indicates user's saved auth credential is invalid, the user needs to sign in again.
+ */
+ FIRAuthErrorCodeInvalidUserToken = 17017,
+
+ /** Indicates a network error occurred (such as a timeout, interrupted connection, or
+ unreachable host). These types of errors are often recoverable with a retry. The @c
+ NSUnderlyingError field in the @c NSError.userInfo dictionary will contain the error
+ encountered.
+ */
+ FIRAuthErrorCodeNetworkError = 17020,
+
+ /** Indicates the saved token has expired, for example, the user may have changed account
+ password on another device. The user needs to sign in again on the device that made this
+ request.
+ */
+ FIRAuthErrorCodeUserTokenExpired = 17021,
+
+ /** Indicates an invalid API key was supplied in the request.
+ */
+ FIRAuthErrorCodeInvalidAPIKey = 17023,
+
+ /** Indicates that an attempt was made to reauthenticate with a user which is not the current
+ user.
+ */
+ FIRAuthErrorCodeUserMismatch = 17024,
+
+ /** Indicates an attempt to link with a credential that has already been linked with a
+ different Firebase account
+ */
+ FIRAuthErrorCodeCredentialAlreadyInUse = 17025,
+
+ /** Indicates an attempt to set a password that is considered too weak.
+ */
+ FIRAuthErrorCodeWeakPassword = 17026,
+
+ /** Indicates the App is not authorized to use Firebase Authentication with the
+ provided API Key.
+ */
+ FIRAuthErrorCodeAppNotAuthorized = 17028,
+
+ /** Indicates the OOB code is expired.
+ */
+ FIRAuthErrorCodeExpiredActionCode = 17029,
+
+ /** Indicates the OOB code is invalid.
+ */
+ FIRAuthErrorCodeInvalidActionCode = 17030,
+
+ /** Indicates that there are invalid parameters in the payload during a "send password reset
+ * email" attempt.
+ */
+ FIRAuthErrorCodeInvalidMessagePayload = 17031,
+
+ /** Indicates that the sender email is invalid during a "send password reset email" attempt.
+ */
+ FIRAuthErrorCodeInvalidSender = 17032,
+
+ /** Indicates that the recipient email is invalid.
+ */
+ FIRAuthErrorCodeInvalidRecipientEmail = 17033,
+
+ // The enum values between 17033 and 17041 are reserved and should NOT be used for new error
+ // codes.
+
+ /** Indicates that a phone number was not provided in a call to @c
+ verifyPhoneNumber:completion:.
+ */
+ FIRAuthErrorCodeMissingPhoneNumber = 17041,
+
+ /** Indicates that an invalid phone number was provided in a call to @c
+ verifyPhoneNumber:completion:.
+ */
+ FIRAuthErrorCodeInvalidPhoneNumber = 17042,
+
+ /** Indicates that the phone auth credential was created with an empty verification code.
+ */
+ FIRAuthErrorCodeMissingVerificationCode = 17043,
+
+ /** Indicates that an invalid verification code was used in the verifyPhoneNumber request.
+ */
+ FIRAuthErrorCodeInvalidVerificationCode = 17044,
+
+ /** Indicates that the phone auth credential was created with an empty verification ID.
+ */
+ FIRAuthErrorCodeMissingVerificationID = 17045,
+
+ /** Indicates that an invalid verification ID was used in the verifyPhoneNumber request.
+ */
+ FIRAuthErrorCodeInvalidVerificationID = 17046,
+
+ /** Indicates that the APNS device token is missing in the verifyClient request.
+ */
+ FIRAuthErrorCodeMissingAppCredential = 17047,
+
+ /** Indicates that an invalid APNS device token was used in the verifyClient request.
+ */
+ FIRAuthErrorCodeInvalidAppCredential = 17048,
+
+ // The enum values between 17048 and 17051 are reserved and should NOT be used for new error
+ // codes.
+
+ /** Indicates that the SMS code has expired.
+ */
+ FIRAuthErrorCodeSessionExpired = 17051,
+
+ /** Indicates that the quota of SMS messages for a given project has been exceeded.
+ */
+ FIRAuthErrorCodeQuotaExceeded = 17052,
+
+ /** Indicates that the APNs device token could not be obtained. The app may not have set up
+ remote notification correctly, or may fail to forward the APNs device token to FIRAuth
+ if app delegate swizzling is disabled.
+ */
+ FIRAuthErrorCodeMissingAppToken = 17053,
+
+ /** Indicates that the app fails to forward remote notification to FIRAuth.
+ */
+ FIRAuthErrorCodeNotificationNotForwarded = 17054,
+
+ /** Indicates that the app could not be verified by Firebase during phone number authentication.
+ */
+ FIRAuthErrorCodeAppNotVerified = 17055,
+
+ /** Indicates an error occurred while attempting to access the keychain.
+ */
+ FIRAuthErrorCodeKeychainError = 17995,
+
+ /** Indicates an internal error occurred.
+ */
+ FIRAuthErrorCodeInternalError = 17999,
+} FIR_SWIFT_NAME(AuthErrorCode);
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthExceptionUtils.h b/Firebase/Auth/Source/FIRAuthExceptionUtils.h
new file mode 100644
index 0000000..3ae9159
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthExceptionUtils.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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthExceptionUtils
+ @brief Utility class used to raise standardized Auth related exceptions.
+*/
+@interface FIRAuthExceptionUtils : NSObject
+
+/** @fn raiseInvalidParameterExceptionWithReason:
+ @brief raises the "invalid parameter" exception
+ @param reason string will contain a description of the error.
+ */
++ (void)raiseInvalidParameterExceptionWithReason:(nullable NSString *)reason;
+
+/** @fn raiseMethodNotImplementedExceptionWithReason:
+ @brief raises the "method not implemented" exception
+ @param reason string will contain a description of the error.
+ @see FIRMethodNotImplementedException
+ */
++ (void)raiseMethodNotImplementedExceptionWithReason:(nullable NSString *)reason;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthExceptionUtils.m b/Firebase/Auth/Source/FIRAuthExceptionUtils.m
new file mode 100644
index 0000000..0adcd34
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthExceptionUtils.m
@@ -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 "FIRAuthExceptionUtils.h"
+
+/** @var FIRMethodNotImplementedException
+ @brief The name of the "Method Not Implemented" exception.
+ */
+static NSString *const FIRMethodNotImplementedException = @"FIRMethodNotImplementedException";
+
+@implementation FIRAuthExceptionUtils
+
++ (void)raiseInvalidParameterExceptionWithReason:(NSString *)reason {
+ [NSException raise:NSInvalidArgumentException format:@"%@", reason];
+}
+
++ (void)raiseMethodNotImplementedExceptionWithReason:(nullable NSString *)reason {
+ NSException *exception =
+ [NSException exceptionWithName:FIRMethodNotImplementedException reason:reason userInfo:nil];
+ [exception raise];
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m b/Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m
new file mode 100644
index 0000000..8780959
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m
@@ -0,0 +1,26 @@
+/*
+ * 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 "Private/FIRAuthGlobalWorkQueue.h"
+
+dispatch_queue_t FIRAuthGlobalWorkQueue() {
+ static dispatch_once_t once;
+ static dispatch_queue_t queue;
+ dispatch_once(&once, ^{
+ queue = dispatch_queue_create("com.google.firebase.auth.globalWorkQueue", NULL);
+ });
+ return queue;
+}
diff --git a/Firebase/Auth/Source/FIRAuthKeychain.m b/Firebase/Auth/Source/FIRAuthKeychain.m
new file mode 100644
index 0000000..68cf2f2
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthKeychain.m
@@ -0,0 +1,256 @@
+/*
+ * 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 "Private/FIRAuthKeychain.h"
+
+#import <Security/Security.h>
+
+#import "Private/FIRAuthErrorUtils.h"
+#import "Private/FIRAuthUserDefaultsStorage.h"
+
+#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+#import <UIKit/UIKit.h>
+
+/** @var kOSVersionMatcherForUsingUserDefaults
+ @brief The regular expression to match all OS versions that @c FIRAuthUserDefaultsStorage is
+ used instead if available.
+ */
+static NSString *const kOSVersionMatcherForUsingUserDefaults = @"^10\\.[01](\\..*)?$";
+
+#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+/** @var kAccountPrefix
+ @brief The prefix string for keychain item account attribute before the key.
+ @remarks A number "1" is encoded in the prefix in case we need to upgrade the scheme in future.
+ */
+static NSString *const kAccountPrefix = @"firebase_auth_1_";
+
+@implementation FIRAuthKeychain {
+ /** @var _service
+ @brief The name of the keychain service.
+ */
+ NSString *_service;
+
+ /** @var _legacyItemDeletedForKey
+ @brief Indicates whether or not this class knows that the legacy item for a particular key has
+ been deleted.
+ @remarks This dictionary is to avoid unecessary keychain operations against legacy items.
+ */
+ NSMutableDictionary *_legacyEntryDeletedForKey;
+}
+
+- (id<FIRAuthStorage>)initWithService:(NSString *)service {
+
+#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+ NSString *OSVersion = [UIDevice currentDevice].systemVersion;
+ NSRegularExpression *regex =
+ [NSRegularExpression regularExpressionWithPattern:kOSVersionMatcherForUsingUserDefaults
+ options:0
+ error:NULL];
+ if ([regex numberOfMatchesInString:OSVersion options:0 range:NSMakeRange(0, OSVersion.length)]) {
+ return (id<FIRAuthStorage>)[[FIRAuthUserDefaultsStorage alloc] initWithService:service];
+ }
+
+#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+ self = [super init];
+ if (self) {
+ _service = [service copy];
+ _legacyEntryDeletedForKey = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error {
+ if (!key.length) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"%@", @"The key cannot be nil or empty."];
+ return nil;
+ }
+ NSData *data = [self itemWithQuery:[self genericPasswordQueryWithKey:key] error:error];
+ if (error && *error) {
+ return nil;
+ }
+ if (data) {
+ return data;
+ }
+ // Check for legacy form.
+ if (_legacyEntryDeletedForKey[key]) {
+ return nil;
+ }
+ data = [self itemWithQuery:[self legacyGenericPasswordQueryWithKey:key] error:error];
+ if (error && *error) {
+ return nil;
+ }
+ if (!data) {
+ // Mark legacy data as non-existing so we don't have to query it again.
+ _legacyEntryDeletedForKey[key] = @YES;
+ return nil;
+ }
+ // Move the data to current form.
+ if (![self setData:data forKey:key error:error]) {
+ return nil;
+ }
+ [self deleteLegacyItemWithKey:key];
+ return data;
+}
+
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key error:(NSError **_Nullable)error {
+ if (!key.length) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"%@", @"The key cannot be nil or empty."];
+ return NO;
+ }
+ NSDictionary *attributes = @{
+ (__bridge id)kSecValueData : data,
+ (__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
+ };
+ return [self setItemWithQuery:[self genericPasswordQueryWithKey:key]
+ attributes:attributes
+ error:error];
+}
+
+- (BOOL)removeDataForKey:(NSString *)key error:(NSError **_Nullable)error {
+ if (!key.length) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"%@", @"The key cannot be nil or empty."];
+ return NO;
+ }
+ if (![self deleteItemWithQuery:[self genericPasswordQueryWithKey:key] error:error]) {
+ return NO;
+ }
+ // Legacy form item, if exists, also needs to be removed, otherwise it will be exposed when
+ // current form item is removed, leading to incorrect semantics.
+ [self deleteLegacyItemWithKey:key];
+ return YES;
+}
+
+#pragma mark - Private
+
+- (NSData *)itemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
+ NSMutableDictionary *returningQuery = [query mutableCopy];
+ returningQuery[(__bridge id)kSecReturnData] = @YES;
+ returningQuery[(__bridge id)kSecReturnAttributes] = @YES;
+ // Using a match limit of 2 means that we can check whether there is more than one item.
+ // If we used a match limit of 1 we would never find out.
+ returningQuery[(__bridge id)kSecMatchLimit] = @2;
+
+ CFArrayRef result = NULL;
+ OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)returningQuery,
+ (CFTypeRef *)&result);
+
+ if (status == noErr && result != NULL) {
+ NSArray *items = (__bridge_transfer NSArray *)result;
+ if (items.count != 1) {
+ if (error) {
+ *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching"
+ status:status];
+ }
+ return nil;
+ }
+
+ if (error) {
+ *error = nil;
+ }
+ NSDictionary *item = items[0];
+ return item[(__bridge id)kSecValueData];
+ }
+
+ if (status == errSecItemNotFound) {
+ if (error) {
+ *error = nil;
+ }
+ } else {
+ if (error) {
+ *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
+ }
+ }
+ return nil;
+}
+
+- (BOOL)setItemWithQuery:(NSDictionary *)query
+ attributes:(NSDictionary *)attributes
+ error:(NSError **_Nullable)error {
+ NSMutableDictionary *combined = [attributes mutableCopy];
+ [combined addEntriesFromDictionary:query];
+ BOOL hasItem = NO;
+ OSStatus status = SecItemAdd((__bridge CFDictionaryRef)combined, NULL);
+
+ if (status == errSecDuplicateItem) {
+ hasItem = YES;
+ status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes);
+ }
+
+ if (status == noErr) {
+ return YES;
+ }
+ if (error) {
+ NSString *function = hasItem ? @"SecItemUpdate" : @"SecItemAdd";
+ *error = [FIRAuthErrorUtils keychainErrorWithFunction:function status:status];
+ }
+ return NO;
+}
+
+- (BOOL)deleteItemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error {
+ OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
+ if (status == noErr || status == errSecItemNotFound) {
+ return YES;
+ }
+ if (error) {
+ *error = [FIRAuthErrorUtils keychainErrorWithFunction:@"SecItemDelete" status:status];
+ }
+ return NO;
+}
+
+/** @fn deleteLegacyItemsWithKey:
+ @brief Deletes legacy item from the keychain if it is not already known to be deleted.
+ @param key The key for the item.
+ */
+- (void)deleteLegacyItemWithKey:(NSString *)key {
+ if (_legacyEntryDeletedForKey[key]) {
+ return;
+ }
+ NSDictionary *query = [self legacyGenericPasswordQueryWithKey:key];
+ SecItemDelete((__bridge CFDictionaryRef)query);
+ _legacyEntryDeletedForKey[key] = @YES;
+}
+
+/** @fn genericPasswordQueryWithKey:
+ @brief Returns a keychain query of generic password to be used to manipulate key'ed value.
+ @param key The key for the value being manipulated, used as the account field in the query.
+ */
+- (NSDictionary *)genericPasswordQueryWithKey:(NSString *)key {
+ return @{
+ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+ (__bridge id)kSecAttrAccount : [kAccountPrefix stringByAppendingString:key],
+ (__bridge id)kSecAttrService : _service,
+ };
+}
+
+/** @fn legacyGenericPasswordQueryWithKey:
+ @brief Returns a keychain query of generic password without service field, which is used by
+ previous version of this class.
+ @param key The key for the value being manipulated, used as the account field in the query.
+ */
+- (NSDictionary *)legacyGenericPasswordQueryWithKey:(NSString *)key {
+ return @{
+ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+ (__bridge id)kSecAttrAccount : key,
+ };
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthNotificationManager.m b/Firebase/Auth/Source/FIRAuthNotificationManager.m
new file mode 100644
index 0000000..0692562
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthNotificationManager.m
@@ -0,0 +1,175 @@
+/*
+ * 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 "Private/FIRAuthNotificationManager.h"
+
+#import "FIRLogger.h"
+#import "Private/FIRAuthAppCredential.h"
+#import "Private/FIRAuthAppCredentialManager.h"
+#import "FIRAuthGlobalWorkQueue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kNotificationKey
+ @brief The key to locate payload data in the remote notification.
+ */
+static NSString *const kNotificationDataKey = @"com.google.firebase.auth";
+
+/** @var kNotificationReceiptKey
+ @brief The key for the receipt in the remote notification payload data.
+ */
+static NSString *const kNotificationReceiptKey = @"receipt";
+
+/** @var kNotificationSecretKey
+ @brief The key for the secret in the remote notification payload data.
+ */
+static NSString *const kNotificationSecretKey = @"secret";
+
+/** @var kNotificationProberKey
+ @brief The key for marking the prober in the remote notification payload data.
+ */
+static NSString *const kNotificationProberKey = @"warning";
+
+/** @var kProbingTimeout
+ @brief Timeout for probing whether the app delegate forwards the remote notification to us.
+ */
+static const NSTimeInterval kProbingTimeout = 1;
+
+@implementation FIRAuthNotificationManager {
+ /** @var _application
+ @brief The application.
+ */
+ UIApplication *_application;
+
+ /** @var _appCredentialManager
+ @brief The object to handle app credentials delivered via notification.
+ */
+ FIRAuthAppCredentialManager *_appCredentialManager;
+
+ /** @var _hasCheckedNotificationForwarding
+ @brief Whether notification forwarding has been checked or not.
+ */
+ BOOL _hasCheckedNotificationForwarding;
+
+ /** @var _isNotificationBeingForwarded
+ @brief Whether or not notification is being forwarded
+ */
+ BOOL _isNotificationBeingForwarded;
+
+ /** @var _pendingCallbacks
+ @brief All pending callbacks while a check is being performed.
+ */
+ NSMutableArray<FIRAuthNotificationForwardingCallback> *_pendingCallbacks;
+}
+
+- (instancetype)initWithApplication:(UIApplication *)application
+ appCredentialManager:(FIRAuthAppCredentialManager *)appCredentialManager {
+ self = [super init];
+ if (self) {
+ _application = application;
+ _appCredentialManager = appCredentialManager;
+ _timeout = kProbingTimeout;
+ }
+ return self;
+}
+
+- (void)checkNotificationForwardingWithCallback:(FIRAuthNotificationForwardingCallback)callback {
+ if (_pendingCallbacks) {
+ [_pendingCallbacks addObject:callback];
+ return;
+ }
+ if (_hasCheckedNotificationForwarding) {
+ callback(_isNotificationBeingForwarded);
+ return;
+ }
+ _hasCheckedNotificationForwarding = YES;
+ _pendingCallbacks =
+ [[NSMutableArray<FIRAuthNotificationForwardingCallback> alloc] initWithObjects:callback, nil];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ NSDictionary *proberNotification = @{
+ kNotificationDataKey : @{
+ kNotificationProberKey : @"This fake notification should be forwarded to Firebase Auth."
+ }
+ };
+ if ([_application.delegate respondsToSelector:
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]) {
+ [_application.delegate application:_application
+ didReceiveRemoteNotification:proberNotification
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {}];
+ } else if ([_application.delegate respondsToSelector:
+ @selector(application:didReceiveRemoteNotification:)]) {
+ [_application.delegate application:_application
+ didReceiveRemoteNotification:proberNotification];
+ } else {
+ FIRLogError(kFIRLoggerAuth, @"I-AUT000015",
+ @"The UIApplicationDelegate must handle remote notifcation for phone number "
+ @"authentication to work.");
+ }
+ });
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_timeout * NSEC_PER_SEC)),
+ FIRAuthGlobalWorkQueue(), ^{
+ [self callBack];
+ });
+}
+
+- (BOOL)canHandleNotification:(NSDictionary *)notification {
+ NSDictionary *data = notification[kNotificationDataKey];
+ if ([data isKindOfClass:[NSString class]]) {
+ // Deserialize in case the data is a JSON string.
+ NSData *JSONData = [((NSString *)data) dataUsingEncoding:NSUTF8StringEncoding];
+ data = [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:NULL];
+ }
+ if (![data isKindOfClass:[NSDictionary class]]) {
+ return NO;
+ }
+ if (data[kNotificationProberKey]) {
+ if (!_pendingCallbacks) {
+ // The prober notification probably comes from another instance, so pass it along.
+ return NO;
+ }
+ _isNotificationBeingForwarded = YES;
+ [self callBack];
+ return YES;
+ }
+ NSString *receipt = data[kNotificationReceiptKey];
+ if (![receipt isKindOfClass:[NSString class]]) {
+ return NO;
+ }
+ NSString *secret = data[kNotificationSecretKey];
+ if (![receipt isKindOfClass:[NSString class]]) {
+ return NO;
+ }
+ return [_appCredentialManager canFinishVerificationWithReceipt:receipt secret:secret];
+}
+
+#pragma mark - Internal methods
+
+/** @fn callBack
+ @brief Calls back all pending callbacks with the result of notification forwarding check.
+ */
+- (void)callBack {
+ if (!_pendingCallbacks) {
+ return;
+ }
+ NSArray<FIRAuthNotificationForwardingCallback> *allCallbacks = _pendingCallbacks;
+ _pendingCallbacks = nil;
+ for (FIRAuthNotificationForwardingCallback callback in allCallbacks) {
+ callback(_isNotificationBeingForwarded);
+ }
+};
+
+@end
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthProvider.m b/Firebase/Auth/Source/FIRAuthProvider.m
new file mode 100644
index 0000000..6df86d7
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthProvider.m
@@ -0,0 +1,38 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+// Declared 'extern' in FIRGoogleAuthProvider.h
+NSString *const FIRGoogleAuthProviderID = @"google.com";
+
+// Declared 'extern' in FIRFacebookAuthProvider.h
+NSString *const FIRFacebookAuthProviderID = @"facebook.com";
+
+// Declared 'extern' in FIREmailAuthProvider.h
+NSString *const FIREmailAuthProviderID = @"password";
+
+// Declared 'extern' in FIREmailAuthProvider.h
+NSString *const FIREmailPasswordAuthProviderID = @"password";
+
+// Declared 'extern' in FIRTwitterAuthProvider.h
+NSString *const FIRTwitterAuthProviderID = @"twitter.com";
+
+// Declared 'extern' in FIRGitHubAuthProvider.h
+NSString *const FIRGitHubAuthProviderID = @"github.com";
+
+// Declared 'extern' in FIRPhoneAuthProvider.h
+NSString *const FIRPhoneAuthProviderID = @"phone";
diff --git a/Firebase/Auth/Source/FIRAuthSerialTaskQueue.m b/Firebase/Auth/Source/FIRAuthSerialTaskQueue.m
new file mode 100644
index 0000000..47e6cd5
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthSerialTaskQueue.m
@@ -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 "Private/FIRAuthSerialTaskQueue.h"
+
+#import "Private/FIRAuthGlobalWorkQueue.h"
+
+@implementation FIRAuthSerialTaskQueue {
+ /** @var _dispatchQueue
+ @brief The asyncronous dispatch queue into which tasks are enqueued and processed
+ serially.
+ */
+ dispatch_queue_t _dispatchQueue;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _dispatchQueue = dispatch_queue_create("com.google.firebase.auth.serialTaskQueue", NULL);
+ dispatch_set_target_queue(_dispatchQueue, FIRAuthGlobalWorkQueue());
+ }
+ return self;
+}
+
+- (void)enqueueTask:(FIRAuthSerialTask)task {
+ // This dispatch queue will run tasks serially in FIFO order, as long as it's not suspended.
+ dispatch_async(_dispatchQueue, ^{
+ // But as soon as a task is started, stop other tasks from running until the task calls it's
+ // completion handler, which allows the queue to resume processing of tasks. This allows the
+ // task to perform other asyncronous actions on other dispatch queues and "get back to us" when
+ // all of their sub-tasks are complete.
+ dispatch_suspend(_dispatchQueue);
+ task(^{
+ dispatch_resume(_dispatchQueue);
+ });
+ });
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRAuthSwiftNameSupport.h b/Firebase/Auth/Source/FIRAuthSwiftNameSupport.h
new file mode 100644
index 0000000..f58bdd7
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthSwiftNameSupport.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.
+ */
+
+#ifndef FIR_SWIFT_NAME
+
+#import <Foundation/Foundation.h>
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// // Wrap it in our own macro if it's a non-compatible SDK.
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+
+#endif // FIR_SWIFT_NAME
diff --git a/Firebase/Auth/Source/FIRAuthUserDefaultsStorage.m b/Firebase/Auth/Source/FIRAuthUserDefaultsStorage.m
new file mode 100644
index 0000000..ad23f41
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthUserDefaultsStorage.m
@@ -0,0 +1,78 @@
+/*
+ * 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 "Private/FIRAuthUserDefaultsStorage.h"
+
+#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const kPersistentDomainNamePrefix = @"com.google.Firebase.Auth.";
+
+@implementation FIRAuthUserDefaultsStorage {
+ /** @var _persistentDomainName
+ @brief The name of the persistent domain in user defaults.
+ */
+ NSString *_persistentDomainName;
+
+ /** @var _storage
+ @brief The backing NSUserDefaults storage for this instance.
+ */
+ NSUserDefaults *_storage;
+}
+
+- (id<FIRAuthStorage>)initWithService:(NSString *)service {
+ self = [super init];
+ if (self) {
+ _persistentDomainName = [kPersistentDomainNamePrefix stringByAppendingString:service];
+ _storage = [[NSUserDefaults alloc] init];
+ }
+ return self;
+}
+
+- (nullable NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error {
+ if (error) {
+ *error = nil;
+ }
+ NSDictionary<NSString *, id> *allData = [_storage persistentDomainForName:_persistentDomainName];
+ return allData[key];
+}
+
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key error:(NSError **_Nullable)error {
+ NSMutableDictionary<NSString *, id> *allData =
+ [([_storage persistentDomainForName:_persistentDomainName] ?: @{}) mutableCopy];
+ allData[key] = data;
+ [_storage setPersistentDomain:allData forName:_persistentDomainName];
+ return YES;
+}
+
+- (BOOL)removeDataForKey:(NSString *)key error:(NSError **_Nullable)error {
+ NSMutableDictionary<NSString *, id> *allData =
+ [[_storage persistentDomainForName:_persistentDomainName] mutableCopy];
+ [allData removeObjectForKey:key];
+ [_storage setPersistentDomain:allData forName:_persistentDomainName];
+ return YES;
+}
+
+- (void)clear {
+ [_storage setPersistentDomain:@{} forName:_persistentDomainName];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
diff --git a/Firebase/Auth/Source/FIRSecureTokenService.h b/Firebase/Auth/Source/FIRSecureTokenService.h
new file mode 100644
index 0000000..cb29127
--- /dev/null
+++ b/Firebase/Auth/Source/FIRSecureTokenService.h
@@ -0,0 +1,96 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRFetchAccessTokenCallback
+ @brief The callback used to return the value of attempting to fetch an access token.
+
+ In the event the operation was successful @c token will be set and @c error will be @c nil.
+ In the event of failure @c token will be @c nil and @c error will be set.
+ @c tokenUpdated indicates whether either the access or the refresh token has been updated.
+
+ The token returned should be considered ephemeral and not cached. It should be used immediately
+ and discarded. All operations that need this token should call fetchAccessToken and do their
+ work from the callback.
+ */
+typedef void(^FIRFetchAccessTokenCallback)(NSString *_Nullable token,
+ NSError *_Nullable error,
+ BOOL tokenUpdated);
+
+/** @class FIRSecureTokenService
+ @brief Provides services for token exchanges and refreshes.
+ */
+@interface FIRSecureTokenService : NSObject <NSSecureCoding>
+
+/** @property rawAccessToken
+ @brief The cached access token.
+ @remarks This method is specifically for providing the access token to internal clients during
+ deserialization and sign-in events, and should not be used to retrieve the access token by
+ anyone else.
+ */
+@property(nonatomic, copy, readonly) NSString *rawAccessToken;
+
+/** @property refreshToken
+ @brief The refresh token for the user, or @c nil if the user has yet completed sign-in flow.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *refreshToken;
+
+/** @property accessTokenExpirationDate
+ @brief The expiration date of the cached access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *accessTokenExpirationDate;
+
+/** @fn init
+ @brief Please use @c initWithAPIKey:authorizationCode: .
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:authorizationCode:
+ @brief Creates a @c FIRSecureTokenService with an authroization code.
+ @param APIKey A Google API key for making STS requests.
+ @param authorizationCode An authorization code which needs to be exchanged for STS tokens.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ authorizationCode:(NSString *)authorizationCode;
+
+/** @fn initWithAPIKey:authorizationCode:
+ @brief Creates a @c FIRSecureTokenService with an authroization code.
+ @param APIKey A Google API key for making STS requests.
+ @param accessToken The STS access token.
+ @param accessTokenExpirationDate The approximate expiration date of the access token.
+ @param refreshToken The STS refresh token.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ accessToken:(nullable NSString *)accessToken
+ accessTokenExpirationDate:(nullable NSDate *)accessTokenExpirationDate
+ refreshToken:(NSString *)refreshToken;
+
+/** @fn fetchAccessTokenForcingRefresh:callback:
+ @brief Fetch a fresh ephemeral access token for the ID associated with this instance. The token
+ received in the callback should be considered short lived and not cached.
+ @param forceRefresh Forces the token to be refreshed.
+ @param callback Callback block that will be called to return either the token or an error.
+ Invoked asyncronously on the auth global work queue in the future.
+ */
+- (void)fetchAccessTokenForcingRefresh:(BOOL)forceRefresh
+ callback:(FIRFetchAccessTokenCallback)callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRSecureTokenService.m b/Firebase/Auth/Source/FIRSecureTokenService.m
new file mode 100644
index 0000000..e88b41c
--- /dev/null
+++ b/Firebase/Auth/Source/FIRSecureTokenService.m
@@ -0,0 +1,214 @@
+/*
+ * 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 "FIRSecureTokenService.h"
+
+#import "FIRAuth.h"
+#import "Private/FIRAuthKeychain.h"
+#import "Private/FIRAuthSerialTaskQueue.h"
+#import "FIRAuthBackend.h"
+#import "FIRSecureTokenRequest.h"
+#import "FIRSecureTokenResponse.h"
+
+/** @var kAPIKeyCodingKey
+ @brief The key used to encode the APIKey for NSSecureCoding.
+ */
+static NSString *const kAPIKeyCodingKey = @"APIKey";
+
+/** @var kRefreshTokenKey
+ @brief The key used to encode the refresh token for NSSecureCoding.
+ */
+static NSString *const kRefreshTokenKey = @"refreshToken";
+
+/** @var kAccessTokenKey
+ @brief The key used to encode the access token for NSSecureCoding.
+ */
+static NSString *const kAccessTokenKey = @"accessToken";
+
+/** @var kAccessTokenExpirationDateKey
+ @brief The key used to encode the access token expiration date for NSSecureCoding.
+ */
+static NSString *const kAccessTokenExpirationDateKey = @"accessTokenExpirationDate";
+
+/** @var kFiveMinutes
+ @brief Five minutes (in seconds.)
+ */
+static const NSTimeInterval kFiveMinutes = 5 * 60;
+
+@interface FIRSecureTokenService ()
+/** @fn initWithAPIKey:
+ @brief Creates a @c FIRSecureTokenService without a credential.
+ @param APIKey A Google API key for making STS requests.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+@end
+
+@implementation FIRSecureTokenService {
+ /** @var _APIKey
+ @brief A Google API key for making Secure Token Service requests.
+ */
+ NSString *_APIKey;
+
+ /** @var _taskQueue
+ @brief Used to serialize all requests for access tokens.
+ */
+ FIRAuthSerialTaskQueue *_taskQueue;
+
+ /** @var _authorizationCode
+ @brief An authorization code which needs to be exchanged for Secure Token Service tokens.
+ */
+ NSString *_Nullable _authorizationCode;
+
+ /** @var _accessToken
+ @brief The currently cached access token. Or |nil| if no token is currently cached.
+ */
+ NSString *_Nullable _accessToken;
+}
+
+- (instancetype)init {
+ [self doesNotRecognizeSelector:_cmd];
+ return nil;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey {
+ self = [super init];
+ if (self) {
+ _APIKey = [APIKey copy];
+ _taskQueue = [[FIRAuthSerialTaskQueue alloc] init];
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ authorizationCode:(NSString *)authorizationCode {
+ self = [self initWithAPIKey:APIKey];
+ if (self) {
+ _authorizationCode = [authorizationCode copy];
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ accessToken:(nullable NSString *)accessToken
+ accessTokenExpirationDate:(nullable NSDate *)accessTokenExpirationDate
+ refreshToken:(NSString *)refreshToken {
+ self = [self initWithAPIKey:APIKey];
+ if (self) {
+ _accessToken = [accessToken copy];
+ _accessTokenExpirationDate = [accessTokenExpirationDate copy];
+ _refreshToken = [refreshToken copy];
+ }
+ return self;
+}
+
+- (void)fetchAccessTokenForcingRefresh:(BOOL)forceRefresh
+ callback:(FIRFetchAccessTokenCallback)callback {
+ [_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock complete) {
+ if (!forceRefresh && [self hasValidAccessToken]) {
+ complete();
+ callback(_accessToken, nil, NO);
+ } else {
+ [self requestAccessToken:^(NSString *_Nullable token,
+ NSError *_Nullable error,
+ BOOL tokenUpdated) {
+ complete();
+ callback(token, error, tokenUpdated);
+ }];
+ }
+ }];
+}
+
+- (NSString *)rawAccessToken {
+ return _accessToken;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ NSString *APIKey = [aDecoder decodeObjectOfClass:[NSString class] forKey:kAPIKeyCodingKey];
+ NSString *refreshToken = [aDecoder decodeObjectOfClass:[NSString class] forKey:kRefreshTokenKey];
+ NSString *accessToken = [aDecoder decodeObjectOfClass:[NSString class] forKey:kAccessTokenKey];
+ NSDate *accessTokenExpirationDate =
+ [aDecoder decodeObjectOfClass:[NSDate class] forKey:kAccessTokenExpirationDateKey];
+ if (!APIKey || !refreshToken) {
+ return nil;
+ }
+ self = [self initWithAPIKey:APIKey];
+ if (self) {
+ _refreshToken = refreshToken;
+ _accessToken = accessToken;
+ _accessTokenExpirationDate = accessTokenExpirationDate;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_APIKey forKey:kAPIKeyCodingKey];
+ // Authorization code is not encoded because it is not long-lived.
+ [aCoder encodeObject:_refreshToken forKey:kRefreshTokenKey];
+ [aCoder encodeObject:_accessToken forKey:kAccessTokenKey];
+ [aCoder encodeObject:_accessTokenExpirationDate forKey:kAccessTokenExpirationDateKey];
+}
+
+#pragma mark - Private methods
+
+/** @fn requestAccessToken:
+ @brief Makes a request to STS for an access token.
+ @details This handles both the case that the token has not been granted yet and that it just
+ needs to be refreshed. The caller is responsible for making sure that this is occurring in
+ a @c _taskQueue task.
+ @param callback Called when the fetch is complete. Invoked asynchronously on the main thread in
+ the future.
+ @remarks Because this method is guaranteed to only be called from tasks enqueued in
+ @c _taskQueue, we do not need any @synchronized guards around access to _accessToken/etc.
+ since only one of those tasks is ever running at a time, and those tasks are the only
+ access to and mutation of these instance variables.
+ */
+- (void)requestAccessToken:(FIRFetchAccessTokenCallback)callback {
+ FIRSecureTokenRequest *request;
+ if (_refreshToken.length) {
+ request = [FIRSecureTokenRequest refreshRequestWithRefreshToken:_refreshToken APIKey:_APIKey];
+ } else {
+ request = [FIRSecureTokenRequest authCodeRequestWithCode:_authorizationCode APIKey:_APIKey];
+ }
+ [FIRAuthBackend secureToken:request
+ callback:^(FIRSecureTokenResponse *_Nullable response,
+ NSError *_Nullable error) {
+ BOOL tokenUpdated = NO;
+ NSString *newAccessToken = response.accessToken;
+ if (newAccessToken.length && ![newAccessToken isEqualToString:_accessToken]) {
+ _accessToken = [newAccessToken copy];
+ _accessTokenExpirationDate = response.approximateExpirationDate;
+ tokenUpdated = YES;
+ }
+ NSString *newRefreshToken = response.refreshToken;
+ if (newRefreshToken.length && ![newRefreshToken isEqualToString:_refreshToken]) {
+ _refreshToken = [newRefreshToken copy];
+ tokenUpdated = YES;
+ }
+ callback(newAccessToken, error, tokenUpdated);
+ }];
+}
+
+- (BOOL)hasValidAccessToken {
+ return _accessToken && [_accessTokenExpirationDate timeIntervalSinceNow] > kFiveMinutes;
+}
+
+@end
diff --git a/Firebase/Auth/Source/FIRUser.h b/Firebase/Auth/Source/FIRUser.h
new file mode 100644
index 0000000..ebe8b81
--- /dev/null
+++ b/Firebase/Auth/Source/FIRUser.h
@@ -0,0 +1,463 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuth.h"
+#import "FIRAuthDataResult.h"
+#import "FIRAuthSwiftNameSupport.h"
+#import "FIRUserInfo.h"
+
+@class FIRPhoneAuthCredential;
+@class FIRUserProfileChangeRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthTokenCallback
+ @brief The type of block called when a token is ready for use.
+ @see FIRUser.getIDTokenWithCompletion:
+ @see FIRUser.getIDTokenForcingRefresh:withCompletion:
+
+ @param token Optionally; an access token if the request was successful.
+ @param error Optionally; the error which occurred - or nil if the request was successful.
+
+ @remarks One of: @c token or @c error will always be non-nil.
+ */
+typedef void (^FIRAuthTokenCallback)(NSString *_Nullable token, NSError *_Nullable error)
+ FIR_SWIFT_NAME(AuthTokenCallback);
+
+/** @typedef FIRUserProfileChangeCallback
+ @brief The type of block called when a user profile change has finished.
+
+ @param error Optionally; the error which occurred - or nil if the request was successful.
+ */
+typedef void (^FIRUserProfileChangeCallback)(NSError *_Nullable error)
+ FIR_SWIFT_NAME(UserProfileChangeCallback);
+
+/** @typedef FIRSendEmailVerificationCallback
+ @brief The type of block called when a request to send an email verification has finished.
+
+ @param error Optionally; the error which occurred - or nil if the request was successful.
+ */
+typedef void (^FIRSendEmailVerificationCallback)(NSError *_Nullable error)
+ FIR_SWIFT_NAME(SendEmailVerificationCallback);
+
+/** @class FIRUser
+ @brief Represents a user.
+ @remarks This class is thread-safe.
+ */
+FIR_SWIFT_NAME(User)
+@interface FIRUser : NSObject <FIRUserInfo>
+
+/** @property anonymous
+ @brief Indicates the user represents an anonymous user.
+ */
+@property(nonatomic, readonly, getter=isAnonymous) BOOL anonymous;
+
+/** @property emailVerified
+ @brief Indicates the email address associated with this user has been verified.
+ */
+@property(nonatomic, readonly, getter=isEmailVerified) BOOL emailVerified;
+
+/** @property refreshToken
+ @brief A refresh token; useful for obtaining new access tokens independently.
+ @remarks This property should only be used for advanced scenarios, and is not typically needed.
+ */
+@property(nonatomic, readonly, nullable) NSString *refreshToken;
+
+/** @property providerData
+ @brief Profile data for each identity provider, if any.
+ @remarks This data is cached on sign-in and updated when linking or unlinking.
+ */
+@property(nonatomic, readonly, nonnull) NSArray<id<FIRUserInfo>> *providerData;
+
+/** @fn init
+ @brief This class should not be instantiated.
+ @remarks To retrieve the current user, use @c FIRAuth.currentUser. To sign a user
+ in or out, use the methods on @c FIRAuth.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn updateEmail:completion:
+ @brief Updates the email address for the user. On success, the cached user profile data is
+ updated.
+ @remarks May fail if there is already an account with this email address that was created using
+ email and password authentication.
+
+ @param email The email address for the user.
+ @param completion Optionally; the block invoked when the user profile change has finished.
+ Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidRecipientEmail - Indicates an invalid recipient email was
+ sent in the request.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidSender - Indicates an invalid sender email is set in
+ the console for this action.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidMessagePayload - Indicates an invalid email template for
+ sending update email.
+ </li>
+ <li>@c FIRAuthErrorCodeEmailAlreadyInUse - Indicates the email is already in use by another
+ account.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.
+ </li>
+ <li>@c FIRAuthErrorCodeRequiresRecentLogin - Updating a user’s email is a security
+ sensitive operation that requires a recent login from the user. This error indicates
+ the user has not signed in recently enough. To resolve, reauthenticate the user by
+ invoking reauthenticateWithCredential:completion: on FIRUser.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)updateEmail:(NSString *)email completion:(nullable FIRUserProfileChangeCallback)completion
+ FIR_SWIFT_NAME(updateEmail(to:completion:));
+
+/** @fn updatePassword:completion:
+ @brief Updates the password for the user. On success, the cached user profile data is updated.
+
+ @param password The new password for the user.
+ @param completion Optionally; the block invoked when the user profile change has finished.
+ Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates the administrator disabled
+ sign in with the specified identity provider.
+ </li>
+ <li>@c FIRAuthErrorCodeRequiresRecentLogin - Updating a user’s password is a security
+ sensitive operation that requires a recent login from the user. This error indicates
+ the user has not signed in recently enough. To resolve, reauthenticate the user by
+ invoking reauthenticateWithCredential:completion: on FIRUser.
+ </li>
+ <li>@c FIRAuthErrorCodeWeakPassword - Indicates an attempt to set a password that is
+ considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo
+ dictionary object will contain more detailed explanation that can be shown to the user.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)updatePassword:(NSString *)password
+ completion:(nullable FIRUserProfileChangeCallback)completion
+ FIR_SWIFT_NAME(updatePassword(to:completion:));
+
+/** @fn updatePhoneNumberCredential:completion:
+ @brief Updates the phone number for the user. On success, the cached user profile data is
+ updated.
+
+ @param phoneNumberCredential The new phone number credential corresponding to the phone number
+ to be added to the firebaes account, if a phone number is already linked to the account this
+ new phone number will replace it.
+ @param completion Optionally; the block invoked when the user profile change has finished.
+ Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeRequiresRecentLogin - Updating a user’s phone number is a security
+ sensitive operation that requires a recent login from the user. This error indicates
+ the user has not signed in recently enough. To resolve, reauthenticate the user by
+ invoking reauthenticateWithCredential:completion: on FIRUser.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)updatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneNumberCredential
+ completion:(nullable FIRUserProfileChangeCallback)completion;
+
+/** @fn profileChangeRequest
+ @brief Creates an object which may be used to change the user's profile data.
+
+ @remarks Set the properties of the returned object, then call
+ @c FIRUserProfileChangeRequest.commitChangesWithCallback: to perform the updates atomically.
+
+ @return An object which may be used to change the user's profile data atomically.
+ */
+- (FIRUserProfileChangeRequest *)profileChangeRequest FIR_SWIFT_NAME(createProfileChangeRequest());
+
+/** @fn reloadWithCompletion:
+ @brief Reloads the user's profile data from the server.
+
+ @param completion Optionally; the block invoked when the reload has finished. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks May fail with a @c FIRAuthErrorCodeRequiresRecentLogin error code. In this case
+ you should call @c FIRUser.reauthenticateWithCredential:completion: before re-invoking
+ @c FIRUser.updateEmail:completion:.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)reloadWithCompletion:(nullable FIRUserProfileChangeCallback)completion;
+
+/** @fn reauthenticateWithCredential:completion:
+ @brief Convenience method for @c reauthenticateAndRetrieveDataWithCredential:completion: This
+ method doesn't return additional identity provider data.
+ */
+- (void)reauthenticateWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRUserProfileChangeCallback)completion;
+
+/** @fn reauthenticateWithCredential:completion:
+ @brief Renews the user's authentication tokens by validating a fresh set of credentials supplied
+ by the user and returns additional identity provider data.
+
+ @param credential A user-supplied credential, which will be validated by the server. This can be
+ a successful third-party identity provider sign-in, or an email address and password.
+ @param completion Optionally; the block invoked when the re-authentication operation has
+ finished. Invoked asynchronously on the main thread in the future.
+
+ @remarks If the user associated with the supplied credential is different from the current user,
+ or if the validation of the supplied credentials fails; an error is returned and the current
+ user remains signed in.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidCredential - Indicates the supplied credential is invalid.
+ This could happen if it has expired or it is malformed.
+ </li>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that accounts with the
+ identity provider represented by the credential are not enabled. Enable them in the
+ Auth section of the Firebase console.
+ </li>
+ <li>@c FIRAuthErrorCodeEmailAlreadyInUse - 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 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. Please note that the
+ error code raised in this specific situation may not be the same on Web and Android.
+ </li>
+ <li>@c FIRAuthErrorCodeUserDisabled - Indicates the user's account is disabled.
+ </li>
+ <li>@c FIRAuthErrorCodeWrongPassword - Indicates the user attempted reauthentication with
+ an incorrect password, if credential is of the type EmailPasswordAuthCredential.
+ </li>
+ <li>@c FIRAuthErrorCodeUserMismatch - Indicates that an attempt was made to
+ reauthenticate with a user which is not the current user.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidEmail - Indicates the email address is malformed.</li>
+ </ul>
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)reauthenticateAndRetrieveDataWithCredential:(FIRAuthCredential *) credential
+ completion:(nullable FIRAuthDataResultCallback) completion;
+
+/** @fn getIDTokenWithCompletion:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
+
+ @param completion Optionally; the block invoked when the token is available. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)getIDTokenWithCompletion:(nullable FIRAuthTokenCallback)completion
+ FIR_SWIFT_NAME(getIDToken(completion:));
+
+/** @fn getTokenWithCompletion:
+ @brief Please use @c getIDTokenWithCompletion: instead.
+
+ @param completion Optionally; the block invoked when the token is available. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)getTokenWithCompletion:(nullable FIRAuthTokenCallback)completion
+ FIR_SWIFT_NAME(getToken(completion:)) __attribute__((deprecated));
+
+/** @fn getIDTokenForcingRefresh:completion:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
+
+ @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason
+ other than an expiration.
+ @param completion Optionally; the block invoked when the token is available. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks The authentication token will be refreshed (by making a network request) if it has
+ expired, or if @c forceRefresh is YES.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)getIDTokenForcingRefresh:(BOOL)forceRefresh
+ completion:(nullable FIRAuthTokenCallback)completion;
+
+/** @fn getTokenForcingRefresh:completion:
+ @brief Please use getIDTokenForcingRefresh:completion instead.
+
+ @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason
+ other than an expiration.
+ @param completion Optionally; the block invoked when the token is available. Invoked
+ asynchronously on the main thread in the future.
+
+ @remarks The authentication token will be refreshed (by making a network request) if it has
+ expired, or if @c forceRefresh is YES.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods.
+ */
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh
+ completion:(nullable FIRAuthTokenCallback)completion
+ __attribute__((deprecated));
+
+/** @fn linkWithCredential:completion:
+ @brief Convenience method for @c linkAndRetrieveDataWithCredential:completion: This method
+ doesn't return additional identity provider data.
+ */
+- (void)linkWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn linkAndRetrieveDataWithCredential:completion:
+ @brief Associates a user account from a third-party identity provider with this user and
+ returns additional identity provider data.
+
+ @param credential The credential for the identity provider.
+ @param completion Optionally; the block invoked when the unlinking is complete, or fails.
+ Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeProviderAlreadyLinked - Indicates an attempt to link a provider of a
+ type already linked to this account.
+ </li>
+ <li>@c FIRAuthErrorCodeCredentialAlreadyInUse - Indicates an attempt to link with a
+ credential
+ that has already been linked with a different Firebase account.
+ </li>
+ <li>@c FIRAuthErrorCodeOperationNotAllowed - Indicates that accounts with the identity
+ provider represented by the credential are not enabled. Enable them in the Auth section
+ of the Firebase console.
+ </li>
+ </ul>
+
+ @remarks This method may also return error codes associated with updateEmail:completion: and
+ updatePassword:completion: on FIRUser.
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)linkAndRetrieveDataWithCredential:(FIRAuthCredential *) credential
+ completion:(nullable FIRAuthDataResultCallback) completion;
+
+/** @fn unlinkFromProvider:completion:
+ @brief Disassociates a user account from a third-party identity provider with this user.
+
+ @param provider The provider ID of the provider to unlink.
+ @param completion Optionally; the block invoked when the unlinking is complete, or fails.
+ Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeNoSuchProvider - Indicates an attempt to unlink a provider
+ that is not linked to the account.
+ </li>
+ <li>@c FIRAuthErrorCodeRequiresRecentLogin - Updating email is a security sensitive
+ operation that requires a recent login from the user. This error indicates the user
+ has not signed in recently enough. To resolve, reauthenticate the user by invoking
+ reauthenticateWithCredential:completion: on FIRUser.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)unlinkFromProvider:(NSString *)provider
+ completion:(nullable FIRAuthResultCallback)completion;
+
+/** @fn sendEmailVerificationWithCompletion:
+ @brief Initiates email verification for the user.
+
+ @param completion Optionally; the block invoked when the request to send an email verification
+ is complete, or fails. Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeInvalidRecipientEmail - Indicates an invalid recipient email was
+ sent in the request.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidSender - Indicates an invalid sender email is set in
+ the console for this action.
+ </li>
+ <li>@c FIRAuthErrorCodeInvalidMessagePayload - Indicates an invalid email template for
+ sending update email.
+ </li>
+ <li>@c FIRAuthErrorCodeUserNotFound - Indicates the user account was not found.</li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+ */
+- (void)sendEmailVerificationWithCompletion:(nullable FIRSendEmailVerificationCallback)completion;
+
+/** @fn deleteWithCompletion:
+ @brief Deletes the user account (also signs out the user, if this was the current user).
+
+ @param completion Optionally; the block invoked when the request to delete the account is
+ complete, or fails. Invoked asynchronously on the main thread in the future.
+
+ @remarks Possible error codes:
+ <ul>
+ <li>@c FIRAuthErrorCodeRequiresRecentLogin - Updating email is a security sensitive
+ operation that requires a recent login from the user. This error indicates the user
+ has not signed in recently enough. To resolve, reauthenticate the user by invoking
+ reauthenticateWithCredential:completion: on FIRUser.
+ </li>
+ </ul>
+
+ @remarks See @c FIRAuthErrors for a list of error codes that are common to all FIRUser methods.
+
+ */
+- (void)deleteWithCompletion:(nullable FIRUserProfileChangeCallback)completion;
+
+@end
+
+/** @class FIRUserProfileChangeRequest
+ @brief Represents an object capable of updating a user's profile data.
+ @remarks Properties are marked as being part of a profile update when they are set. Setting a
+ property value to nil is not the same as leaving the property unassigned.
+ */
+FIR_SWIFT_NAME(UserProfileChangeRequest)
+@interface FIRUserProfileChangeRequest : NSObject
+
+/** @fn init
+ @brief Please use @c FIRUser.profileChangeRequest
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @property displayName
+ @brief The user's display name.
+ @remarks It is an error to set this property after calling
+ @c FIRUserProfileChangeRequest.commitChangesWithCallback:
+ */
+@property(nonatomic, copy, nullable) NSString *displayName;
+
+/** @property photoURL
+ @brief The user's photo URL.
+ @remarks It is an error to set this property after calling
+ @c FIRUserProfileChangeRequest.commitChangesWithCallback:
+ */
+@property(nonatomic, copy, nullable) NSURL *photoURL;
+
+/** @fn commitChangesWithCompletion:
+ @brief Commits any pending changes.
+ @remarks This method should only be called once. Once called, property values should not be
+ changed.
+
+ @param completion Optionally; the block invoked when the user profile change has been applied.
+ Invoked asynchronously on the main thread in the future.
+ */
+- (void)commitChangesWithCompletion:(nullable FIRUserProfileChangeCallback)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRUser.m b/Firebase/Auth/Source/FIRUser.m
new file mode 100644
index 0000000..f0c3226
--- /dev/null
+++ b/Firebase/Auth/Source/FIRUser.m
@@ -0,0 +1,1170 @@
+/*
+ * 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 "Private/FIRUser_Internal.h"
+
+#import "AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.h"
+#import "AuthProviders/EmailPassword/FIREmailAuthProvider.h"
+#import "AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h"
+#import "AuthProviders/Phone/FIRPhoneAuthProvider.h"
+#import "Private/FIRAdditionalUserInfo_Internal.h"
+#import "FIRAuth.h"
+#import "Private/FIRAuthCredential_Internal.h"
+#import "Private/FIRAuthDataResult_Internal.h"
+#import "Private/FIRAuthErrorUtils.h"
+#import "Private/FIRAuthGlobalWorkQueue.h"
+#import "Private/FIRAuthSerialTaskQueue.h"
+#import "Private/FIRAuth_Internal.h"
+#import "FIRSecureTokenService.h"
+#import "FIRUserInfoImpl.h"
+#import "FIRAuthBackend.h"
+#import "FIRDeleteAccountRequest.h"
+#import "FIRDeleteAccountResponse.h"
+#import "FIRGetAccountInfoRequest.h"
+#import "FIRGetAccountInfoResponse.h"
+#import "FIRGetOOBConfirmationCodeRequest.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRSetAccountInfoRequest.h"
+#import "FIRSetAccountInfoResponse.h"
+#import "FIRVerifyAssertionRequest.h"
+#import "FIRVerifyAssertionResponse.h"
+#import "FIRVerifyCustomTokenRequest.h"
+#import "FIRVerifyCustomTokenResponse.h"
+#import "FIRVerifyPasswordRequest.h"
+#import "FIRVerifyPasswordResponse.h"
+#import "FIRVerifyPhoneNumberRequest.h"
+#import "FIRVerifyPhoneNumberResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kUserIDCodingKey
+ @brief The key used to encode the user ID for NSSecureCoding.
+ */
+static NSString *const kUserIDCodingKey = @"userID";
+
+/** @var kHasEmailPasswordCredentialCodingKey
+ @brief The key used to encode the hasEmailPasswordCredential property for NSSecureCoding.
+ */
+static NSString *const kHasEmailPasswordCredentialCodingKey = @"hasEmailPassword";
+
+/** @var kAnonymousCodingKey
+ @brief The key used to encode the anonymous property for NSSecureCoding.
+ */
+static NSString *const kAnonymousCodingKey = @"anonymous";
+
+/** @var kEmailCodingKey
+ @brief The key used to encode the email property for NSSecureCoding.
+ */
+static NSString *const kEmailCodingKey = @"email";
+
+/** @var kEmailVerifiedCodingKey
+ @brief The key used to encode the isEmailVerified property for NSSecureCoding.
+ */
+static NSString *const kEmailVerifiedCodingKey = @"emailVerified";
+
+/** @var kDisplayNameCodingKey
+ @brief The key used to encode the displayName property for NSSecureCoding.
+ */
+static NSString *const kDisplayNameCodingKey = @"displayName";
+
+/** @var kPhotoURLCodingKey
+ @brief The key used to encode the photoURL property for NSSecureCoding.
+ */
+static NSString *const kPhotoURLCodingKey = @"photoURL";
+
+/** @var kProviderDataKey
+ @brief The key used to encode the providerData instance variable for NSSecureCoding.
+ */
+static NSString *const kProviderDataKey = @"providerData";
+
+/** @var kAPIKeyCodingKey
+ @brief The key used to encode the APIKey instance variable for NSSecureCoding.
+ */
+static NSString *const kAPIKeyCodingKey = @"APIKey";
+
+/** @var kTokenServiceCodingKey
+ @brief The key used to encode the tokenService instance variable for NSSecureCoding.
+ */
+static NSString *const kTokenServiceCodingKey = @"tokenService";
+
+/** @var kMissingUsersErrorMessage
+ @brief The error message when there is no users array in the getAccountInfo response.
+ */
+static NSString *const kMissingUsersErrorMessage = @"users";
+
+/** @typedef CallbackWithError
+ @brief The type for a callback block that only takes an error parameter.
+ */
+typedef void (^CallbackWithError)(NSError *_Nullable);
+
+/** @typedef CallbackWithUserAndError
+ @brief The type for a callback block that takes a user parameter and an error parameter.
+ */
+typedef void (^CallbackWithUserAndError)(FIRUser *_Nullable, NSError *_Nullable);
+
+/** @typedef CallbackWithUserAndError
+ @brief The type for a callback block that takes a user parameter and an error parameter.
+ */
+typedef void (^CallbackWithAuthDataResultAndError)(FIRAuthDataResult *_Nullable,
+ NSError *_Nullable);
+
+/** @var kMissingPasswordReason
+ @brief The reason why the @c FIRAuthErrorCodeWeakPassword error is thrown.
+ @remarks This error message will be localized in the future.
+ */
+static NSString *const kMissingPasswordReason = @"Missing Password";
+
+/** @fn callInMainThreadWithError
+ @brief Calls a callback in main thread with error.
+ @param callback The callback to be called in main thread.
+ @param error The error to pass to callback.
+ */
+static void callInMainThreadWithError(_Nullable CallbackWithError callback,
+ NSError *_Nullable error) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(error);
+ });
+ }
+}
+
+/** @fn callInMainThreadWithUserAndError
+ @brief Calls a callback in main thread with user and error.
+ @param callback The callback to be called in main thread.
+ @param user The user to pass to callback if there is no error.
+ @param error The error to pass to callback.
+ */
+static void callInMainThreadWithUserAndError(_Nullable CallbackWithUserAndError callback,
+ FIRUser *_Nonnull user,
+ NSError *_Nullable error) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(error ? nil : user, error);
+ });
+ }
+}
+
+/** @fn callInMainThreadWithUserAndError
+ @brief Calls a callback in main thread with user and error.
+ @param callback The callback to be called in main thread.
+ @param result The result to pass to callback if there is no error.
+ @param error The error to pass to callback.
+ */
+static void callInMainThreadWithAuthDataResultAndError(
+ _Nullable CallbackWithAuthDataResultAndError callback,
+ FIRAuthDataResult *_Nullable result,
+ NSError *_Nullable error) {
+ if (callback) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ callback(result, error);
+ });
+ }
+}
+
+@interface FIRUserProfileChangeRequest ()
+
+/** @fn initWithUser:
+ @brief Designated initializer.
+ @param user The user for which we are updating profile information.
+ */
+- (nullable instancetype)initWithUser:(FIRUser *)user NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface FIRUser ()
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer
+ @param APIKey The client API key for making RPCs.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FIRUser {
+ /** @var _hasEmailPasswordCredential
+ @brief Whether or not the user can be authenticated by using Firebase email and password.
+ */
+ BOOL _hasEmailPasswordCredential;
+
+ /** @var _providerData
+ @brief Provider specific user data.
+ */
+ NSDictionary<NSString *, FIRUserInfoImpl *> *_providerData;
+
+ /** @var _APIKey
+ @brief The application's API Key.
+ */
+ NSString *_APIKey;
+
+ /** @var _taskQueue
+ @brief Used to serialize the update profile calls.
+ */
+ FIRAuthSerialTaskQueue *_taskQueue;
+
+ /** @var _tokenService
+ @brief A secure token service associated with this user. For performing token exchanges and
+ refreshing access tokens.
+ */
+ FIRSecureTokenService *_tokenService;
+}
+
+#pragma mark - Properties
+
+// Explicitly @synthesize because these properties are defined in FIRUserInfo protocol.
+@synthesize uid = _userID;
+@synthesize displayName = _displayName;
+@synthesize photoURL = _photoURL;
+@synthesize email = _email;
+@synthesize phoneNumber = _phoneNumber;
+
+#pragma mark -
+
++ (void)retrieveUserWithAPIKey:(NSString *)APIKey
+ accessToken:(NSString *)accessToken
+ accessTokenExpirationDate:(NSDate *)accessTokenExpirationDate
+ refreshToken:(NSString *)refreshToken
+ anonymous:(BOOL)anonymous
+ callback:(FIRRetrieveUserCallback)callback {
+ FIRSecureTokenService *tokenService =
+ [[FIRSecureTokenService alloc] initWithAPIKey:APIKey
+ accessToken:accessToken
+ accessTokenExpirationDate:accessTokenExpirationDate
+ refreshToken:refreshToken];
+ FIRUser *user = [[self alloc] initWithAPIKey:APIKey
+ tokenService:tokenService];
+ [user internalGetTokenWithCallback:^(NSString *_Nullable accessToken, NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ FIRGetAccountInfoRequest *getAccountInfoRequest =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:APIKey accessToken:accessToken];
+ [FIRAuthBackend getAccountInfo:getAccountInfoRequest
+ callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ user->_anonymous = anonymous;
+ [user updateWithGetAccountInfoResponse:response];
+ callback(user, nil);
+ }];
+ }];
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey {
+ self = [super init];
+ if (self) {
+ _APIKey = [APIKey copy];
+ _providerData = @{ };
+ _taskQueue = [[FIRAuthSerialTaskQueue alloc] init];
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ tokenService:(FIRSecureTokenService *)tokenService {
+ self = [self initWithAPIKey:APIKey];
+ if (self) {
+ _tokenService = tokenService;
+ }
+ return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ NSString *userID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUserIDCodingKey];
+ BOOL hasAnonymousKey = [aDecoder containsValueForKey:kAnonymousCodingKey];
+ BOOL anonymous = [aDecoder decodeBoolForKey:kAnonymousCodingKey];
+ BOOL hasEmailPasswordCredential =
+ [aDecoder decodeBoolForKey:kHasEmailPasswordCredentialCodingKey];
+ NSString *displayName =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kDisplayNameCodingKey];
+ NSURL *photoURL =
+ [aDecoder decodeObjectOfClass:[NSURL class] forKey:kPhotoURLCodingKey];
+ NSString *email =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kEmailCodingKey];
+ BOOL emailVerified = [aDecoder decodeBoolForKey:kEmailVerifiedCodingKey];
+ NSSet *providerDataClasses = [NSSet setWithArray:@[
+ [NSDictionary class],
+ [NSString class],
+ [FIRUserInfoImpl class]
+ ]];
+ NSDictionary<NSString *, FIRUserInfoImpl *> *providerData =
+ [aDecoder decodeObjectOfClasses:providerDataClasses forKey:kProviderDataKey];
+ NSString *APIKey =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kAPIKeyCodingKey];
+ FIRSecureTokenService *tokenService =
+ [aDecoder decodeObjectOfClass:[FIRSecureTokenService class] forKey:kTokenServiceCodingKey];
+ if (!userID || !APIKey || !tokenService) {
+ return nil;
+ }
+ self = [self initWithAPIKey:APIKey];
+ if (self) {
+ _tokenService = tokenService;
+ _userID = userID;
+ // Previous version of this code didn't save 'anonymous' bit directly but deduced it from
+ // 'hasEmailPasswordCredential' and 'providerData' instead, so here backward compatibility is
+ // provided to read old format data.
+ _anonymous = hasAnonymousKey ? anonymous : (!hasEmailPasswordCredential && !providerData.count);
+ _hasEmailPasswordCredential = hasEmailPasswordCredential;
+ _email = email;
+ _emailVerified = emailVerified;
+ _displayName = displayName;
+ _photoURL = photoURL;
+ _providerData = providerData;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_userID forKey:kUserIDCodingKey];
+ [aCoder encodeBool:_anonymous forKey:kAnonymousCodingKey];
+ [aCoder encodeBool:_hasEmailPasswordCredential forKey:kHasEmailPasswordCredentialCodingKey];
+ [aCoder encodeObject:_providerData forKey:kProviderDataKey];
+ [aCoder encodeObject:_email forKey:kEmailCodingKey];
+ [aCoder encodeBool:_emailVerified forKey:kEmailVerifiedCodingKey];
+ [aCoder encodeObject:_photoURL forKey:kPhotoURLCodingKey];
+ [aCoder encodeObject:_displayName forKey:kDisplayNameCodingKey];
+ [aCoder encodeObject:_APIKey forKey:kAPIKeyCodingKey];
+ [aCoder encodeObject:_tokenService forKey:kTokenServiceCodingKey];
+}
+
+#pragma mark -
+
+- (NSString *)providerID {
+ return @"Firebase";
+}
+
+- (NSArray<id<FIRUserInfo>> *)providerData {
+ return _providerData.allValues;
+}
+
+/** @fn getAccountInfoRefreshingCache:
+ @brief Gets the users's account data from the server, updating our local values.
+ @param callback Invoked when the request to getAccountInfo has completed, or when an error has
+ been detected. Invoked asynchronously on the auth global work queue in the future.
+ */
+- (void)getAccountInfoRefreshingCache:(void(^)(FIRGetAccountInfoResponseUser *_Nullable user,
+ NSError *_Nullable error))callback {
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken, NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ FIRGetAccountInfoRequest *getAccountInfoRequest =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:_APIKey accessToken:accessToken];
+ [FIRAuthBackend getAccountInfo:getAccountInfoRequest
+ callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ [self updateWithGetAccountInfoResponse:response];
+ if (![self updateKeychain:&error]) {
+ callback(nil, error);
+ return;
+ }
+ callback(response.users.firstObject, nil);
+ }];
+ }];
+}
+
+- (void)updateWithGetAccountInfoResponse:(FIRGetAccountInfoResponse *)response {
+ FIRGetAccountInfoResponseUser *user = response.users.firstObject;
+ _userID = user.localID;
+ _email = user.email;
+ _emailVerified = user.emailVerified;
+ _displayName = user.displayName;
+ _photoURL = user.photoURL;
+ _phoneNumber = user.phoneNumber;
+ _hasEmailPasswordCredential = user.passwordHash.length > 0;
+
+ NSMutableDictionary<NSString *, FIRUserInfoImpl *> *providerData =
+ [NSMutableDictionary dictionary];
+ for (FIRGetAccountInfoResponseProviderUserInfo *providerUserInfo in user.providerUserInfo) {
+ FIRUserInfoImpl *userInfo =
+ [FIRUserInfoImpl userInfoWithGetAccountInfoResponseProviderUserInfo:providerUserInfo];
+ if (userInfo) {
+ providerData[providerUserInfo.providerID] = userInfo;
+ }
+ }
+ _providerData = [providerData copy];
+}
+
+/** @fn executeUserUpdateWithChanges:callback:
+ @brief Performs a setAccountInfo request by mutating the results of a getAccountInfo response,
+ atomically in regards to other calls to this method.
+ @param changeBlock A block responsible for mutating a template @c FIRSetAccountInfoRequest
+ @param callback A block to invoke when the change is complete. Invoked asynchronously on the
+ auth global work queue in the future.
+ */
+- (void)executeUserUpdateWithChanges:(void(^)(FIRGetAccountInfoResponseUser *,
+ FIRSetAccountInfoRequest *))changeBlock
+ callback:(nonnull FIRUserProfileChangeCallback)callback {
+ [_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock _Nonnull complete) {
+ [self getAccountInfoRefreshingCache:^(FIRGetAccountInfoResponseUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ complete();
+ callback(error);
+ return;
+ }
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ complete();
+ callback(error);
+ return;
+ }
+ // Mutate setAccountInfoRequest in block:
+ FIRSetAccountInfoRequest *setAccountInfoRequest =
+ [[FIRSetAccountInfoRequest alloc] initWithAPIKey:_APIKey];
+ setAccountInfoRequest.accessToken = accessToken;
+ changeBlock(user, setAccountInfoRequest);
+ // Execute request:
+ [FIRAuthBackend setAccountInfo:setAccountInfoRequest
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ complete();
+ callback(error);
+ return;
+ }
+ if (response.IDToken && response.refreshToken) {
+ FIRSecureTokenService *tokenService =
+ [[FIRSecureTokenService alloc] initWithAPIKey:_APIKey
+ accessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken];
+ [self setTokenService:tokenService callback:^(NSError *_Nullable error) {
+ complete();
+ callback(error);
+ }];
+ return;
+ }
+ complete();
+ callback(nil);
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn updateKeychain:
+ @brief Updates the keychain for user token or info changes.
+ @param error The error if NO is returned.
+ @return Wether the operation is successful.
+ */
+- (BOOL)updateKeychain:(NSError *_Nullable *_Nullable)error {
+ return !_auth || [_auth updateKeychainWithUser:self error:error];
+}
+
+/** @fn setTokenService:callback:
+ @brief Sets a new token service for the @c FIRUser instance.
+ @param tokenService The new token service object.
+ @param callback The block to be called in the global auth working queue once finished.
+ @remarks The method makes sure the token service has access and refresh token and the new tokens
+ are saved in the keychain before calling back.
+ */
+- (void)setTokenService:(FIRSecureTokenService *)tokenService
+ callback:(nonnull CallbackWithError)callback {
+ [tokenService fetchAccessTokenForcingRefresh:NO
+ callback:^(NSString *_Nullable token,
+ NSError *_Nullable error,
+ BOOL tokenUpdated) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ _tokenService = tokenService;
+ if (![self updateKeychain:&error]) {
+ callback(error);
+ return;
+ }
+ [_auth notifyListenersOfAuthStateChangeWithUser:self token:token];
+ callback(nil);
+ }];
+}
+
+#pragma mark -
+
+/** @fn updateEmail:password:callback:
+ @brief Updates email address and/or password for the current user.
+ @remarks May fail if there is already an email/password-based account for the same email
+ address.
+ @param email The email address for the user, if to be updated.
+ @param password The new password for the user, if to be updated.
+ @param callback The block called when the user profile change has finished. Invoked
+ asynchronously on the auth global work queue in the future.
+ @remarks May fail with a @c FIRAuthErrorCodeRequiresRecentLogin error code.
+ Call @c reauthentateWithCredential:completion: beforehand to avoid this error case.
+ */
+- (void)updateEmail:(nullable NSString *)email
+ password:(nullable NSString *)password
+ callback:(nonnull FIRUserProfileChangeCallback)callback {
+ if (password && ![password length]){
+ callback([FIRAuthErrorUtils weakPasswordErrorWithServerResponseReason:kMissingPasswordReason]);
+ return;
+ }
+ BOOL hadEmailPasswordCredential = _hasEmailPasswordCredential;
+ [self executeUserUpdateWithChanges:^(FIRGetAccountInfoResponseUser *user,
+ FIRSetAccountInfoRequest *request) {
+ if (email) {
+ request.email = email;
+ }
+ if (password) {
+ request.password = password;
+ }
+ }
+ callback:^(NSError *error) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ if (email) {
+ _email = email;
+ }
+ if (_email && password) {
+ _anonymous = NO;
+ _hasEmailPasswordCredential = YES;
+ if (!hadEmailPasswordCredential) {
+ // The list of providers need to be updated for the newly added email-password provider.
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ FIRGetAccountInfoRequest *getAccountInfoRequest =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:_APIKey accessToken:accessToken];
+ [FIRAuthBackend getAccountInfo:getAccountInfoRequest
+ callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ callback(error);
+ return;
+ }
+ [self updateWithGetAccountInfoResponse:response];
+ if (![self updateKeychain:&error]) {
+ callback(error);
+ return;
+ }
+ callback(nil);
+ }];
+ }];
+ return;
+ }
+ }
+ if (![self updateKeychain:&error]) {
+ callback(error);
+ return;
+ }
+ callback(nil);
+ }];
+}
+
+- (void)updateEmail:(NSString *)email completion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self updateEmail:email password:nil callback:^(NSError *_Nullable error) {
+ callInMainThreadWithError(completion, error);
+ }];
+ });
+}
+
+- (void)updatePassword:(NSString *)password
+ completion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self updateEmail:nil password:password callback:^(NSError *_Nullable error){
+ callInMainThreadWithError(completion, error);
+ }];
+ });
+}
+
+/** @fn internalUpdatePhoneNumberCredential:completion:
+ @brief Updates the phone number for the user. On success, the cached user profile data is
+ updated.
+
+ @param phoneAuthCredential The new phone number credential corresponding to the phone number
+ to be added to the firebaes account, if a phone number is already linked to the account this
+ new phone number will replace it.
+ @param completion Optionally; the block invoked when the user profile change has finished.
+ Invoked asynchronously on the global work queue in the future.
+ */
+- (void)internalUpdatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneAuthCredential
+ completion:(FIRUserProfileChangeCallback)completion {
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ completion(error);
+ return;
+ }
+ FIRVerifyPhoneNumberRequest *request = [[FIRVerifyPhoneNumberRequest alloc]
+ initWithVerificationID:phoneAuthCredential.verificationID
+ verificationCode:phoneAuthCredential.verificationCode
+ APIKey:_APIKey];
+ request.accessToken = accessToken;
+ [FIRAuthBackend verifyPhoneNumber:request
+ callback:^(FIRVerifyPhoneNumberResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ completion(error);;
+ return;
+ }
+ // Get account info to update cached user info.
+ [self getAccountInfoRefreshingCache:^(FIRGetAccountInfoResponseUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (![self updateKeychain:&error]) {
+ completion(error);
+ return;
+ }
+ completion(nil);
+ }];
+ }];
+ }];
+}
+
+- (void)updatePhoneNumberCredential:(FIRPhoneAuthCredential *)phoneAuthCredential
+ completion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self internalUpdatePhoneNumberCredential:phoneAuthCredential
+ completion:^(NSError *_Nullable error) {
+ callInMainThreadWithError(completion, error);
+ }];
+ });
+}
+
+- (FIRUserProfileChangeRequest *)profileChangeRequest {
+ __block FIRUserProfileChangeRequest *result;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ result = [[FIRUserProfileChangeRequest alloc] initWithUser:self];
+ });
+ return result;
+}
+
+- (void)setDisplayName:(NSString *)displayName {
+ _displayName = [displayName copy];
+}
+
+- (void)setPhotoURL:(NSURL *)photoURL {
+ _photoURL = [photoURL copy];
+}
+
+- (NSString *)rawAccessToken {
+ return _tokenService.rawAccessToken;
+}
+
+- (NSDate *)accessTokenExpirationDate {
+ return _tokenService.accessTokenExpirationDate;
+}
+
+#pragma mark -
+
+- (void)reloadWithCompletion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self getAccountInfoRefreshingCache:^(FIRGetAccountInfoResponseUser *_Nullable user,
+ NSError *_Nullable error) {
+ callInMainThreadWithError(completion, error);
+ }];
+ });
+}
+
+#pragma mark -
+
+- (void)reauthenticateWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRUserProfileChangeCallback)completion {
+ FIRAuthDataResultCallback callback = ^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ completion(error);
+ };
+ [self reauthenticateAndRetrieveDataWithCredential:credential completion:callback];
+}
+
+- (void)
+ reauthenticateAndRetrieveDataWithCredential:(FIRAuthCredential *) credential
+ completion:(nullable FIRAuthDataResultCallback) completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [_auth internalSignInAndRetrieveDataWithCredential:credential
+ isReauthentication:YES
+ callback:^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ if (error) {
+ // If "user not found" error returned by backend, translate to user mismatch error which is
+ // more accurate.
+ if (error.code == FIRAuthErrorCodeUserNotFound) {
+ error = [FIRAuthErrorUtils userMismatchError];
+ }
+ callInMainThreadWithAuthDataResultAndError(completion, authResult, error);
+ return;
+ }
+ if (![authResult.user.uid isEqual:_auth.currentUser.uid]) {
+ callInMainThreadWithAuthDataResultAndError(completion, authResult,
+ [FIRAuthErrorUtils userMismatchError]);
+ return;
+ }
+ // Successful reauthenticate
+ [self setTokenService:authResult.user->_tokenService callback:^(NSError *_Nullable error) {
+ callInMainThreadWithAuthDataResultAndError(completion, authResult, error);
+ }];
+ }];
+ });
+}
+
+- (nullable NSString *)refreshToken {
+ __block NSString *result;
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ result = _tokenService.refreshToken;
+ });
+ return result;
+}
+
+- (void)getIDTokenWithCompletion:(nullable FIRAuthTokenCallback)completion {
+ // |getTokenForcingRefresh:completion:| is also a public API so there is no need to dispatch to
+ // global work queue here.
+ [self getIDTokenForcingRefresh:NO completion:completion];
+}
+
+- (void)getTokenWithCompletion:(nullable FIRAuthTokenCallback)completion {
+ [self getIDTokenWithCompletion:completion];
+}
+
+- (void)getIDTokenForcingRefresh:(BOOL)forceRefresh
+ completion:(nullable FIRAuthTokenCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self internalGetTokenForcingRefresh:forceRefresh
+ callback:^(NSString *_Nullable token, NSError *_Nullable error) {
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(token, error);
+ });
+ }
+ }];
+ });
+}
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh
+ completion:(nullable FIRAuthTokenCallback)completion {
+ [self getIDTokenForcingRefresh:forceRefresh completion:completion];
+}
+
+/** @fn internalGetTokenForcingRefresh:callback:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
+ @param callback The block to invoke when the token is available. Invoked asynchronously on the
+ global work thread in the future.
+ */
+- (void)internalGetTokenWithCallback:(nonnull FIRAuthTokenCallback)callback {
+ [self internalGetTokenForcingRefresh:NO callback:callback];
+}
+
+- (void)internalGetTokenForcingRefresh:(BOOL)forceRefresh
+ callback:(nonnull FIRAuthTokenCallback)callback {
+ [_tokenService fetchAccessTokenForcingRefresh:forceRefresh
+ callback:^(NSString *_Nullable token,
+ NSError *_Nullable error,
+ BOOL tokenUpdated) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ if (tokenUpdated) {
+ if (![self updateKeychain:&error]) {
+ callback(nil, error);
+ return;
+ }
+ [_auth notifyListenersOfAuthStateChangeWithUser:self token:token];
+ }
+ callback(token, nil);
+ }];
+}
+
+- (void)linkWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthResultCallback)completion {
+ FIRAuthDataResultCallback callback = ^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ completion(authResult.user, error);
+ };
+ [self linkAndRetrieveDataWithCredential:credential completion:callback];
+}
+
+- (void)linkAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
+ completion:(nullable FIRAuthDataResultCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ if (_providerData[credential.provider]) {
+ callInMainThreadWithAuthDataResultAndError(completion,
+ nil,
+ [FIRAuthErrorUtils providerAlreadyLinkedError]);
+ return;
+ }
+ FIRAuthDataResult *result =
+ [[FIRAuthDataResult alloc] initWithUser:self additionalUserInfo:nil];
+ if ([credential isKindOfClass:[FIREmailPasswordAuthCredential class]]) {
+ if (_hasEmailPasswordCredential) {
+ callInMainThreadWithAuthDataResultAndError(completion,
+ nil,
+ [FIRAuthErrorUtils providerAlreadyLinkedError]);
+ return;
+ }
+ FIREmailPasswordAuthCredential *emailPasswordCredential =
+ (FIREmailPasswordAuthCredential *)credential;
+ [self updateEmail:emailPasswordCredential.email
+ password:emailPasswordCredential.password
+ callback:^(NSError *error) {
+ if (error) {
+ callInMainThreadWithAuthDataResultAndError(completion, nil, error);
+ } else {
+ callInMainThreadWithAuthDataResultAndError(completion, result, nil);
+ }
+ }];
+ return;
+ }
+
+ if ([credential isKindOfClass:[FIRPhoneAuthCredential class]]) {
+ FIRPhoneAuthCredential *phoneAuthCredential = (FIRPhoneAuthCredential *)credential;
+ [self internalUpdatePhoneNumberCredential:phoneAuthCredential
+ completion:^(NSError *_Nullable error) {
+ if (error){
+ callInMainThreadWithAuthDataResultAndError(completion, nil, error);
+ } else {
+ callInMainThreadWithAuthDataResultAndError(completion, result, nil);
+ }
+ }];
+ return;
+ }
+
+ [_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock _Nonnull complete) {
+ CallbackWithAuthDataResultAndError completeWithError =
+ ^(FIRAuthDataResult *result, NSError *error) {
+ complete();
+ callInMainThreadWithAuthDataResultAndError(completion, result, error);
+ };
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ completeWithError(nil, error);
+ return;
+ }
+ FIRVerifyAssertionRequest *request =
+ [[FIRVerifyAssertionRequest alloc] initWithAPIKey:_APIKey providerID:credential.provider];
+ [credential prepareVerifyAssertionRequest:request];
+ request.accessToken = accessToken;
+ [FIRAuthBackend verifyAssertion:request
+ callback:^(FIRVerifyAssertionResponse *response, NSError *error) {
+ if (error) {
+ completeWithError(nil, error);
+ return;
+ }
+ FIRAdditionalUserInfo *additionalUserInfo =
+ [FIRAdditionalUserInfo userInfoWithVerifyAssertionResponse:response];
+ FIRAuthDataResult *result =
+ [[FIRAuthDataResult alloc] initWithUser:self additionalUserInfo:additionalUserInfo];
+ // Update the new token and refresh user info again.
+ _tokenService =
+ [[FIRSecureTokenService alloc] initWithAPIKey:_APIKey
+ accessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken];
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ completeWithError(nil, error);
+ return;
+ }
+ FIRGetAccountInfoRequest *getAccountInfoRequest =
+ [[FIRGetAccountInfoRequest alloc] initWithAPIKey:_APIKey accessToken:accessToken];
+ [FIRAuthBackend getAccountInfo:getAccountInfoRequest
+ callback:^(FIRGetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ completeWithError(nil, error);
+ return;
+ }
+ _anonymous = NO;
+ [self updateWithGetAccountInfoResponse:response];
+ if (![self updateKeychain:&error]) {
+ completeWithError(nil, error);
+ return;
+ }
+ completeWithError(result, nil);
+ }];
+ }];
+ }];
+ }];
+ }];
+ });
+}
+
+- (void)unlinkFromProvider:(NSString *)provider
+ completion:(nullable FIRAuthResultCallback)completion {
+ [_taskQueue enqueueTask:^(FIRAuthSerialTaskCompletionBlock _Nonnull complete) {
+ CallbackWithError completeAndCallbackWithError = ^(NSError *error) {
+ complete();
+ callInMainThreadWithUserAndError(completion, self, error);
+ };
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ completeAndCallbackWithError(error);
+ return;
+ }
+ FIRSetAccountInfoRequest *setAccountInfoRequest =
+ [[FIRSetAccountInfoRequest alloc] initWithAPIKey:_APIKey];
+ setAccountInfoRequest.accessToken = accessToken;
+ BOOL isEmailPasswordProvider = [provider isEqualToString:FIREmailAuthProviderID];
+ if (isEmailPasswordProvider) {
+ if (!_hasEmailPasswordCredential) {
+ completeAndCallbackWithError([FIRAuthErrorUtils noSuchProviderError]);
+ return;
+ }
+ setAccountInfoRequest.deleteAttributes = @[ FIRSetAccountInfoUserAttributePassword ];
+ } else {
+ if (!_providerData[provider]) {
+ completeAndCallbackWithError([FIRAuthErrorUtils noSuchProviderError]);
+ return;
+ }
+ setAccountInfoRequest.deleteProviders = @[ provider ];
+ }
+ [FIRAuthBackend setAccountInfo:setAccountInfoRequest
+ callback:^(FIRSetAccountInfoResponse *_Nullable response,
+ NSError *_Nullable error) {
+ if (error) {
+ completeAndCallbackWithError(error);
+ return;
+ }
+ if (isEmailPasswordProvider) {
+ _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 = [_providerData mutableCopy];
+ [mutableProviderData removeObjectForKey:provider];
+ _providerData = [mutableProviderData copy];
+
+ // After successfully unlinking a phone auth provider, remove the phone number from the
+ // cached user info.
+ if ([provider isEqualToString:FIRPhoneAuthProviderID]) {
+ _phoneNumber = nil;
+ }
+ }
+ if (response.IDToken && response.refreshToken) {
+ FIRSecureTokenService *tokenService =
+ [[FIRSecureTokenService alloc] initWithAPIKey:_APIKey
+ accessToken:response.IDToken
+ accessTokenExpirationDate:response.approximateExpirationDate
+ refreshToken:response.refreshToken];
+ [self setTokenService:tokenService callback:^(NSError *_Nullable error) {
+ completeAndCallbackWithError(error);
+ }];
+ return;
+ }
+ if (![self updateKeychain:&error]) {
+ completeAndCallbackWithError(error);
+ return;
+ }
+ completeAndCallbackWithError(nil);
+ }];
+ }];
+ }];
+}
+
+- (void)sendEmailVerificationWithCompletion:(nullable FIRSendEmailVerificationCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ FIRGetOOBConfirmationCodeRequest *request =
+ [FIRGetOOBConfirmationCodeRequest verifyEmailRequestWithAccessToken:accessToken
+ APIKey:_APIKey];
+ [FIRAuthBackend getOOBConfirmationCode:request
+ callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable
+ response,
+ NSError *_Nullable error) {
+ callInMainThreadWithError(completion, error);
+ }];
+ }];
+ });
+}
+
+- (void)deleteWithCompletion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+ [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken,
+ NSError *_Nullable error) {
+ if (error) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ FIRDeleteAccountRequest *deleteUserRequest =
+ [[FIRDeleteAccountRequest alloc] initWithAPIKey:_APIKey
+ localID:_userID
+ accessToken:accessToken];
+ [FIRAuthBackend deleteAccount:deleteUserRequest callback:^(NSError *_Nullable error) {
+ if (error) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ if (![[FIRAuth auth] signOutByForceWithUserID:_userID error:&error]) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ callInMainThreadWithError(completion, error);
+ }];
+ }];
+ });
+}
+
+@end
+
+@implementation FIRUserProfileChangeRequest {
+ /** @var _user
+ @brief The user associated with the change request.
+ */
+ FIRUser *_user;
+
+ /** @var _displayName
+ @brief The display name value to set if @c _displayNameSet is YES.
+ */
+ NSString *_displayName;
+
+ /** @var _displayNameSet
+ @brief Indicates the display name should be part of the change request.
+ */
+ BOOL _displayNameSet;
+
+ /** @var _photoURL
+ @brief The photo URL value to set if @c _displayNameSet is YES.
+ */
+ NSURL *_photoURL;
+
+ /** @var _photoURLSet
+ @brief Indicates the photo URL should be part of the change request.
+ */
+ BOOL _photoURLSet;
+
+ /** @var _consumed
+ @brief Indicates the @c commitChangesWithCallback: method has already been invoked.
+ */
+ BOOL _consumed;
+}
+
+- (nullable instancetype)initWithUser:(FIRUser *)user {
+ self = [super init];
+ if (self) {
+ _user = user;
+ }
+ return self;
+}
+
+- (nullable NSString *)displayName {
+ return _displayName;
+}
+
+- (void)setDisplayName:(nullable NSString *)displayName {
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ if (_consumed) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"%@",
+ @"Invalid call to setDisplayName: after commitChangesWithCallback:."];
+ return;
+ }
+ _displayNameSet = YES;
+ _displayName = [displayName copy];
+ });
+}
+
+- (nullable NSURL *)photoURL {
+ return _photoURL;
+}
+
+- (void)setPhotoURL:(nullable NSURL *)photoURL {
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ if (_consumed) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"%@",
+ @"Invalid call to setPhotoURL: after commitChangesWithCallback:."];
+ return;
+ }
+ _photoURLSet = YES;
+ _photoURL = [photoURL copy];
+ });
+}
+
+/** @fn hasUpdates
+ @brief Indicates at least one field has a value which needs to be committed.
+ */
+- (BOOL)hasUpdates {
+ return _displayNameSet || _photoURLSet;
+}
+
+- (void)commitChangesWithCompletion:(nullable FIRUserProfileChangeCallback)completion {
+ dispatch_sync(FIRAuthGlobalWorkQueue(), ^{
+ if (_consumed) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"%@",
+ @"commitChangesWithCallback: should only be called once."];
+ return;
+ }
+ _consumed = YES;
+ // Return fast if there is nothing to update:
+ if (![self hasUpdates]) {
+ callInMainThreadWithError(completion, nil);
+ return;
+ }
+ NSString *displayName = [_displayName copy];
+ BOOL displayNameWasSet = _displayNameSet;
+ NSURL *photoURL = [_photoURL copy];
+ BOOL photoURLWasSet = _photoURLSet;
+ [_user executeUserUpdateWithChanges:^(FIRGetAccountInfoResponseUser *user,
+ FIRSetAccountInfoRequest *request) {
+ if (photoURLWasSet) {
+ request.photoURL = photoURL;
+ }
+ if (displayNameWasSet) {
+ request.displayName = displayName;
+ }
+ }
+ callback:^(NSError *_Nullable error) {
+ if (error) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ if (displayNameWasSet) {
+ [_user setDisplayName:displayName];
+ }
+ if (photoURLWasSet) {
+ [_user setPhotoURL:photoURL];
+ }
+ if (![_user updateKeychain:&error]) {
+ callInMainThreadWithError(completion, error);
+ return;
+ }
+ callInMainThreadWithError(completion, nil);
+ }];
+ });
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRUserInfo.h b/Firebase/Auth/Source/FIRUserInfo.h
new file mode 100644
index 0000000..03f2038
--- /dev/null
+++ b/Firebase/Auth/Source/FIRUserInfo.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 <Foundation/Foundation.h>
+
+#import "FIRAuthSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief Represents user data returned from an identity provider.
+ */
+FIR_SWIFT_NAME(UserInfo)
+@protocol FIRUserInfo <NSObject>
+
+/** @property providerID
+ @brief The provider identifier.
+ */
+@property(nonatomic, copy, readonly) NSString *providerID;
+
+/** @property uid
+ @brief The provider's user ID for the user.
+ */
+@property(nonatomic, copy, readonly) NSString *uid;
+
+/** @property displayName
+ @brief The name of the user.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *displayName;
+
+/** @property photoURL
+ @brief The URL of the user's profile photo.
+ */
+@property(nonatomic, copy, readonly, nullable) NSURL *photoURL;
+
+/** @property email
+ @brief The user's email address.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *email;
+
+/** @property phoneNumber
+ @brief A phone number associated with the user.
+ @remarks This property is only available for users authenticated via phone number auth.
+ */
+@property(nonatomic, readonly, nullable) NSString *phoneNumber;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRUserInfoImpl.h b/Firebase/Auth/Source/FIRUserInfoImpl.h
new file mode 100644
index 0000000..0022a68
--- /dev/null
+++ b/Firebase/Auth/Source/FIRUserInfoImpl.h
@@ -0,0 +1,61 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRUserInfo.h"
+
+@class FIRGetAccountInfoResponseProviderUserInfo;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRUserInfoImpl : NSObject <FIRUserInfo, NSSecureCoding>
+
+/** @fn userInfoWithGetAccountInfoResponseProviderUserInfo:
+ @brief A convenience factory method for constructing a @c FIRUserInfo instance from data
+ returned by the getAccountInfo endpoint.
+ @param providerUserInfo Data returned by the getAccountInfo endpoint.
+ @return A new instance of @c FIRUserInfo using data from the getAccountInfo endpoint.
+ */
++ (nullable instancetype)userInfoWithGetAccountInfoResponseProviderUserInfo:
+ (FIRGetAccountInfoResponseProviderUserInfo *)providerUserInfo;
+
+/** @fn init
+ @brief This class should not be initialized manually.
+ @see FIRUser.providerData
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithProviderID:userID:displayName:photoURL:email:
+ @brief Designated initializer.
+ @param providerID The provider identifier.
+ @param userID The unique user ID for the user (the value of the @c uid field in the token.)
+ @param displayName The name of the user.
+ @param photoURL The URL of the user's profile photo.
+ @param email The user's email address.
+ @param phoneNumber The user's phone number.
+ */
+- (nullable instancetype)initWithProviderID:(NSString *)providerID
+ userID:(NSString *)userID
+ displayName:(nullable NSString *)displayName
+ photoURL:(nullable NSURL *)photoURL
+ email:(nullable NSString *)email
+ phoneNumber:(nullable NSString *)phoneNumber
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRUserInfoImpl.m b/Firebase/Auth/Source/FIRUserInfoImpl.m
new file mode 100644
index 0000000..d172481
--- /dev/null
+++ b/Firebase/Auth/Source/FIRUserInfoImpl.m
@@ -0,0 +1,127 @@
+/*
+ * 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 "FIRUserInfoImpl.h"
+
+#import "FIRGetAccountInfoResponse.h"
+
+/** @var kProviderIDCodingKey
+ @brief The key used to encode the providerID property for NSSecureCoding.
+ */
+static NSString *const kProviderIDCodingKey = @"providerID";
+
+/** @var kUserIDCodingKey
+ @brief The key used to encode the userID property for NSSecureCoding.
+ */
+static NSString *const kUserIDCodingKey = @"userID";
+
+/** @var kDisplayNameCodingKey
+ @brief The key used to encode the displayName property for NSSecureCoding.
+ */
+static NSString *const kDisplayNameCodingKey = @"displayName";
+
+/** @var kProfileURLCodingKey
+ @brief The key used to encode the profileURL property for NSSecureCoding.
+ */
+static NSString *const kProfileURLCodingKey = @"profileURL";
+
+/** @var kPhotoURLCodingKey
+ @brief The key used to encode the photoURL property for NSSecureCoding.
+ */
+static NSString *const kPhotoURLCodingKey = @"photoURL";
+
+/** @var kEmailCodingKey
+ @brief The key used to encode the email property for NSSecureCoding.
+ */
+static NSString *const kEmailCodingKey = @"email";
+
+/** @var kPhoneNumberCodingKey
+ @brief The key used to encode the phoneNumber property for NSSecureCoding.
+ */
+static NSString *const kPhoneNumberCodingKey = @"phoneNumber";
+
+@implementation FIRUserInfoImpl
+
+@synthesize providerID = _providerID;
+@synthesize uid = _userID;
+@synthesize displayName = _displayName;
+@synthesize photoURL = _photoURL;
+@synthesize email = _email;
+@synthesize phoneNumber = _phoneNumber;
+
++ (nullable instancetype)userInfoWithGetAccountInfoResponseProviderUserInfo:
+ (FIRGetAccountInfoResponseProviderUserInfo *)providerUserInfo {
+ return [[self alloc] initWithProviderID:providerUserInfo.providerID
+ userID:providerUserInfo.federatedID
+ displayName:providerUserInfo.displayName
+ photoURL:providerUserInfo.photoURL
+ email:providerUserInfo.email
+ phoneNumber:providerUserInfo.phoneNumber];
+}
+
+- (nullable instancetype)initWithProviderID:(NSString *)providerID
+ userID:(NSString *)userID
+ displayName:(nullable NSString *)displayName
+ photoURL:(nullable NSURL *)photoURL
+ email:(nullable NSString *)email
+ phoneNumber:(nullable NSString *)phoneNumber {
+ self = [super init];
+ if (self) {
+ _providerID = [providerID copy];
+ _userID = [userID copy];
+ _displayName = [displayName copy];
+ _photoURL = [photoURL copy];
+ _email = [email copy];
+ _phoneNumber = [phoneNumber copy];
+ }
+ return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+ NSString *providerID =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kProviderIDCodingKey];
+ NSString *userID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUserIDCodingKey];
+ NSString *displayName =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kDisplayNameCodingKey];
+ NSURL *photoURL = [aDecoder decodeObjectOfClass:[NSURL class] forKey:kPhotoURLCodingKey];
+ NSString *email = [aDecoder decodeObjectOfClass:[NSString class] forKey:kEmailCodingKey];
+ NSString *phoneNumber =
+ [aDecoder decodeObjectOfClass:[NSString class] forKey:kPhoneNumberCodingKey];
+
+ return [self initWithProviderID:providerID
+ userID:userID
+ displayName:displayName
+ photoURL:photoURL
+ email:email
+ phoneNumber:phoneNumber];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_providerID forKey:kProviderIDCodingKey];
+ [aCoder encodeObject:_userID forKey:kUserIDCodingKey];
+ [aCoder encodeObject:_displayName forKey:kDisplayNameCodingKey];
+ [aCoder encodeObject:_photoURL forKey:kPhotoURLCodingKey];
+ [aCoder encodeObject:_email forKey:kEmailCodingKey];
+ [aCoder encodeObject:_phoneNumber forKey:kPhoneNumberCodingKey];
+}
+
+@end
diff --git a/Firebase/Auth/Source/FirebaseAuth.h b/Firebase/Auth/Source/FirebaseAuth.h
new file mode 100644
index 0000000..8dba24e
--- /dev/null
+++ b/Firebase/Auth/Source/FirebaseAuth.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "FIREmailAuthProvider.h"
+#import "FIRFacebookAuthProvider.h"
+#import "FIRGitHubAuthProvider.h"
+#import "FIRGoogleAuthProvider.h"
+#import "FIRTwitterAuthProvider.h"
+#import "FIRAuth.h"
+#import "FIRAuthCredential.h"
+#import "FIRAuthErrors.h"
+#import "FIRUser.h"
+#import "FIRUserInfo.h"
+#import "FirebaseAuthVersion.h"
diff --git a/Firebase/Auth/Source/FirebaseAuthVersion.h b/Firebase/Auth/Source/FirebaseAuthVersion.h
new file mode 100755
index 0000000..1b2d06a
--- /dev/null
+++ b/Firebase/Auth/Source/FirebaseAuthVersion.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+/**
+ Version number for FirebaseAuth.
+ */
+extern const double FirebaseAuthVersionNumber;
+
+/**
+ Version string for FirebaseAuth.
+ */
+extern const unsigned char *const FirebaseAuthVersionString;
diff --git a/Firebase/Auth/Source/FirebaseAuthVersion.m b/Firebase/Auth/Source/FirebaseAuthVersion.m
new file mode 100644
index 0000000..fe4055b
--- /dev/null
+++ b/Firebase/Auth/Source/FirebaseAuthVersion.m
@@ -0,0 +1,26 @@
+/*
+ * 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 "FirebaseAuthVersion.h"
+
+// Convert the macro to a string
+#define STR(x) STR_EXPAND(x)
+#define STR_EXPAND(x) #x
+
+const double FirebaseAuthVersionNumber = FIRAuth_MINOR_VERSION;
+
+const unsigned char *const FirebaseAuthVersionString =
+ (const unsigned char *const)STR(FIRAuth_VERSION);
diff --git a/Firebase/Auth/Source/Private/FIRActionCodeSettings.h b/Firebase/Auth/Source/Private/FIRActionCodeSettings.h
new file mode 100644
index 0000000..adb9cbc
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRActionCodeSettings.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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRActionCodeSettings
+ @brief Used to set and retrieve settings related to the handling action codes.
+ */
+@interface FIRActionCodeSettings : NSObject
+
+/** @property URL
+ @brief This URL represents the state/Continue URL in the form of a universal link.
+ @remarks This URL can should be contructed as a universal link that would either directly open
+ the app where the action code would be handled or continue to the app after the action code
+ handled by Firebase.
+ */
+@property(nonatomic, copy, nullable) NSURL *URL;
+
+/** @property handleCodeInApp
+ @brief Indicates whether or not the action code link will open the app directly or after being
+ redirected from a Firebase owned web widget.
+ */
+@property(assign, nonatomic) BOOL handleCodeInApp;
+
+/** @property iOSBundleID
+ @brief The iOS bundle ID, if available.
+ */
+@property(copy, nonatomic, readonly, nullable) NSString *iOSBundleID;
+
+/** @property iOSAppStoreID
+ @brief The iOS app store identifier, if available.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *iOSAppStoreID;
+
+/** @property androidPackageName
+ @brief The Android package name, if available.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *androidPackageName;
+
+/** @property androidMinimumVersion
+ @brief The minimum Android version supported, if available.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *androidMinimumVersion;
+
+/** @property androidInstallIfNotAvailable
+ @brief Indicates whether or not the Android app should be installed if not already available.
+ */
+@property(nonatomic, assign, readonly) BOOL androidInstallIfNotAvailable;
+
+/** @fn setIOSBundleID:appStoreID
+ @brief Sets the iOS bundle Id and appStoreID.
+ @param iOSBundleID The iOS bundle ID.
+ @param appStoreID The app's AppStore ID.
+ @remarks If the app is not already installed on an iOS device and an appStoreId is provided, the
+ app store page of the app will be opened. If no app store ID is provided, the web app link
+ will be used instead.
+ */
+- (void)setIOSBundleID:(NSString *)iOSBundleID appStoreID:(nullable NSString *)appStoreID;
+
+/** @fn setAndroidPackageName:installIfNotAvailable:minimumVersion:
+ @brief Sets the Android package name, the flag to indicate whether or not to install the app and
+ the minimum Android version supported.
+ @param androidPackageName The Android package name.
+ @param installIfNotAvailable Indicates whether or not the app should be installed if not
+ available.
+ @param minimumVersion The minimum version of Android supported.
+ @remarks If installIfNotAvailable is set to YES and the link is opened on an android device, it
+ will try to install the app if not already available. Otherwise the web URL is used.
+ */
+- (void)setAndroidPackageName:(NSString *)androidPackageName
+ installIfNotAvailable:(BOOL)installIfNotAvailable
+ minimumVersion:(nullable NSString *)minimumVersion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAdditionalUserInfo_Internal.h b/Firebase/Auth/Source/Private/FIRAdditionalUserInfo_Internal.h
new file mode 100644
index 0000000..a813566
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAdditionalUserInfo_Internal.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "FIRAdditionalUserInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAdditionalUserInfo () <NSSecureCoding>
+
+/** @fn userInfoWithVerifyAssertionResponse:
+ @brief A convenience factory method for constructing a @c FIRAdditionalUserInfo instance from
+ data returned by the verifyAssertion endpoint.
+ @param verifyAssertionResponse Data returned by the verifyAssertion endpoint.
+ @return A new instance of @c FIRAdditionalUserInfo using data from the verifyAssertion endpoint.
+ */
++ (nullable instancetype)userInfoWithVerifyAssertionResponse:
+ (FIRVerifyAssertionResponse *)verifyAssertionResponse;
+
+/** @fn initWithProviderID:profile:username:
+ @brief Designated initializer.
+ @param providerID The provider identifier.
+ @param profile Dictionary containing the additional IdP specific information.
+ @param username The name of the user.
+ @param isNewUser Indicates whether or not the current user was signed in for the first time.
+ */
+- (nullable instancetype)initWithProviderID:(NSString *)providerID
+ profile:(nullable NSDictionary<NSString *, NSObject *> *)profile
+ username:(nullable NSString *)username
+ isNewUser:(BOOL)isNewUser NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthAPNSToken.h b/Firebase/Auth/Source/Private/FIRAuthAPNSToken.h
new file mode 100644
index 0000000..8efd4e1
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthAPNSToken.h
@@ -0,0 +1,54 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthAPNSTokenType.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthAPNSToken
+ @brief A data structure for an APNs token.
+ */
+@interface FIRAuthAPNSToken : NSObject
+
+/** @property data
+ @brief The APNs token data.
+ */
+@property(nonatomic, strong, readonly) NSData *data;
+
+/** @property type
+ @brief The APNs token type.
+ */
+@property(nonatomic, assign, readonly) FIRAuthAPNSTokenType type;
+
+/** @fn initWithData:type:
+ @brief Initializes the instance.
+ @param data The APNs token data.
+ @param type The APNs token type.
+ @return The initialized instance.
+ */
+- (instancetype)initWithData:(NSData *)data type:(FIRAuthAPNSTokenType)type
+ NS_DESIGNATED_INITIALIZER;
+
+/** @fn init
+ @brief Call @c initWithData:type: to get an instance of this class.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthAPNSTokenManager.h b/Firebase/Auth/Source/Private/FIRAuthAPNSTokenManager.h
new file mode 100644
index 0000000..a2d6f4c
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthAPNSTokenManager.h
@@ -0,0 +1,69 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+@class FIRAuthAPNSToken;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthAPNSTokenCallback
+ @brief The type of block to receive an APNs token.
+ @param token The APNs token if one is available.
+ */
+typedef void (^FIRAuthAPNSTokenCallback)(FIRAuthAPNSToken *_Nullable token);
+
+/** @class FIRAuthAPNSTokenManager
+ @brief A class to manage APNs token in memory.
+ */
+@interface FIRAuthAPNSTokenManager : NSObject
+
+/** @property token
+ @brief The APNs token, if one is available.
+ @remarks Setting a token with FIRAuthAPNSTokenTypeUnknown will automatically converts it to
+ a token with the automatically detected type.
+ */
+@property(nonatomic, strong, nullable) FIRAuthAPNSToken *token;
+
+/** @property timeout
+ @brief The timeout for registering for remote notification.
+ @remarks Only tests should access this property.
+ */
+@property(nonatomic, assign) NSTimeInterval timeout;
+
+/** @fn init
+ @brief Call @c initWithApplication: to initialize an instance of this class.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithApplication:bundle
+ @brief Initializes the instance.
+ @param application The @c UIApplication to request the token from.
+ @return The initialized instance.
+ */
+- (instancetype)initWithApplication:(UIApplication *)application NS_DESIGNATED_INITIALIZER;
+
+/** @fn getTokenWithCallback:
+ @brief Attempts to get the APNs token.
+ @param callback The block to be called either immediately or in future, either when a token
+ becomes available, or when timeout occurs, whichever happens earlier.
+ */
+- (void)getTokenWithCallback:(FIRAuthAPNSTokenCallback)callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthAppCredential.h b/Firebase/Auth/Source/Private/FIRAuthAppCredential.h
new file mode 100644
index 0000000..57fa83a
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthAppCredential.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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthAppCredential
+ @brief A class represents a credential that proves the identity of the app.
+ */
+@interface FIRAuthAppCredential : NSObject <NSSecureCoding>
+
+/** @property receipt
+ @brief The server acknowledgement of receiving client's claim of identity.
+ */
+@property(nonatomic, strong, readonly) NSString *receipt;
+
+/** @property secret
+ @brief The secret that the client received from server via a trusted channel, if ever.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *secret;
+
+/** @fn initWithReceipt:secret:
+ @brief Initializes the instance.
+ @param receipt The server acknowledgement of receiving client's claim of identity.
+ @param secret The secret that the client received from server via a trusted channel, if ever.
+ @return The initialized instance.
+ */
+- (instancetype)initWithReceipt:(NSString *)receipt secret:(nullable NSString *)secret
+ NS_DESIGNATED_INITIALIZER;
+
+/** @fn init
+ @brief Call @c initWithReceipt:secret: to get an instance of this class.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthAppCredentialManager.h b/Firebase/Auth/Source/Private/FIRAuthAppCredentialManager.h
new file mode 100644
index 0000000..21c1545
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthAppCredentialManager.h
@@ -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 <Foundation/Foundation.h>
+
+@class FIRAuthAppCredential;
+@class FIRAuthKeychain;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthAppCredentialCallback
+ @brief The type of block to receive an app crdential.
+ @param credential The best available app credential at the time.
+ */
+typedef void (^FIRAuthAppCredentialCallback)(FIRAuthAppCredential *credential);
+
+/** @class FIRAuthAppCredentialManager
+ @brief A class to manage app credentials backed by iOS Keychain.
+ */
+@interface FIRAuthAppCredentialManager : NSObject
+
+/** @property credential
+ @brief The full credential (which has a secret) to be used by the app, if one is available.
+ */
+@property(nonatomic, strong, readonly, nullable) FIRAuthAppCredential *credential;
+
+/** @property maximumNumberOfPendingReceipts
+ @brief The maximum (but not necessarily the minimum) number of pending receipts to be kept.
+ @remarks Only tests should access this property.
+ */
+@property(nonatomic, assign, readonly) NSUInteger maximumNumberOfPendingReceipts;
+
+/** @fn init
+ @brief Call @c initWithKeychain: to initialize an instance of this class.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithKeychain:
+ @brief Initializes the instance.
+ @param keychain The iOS Keychain storage to back up the app credential with.
+ @return The initialized instance.
+ */
+- (instancetype)initWithKeychain:(FIRAuthKeychain *)keychain NS_DESIGNATED_INITIALIZER;
+
+/** @fn didStartVerificationWithReceipt:timeout:callback:
+ @brief Notifies that the app verification process has started.
+ @param receipt The receipt for verification.
+ @param timeout The timeout value for how long the callback is waited to be called.
+ @param callback The block to be called in future either when the verification finishes, or
+ when timeout occurs, whichever happens earlier.
+ */
+- (void)didStartVerificationWithReceipt:(NSString *)receipt
+ timeout:(NSTimeInterval)timeout
+ callback:(FIRAuthAppCredentialCallback)callback;
+
+/** @fn canFinishVerificationWithReceipt:
+ @brief Attempts to finish verification.
+ @param receipt The receipt to match the original receipt obtained when verification started.
+ @param secret The secret to complete the verification.
+ @return Whether or not the receipt matches a pending verification, and finishes verification
+ if it does.
+ */
+- (BOOL)canFinishVerificationWithReceipt:(NSString *)receipt secret:(NSString *)secret;
+
+/** @fn clearCredential
+ @brief Clears the saved credential, to be used in the case that it is rejected by the server.
+ */
+- (void)clearCredential;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthAppDelegateProxy.h b/Firebase/Auth/Source/Private/FIRAuthAppDelegateProxy.h
new file mode 100644
index 0000000..656e4f2
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthAppDelegateProxy.h
@@ -0,0 +1,74 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @protocol FIRAuthAppDelegateHandler
+ @brief The protocol to handle app delegate methods.
+ */
+@protocol FIRAuthAppDelegateHandler <NSObject>
+
+/** @fn setAPNSToken:
+ @brief Sets the APNs device token.
+ @param token The APNs device token.
+ */
+- (void)setAPNSToken:(NSData *)token;
+
+/** @fn canHandleNotification:
+ @brief Checks whether the notification can be handled by the receiver, and handles it if so.
+ @param notification The notification in question, which will be consumed if returns @c YES.
+ @return Whether the notification can be (and already has been) handled by the receiver.
+ */
+- (BOOL)canHandleNotification:(nonnull NSDictionary *)notification;
+
+@end
+
+/** @class FIRAuthAppDelegateProxy
+ @brief A manager for swizzling @c UIApplicationDelegate methods.
+ */
+@interface FIRAuthAppDelegateProxy : NSObject
+
+/** @fn initWithApplication
+ @brief Initialize the instance with the given @c UIApplication.
+ @returns An initialized instance, or @c nil if a proxy cannot be established.
+ @remarks This method should only be called from tests if called outside of this class.
+ */
+- (nullable instancetype)initWithApplication:(nullable UIApplication *)application
+ NS_DESIGNATED_INITIALIZER;
+
+/** @fn init
+ @brief Call @c sharedInstance to get an instance of this class.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn addHandler:
+ @brief Adds a handler for UIApplicationDelegate methods.
+ @param handler The handler to be added.
+ */
+- (void)addHandler:(__weak id<FIRAuthAppDelegateHandler>)handler;
+
+/** @fn sharedInstance
+ @brief Gets the shared instance of this class.
+ @returns The shared instance of this class.
+ */
++ (nullable instancetype)sharedInstance;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthCredential_Internal.h b/Firebase/Auth/Source/Private/FIRAuthCredential_Internal.h
new file mode 100644
index 0000000..e060cda
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthCredential_Internal.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 "FIRAuthCredential.h"
+
+@class FIRVerifyAssertionRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAuthCredential ()
+
+/** @fn initWithProvider:
+ @brief Designated initializer.
+ @remarks This is the designated initializer for internal/friend subclasses.
+ @param provider The provider name.
+ */
+- (nullable instancetype)initWithProvider:(NSString *)provider NS_DESIGNATED_INITIALIZER;
+
+/** @fn prepareVerifyAssertionRequest:
+ @brief Called immediately before a request to the verifyAssertion endpoint is made. Implementers
+ should update the passed request instance with their credentials.
+ @param request The request to be updated with credentials.
+ */
+- (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthDataResult_Internal.h b/Firebase/Auth/Source/Private/FIRAuthDataResult_Internal.h
new file mode 100644
index 0000000..b95edc2
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthDataResult_Internal.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "FIRAuthDataResult.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAuthDataResult () <NSSecureCoding>
+
+/** @fn initWithUser:additionalUserInfo:
+ @brief Designated initializer.
+ @param user The signed in user reference.
+ @param additionalUserInfo The additional user info if available.
+ */
+- (nullable instancetype)initWithUser:(FIRUser *)user
+ additionalUserInfo:(nullable FIRAdditionalUserInfo *)additionalUserInfo
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthDispatcher.h b/Firebase/Auth/Source/Private/FIRAuthDispatcher.h
new file mode 100644
index 0000000..f8ddca5
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthDispatcher.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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthDispatcherImplBlock
+ @brief The type of block which can be set as the implementation for @c
+ dispatchAfterDelay:queue:callback: .
+
+ @param delay The delay in seconds after which the task will be scheduled to execute.
+ @param queue The dispatch queue on which the task will be submitted.
+ @param task The task (block) to be scheduled for future execution.
+ */
+typedef void(^FIRAuthDispatcherImplBlock)(NSTimeInterval delay,
+ dispatch_queue_t queue,
+ void (^task)(void));
+
+/** @class FIRAuthDispatchAfter
+ @brief A utility class used to facilitate scheduling tasks to be executed in the future.
+ */
+@interface FIRAuthDispatcher : NSObject
+
+/** @property dispatchAfterImplementation
+ @brief Allows custom implementation of dispatchAfterDelay:queue:callback:.
+ @remarks Set to nil to restore default implementation.
+ */
+@property(nonatomic, nullable, copy) FIRAuthDispatcherImplBlock dispatchAfterImplementation;
+
+/** @fn dispatchAfterDelay:queue:callback:
+ @brief Schedules task in the future after a specified delay.
+
+ @param delay The delay in seconds after which the task will be scheduled to execute.
+ @param queue The dispatch queue on which the task will be submitted.
+ @param task The task (block) to be scheduled for future execution.
+ */
+ - (void)dispatchAfterDelay:(NSTimeInterval)delay
+ queue:(dispatch_queue_t)queue
+ task:(void (^)(void))task;
+
+/** @fn sharedInstance
+ @brief Gets the shared instance of this class.
+ @returns The shared instance of this clss
+ */
++ (instancetype)sharedInstance;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthErrorUtils.h b/Firebase/Auth/Source/Private/FIRAuthErrorUtils.h
new file mode 100644
index 0000000..e2ad4aa
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthErrorUtils.h
@@ -0,0 +1,418 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FIRPhoneAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthErrorUtils
+ @brief Utility class used to construct @c NSError instances.
+ */
+@interface FIRAuthErrorUtils : NSObject
+
+/** @fn RPCRequestEncodingErrorWithUnderlyingError
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeRPCRequestEncodingError
+ code and a populated @c NSUnderlyingErrorKey in the @c NSError.userInfo dictionary.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key.
+ @remarks This error is used when an @c FIRAuthRPCRequest.unencodedHTTPRequestBodyWithError:
+ invocation returns an error. The error returned is wrapped in this internal error code.
+ */
++ (NSError *)RPCRequestEncodingErrorWithUnderlyingError:(NSError *)underlyingError;
+
+/** @fn JSONSerializationErrorForUnencodableType
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeJSONSerializationError code.
+ @remarks This error is used when an @c NSJSONSerialization.isValidJSONObject: check fails, not
+ for when an error is returned from @c NSJSONSerialization.dataWithJSONObject:options:error:.
+ */
++ (NSError *)JSONSerializationErrorForUnencodableType;
+
+/** @fn JSONSerializationErrorWithUnderlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeJSONSerializationError code, and the
+ @c underlyingError as the @c NSUnderlyingErrorKey value in the @c NSError.userInfo
+ dictionary.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key.
+ @remarks This error is used when an invocation of
+ @c NSJSONSerialization.dataWithJSONObject:options:error: returns an error.
+ */
++ (NSError *)JSONSerializationErrorWithUnderlyingError:(NSError *)underlyingError;
+
+/** @fn networkErrorWithUnderlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNetworkError code, and the
+ @c underlyingError as the @c NSUnderlyingErrorKey value in the @c NSError.userInfo
+ dictionary.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key. Should be the error from
+ GTM.
+ @remarks This error is used when a network request results in an error, and no body data was
+ returned.
+ */
++ (NSError *)networkErrorWithUnderlyingError:(NSError *)underlyingError;
+
+/** @fn unexpectedErrorResponseWithUnderlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNetworkError code, and the
+ @c underlyingError as the @c NSUnderlyingErrorKey value.
+ @param data The value of the @c FIRAuthErrorUserInfoDataKey key in the @c NSError.userInfo
+ dictionary.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key in the @c NSError.userInfo
+ dictionary.
+ @remarks This error is used when a network request results in an error, and unserializable body
+ data was returned.
+ */
++ (NSError *)unexpectedErrorResponseWithData:(NSData *)data
+ underlyingError:(NSError *)underlyingError;
+
+/** @fn unexpectedErrorResponseWithDeserializedResponse:
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeUnexpectedErrorResponse
+ code, and a populated @c FIRAuthErrorUserInfoDeserializedResponseKey key in the
+ @c NSError.userInfo dictionary.
+ @param deserializedResponse The value of the @c FIRAuthErrorUserInfoDeserializedResponseKey key.
+ @remarks This error is used when a network request results in an error, and the body data was
+ deserializable as JSON, but couldn't be decoded as an error.
+ */
++ (NSError *)unexpectedErrorResponseWithDeserializedResponse:(id)deserializedResponse;
+
+/** @fn unexpectedResponseWithData:underlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeUnexpectedResponse
+ code, and a populated @c FIRAuthErrorUserInfoDataKey key in the @c NSError.userInfo
+ dictionary.
+ @param data The value of the @c FIRAuthErrorUserInfoDataKey key in the @c NSError.userInfo
+ dictionary.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key in the @c NSError.userInfo
+ dictionary.
+ @remarks This error is used when a network request is apparently successful, but the body data
+ couldn't be deserialized as JSON.
+ */
++ (NSError *)unexpectedResponseWithData:(NSData *)data
+ underlyingError:(NSError *)underlyingError;;
+
+/** @fn unexpectedResponseWithDeserializedResponse:
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeUnexpectedResponse
+ code, and a populated @c FIRAuthErrorUserInfoDeserializedResponseKey key in the
+ @c NSError.userInfo dictionary.
+ @param deserializedResponse The value of the @c FIRAuthErrorUserInfoDeserializedResponseKey key.
+ @remarks This error is used when a network request is apparently successful, the body data was
+ successfully deserialized as JSON, but the JSON wasn't a dictionary.
+ */
++ (NSError *)unexpectedResponseWithDeserializedResponse:(id)deserializedResponse;
+
+/** @fn unexpectedResponseWithDeserializedResponse:underlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeUnexpectedResponse
+ code, and populated @c FIRAuthErrorUserInfoDeserializedResponseKey and
+ @c NSUnderlyingErrorKey keys in the @c NSError.userInfo dictionary.
+ @param deserializedResponse The value of the @c FIRAuthErrorUserInfoDeserializedResponseKey key.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key.
+ @remarks This error is used when a network request was apparently successful, the body data was
+ successfully deserialized as JSON, but the data type of the response was unexpected.
+ */
++ (NSError *)unexpectedResponseWithDeserializedResponse:(nullable id)deserializedResponse
+ underlyingError:(NSError *)underlyingError;
+
+/** @fn RPCResponseDecodingErrorWithDeserializedResponse:underlyingError:
+ @brief Constructs an @c NSError with the @c FIRAuthInternalErrorCodeRPCResponseDecodingError
+ code, and populated @c FIRAuthErrorUserInfoDeserializedResponseKey and
+ @c NSUnderlyingErrorKey keys in the @c NSError.userInfo dictionary.
+ @param deserializedResponse The value of the @c FIRAuthErrorUserInfoDeserializedResponseKey key.
+ @param underlyingError The value of the @c NSUnderlyingErrorKey key.
+ @remarks This error is used when an invocation of @c FIRAuthRPCResponse.setWithDictionary:error:
+ resulted in an error.
+ */
++ (NSError *)RPCResponseDecodingErrorWithDeserializedResponse:(id)deserializedResponse
+ underlyingError:(NSError *)underlyingError;
+
+/** @fn emailAlreadyInUseErrorWithEmail:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeEmailExists code.
+ @param email The email address that is already in use.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)emailAlreadyInUseErrorWithEmail:(nullable NSString *)email;
+
+/** @fn userDisabledErrorWithMessageWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeUserDisabled code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)userDisabledErrorWithMessage:(nullable NSString *)message;
+
+/** @fn wrongPasswordErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeWrongPassword code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)wrongPasswordErrorWithMessage:(nullable NSString *)message;
+
+/** @fn tooManyRequestsErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeTooManyRequests Code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)tooManyRequestsErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidCustomTokenErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidCustomToken code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidCustomTokenErrorWithMessage:(nullable NSString *)message;
+
+/** @fn customTokenMistmatchErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeCustomTokenMismatch code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)customTokenMistmatchErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidCredentialErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidCredential code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidCredentialErrorWithMessage:(nullable NSString *)message;
+
+/** @fn requiresRecentLoginError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeRequiresRecentLogin code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)requiresRecentLoginErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidUserTokenErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidUserToken code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidUserTokenErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidEmailErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidEmail code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidEmailErrorWithMessage:(nullable NSString *)message;
+
+/** @fn accountExistsWithDifferentCredentialErrorWithEmail:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorAccountExistsWithDifferentCredential
+ code.
+ @param Email The email address that is already associated with an existing account
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)accountExistsWithDifferentCredentialErrorWithEmail:(nullable NSString *)Email;
+
+/** @fn providerAlreadyLinkedErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeProviderAlreadyLinked code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)providerAlreadyLinkedError;
+
+/** @fn noSuchProviderError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNoSuchProvider code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)noSuchProviderError;
+
+/** @fn userTokenExpiredErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeUserTokenExpired code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)userTokenExpiredErrorWithMessage:(nullable NSString *)message;
+
+/** @fn userNotFoundErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeUserNotFound code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)userNotFoundErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidLocalAPIKeyErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidAPIKey code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidAPIKeyError;
+
+/** @fn userMismatchError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeUserMismatch code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)userMismatchError;
+
+/** @fn credentialAlreadyInUseErrorWithMessage:
+ @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.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)credentialAlreadyInUseErrorWithMessage:(nullable NSString *)message
+ credential:(nullable FIRPhoneAuthCredential *)credential;
+
+/** @fn operationNotAllowedErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeOperationNotAllowed code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)operationNotAllowedErrorWithMessage:(nullable NSString *)message;
+
+/** @fn weakPasswordErrorWithServerResponseReason:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeWeakPassword code.
+ @param serverResponseReason A more detailed explanation string from server response.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)weakPasswordErrorWithServerResponseReason:(NSString *)serverResponseReason;
+
+/** @fn appNotAuthorizedError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeAppNotAuthorized code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)appNotAuthorizedError;
+
+/** @fn expiredActionCodeErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeExpiredActionCode code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)expiredActionCodeErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidActionCodeErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidActionCode code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidActionCodeErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidMessagePayloadError:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidMessagePayload code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidMessagePayloadErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidSenderErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidSender code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidSenderErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidRecipientEmailError:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidRecipientEmail code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidRecipientEmailErrorWithMessage:(nullable NSString *)message;
+
+/** @fn missingPhoneNumberErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeMissingPhoneNumber code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)missingPhoneNumberErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidPhoneNumberErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidPhoneNumber code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidPhoneNumberErrorWithMessage:(nullable NSString *)message;
+
+/** @fn missingVerificationCodeErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeMissingVerificationCode code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)missingVerificationCodeErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidVerificationCodeErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidVerificationCode code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidVerificationCodeErrorWithMessage:(nullable NSString *)message;
+
+/** @fn missingVerificationIDErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeMissingVerificationID code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)missingVerificationIDErrorWithMessage:(nullable NSString *)message;
+
+/** @fn invalidVerificationIDErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidVerificationID code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidVerificationIDErrorWithMessage:(nullable NSString *)message;
+
+/** @fn sessionExpiredErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeSessionExpired code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)sessionExpiredErrorWithMessage:(nullable NSString *)message;
+
+/** @fn missingAppCredentialWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorMissingCredential code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)missingAppCredentialWithMessage:(nullable NSString *)message;
+
+/** @fn invalidAppCredentialWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorInvalidCredential code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)invalidAppCredentialWithMessage:(nullable NSString *)message;
+
+/** @fn quotaExceededErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeQuotaExceeded code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)quotaExceededErrorWithMessage:(nullable NSString *)message;
+
+/** @fn missingAppTokenError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeMissingAppToken code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)missingAppTokenError;
+
+/** @fn notificationNotForwardedError
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNotificationNotForwarded code.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)notificationNotForwardedError;
+
+/** @fn appNotVerifiedErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeAppNotVerified code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)appNotVerifiedErrorWithMessage:(nullable NSString *)message;
+
+/** @fn keychainErrorWithFunction:status:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeKeychainError code.
+ @param keychainFunction The keychain function which was invoked and yielded an unexpected
+ response. The @c NSLocalizedFailureReasonErrorKey field in the @c NSError.userInfo
+ dictionary will contain a string partially comprised of this value.
+ @param status The response status from the invoked keychain function. The
+ @c NSLocalizedFailureReasonErrorKey field in the @c NSError.userInfo dictionary will contain
+ a string partially comprised of this value.
+ */
++ (NSError *)keychainErrorWithFunction:(NSString *)keychainFunction status:(OSStatus)status;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthGlobalWorkQueue.h b/Firebase/Auth/Source/Private/FIRAuthGlobalWorkQueue.h
new file mode 100644
index 0000000..0950ff4
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthGlobalWorkQueue.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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @fn FIRAuthGlobalWorkQueue
+ @brief Retrieves the global serial work queue for Firebase Auth.
+ @return The global serial dispatch queue.
+ @remarks To ensure thread safety, all auth code must be executed in either this global work
+ queue, or a serial queue that has its target queue set to this work queue. All public method
+ implementations that may involve contested code shall dispatch to this work queue as the
+ first thing they do.
+ */
+extern dispatch_queue_t FIRAuthGlobalWorkQueue();
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthInternalErrors.h b/Firebase/Auth/Source/Private/FIRAuthInternalErrors.h
new file mode 100644
index 0000000..7eebbde
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthInternalErrors.h
@@ -0,0 +1,365 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthErrors.h"
+
+/** @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
+ code is @c FIRAuthErrorCodeInternalError.
+ */
+static const NSInteger FIRAuthPublicErrorCodeFlag = 1 << 20;
+
+/** @var FIRAuthInternalErrorDomain
+ @brief The Firebase Auth error domain for internal errors.
+ */
+extern NSString *const FIRAuthInternalErrorDomain;
+
+/** @var FIRAuthErrorUserInfoDeserializedResponseKey
+ @brief Errors with the code @c FIRAuthErrorCodeUnexpectedResponseError,
+ @c FIRAuthErrorCodeUnexpectedErrorResponseError, and
+ @c FIRAuthInternalErrorCodeRPCResponseDecodingError may contain an @c NSError.userInfo
+ dictionary which contains this key. The value associated with this key is an object of
+ unspecified contents containing the deserialized server response.
+ */
+extern NSString *const FIRAuthErrorUserInfoDeserializedResponseKey;
+
+/** @var FIRAuthErrorUserInfoDataKey
+ @brief Errors with the code @c FIRAuthErrorCodeUnexpectedResponseError or
+ @c FIRAuthErrorCodeUnexpectedErrorResponseError may contain an @c NSError.userInfo
+ dictionary which contains this key. The value associated with this key is an @c NSString
+ which represents the response from a server to an RPC which could not be deserialized.
+ */
+extern NSString *const FIRAuthErrorUserInfoDataKey;
+
+
+/** @var FIRAuthInternalErrorCode
+ @brief Error codes used internally by Firebase Auth.
+ @remarks All errors are generated using an internal error code. These errors are automatically
+ converted to the appropriate public version of the @c NSError by the methods in
+ @c FIRAuthErrorUtils
+ */
+typedef NS_ENUM(NSInteger, FIRAuthInternalErrorCode) {
+ /** @var FIRAuthInternalErrorCodeNetworkError
+ @brief Indicates a network error occurred (such as a timeout, interrupted connection, or
+ unreachable host.)
+ @remarks These types of errors are often recoverable with a retry.
+
+ See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details about
+ the network error which occurred.
+ */
+ FIRAuthInternalErrorCodeNetworkError = FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeNetworkError,
+
+ /** @var FIRAuthInternalErrorCodeEmailAlreadyInUse
+ @brief The email used to attempt a sign-up already exists.
+ */
+ FIRAuthInternalErrorCodeEmailAlreadyInUse =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeEmailAlreadyInUse,
+
+ /** @var FIRAuthInternalErrorCodeUserDisabled
+ @brief Indicates the user's account is disabled on the server side.
+ */
+ FIRAuthInternalErrorCodeUserDisabled = FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeUserDisabled,
+
+ /** @var FIRAuthInternalErrorCodeWrongPassword
+ @brief Indicates the user attempted sign in with a wrong password
+ */
+ FIRAuthInternalErrorCodeWrongPassword =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeWrongPassword,
+
+ /** @var FIRAuthInternalErrorCodeKeychainError
+ @brief Indicates an error occurred accessing the keychain.
+ @remarks The @c NSLocalizedFailureReasonErrorKey field in the @c NSError.userInfo dictionary
+ will contain more information about the error encountered.
+ */
+ FIRAuthInternalErrorCodeKeychainError =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeKeychainError,
+
+ /** @var FIRAuthInternalErrorCodeInternalError
+ @brief An internal error occurred.
+ @remarks This value is here for consistency. It's also used to make the implementation of
+ wrapping internal errors simpler.
+ */
+ FIRAuthInternalErrorCodeInternalError =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInternalError,
+
+ /** @var FIRAuthInternalErrorCodeTooManyRequests
+ @brief Indicates that too many requests were made to a server method.
+ */
+ FIRAuthInternalErrorCodeTooManyRequests =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeTooManyRequests,
+
+ /** @var FIRAuthInternalErrorCodeInvalidCustomToken
+ @brief Indicates a validation error with the custom token.
+ */
+ FIRAuthInternalErrorCodeInvalidCustomToken =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidCustomToken,
+
+ /** @var FIRAuthInternalErrorCodeCredentialMismatch
+ @brief Indicates the service account and the API key belong to different projects.
+ */
+ FIRAuthInternalErrorCodeCustomTokenMismatch =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeCustomTokenMismatch,
+
+ /** @var FIRAuthInternalErrorCodeInvalidCredential
+ @brief Indicates the IDP token or requestUri is invalid.
+ */
+ FIRAuthInternalErrorCodeInvalidCredential =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidCredential,
+
+ /** @var FIRAuthInternalErrorCodeRequiresRecentLogin
+ @brief Indicates the user has attemped to change email or password more than 5 minutes after
+ signing in.
+ */
+ FIRAuthInternalErrorCodeRequiresRecentLogin =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeRequiresRecentLogin,
+
+ /** @var FIRAuthInternalErrorCodeInvalidUserToken
+ @brief Indicates user's saved auth credential is invalid, the user needs to sign in again.
+ */
+ FIRAuthInternalErrorCodeInvalidUserToken =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidUserToken,
+
+ /** @var FIRAuthInternalErrorCodeInvalidEmail
+ @brief Indicates the email identifier is invalid.
+ */
+ FIRAuthInternalErrorCodeInvalidEmail =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidEmail,
+
+ /** @var FIRAuthInternalErrorCodeAccountExistsWithDifferentCredential
+ @brief Indicates account linking is needed.
+ */
+ FIRAuthInternalErrorCodeAccountExistsWithDifferentCredential =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeAccountExistsWithDifferentCredential,
+
+ /** @var FIRAuthInternalErrorCodeProviderAlreadyLinked
+ @brief Indicates an attempt to link a provider to which we are already linked.
+ */
+ FIRAuthInternalErrorCodeProviderAlreadyLinked =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeProviderAlreadyLinked,
+
+ /** @var FIRAuthInternalErrorCodeNoSuchProvider
+ @brief Indicates an attempt to unlink a provider that is not is not linked.
+ */
+ FIRAuthInternalErrorCodeNoSuchProvider =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeNoSuchProvider,
+
+ /** @var FIRAuthInternalErrorCodeUserTokenExpired
+ @brief Indicates the token issue time is older than account's valid_since time.
+ */
+ FIRAuthInternalErrorCodeUserTokenExpired =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeUserTokenExpired,
+
+ /** @var FIRAuthInternalErrorCodeUserNotFound
+ @brief Indicates the user account was been found.
+ */
+ FIRAuthInternalErrorCodeUserNotFound =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeUserNotFound,
+
+ /** @var FIRAuthInternalErrorCodeInvalidAPIKey
+ @brief Indicates an invalid API Key was supplied in the request.
+ */
+ FIRAuthInternalErrorCodeInvalidAPIKey =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidAPIKey,
+
+ /** @var FIRAuthInternalErrorCodeOperationNotAllowed
+ @brief Indicates that admin disabled sign-in with the specified IDP.
+ */
+ FIRAuthInternalErrorCodeOperationNotAllowed =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeOperationNotAllowed,
+
+ /** @var FIRAuthInternalErrorCodeUserMismatch
+ @brief Indicates that user attempted to reauthenticate with a user other than the current
+ user.
+ */
+ FIRAuthInternalErrorCodeUserMismatch =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeUserMismatch,
+
+ /** @var FIRAuthInternalErrorCodeCredentialAlreadyInUse
+ @brief Indicates an attempt to link with a credential that has already been linked with a
+ different Firebase account.
+ */
+ FIRAuthInternalErrorCodeCredentialAlreadyInUse =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeCredentialAlreadyInUse,
+
+ /** @var FIRAuthInternalErrorCodeWeakPassword
+ @brief Indicates an attempt to set a password that is considered too weak.
+ */
+ FIRAuthInternalErrorCodeWeakPassword =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeWeakPassword,
+
+ /** @var FIRAuthInternalErrorCodeAppNotAuthorized
+ @brief Indicates the App is not authorized to use Firebase Authentication with the
+ provided API Key.
+ */
+ FIRAuthInternalErrorCodeAppNotAuthorized =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeAppNotAuthorized,
+
+ /** @var FIRAuthInternalErrorCodeExpiredActionCode
+ @brief Indicates the OOB code is expired.
+ */
+ FIRAuthInternalErrorCodeExpiredActionCode =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeExpiredActionCode,
+
+ /** @var FIRAuthInternalErrorCodeInvalidActionCode
+ @brief Indicates the OOB code is invalid.
+ */
+ FIRAuthInternalErrorCodeInvalidActionCode =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidActionCode,
+
+ /** Indicates that there are invalid parameters in the payload during a "send password reset email
+ * " attempt.
+ */
+ FIRAuthInternalErrorCodeInvalidMessagePayload =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidMessagePayload,
+
+ /** Indicates that the sender email is invalid during a "send password reset email" attempt.
+ */
+ FIRAuthInternalErrorCodeInvalidSender =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidSender,
+
+ /** Indicates that the recipient email is invalid.
+ */
+ FIRAuthInternalErrorCodeInvalidRecipientEmail =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidRecipientEmail,
+
+ /** Indicates that a phone number was not provided in a call to @c verifyPhoneNumber:completion:.
+ */
+ FIRAuthInternalErrorCodeMissingPhoneNumber =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeMissingPhoneNumber,
+
+ /** Indicates that an invalid phone number was provided in a call to @c
+ verifyPhoneNumber:completion:.
+ */
+ FIRAuthInternalErrorCodeInvalidPhoneNumber =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidPhoneNumber,
+
+ /** Indicates that the phone auth credential was created with an empty verification code.
+ */
+ FIRAuthInternalErrorCodeMissingVerificationCode =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeMissingVerificationCode,
+
+ /** Indicates that an invalid verification code was used in the verifyPhoneNumber request.
+ */
+ FIRAuthInternalErrorCodeInvalidVerificationCode =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidVerificationCode,
+
+ /** Indicates that the phone auth credential was created with an empty verification ID.
+ */
+ FIRAuthInternalErrorCodeMissingVerificationID =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeMissingVerificationID,
+
+ /** Indicates that the APNS device token is missing in the verifyClient request.
+ */
+ FIRAuthInternalErrorCodeMissingAppCredential =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeMissingAppCredential,
+
+ /** Indicates that an invalid APNS device token was used in the verifyClient request.
+ */
+ FIRAuthInternalErrorCodeInvalidAppCredential =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidAppCredential,
+
+ /** Indicates that an invalid verification ID was used in the verifyPhoneNumber request.
+ */
+ FIRAuthInternalErrorCodeInvalidVerificationID =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidVerificationID,
+
+ /** Indicates that the quota of SMS messages for a given project has been exceeded.
+ */
+ FIRAuthInternalErrorCodeQuotaExceeded =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeQuotaExceeded,
+
+ // The enum values between 17046 and 17051 are reserved and should NOT be used for new error
+ // codes.
+
+ /** Indicates that the SMS code has expired
+ */
+ FIRAuthInternalErrorCodeSessionExpired =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeSessionExpired,
+
+ FIRAuthInternalErrorCodeMissingAppToken =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeMissingAppToken,
+
+ FIRAuthInternalErrorCodeNotificationNotForwarded =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeNotificationNotForwarded,
+
+ FIRAuthInternalErrorCodeAppNotVerified =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeAppNotVerified,
+
+ /** @var FIRAuthInternalErrorCodeRPCRequestEncodingError
+ @brief Indicates an error encoding the RPC request.
+ @remarks This is typically due to some sort of unexpected input value.
+
+ See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details.
+ */
+ FIRAuthInternalErrorCodeRPCRequestEncodingError = 1,
+
+ /** @var FIRAuthInternalErrorCodeJSONSerializationError
+ @brief Indicates an error serializing an RPC request.
+ @remarks This is typically due to some sort of unexpected input value.
+
+ If an @c NSJSONSerialization.isValidJSONObject: check fails, the error will contain no
+ @c NSUnderlyingError key in the @c NSError.userInfo dictionary. If an error was
+ encountered calling @c NSJSONSerialization.dataWithJSONObject:options:error:, the
+ resulting error will be associated with the @c NSUnderlyingError key in the
+ @c NSError.userInfo dictionary.
+ */
+ FIRAuthInternalErrorCodeJSONSerializationError = 2,
+
+ /** @var FIRAuthInternalErrorCodeUnexpectedErrorResponse
+ @brief Indicates an HTTP error occurred and the data returned either couldn't be deserialized
+ or couldn't be decoded.
+ @remarks See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details
+ about the HTTP error which occurred.
+
+ If the response could be deserialized as JSON then the @c NSError.userInfo dictionary will
+ contain a value for the key @c FIRAuthErrorUserInfoDeserializedResponseKey which is the
+ deserialized response value.
+
+ If the response could not be deserialized as JSON then the @c NSError.userInfo dictionary
+ will contain values for the @c NSUnderlyingErrorKey and @c FIRAuthErrorUserInfoDataKey
+ keys.
+ */
+ FIRAuthInternalErrorCodeUnexpectedErrorResponse = 3,
+
+ /** @var FIRAuthInternalErrorCodeUnexpectedResponse
+ @brief Indicates the HTTP response indicated the request was a successes, but the response
+ contains something other than a JSON-encoded dictionary, or the data type of the response
+ indicated it is different from the type of response we expected.
+ @remarks See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary.
+ If this key is present in the dictionary, it may contain an error from
+ @c NSJSONSerialization error (indicating the response received was of the wrong data
+ type).
+
+ See the @c FIRAuthErrorUserInfoDeserializedResponseKey value in the @c NSError.userInfo
+ dictionary. If the response could be deserialized, it's deserialized representation will
+ be associated with this key. If the @c NSUnderlyingError value in the @c NSError.userInfo
+ dictionary is @c nil, this indicates the JSON didn't represent a dictionary.
+ */
+ FIRAuthInternalErrorCodeUnexpectedResponse = 4,
+
+ /** @var FIRAuthInternalErrorCodeRPCResponseDecodingError
+ @brief Indicates an error decoding the RPC response.
+ This is typically due to some sort of unexpected response value from the server.
+ @remarks See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details.
+
+ See the @c FIRErrorUserInfoDecodedResponseKey value in the @c NSError.userInfo dictionary.
+ The deserialized representation of the response will be associated with this key.
+ */
+ FIRAuthInternalErrorCodeRPCResponseDecodingError = 5,
+};
diff --git a/Firebase/Auth/Source/Private/FIRAuthKeychain.h b/Firebase/Auth/Source/Private/FIRAuthKeychain.h
new file mode 100644
index 0000000..c52e26a
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthKeychain.h
@@ -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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ @brief The protocol for permanant data storage.
+ */
+@protocol FIRAuthStorage <NSObject>
+
+/** @fn initWithService:
+ @brief Initialize a @c FIRAuthStorage instance.
+ @param service The name of the storage service to use.
+ @return An initialized @c FIRAuthStorage instance for the specified service.
+ */
+- (id<FIRAuthStorage>)initWithService:(NSString *)service;
+
+/** @fn dataForKey:error:
+ @brief Gets the data for @c key in the storage. The key is set for the attribute
+ @c kSecAttrAccount of a generic password query.
+ @param key The key to use.
+ @param error The address to store any error that occurs during the process, if not NULL.
+ If the operation was successful, its content is set to @c nil .
+ @return The data stored in the storage for @c key, if any.
+ */
+- (nullable NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error;
+
+/** @fn setData:forKey:error:
+ @brief Sets the data for @c key in the storage. The key is set for the attribute
+ @c kSecAttrAccount of a generic password query.
+ @param data The data to store.
+ @param key The key to use.
+ @param error The address to store any error that occurs during the process, if not NULL.
+ @return Whether the operation succeeded or not.
+ */
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key error:(NSError **_Nullable)error;
+
+/** @fn removeDataForKey:error:
+ @brief Removes the data for @c key in the storage. The key is set for the attribute
+ @c kSecAttrAccount of a generic password query.
+ @param key The key to use.
+ @param error The address to store any error that occurs during the process, if not NULL.
+ @return Whether the operation succeeded or not.
+ */
+- (BOOL)removeDataForKey:(NSString *)key error:(NSError **_Nullable)error;
+
+@end
+
+/** @class FIRAuthKeychain
+ @brief The utility class to manipulate data in iOS Keychain.
+ */
+@interface FIRAuthKeychain : NSObject <FIRAuthStorage>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthNotificationManager.h b/Firebase/Auth/Source/Private/FIRAuthNotificationManager.h
new file mode 100644
index 0000000..42e5db8
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthNotificationManager.h
@@ -0,0 +1,71 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+@class FIRAuthAppCredentialManager;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthNotificationForwardingCallback
+ @brief The type of block to receive whether or not remote notifications are being forwarded.
+ @param isNotificationBeingForwarded Whether or not remote notifications are being forwarded.
+ */
+typedef void (^FIRAuthNotificationForwardingCallback)(BOOL isNotificationBeingForwarded);
+
+/** @class FIRAuthNotificationManager
+ */
+@interface FIRAuthNotificationManager : NSObject
+
+/** @property timeout
+ @brief The timeout for checking for notification forwarding.
+ @remarks Only tests should access this property.
+ */
+@property(nonatomic, assign) NSTimeInterval timeout;
+
+/** @fn initWithApplication:appCredentialManager:
+ @brief Initializes the instance.
+ @param application The application.
+ @param appCredentialManager The object to handle app credentials delivered via notification.
+ @return The initialized instance.
+ */
+- (instancetype)initWithApplication:(UIApplication *)application
+ appCredentialManager:(FIRAuthAppCredentialManager *)appCredentialManager
+ NS_DESIGNATED_INITIALIZER;
+
+/** @fn init
+ @brief please use initWithAppCredentialManager: instead.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn checkNotificationForwardingWithCallback:
+ @brief Checks whether or not remote notifications are being forwarded to this class.
+ @param callback The block to be called either immediately or in future once a result
+ is available.
+ */
+- (void)checkNotificationForwardingWithCallback:(FIRAuthNotificationForwardingCallback)callback;
+
+/** @fn canHandleNotification:
+ @brief Attempts to handle the remote notification.
+ @param notification The notification in question.
+ @return Whether or the notification has been handled.
+ */
+- (BOOL)canHandleNotification:(NSDictionary *)notification;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthSerialTaskQueue.h b/Firebase/Auth/Source/Private/FIRAuthSerialTaskQueue.h
new file mode 100644
index 0000000..cdae046
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthSerialTaskQueue.h
@@ -0,0 +1,50 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthSerialTaskCompletionBlock
+ @brief The type of method a @c FIRAuthSerialTask must call when it is complete.
+ */
+typedef void (^FIRAuthSerialTaskCompletionBlock)(void);
+
+/** @typedef FIRAuthSerialTask
+ @brief Represents a unit of work submitted to a task queue.
+ @param complete The task MUST call the complete method when done.
+ */
+typedef void (^FIRAuthSerialTask)(FIRAuthSerialTaskCompletionBlock complete);
+
+/** @class FIRAuthSerialTaskQueue
+ @brief An easy to use serial task queue which supports a callback-based completion notification
+ system for easy asyncronous call chaining.
+ */
+@interface FIRAuthSerialTaskQueue : NSObject
+
+/** @fn enqueueTask:
+ @brief Enqueues a task for serial execution in the queue.
+ @remarks The task MUST call the complete method when done. This method is thread-safe.
+ The task block won't be executed concurrently with any other blocks in other task queues or
+ the global work queue as returned by @c FIRAuthGlobalWorkQueue , but an uncompleted task
+ (e.g. task block finished executation before complete method is called at a later time)
+ does not affect other task queues or the global work queue.
+ */
+- (void)enqueueTask:(FIRAuthSerialTask)task;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRAuthUserDefaultsStorage.h b/Firebase/Auth/Source/Private/FIRAuthUserDefaultsStorage.h
new file mode 100644
index 0000000..13774ab
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuthUserDefaultsStorage.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 <Foundation/Foundation.h>
+
+// This class is only available in the simulator.
+#if TARGET_OS_SIMULATOR
+#ifndef FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+#define FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE 1
+#endif
+#endif
+
+#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
+
+#import "FIRAuthKeychain.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthUserDefaultsStorage
+ @brief The utility class to storage data in NSUserDefaults.
+ */
+@interface FIRAuthUserDefaultsStorage : NSObject <FIRAuthStorage>
+
+/** @fn clear
+ @brief Clears all data from the storage.
+ @remarks This method is only supposed to be called from tests.
+ */
+- (void)clear;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE
diff --git a/Firebase/Auth/Source/Private/FIRAuth_Internal.h b/Firebase/Auth/Source/Private/FIRAuth_Internal.h
new file mode 100644
index 0000000..bdbefce
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRAuth_Internal.h
@@ -0,0 +1,123 @@
+/*
+ * 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 "FIRAuth.h"
+
+@class FIRAuthAPNSTokenManager;
+@class FIRAuthAppCredentialManager;
+@class FIRAuthNotificationManager;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var FIRAuthStateDidChangeInternalNotification
+ @brief The name of the @c NSNotificationCenter notification which is posted when the auth state
+ changes (e.g. a new token has been produced, a user logs in or out). The object parameter of
+ the notification is a dictionary possibly containing the key:
+ @c FIRAuthStateDidChangeInternalNotificationTokenKey (the new access token.) If it does not
+ contain this key it indicates a sign-out event took place.
+ */
+extern NSString *const FIRAuthStateDidChangeInternalNotification;
+
+/** @var FIRAuthStateDidChangeInternalNotificationTokenKey
+ @brief A key present in the dictionary object parameter of the
+ @c FIRAuthStateDidChangeInternalNotification notification. The value associated with this
+ key will contain the new access token.
+ */
+extern NSString *const FIRAuthStateDidChangeInternalNotificationTokenKey;
+
+@interface FIRAuth ()
+
+/** @property APIKey
+ @brief The Google API key.
+ @remarks Needed for calls to identity toolkit and secure token service.
+ */
+@property(nonatomic, copy, readonly) NSString *APIKey;
+
+/** @property tokenManager
+ @brief The manager for APNs tokens used by phone number auth.
+ */
+@property(nonatomic, strong, readonly) FIRAuthAPNSTokenManager *tokenManager;
+
+/** @property appCredentailManager
+ @brief The manager for app credentials used by phone number auth.
+ */
+@property(nonatomic, strong, readonly) FIRAuthAppCredentialManager *appCredentialManager;
+
+/** @property notificationManager
+ @brief The manager for remote notifications used by phone number auth.
+ */
+@property(nonatomic, strong, readonly) FIRAuthNotificationManager *notificationManager;
+
+/** @fn initWithAPIKey:appName:
+ @brief Designated initializer.
+ @param APIKey The Google Developers Console API key for making requests from your app.
+ @param appName The name property of the previously created @c FIRApp instance.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ appName:(NSString *)appName NS_DESIGNATED_INITIALIZER;
+
+/** @fn notifyListenersOfAuthStateChange
+ @brief Posts the @c FIRAuthStateDidChangeNotification notification.
+ @remarks Called by @c FIRUser when token changes occur.
+ @param user The user whose tokens changed.
+ @param token The new access token associated with the user.
+ */
+- (void)notifyListenersOfAuthStateChangeWithUser:(nullable FIRUser *)user
+ token:(nullable NSString *)token;
+
+/** @fn updateKeychainWithUser:error:
+ @brief Updates the keychain for the given user.
+ @param user The user to be updated.
+ @param error The error caused the method to fail if the method returns NO.
+ @return Whether updating keychain has succeeded or not.
+ @remarks Called by @c FIRUser when user info or token changes occur.
+ */
+- (BOOL)updateKeychainWithUser:(FIRUser *)user error:(NSError *_Nullable *_Nullable)error;
+
+/** @fn internalSignInWithCredential:callback:
+ @brief Convenience method for @c internalSignInAndRetrieveDataWithCredential:callback:
+ This method doesn't return additional identity provider data.
+*/
+- (void)internalSignInWithCredential:(FIRAuthCredential *)credential
+ callback:(FIRAuthResultCallback)callback;
+
+/** @fn internalSignInAndRetrieveDataWithCredential:callback:
+ @brief Asynchronously signs in 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
+ identity provider data.
+ @param credential The credential supplied by the IdP.
+ @param isReauthentication Indicates whether or not the current invocation originated from an
+ attempt to reauthenticate.
+ @param callback A block which is invoked when the sign in finishes (or is cancelled.) Invoked
+ asynchronously on the auth global work queue in the future.
+ @remarks This is the internal counterpart of this method, which uses a callback that does not
+ update the current user.
+ */
+- (void)internalSignInAndRetrieveDataWithCredential:(FIRAuthCredential *)credential
+ isReauthentication:(BOOL)isReauthentication
+ callback:(nullable FIRAuthDataResultCallback)callback;
+
+/** @fn signOutByForceWithUserID:error:
+ @brief Signs out the current user.
+ @param userID The ID of the user to force sign out.
+ @param error An optional out parameter for error results.
+ @return @YES when the sign out request was successful. @NO otherwise.
+ */
+- (BOOL)signOutByForceWithUserID:(NSString *)userID error:(NSError *_Nullable *_Nullable)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Private/FIRUser_Internal.h b/Firebase/Auth/Source/Private/FIRUser_Internal.h
new file mode 100644
index 0000000..1447607
--- /dev/null
+++ b/Firebase/Auth/Source/Private/FIRUser_Internal.h
@@ -0,0 +1,80 @@
+/*
+ * 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 "FIRUser.h"
+
+@class FIRAuth;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRRetrieveUserCallback
+ @brief The type of block that is invoked when the construction of a user succeeds or fails.
+ @param user The user that was constructed, or nil if user construction failed.
+ @param error The error which occurred, or nil if the request was successful.
+ */
+typedef void(^FIRRetrieveUserCallback)(FIRUser *_Nullable user, NSError *_Nullable error);
+
+@interface FIRUser () <NSSecureCoding>
+
+/** @property rawAccessToken
+ @brief The cached access token.
+ @remarks This method is specifically for providing the access token to internal clients during
+ deserialization and sign-in events, and should not be used to retrieve the access token by
+ anyone else.
+ */
+@property(nonatomic, copy, readonly) NSString *rawAccessToken;
+
+/** @property auth
+ @brief A weak reference to a FIRAuth instance which is used to notify auth of token changes.
+ */
+@property(nonatomic, weak) FIRAuth *auth;
+
+/** @var accessTokenExpirationDate
+ @brief The expiration date of the cached access token.
+ */
+@property(nonatomic, copy, readonly) NSDate *accessTokenExpirationDate;
+
+/** @fn retrieveUserWithAPIKey:accessToken:accessTokenExpirationDate:refreshToken:callback:
+ @brief Constructs a user with Secure Token Service tokens, and obtains user details from the
+ getAccountInfo endpoint.
+ @param APIKey The client API key for making RPCs.
+ @param accessToken The Secure Token Service access token.
+ @param accessTokenExpirationDate The approximate expiration date of the access token.
+ @param refreshToken The Secure Token Service refresh token.
+ @param anonymous Whether or not the user is anonymous.
+ @param callback A block which is invoked when the construction succeeds or fails. Invoked
+ asynchronously on the auth global work queue in the future.
+ */
++ (void)retrieveUserWithAPIKey:(NSString *)APIKey
+ accessToken:(NSString *)accessToken
+ accessTokenExpirationDate:(NSDate *)accessTokenExpirationDate
+ refreshToken:(NSString *)refreshToken
+ anonymous:(BOOL)anonymous
+ callback:(FIRRetrieveUserCallback)callback;
+
+/** @fn internalGetTokenForcingRefresh:callback:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
+ @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason
+ other than an expiration.
+ @param callback The block to invoke when the token is available. Invoked asynchronously on the
+ global work thread in the future.
+ */
+- (void)internalGetTokenForcingRefresh:(BOOL)forceRefresh
+ callback:(nonnull FIRAuthTokenCallback)callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRAuthBackend.h b/Firebase/Auth/Source/RPCs/FIRAuthBackend.h
new file mode 100644
index 0000000..519a6e7
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRAuthBackend.h
@@ -0,0 +1,496 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FIRCreateAuthURIRequest;
+@class FIRCreateAuthURIResponse;
+@class FIRGetAccountInfoRequest;
+@class FIRGetAccountInfoResponse;
+@class FIRGetOOBConfirmationCodeRequest;
+@class FIRGetOOBConfirmationCodeResponse;
+@class FIRResetPasswordRequest;
+@class FIRResetPasswordResponse;
+@class FIRSecureTokenRequest;
+@class FIRSecureTokenResponse;
+@class FIRSetAccountInfoRequest;
+@class FIRSetAccountInfoResponse;
+@class FIRVerifyAssertionRequest;
+@class FIRVerifyAssertionResponse;
+@class FIRVerifyClientRequest;
+@class FIRVerifyClientResponse;
+@class FIRVerifyCustomTokenRequest;
+@class FIRVerifyCustomTokenResponse;
+@class FIRVerifyPasswordRequest;
+@class FIRVerifyPasswordResponse;
+@class FIRVerifyPhoneNumberRequest;
+@class FIRVerifyPhoneNumberResponse;
+@class FIRSendVerificationCodeRequest;
+@class FIRSendVerificationCodeResponse;
+@class FIRSignUpNewUserRequest;
+@class FIRSignUpNewUserResponse;
+@class FIRDeleteAccountRequest;
+@class FIRDeleteAccountResponse;
+@protocol FIRAuthBackendImplementation;
+@protocol FIRAuthBackendRPCIssuer;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRAuthBackendRPCIssuerCompletionHandler
+ @brief The type of block used to return the result of a call to an endpoint.
+ @param data The HTTP response body.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRAuthBackendRPCIssuerCompletionHandler)(NSData *_Nullable data,
+ NSError *_Nullable error);
+
+/** @typedef FIRCreateAuthURIResponseCallback
+ @brief The type of block used to return the result of a call to the createAuthURI
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRCreateAuthURIResponseCallback)
+ (FIRCreateAuthURIResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRGetAccountInfoResponseCallback
+ @brief The type of block used to return the result of a call to the getAccountInfo
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRGetAccountInfoResponseCallback)
+ (FIRGetAccountInfoResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRSetAccountInfoResponseCallback
+ @brief The type of block used to return the result of a call to the setAccountInfo
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRSetAccountInfoResponseCallback)
+ (FIRSetAccountInfoResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRSecureTokenResponseCallback
+ @brief The type of block used to return the result of a call to the token endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRSecureTokenResponseCallback)
+ (FIRSecureTokenResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRVerifyAssertionResponseCallback
+ @brief The type of block used to return the result of a call to the verifyAssertion
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRVerifyAssertionResponseCallback)
+ (FIRVerifyAssertionResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRVerifyPasswordResponseCallback
+ @brief The type of block used to return the result of a call to the verifyPassword
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRVerifyPasswordResponseCallback)
+ (FIRVerifyPasswordResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRVerifyCustomTokenResponseCallback
+ @brief The type of block used to return the result of a call to the verifyCustomToken
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRVerifyCustomTokenResponseCallback)
+ (FIRVerifyCustomTokenResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRDeleteCallBack
+ @brief The type of block called when a request delete account has finished.
+ @param error The error which occurred, or nil if the request was successful.
+ */
+typedef void (^FIRDeleteCallBack)(NSError *_Nullable error);
+
+/** @typedef FIRGetOOBConfirmationCodeResponseCallback
+ @brief The type of block used to return the result of a call to the getOOBConfirmationCode
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRGetOOBConfirmationCodeResponseCallback)
+ (FIRGetOOBConfirmationCodeResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRSignupNewUserCallback
+ @brief The type of block used to return the result of a call to the signupNewUser endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRSignupNewUserCallback)
+ (FIRSignUpNewUserResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRResetPasswordCallback
+ @brief The type of block used to return the result of a call to the resetPassword endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRResetPasswordCallback)
+ (FIRResetPasswordResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRSendVerificationCodeResponseCallback
+ @brief The type of block used to return the result of a call to the sendVerificationCode
+ endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRSendVerificationCodeResponseCallback)
+ (FIRSendVerificationCodeResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRVerifyPhoneNumberResponseCallback
+ @brief The type of block used to return the result of a call to the verifyPhoneNumber endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRVerifyPhoneNumberResponseCallback)
+ (FIRVerifyPhoneNumberResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRVerifyClientResponseCallback
+ @brief The type of block used to return the result of a call to the verifyClient endpoint.
+ @param response The received response, if any.
+ @param error The error which occurred, if any.
+ @remarks One of response or error will be non-nil.
+ */
+typedef void (^FIRVerifyClientResponseCallback)
+ (FIRVerifyClientResponse *_Nullable response, NSError *_Nullable error);
+
+/** @class FIRAuthBackend
+ @brief Simple static class with methods representing the backend RPCs.
+ @remarks All callback blocks passed as method parameters are invoked asynchronously on the
+ global work queue in the future. See
+ https://github.com/firebase/firebase-ios-sdk/tree/master/Firebase/Auth/Docs/threading.ml
+ */
+@interface FIRAuthBackend : NSObject
+
+/** @fn setBackendImplementation:
+ @brief Changes the default backend implementation to something else.
+ @param backendImplementation The backend implementation to use.
+ @remarks This is not, generally, safe to call in a scenario where other backend requests may
+ be occuring. This is specifically to help mock the backend for testing purposes.
+ */
++ (void)setBackendImplementation:(id<FIRAuthBackendImplementation>)backendImplementation;
+
+/** @fn setDefaultBackendImplementationWithRPCIssuer:
+ @brief Uses the default backend implementation, but with a custom RPC issuer.
+ @param RPCIssuer The RPC issuer to use. If @c nil, will use the default implementation.
+ @remarks This is not, generally, safe to call in a scenario where other backend requests may
+ be occuring. This is specifically to help test the backend interfaces (requests, responses,
+ and shared FIRAuthBackend logic.)
+ */
++ (void)setDefaultBackendImplementationWithRPCIssuer:
+ (nullable id<FIRAuthBackendRPCIssuer>)RPCIssuer;
+
+/** @fn createAuthURI:callback:
+ @brief Calls the createAuthURI endpoint, which is responsible for creating the URI used by the
+ IdP to authenticate the user.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)createAuthURI:(FIRCreateAuthURIRequest *)request
+ callback:(FIRCreateAuthURIResponseCallback)callback;
+
+/** @fn getAccountInfo:callback:
+ @brief Calls the getAccountInfo endpoint, which returns account info for a given account.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)getAccountInfo:(FIRGetAccountInfoRequest *)request
+ callback:(FIRGetAccountInfoResponseCallback)callback;
+
+/** @fn setAccountInfo:callback:
+ @brief Calls the setAccountInfo endpoint, which is responsible for setting account info for a
+ user, for example, to sign up a new user with email and password.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)setAccountInfo:(FIRSetAccountInfoRequest *)request
+ callback:(FIRSetAccountInfoResponseCallback)callback;
+
+/** @fn verifyAssertion:callback:
+ @brief Calls the verifyAssertion endpoint, which is responsible for authenticating a
+ user who has IDP-related credentials (an ID Token, an Access Token, etc.)
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)verifyAssertion:(FIRVerifyAssertionRequest *)request
+ callback:(FIRVerifyAssertionResponseCallback)callback;
+
+/** @fn verifyCustomToken:callback:
+ @brief Calls the verifyCustomToken endpoint, which is responsible for authenticating a
+ user who has BYOAuth credentials (a self-signed token using their BYOAuth private key.)
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)verifyCustomToken:(FIRVerifyCustomTokenRequest *)request
+ callback:(FIRVerifyCustomTokenResponseCallback)callback;
+
+/** @fn verifyPassword:callback:
+ @brief Calls the verifyPassword endpoint, which is responsible for authenticating a
+ user who has email and password credentials.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)verifyPassword:(FIRVerifyPasswordRequest *)request
+ callback:(FIRVerifyPasswordResponseCallback)callback;
+
+/** @fn secureToken:callback:
+ @brief Calls the token endpoint, which is responsible for performing STS token exchanges and
+ token refreshes.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)secureToken:(FIRSecureTokenRequest *)request
+ callback:(FIRSecureTokenResponseCallback)callback;
+
+/** @fn getOOBConfirmationCode:callback:
+ @brief Calls the getOOBConfirmationCode endpoint, which is responsible for sending email change
+ request emails, and password reset emails.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)getOOBConfirmationCode:(FIRGetOOBConfirmationCodeRequest *)request
+ callback:(FIRGetOOBConfirmationCodeResponseCallback)callback;
+
+/** @fn signUpNewUser:
+ @brief Calls the signUpNewUser endpoint, which is responsible anonymously signing up a user
+ or signing in a user anonymously.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)signUpNewUser:(FIRSignUpNewUserRequest *)request
+ callback:(FIRSignupNewUserCallback)callback;
+
+/** @fn resetPassword:callback
+ @brief Calls the resetPassword endpoint, which is responsible for resetting a user's password
+ given an OOB code and new password.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)resetPassword:(FIRResetPasswordRequest *)request
+ callback:(FIRResetPasswordCallback)callback;
+
+/** @fn deleteAccount:
+ @brief Calls the DeleteAccount endpoint, which is responsible for deleting a user.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)deleteAccount:(FIRDeleteAccountRequest *)request
+ callback:(FIRDeleteCallBack)callback;
+
+/** @fn sendVerificationCode:callback:
+ @brief Calls the sendVerificationCode endpoint, which is responsible for sending the
+ verification code to a phone number specified in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)sendVerificationCode:(FIRSendVerificationCodeRequest *)request
+ callback:(FIRSendVerificationCodeResponseCallback)callback;
+
+/** @fn verifyPhoneNumber:callback:
+ @brief Calls the verifyPhoneNumber endpoint, which is responsible for sending the verification
+ code to a phone number specified in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)verifyPhoneNumber:(FIRVerifyPhoneNumberRequest *)request
+ callback:(FIRVerifyPhoneNumberResponseCallback)callback;
+
+/** @fn verifyClient:callback:
+ @brief Calls the verifyClient endpoint, which is responsible for sending the silent push
+ notification used for app validation to the device provided in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
++ (void)verifyClient:(FIRVerifyClientRequest *)request
+ callback:(FIRVerifyClientResponseCallback)callback;
+
+@end
+
+/** @protocol FIRAuthBackendRPCIssuer
+ @brief Used to make FIRAuthBackend
+ */
+@protocol FIRAuthBackendRPCIssuer <NSObject>
+
+/** @fn asyncPostToURL:body:contentType:completionHandler:
+ @brief Asynchronously sends a POST request.
+ @param URL URL of the request.
+ @param body Request body.
+ @param contentType Content type of the body.
+ @param handler provided that handles POST response. Invoked asynchronously on the auth global
+ work queue in the future.
+ */
+- (void)asyncPostToURL:(NSURL *)URL
+ body:(NSData *)body
+ contentType:(NSString *)contentType
+ completionHandler:(FIRAuthBackendRPCIssuerCompletionHandler)handler;
+
+@end
+
+/** @protocol FIRAuthBackendImplementation
+ @brief Used to make FIRAuthBackend provide a layer of indirection to an actual RPC-based backend
+ or a mock backend.
+ */
+@protocol FIRAuthBackendImplementation <NSObject>
+
+/** @fn createAuthURI:callback:
+ @brief Calls the createAuthURI endpoint, which is responsible for creating the URI used by the
+ IdP to authenticate the user.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)createAuthURI:(FIRCreateAuthURIRequest *)request
+ callback:(FIRCreateAuthURIResponseCallback)callback;
+
+/** @fn getAccountInfo:callback:
+ @brief Calls the getAccountInfo endpoint, which returns account info for a given account.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)getAccountInfo:(FIRGetAccountInfoRequest *)request
+ callback:(FIRGetAccountInfoResponseCallback)callback;
+
+/** @fn setAccountInfo:callback:
+ @brief Calls the setAccountInfo endpoint, which is responsible for setting account info for a
+ user, for example, to sign up a new user with email and password.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)setAccountInfo:(FIRSetAccountInfoRequest *)request
+ callback:(FIRSetAccountInfoResponseCallback)callback;
+
+/** @fn verifyAssertion:callback:
+ @brief Calls the verifyAssertion endpoint, which is responsible for authenticating a
+ user who has IDP-related credentials (an ID Token, an Access Token, etc.)
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)verifyAssertion:(FIRVerifyAssertionRequest *)request
+ callback:(FIRVerifyAssertionResponseCallback)callback;
+
+/** @fn verifyCustomToken:callback:
+ @brief Calls the verifyCustomToken endpoint, which is responsible for authenticating a
+ user who has BYOAuth credentials (a self-signed token using their BYOAuth private key.)
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)verifyCustomToken:(FIRVerifyCustomTokenRequest *)request
+ callback:(FIRVerifyCustomTokenResponseCallback)callback;
+
+/** @fn verifyPassword:callback:
+ @brief Calls the verifyPassword endpoint, which is responsible for authenticating a
+ user who has email and password credentials.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)verifyPassword:(FIRVerifyPasswordRequest *)request
+ callback:(FIRVerifyPasswordResponseCallback)callback;
+
+/** @fn secureToken:callback:
+ @brief Calls the token endpoint, which is responsible for performing STS token exchanges and
+ token refreshes.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)secureToken:(FIRSecureTokenRequest *)request
+ callback:(FIRSecureTokenResponseCallback)callback;
+
+/** @fn getOOBConfirmationCode:callback:
+ @brief Calls the getOOBConfirmationCode endpoint, which is responsible for sending email change
+ request emails, and password reset emails.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)getOOBConfirmationCode:(FIRGetOOBConfirmationCodeRequest *)request
+ callback:(FIRGetOOBConfirmationCodeResponseCallback)callback;
+
+/** @fn signUpNewUser:
+ @brief Calls the signUpNewUser endpoint, which is responsible anonymously signing up a user
+ or signing in a user anonymously.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)signUpNewUser:(FIRSignUpNewUserRequest *)request
+ callback:(FIRSignupNewUserCallback)callback;
+
+/** @fn deleteAccount:
+ @brief Calls the DeleteAccount endpoint, which is responsible for deleting a user.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)deleteAccount:(FIRDeleteAccountRequest *)request
+ callback:(FIRDeleteCallBack)callback;
+
+/** @fn sendVerificationCode:callback:
+ @brief Calls the sendVerificationCode endpoint, which is responsible for sending the
+ verification code to a phone number specified in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)sendVerificationCode:(FIRSendVerificationCodeRequest *)request
+ callback:(FIRSendVerificationCodeResponseCallback)callback;
+
+/** @fn verifyPhoneNumber:callback:
+ @brief Calls the verifyPhoneNumber endpoint, which is responsible for sending the verification
+ code to a phone number specified in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)verifyPhoneNumber:(FIRVerifyPhoneNumberRequest *)request
+ callback:(FIRVerifyPhoneNumberResponseCallback)callback;
+
+/** @fn verifyClient:callback:
+ @brief Calls the verifyClient endpoint, which is responsible for sending the silent push
+ notification used for app validation to the device provided in the request parameters.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)verifyClient:(FIRVerifyClientRequest *)request
+ callback:(FIRVerifyClientResponseCallback)callback;
+
+/** @fn resetPassword:callback
+ @brief Calls the resetPassword endpoint, which is responsible for resetting a user's password
+ given an OOB code and new password.
+ @param request The request parameters.
+ @param callback The callback.
+ */
+- (void)resetPassword:(FIRResetPasswordRequest *)request
+ callback:(FIRResetPasswordCallback)callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRAuthBackend.m b/Firebase/Auth/Source/RPCs/FIRAuthBackend.m
new file mode 100644
index 0000000..3efc602
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRAuthBackend.m
@@ -0,0 +1,943 @@
+/*
+ * 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 "FIRAuthBackend.h"
+
+#import "../AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h"
+#import "../AuthProviders/Phone/FIRPhoneAuthProvider.h"
+#import "../Private/FIRAuthErrorUtils.h"
+#import "../Private/FIRAuthGlobalWorkQueue.h"
+#import "FirebaseAuth.h"
+#import "FIRAuthRPCRequest.h"
+#import "FIRAuthRPCResponse.h"
+#import "FIRCreateAuthURIRequest.h"
+#import "FIRCreateAuthURIResponse.h"
+#import "FIRDeleteAccountRequest.h"
+#import "FIRDeleteAccountResponse.h"
+#import "FIRGetAccountInfoRequest.h"
+#import "FIRGetAccountInfoResponse.h"
+#import "FIRGetOOBConfirmationCodeRequest.h"
+#import "FIRGetOOBConfirmationCodeResponse.h"
+#import "FIRResetPasswordRequest.h"
+#import "FIRResetPasswordResponse.h"
+#import "FIRSendVerificationCodeRequest.h"
+#import "FIRSendVerificationCodeResponse.h"
+#import "FIRSecureTokenRequest.h"
+#import "FIRSecureTokenResponse.h"
+#import "FIRSetAccountInfoRequest.h"
+#import "FIRSetAccountInfoResponse.h"
+#import "FIRSignUpNewUserRequest.h"
+#import "FIRSignUpNewUserResponse.h"
+#import "FIRVerifyAssertionRequest.h"
+#import "FIRVerifyAssertionResponse.h"
+#import "FIRVerifyClientRequest.h"
+#import "FIRVerifyClientResponse.h"
+#import "FIRVerifyCustomTokenRequest.h"
+#import "FIRVerifyCustomTokenResponse.h"
+#import "FIRVerifyPasswordRequest.h"
+#import "FIRVerifyPasswordResponse.h"
+#import "FIRVerifyPhoneNumberRequest.h"
+#import "FIRVerifyPhoneNumberResponse.h"
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import <GTMSessionFetcher/GTMSessionFetcherService.h>
+
+/** @var kIosBundleIdentifierHeader
+ @brief HTTP header name for iOS bundle ID.
+ */
+static NSString *const kIosBundleIdentifierHeader = @"X-Ios-Bundle-Identifier";
+
+/** @var kJSONContentType
+ @brief The value of the HTTP content-type header for JSON payloads.
+ */
+static NSString *const kJSONContentType = @"application/json";
+
+/** @var kErrorDataKey
+ @brief Key for error data in NSError returned by @c GTMSessionFetcher.
+ */
+static NSString * const kErrorDataKey = @"data";
+
+/** @var kErrorKey
+ @brief The key for the "error" value in JSON responses from the server.
+ */
+static NSString *const kErrorKey = @"error";
+
+/** @var kErrorsKey
+ @brief The key for the "errors" value in JSON responses from the server.
+ */
+static NSString *const kErrorsKey = @"errors";
+
+/** @var kReasonKey
+ @brief The key for the "reason" value in JSON responses from the server.
+ */
+static NSString *const kReasonKey = @"reason";
+
+/** @var kInvalidKeyReasonValue
+ @brief The value for the "reason" key indicating an invalid API Key was received by the server.
+ */
+static NSString *const kInvalidKeyReasonValue = @"keyInvalid";
+
+/** @var kAppNotAuthorizedReasonValue
+ @brief The value for the "reason" key indicating the App is not authorized to use Firebase
+ Authentication.
+ */
+static NSString *const kAppNotAuthorizedReasonValue = @"ipRefererBlocked";
+
+/** @var kErrorMessageKey
+ @brief The key for an error's "message" value in JSON responses from the server.
+ */
+static NSString *const kErrorMessageKey = @"message";
+
+/** @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.
+ */
+static NSString *const kUserNotFoundErrorMessage = @"USER_NOT_FOUND";
+
+/** @var kUserDeletedErrorMessage
+ @brief This is the error message the server will respond with if the user entered an invalid
+ email address.
+ */
+static NSString *const kUserDeletedErrorMessage = @"EMAIL_NOT_FOUND";
+
+/** @var kInvalidLocalIDErrorMessage
+ @brief This is the error message the server responds with if the user local id in the id token
+ does not exit.
+ */
+static NSString *const kInvalidLocalIDErrorMessage = @"INVALID_LOCAL_ID";
+
+/** @var kUserTokenExpiredErrorMessage
+ @brief The error returned by the server if the token issue time is older than the account's
+ valid_since time.
+ */
+static NSString *const kUserTokenExpiredErrorMessage = @"TOKEN_EXPIRED";
+
+/** @var kTooManyRequestsErrorMessage
+ @brief This is the error message the server will respond with if too many requests were made to
+ a server method.
+ */
+static NSString *const kTooManyRequestsErrorMessage = @"TOO_MANY_ATTEMPTS_TRY_LATER";
+
+/** @var kInvalidTokenCustomErrorMessage
+ @brief This is the error message the server will respond with if there is a validation error
+ with the custom token.
+ */
+static NSString *const kInvalidCustomTokenErrorMessage = @"INVALID_CUSTOM_TOKEN";
+
+/** @var kCustomTokenMismatch
+ @brief This is the error message the server will respond with if the service account and API key
+ belong to different projects.
+ */
+static NSString *const kCustomTokenMismatch = @"CREDENTIAL_MISMATCH";
+
+/** @var kInvalidCredentialErrorMessage
+ @brief This is the error message the server responds with if the IDP token or requestUri is
+ invalid.
+ */
+static NSString *const kInvalidCredentialErrorMessage = @"INVALID_IDP_RESPONSE";
+
+/** @var kUserDisabledErrorMessage
+ @brief The error returned by the server if the user account is diabled.
+ */
+static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED";
+
+/** @var kOperationNotAllowedErrorMessage
+ @brief This is the error message the server will respond with if Admin disables IDP specified by
+ provider.
+ */
+static NSString *const kOperationNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED";
+
+/** @var kPasswordLoginDisabledErrorMessage
+ @brief This is the error message the server responds with if password login is disabled.
+ */
+static NSString *const kPasswordLoginDisabledErrorMessage = @"PASSWORD_LOGIN_DISABLED";
+
+/** @var kEmailAlreadyInUseErrorMessage
+ @brief This is the error message the server responds with if the email address already exists.
+ */
+static NSString *const kEmailAlreadyInUseErrorMessage = @"EMAIL_EXISTS";
+
+/** @var kInvalidEmailErrorMessage
+ @brief The error returned by the server if the email is invalid.
+ */
+static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL";
+
+/** @var kInvalidIdentifierErrorMessage
+ @brief The error returned by the server if the identifier is invalid.
+ */
+static NSString *const kInvalidIdentifierErrorMessage = @"INVALID_IDENTIFIER";
+
+/** @var kWrongPasswordErrorMessage
+ @brief This is the error message the server will respond with if the user entered a wrong
+ password.
+ */
+static NSString *const kWrongPasswordErrorMessage = @"INVALID_PASSWORD";
+
+/** @var kCredentialTooOldErrorMessage
+ @brief This is the error message the server responds with if account change is attempted 5
+ minutes after signing in.
+ */
+static NSString *const kCredentialTooOldErrorMessage = @"CREDENTIAL_TOO_OLD_LOGIN_AGAIN";
+
+/** @var kFederatedUserIDAlreadyLinkedMessage
+ @brief This is the error message the server will respond with if the federated user ID has been
+ already linked with another account.
+ */
+static NSString *const kFederatedUserIDAlreadyLinkedMessage = @"FEDERATED_USER_ID_ALREADY_LINKED";
+
+/** @var kInvalidUserTokenErrorMessage
+ @brief This is the error message the server responds with if user's saved auth credential is
+ invalid, and the user needs to sign in again.
+ */
+static NSString *const kInvalidUserTokenErrorMessage = @"INVALID_ID_TOKEN";
+
+/** @var kWeakPasswordErrorMessagePrefix
+ @brief This is the prefix for the error message the server responds with if user's new password
+ to be set is too weak.
+ */
+static NSString *const kWeakPasswordErrorMessagePrefix = @"WEAK_PASSWORD";
+
+/** @var kExpiredActionCodeErrorMessage
+ @brief This is the error message the server will respond with if the action code is expired.
+ */
+static NSString *const kExpiredActionCodeErrorMessage = @"EXPIRED_OOB_CODE";
+
+/** @var kInvalidActionCodeErrorMessage
+ @brief This is the error message the server will respond with if the action code is invalid.
+ */
+static NSString *const kInvalidActionCodeErrorMessage = @"INVALID_OOB_CODE";
+
+/** @var kInvalidSenderEmailErrorMessage
+ @brief This is the error message the server will respond with if the sender email is invalid
+ during a "send password reset email" attempt.
+ */
+static NSString *const kInvalidSenderEmailErrorMessage = @"INVALID_SENDER";
+
+/** @var kInvalidMessagePayloadErrorMessage
+ @brief This is the error message the server will respond with if there are invalid parameters in
+ the payload during a "send password reset email" attempt.
+ */
+static NSString *const kInvalidMessagePayloadErrorMessage = @"INVALID_MESSAGE_PAYLOAD";
+
+/** @var kInvalidRecipientEmailErrorMessage
+ @brief This is the error message the server will respond with if the recipient email is invalid.
+ */
+static NSString *const kInvalidRecipientEmailErrorMessage = @"INVALID_RECIPIENT_EMAIL";
+
+/** @var kInvalidPhoneNumberErrorMessage
+ @brief This is the error message the server will respond with if an incorrectly formatted phone
+ number is provided.
+ */
+static NSString *const kInvalidPhoneNumberErrorMessage = @"INVALID_PHONE_NUMBER";
+
+/** @var kInvalidVerificationCodeErrorMessage
+ @brief This is the error message the server will respond with if an invalid verification code is
+ provided.
+ */
+static NSString *const kInvalidVerificationCodeErrorMessage = @"INVALID_CODE";
+
+/** @var kInvalidSessionInfoErrorMessage
+ @brief This is the error message the server will respond with if an invalid session info
+ (verification ID) is provided.
+ */
+static NSString *const kInvalidSessionInfoErrorMessage = @"INVALID_SESSION_INFO";
+
+/** @var kSessionExpiredErrorMessage
+ @brief This is the error message the server will respond with if the SMS code has expired before
+ it is used.
+ */
+static NSString *const kSessionExpiredErrorMessage = @"SESSION_EXPIRED";
+
+/** @var kMissingAppCredentialErrorMessage
+ @brief This is the error message the server will respond with if the APNS token is missing in a
+ verifyClient request.
+ */
+static NSString *const kMissingAppCredentialErrorMessage = @"MISSING_APP_CREDENTIAL";
+
+/** @var kMissingAppCredentialErrorMessage
+ @brief This is the error message the server will respond with if the APNS token in a
+ verifyClient request is invalid.
+ */
+static NSString *const kInvalidAppCredentialErrorMessage = @"INVALID_APP_CREDENTIAL";
+
+/** @var kQuoutaExceededErrorMessage
+ @brief This is the error message the server will respond with if the quota for SMS text messages
+ has been exceeded for the project.
+ */
+static NSString *const kQuoutaExceededErrorMessage = @"QUOTA_EXCEEDED";
+
+/** @var kAppNotVerifiedErrorMessage
+ @brief This is the error message the server will respond with if Firebase could not verify the
+ app during a phone authentication flow.
+ */
+static NSString *const kAppNotVerifiedErrorMessage = @"APP_NOT_VERIFIED";
+
+/** @var gBackendImplementation
+ @brief The singleton FIRAuthBackendImplementation instance to use.
+ */
+static id<FIRAuthBackendImplementation> gBackendImplementation;
+
+/** @class FIRAuthBackendRPCImplementation
+ @brief The default RPC-based backend implementation.
+ */
+@interface FIRAuthBackendRPCImplementation : NSObject <FIRAuthBackendImplementation>
+
+/** @property RPCIssuer
+ @brief An instance of FIRAuthBackendRPCIssuer for making RPC requests. Allows the RPC
+ requests/responses to be easily faked.
+ */
+@property(nonatomic, strong) id<FIRAuthBackendRPCIssuer> RPCIssuer;
+
+@end
+
+@implementation FIRAuthBackend
+
++ (id<FIRAuthBackendImplementation>)implementation {
+ if (!gBackendImplementation) {
+ gBackendImplementation = [[FIRAuthBackendRPCImplementation alloc] init];
+ }
+ return gBackendImplementation;
+}
+
++ (void)setBackendImplementation:(id<FIRAuthBackendImplementation>)backendImplementation {
+ gBackendImplementation = backendImplementation;
+}
+
++ (void)setDefaultBackendImplementationWithRPCIssuer:
+ (nullable id<FIRAuthBackendRPCIssuer>)RPCIssuer {
+ FIRAuthBackendRPCImplementation *defaultImplementation =
+ [[FIRAuthBackendRPCImplementation alloc] init];
+ if (RPCIssuer) {
+ defaultImplementation.RPCIssuer = RPCIssuer;
+ }
+ gBackendImplementation = defaultImplementation;
+}
+
++ (void)createAuthURI:(FIRCreateAuthURIRequest *)request
+ callback:(FIRCreateAuthURIResponseCallback)callback {
+ [[self implementation] createAuthURI:request callback:callback];
+}
+
++ (void)getAccountInfo:(FIRGetAccountInfoRequest *)request
+ callback:(FIRGetAccountInfoResponseCallback)callback {
+ [[self implementation] getAccountInfo:request callback:callback];
+}
+
++ (void)setAccountInfo:(FIRSetAccountInfoRequest *)request
+ callback:(FIRSetAccountInfoResponseCallback)callback {
+ [[self implementation] setAccountInfo:request callback:callback];
+}
+
++ (void)verifyAssertion:(FIRVerifyAssertionRequest *)request
+ callback:(FIRVerifyAssertionResponseCallback)callback {
+ [[self implementation] verifyAssertion:request callback:callback];
+}
+
++ (void)verifyCustomToken:(FIRVerifyCustomTokenRequest *)request
+ callback:(FIRVerifyCustomTokenResponseCallback)callback {
+ [[self implementation] verifyCustomToken:request callback:callback];
+}
+
++ (void)verifyPassword:(FIRVerifyPasswordRequest *)request
+ callback:(FIRVerifyPasswordResponseCallback)callback {
+ [[self implementation] verifyPassword:request callback:callback];
+}
+
++ (void)secureToken:(FIRSecureTokenRequest *)request
+ callback:(FIRSecureTokenResponseCallback)callback {
+ [[self implementation] secureToken:request callback:callback];
+}
+
++ (void)getOOBConfirmationCode:(FIRGetOOBConfirmationCodeRequest *)request
+ callback:(FIRGetOOBConfirmationCodeResponseCallback)callback {
+ [[self implementation] getOOBConfirmationCode:request callback:callback];
+}
+
++ (void)signUpNewUser:(FIRSignUpNewUserRequest *)request
+ callback:(FIRSignupNewUserCallback)callback {
+ [[self implementation] signUpNewUser:request callback:callback];
+}
+
++ (void)deleteAccount:(FIRDeleteAccountRequest *)request callback:(FIRDeleteCallBack)callback {
+ [[self implementation] deleteAccount:request callback:callback];
+}
+
++ (void)sendVerificationCode:(FIRSendVerificationCodeRequest *)request
+ callback:(FIRSendVerificationCodeResponseCallback)callback {
+ [[self implementation] sendVerificationCode:request callback:callback];
+}
+
++ (void)verifyPhoneNumber:(FIRVerifyPhoneNumberRequest *)request
+ callback:(FIRVerifyPhoneNumberResponseCallback)callback {
+ [[self implementation] verifyPhoneNumber:request callback:callback];
+}
+
++ (void)verifyClient:(id)request callback:(FIRVerifyClientResponseCallback)callback {
+ [[self implementation] verifyClient:request callback:callback];
+}
+
++ (void)resetPassword:(FIRResetPasswordRequest *)request
+ callback:(FIRResetPasswordCallback)callback {
+ [[self implementation] resetPassword:request callback:callback];
+}
+
+@end
+
+@interface FIRAuthBackendRPCIssuerImplementation : NSObject <FIRAuthBackendRPCIssuer>
+@end
+
+@implementation FIRAuthBackendRPCIssuerImplementation {
+ /** @var The session fetcher service.
+ */
+ GTMSessionFetcherService *_fetcherService;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _fetcherService = [[GTMSessionFetcherService alloc] init];
+ _fetcherService.userAgent = [NSString stringWithFormat:@"FirebaseAuth.iOS/%s %@",
+ FirebaseAuthVersionString, GTMFetcherStandardUserAgentString(nil)];
+ _fetcherService.callbackQueue = FIRAuthGlobalWorkQueue();
+ }
+ return self;
+}
+
+- (void)asyncPostToURL:(NSURL *)URL
+ body:(NSData *)body
+ contentType:(NSString *)contentType
+ completionHandler:(void (^)(NSData *_Nullable, NSError *_Nullable))handler {
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
+ [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
+ NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier];
+ [request setValue:bundleID forHTTPHeaderField:kIosBundleIdentifierHeader];
+
+ NSArray<NSString *> *preferredLocalizations = [NSBundle mainBundle].preferredLocalizations;
+ if (preferredLocalizations.count) {
+ NSString *acceptLanguage = preferredLocalizations.firstObject;
+ [request setValue:acceptLanguage forHTTPHeaderField:@"Accept-Language"];
+ }
+
+ GTMSessionFetcher* fetcher = [_fetcherService fetcherWithRequest:request];
+ fetcher.bodyData = body;
+ [fetcher beginFetchWithCompletionHandler:handler];
+}
+
+@end
+
+@implementation FIRAuthBackendRPCImplementation
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _RPCIssuer = [[FIRAuthBackendRPCIssuerImplementation alloc] init];
+ }
+ return self;
+}
+
+- (void)createAuthURI:(FIRCreateAuthURIRequest *)request
+ callback:(FIRCreateAuthURIResponseCallback)callback {
+ FIRCreateAuthURIResponse *response = [[FIRCreateAuthURIResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)getAccountInfo:(FIRGetAccountInfoRequest *)request
+ callback:(FIRGetAccountInfoResponseCallback)callback {
+ FIRGetAccountInfoResponse *response = [[FIRGetAccountInfoResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)setAccountInfo:(FIRSetAccountInfoRequest *)request
+ callback:(FIRSetAccountInfoResponseCallback)callback {
+ FIRSetAccountInfoResponse *response = [[FIRSetAccountInfoResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)verifyAssertion:(FIRVerifyAssertionRequest *)request
+ callback:(FIRVerifyAssertionResponseCallback)callback {
+ FIRVerifyAssertionResponse *response = [[FIRVerifyAssertionResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ callback(response, nil);
+ }];
+}
+
+- (void)verifyCustomToken:(FIRVerifyCustomTokenRequest *)request
+ callback:(FIRVerifyCustomTokenResponseCallback)callback {
+ FIRVerifyCustomTokenResponse *response = [[FIRVerifyCustomTokenResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)verifyPassword:(FIRVerifyPasswordRequest *)request
+ callback:(FIRVerifyPasswordResponseCallback)callback {
+ FIRVerifyPasswordResponse *response = [[FIRVerifyPasswordResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)secureToken:(FIRSecureTokenRequest *)request
+ callback:(FIRSecureTokenResponseCallback)callback {
+ FIRSecureTokenResponse *response = [[FIRSecureTokenResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)getOOBConfirmationCode:(FIRGetOOBConfirmationCodeRequest *)request
+ callback:(FIRGetOOBConfirmationCodeResponseCallback)callback {
+ FIRGetOOBConfirmationCodeResponse *response = [[FIRGetOOBConfirmationCodeResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)signUpNewUser:(FIRSignUpNewUserRequest *)request
+ callback:(FIRSignupNewUserCallback)callback{
+ FIRSignUpNewUserResponse *response = [[FIRSignUpNewUserResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, nil);
+ }
+ }];
+}
+
+- (void)deleteAccount:(FIRDeleteAccountRequest *)request callback:(FIRDeleteCallBack)callback {
+ FIRDeleteAccountResponse *response = [[FIRDeleteAccountResponse alloc] init];
+ [self postWithRequest:request response:response callback:callback];
+}
+
+- (void)sendVerificationCode:(FIRSendVerificationCodeRequest *)request
+ callback:(FIRSendVerificationCodeResponseCallback)callback {
+ FIRSendVerificationCodeResponse *response = [[FIRSendVerificationCodeResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ } else {
+ callback(response, error);
+ }
+ }];
+}
+
+- (void)verifyPhoneNumber:(FIRVerifyPhoneNumberRequest *)request
+ callback:(FIRVerifyPhoneNumberResponseCallback)callback {
+ FIRVerifyPhoneNumberResponse *response = [[FIRVerifyPhoneNumberResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ // Check whether or not the successful response is actually the special case phone auth flow
+ // that returns a temporary proof and phone number.
+ if (response.phoneNumber.length && response.temporaryProof.length) {
+ FIRPhoneAuthCredential *credential =
+ [[FIRPhoneAuthCredential alloc] initWithTemporaryProof:response.temporaryProof
+ phoneNumber:response.phoneNumber
+ providerID:FIRPhoneAuthProviderID];
+ callback(nil,
+ [FIRAuthErrorUtils credentialAlreadyInUseErrorWithMessage:nil
+ credential:credential]);
+ return;
+ }
+ callback(response, nil);
+ }];
+}
+
+- (void)verifyClient:(id)request callback:(FIRVerifyClientResponseCallback)callback {
+ FIRVerifyClientResponse *response = [[FIRVerifyClientResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ callback(response, nil);
+ }];
+}
+
+- (void)resetPassword:(FIRResetPasswordRequest *)request
+ callback:(FIRResetPasswordCallback)callback {
+ FIRResetPasswordResponse *response = [[FIRResetPasswordResponse alloc] init];
+ [self postWithRequest:request response:response callback:^(NSError *error) {
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ callback(response, nil);
+ }];
+}
+
+#pragma mark - Generic RPC handling methods
+
+/** @fn postWithRequest:response:callback:
+ @brief Calls the RPC using HTTP POST.
+ @remarks Possible error responses:
+ @see FIRAuthInternalErrorCodeRPCRequestEncodingError
+ @see FIRAuthInternalErrorCodeJSONSerializationError
+ @see FIRAuthInternalErrorCodeNetworkError
+ @see FIRAuthInternalErrorCodeUnexpectedErrorResponse
+ @see FIRAuthInternalErrorCodeUnexpectedResponse
+ @see FIRAuthInternalErrorCodeRPCResponseDecodingError
+ @param request The request.
+ @param response The empty response to be filled.
+ @param callback The callback for both success and failure.
+ */
+- (void)postWithRequest:(id<FIRAuthRPCRequest>)request
+ response:(id<FIRAuthRPCResponse>)response
+ callback:(void (^)(NSError *error))callback {
+ NSError *error;
+ id postBody = [request unencodedHTTPRequestBodyWithError:&error];
+ if (!postBody) {
+ callback([FIRAuthErrorUtils RPCRequestEncodingErrorWithUnderlyingError:error]);
+ return;
+ }
+ NSJSONWritingOptions JSONWritingOptions = 0;
+ #if DEBUG
+ JSONWritingOptions |= NSJSONWritingPrettyPrinted;
+ #endif
+
+ NSData *bodyData;
+ if ([NSJSONSerialization isValidJSONObject:postBody]) {
+ bodyData = [NSJSONSerialization dataWithJSONObject:postBody
+ options:JSONWritingOptions
+ error:&error];
+ if (!bodyData) {
+ // This is an untested case. This happens exclusively when there is an error in the framework
+ // implementation of dataWithJSONObject:options:error:. This shouldn't normally occur as
+ // isValidJSONObject: should return NO in any case we should encounter an error.
+ error = [FIRAuthErrorUtils JSONSerializationErrorWithUnderlyingError:error];
+ }
+ } else {
+ error = [FIRAuthErrorUtils JSONSerializationErrorForUnencodableType];
+ }
+ if (!bodyData) {
+ callback(error);
+ return;
+ }
+
+ [_RPCIssuer asyncPostToURL:[request requestURL]
+ body:bodyData
+ contentType:kJSONContentType
+ completionHandler:^(NSData *data, NSError *error) {
+ // If there is an error with no body data at all, then this must be a network error.
+ if (error && !data) {
+ callback([FIRAuthErrorUtils networkErrorWithUnderlyingError:error]);
+ return;
+ }
+
+ // Try to decode the HTTP response data which may contain either a successful response or error
+ // message.
+ NSError *jsonError;
+ NSDictionary * dictionary =
+ [NSJSONSerialization JSONObjectWithData:data
+ options:NSJSONReadingMutableLeaves
+ error:&jsonError];
+ if (!dictionary) {
+ if (error) {
+ // We have an error, but we couldn't decode the body, so we have no additional information
+ // other than the raw response and the original NSError (the jsonError is infered by the
+ // error code (FIRAuthErrorCodeUnexpectedHTTPResponse, and is irrelevant.)
+ callback([FIRAuthErrorUtils unexpectedErrorResponseWithData:data underlyingError:error]);
+ } else {
+ // This is supposed to be a "successful" response, but we couldn't deserialize the body.
+ callback([FIRAuthErrorUtils unexpectedResponseWithData:data underlyingError:jsonError]);
+ }
+ return;
+ }
+ if (![dictionary isKindOfClass:[NSDictionary class]]) {
+ if (error) {
+ callback([FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:dictionary]);
+ } else {
+ callback([FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:dictionary]);
+ }
+ return;
+ }
+
+ // At this point we either have an error with successfully decoded details in the body, or we
+ // have a response which must pass further validation before we know it's truly successful.
+ // We deal with the case where we have an error with successfully decoded error details first:
+ if (error) {
+ NSDictionary *errorDictionary = dictionary[kErrorKey];
+ if ([errorDictionary isKindOfClass:[NSDictionary class]]) {
+ id<NSObject> errorMessage = errorDictionary[kErrorMessageKey];
+ if ([errorMessage isKindOfClass:[NSString class]]) {
+ NSString *errorMessageString = (NSString *)errorMessage;
+
+ // Contruct client error.
+ NSError *clientError = [[self class] clientErrorWithServerErrorMessage:errorMessageString
+ errorDictionary:errorDictionary
+ response:response];
+ if (clientError) {
+ callback(clientError);
+ return;
+ }
+ }
+ // Not a message we know, return the message directly.
+ if (errorMessage) {
+ NSError *unexpecterErrorResponse =
+ [FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:errorDictionary];
+ callback(unexpecterErrorResponse);
+ return;
+ }
+ }
+ // No error message at all, return the decoded response.
+ callback([FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:dictionary]);
+ return;
+ }
+
+ // Finally, we try to populate the response object with the JSON values.
+ if (![response setWithDictionary:dictionary error:&error]) {
+ callback([FIRAuthErrorUtils RPCResponseDecodingErrorWithDeserializedResponse:dictionary
+ underlyingError:error]);
+ return;
+ }
+
+ // Success! The response object originally passed in can be used by the caller.
+ callback(nil);
+ }];
+}
+
+/** @fn clientErrorWithServerErrorMessage:errorDictionary:
+ @brief Translates known server errors to client errors.
+ @param serverErrorMessage The error message from the server.
+ @param errorDictionary The error part of the response from the server.
+ @param response The response from the server RPC.
+ @return A client error, if any.
+ */
++ (nullable NSError *)clientErrorWithServerErrorMessage:(NSString *)serverErrorMessage
+ errorDictionary:(NSDictionary *)errorDictionary
+ response:(id<FIRAuthRPCResponse>)response {
+ NSString *shortErrorMessage = serverErrorMessage;
+ NSString *serverDetailErrorMessage;
+ NSRange colonRange = [serverErrorMessage rangeOfString:@":"];
+ if (colonRange.location != NSNotFound) {
+ shortErrorMessage = [serverErrorMessage substringToIndex:colonRange.location];
+ shortErrorMessage =
+ [shortErrorMessage stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
+ serverDetailErrorMessage = [serverErrorMessage substringFromIndex:colonRange.location + 1];
+ serverDetailErrorMessage = [serverDetailErrorMessage stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceCharacterSet]];
+ }
+
+ // Delegate the responsibility for constructing the client error to the response object,
+ // if possible.
+ SEL clientErrorWithServerErrorMessageSelector =
+ @selector(clientErrorWithShortErrorMessage:detailErrorMessage:);
+ if ([response respondsToSelector:clientErrorWithServerErrorMessageSelector]) {
+ NSError *error = [response clientErrorWithShortErrorMessage:shortErrorMessage
+ detailErrorMessage:serverDetailErrorMessage];
+ if (error) {
+ return error;
+ }
+ }
+
+ if ([shortErrorMessage isEqualToString:kUserNotFoundErrorMessage]) {
+ return [FIRAuthErrorUtils userNotFoundErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kUserDeletedErrorMessage]) {
+ return [FIRAuthErrorUtils userNotFoundErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidLocalIDErrorMessage]) {
+ // This case shouldn't be necessary but it is for now: b/27908364 .
+ return [FIRAuthErrorUtils userNotFoundErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kUserTokenExpiredErrorMessage]) {
+ return [FIRAuthErrorUtils userTokenExpiredErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kTooManyRequestsErrorMessage]) {
+ return [FIRAuthErrorUtils tooManyRequestsErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidCustomTokenErrorMessage]) {
+ return [FIRAuthErrorUtils invalidCustomTokenErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kCustomTokenMismatch]) {
+ return [FIRAuthErrorUtils customTokenMistmatchErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidCredentialErrorMessage]) {
+ return [FIRAuthErrorUtils invalidCredentialErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kUserDisabledErrorMessage]) {
+ return [FIRAuthErrorUtils userDisabledErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kOperationNotAllowedErrorMessage]) {
+ return [FIRAuthErrorUtils operationNotAllowedErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kPasswordLoginDisabledErrorMessage]) {
+ return [FIRAuthErrorUtils operationNotAllowedErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kEmailAlreadyInUseErrorMessage]) {
+ return [FIRAuthErrorUtils emailAlreadyInUseErrorWithEmail:nil];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidEmailErrorMessage]) {
+ return [FIRAuthErrorUtils invalidEmailErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ // "INVALID_IDENTIFIER" can be returned by createAuthURI RPC. Considering email addresses are
+ // currently the only identifiers, we surface the FIRAuthErrorCodeInvalidEmail error code in this
+ // case.
+ if ([shortErrorMessage isEqualToString:kInvalidIdentifierErrorMessage]) {
+ return [FIRAuthErrorUtils invalidEmailErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kWrongPasswordErrorMessage]) {
+ return [FIRAuthErrorUtils wrongPasswordErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kCredentialTooOldErrorMessage]) {
+ return [FIRAuthErrorUtils requiresRecentLoginErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidUserTokenErrorMessage]) {
+ return [FIRAuthErrorUtils invalidUserTokenErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kFederatedUserIDAlreadyLinkedMessage]) {
+ return [FIRAuthErrorUtils credentialAlreadyInUseErrorWithMessage:serverDetailErrorMessage
+ credential:nil];
+ }
+
+ if ([shortErrorMessage isEqualToString:kWeakPasswordErrorMessagePrefix]) {
+ return [FIRAuthErrorUtils weakPasswordErrorWithServerResponseReason:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kExpiredActionCodeErrorMessage]) {
+ return [FIRAuthErrorUtils expiredActionCodeErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidActionCodeErrorMessage]) {
+ return [FIRAuthErrorUtils invalidActionCodeErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidSenderEmailErrorMessage]) {
+ return [FIRAuthErrorUtils invalidSenderErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidMessagePayloadErrorMessage]) {
+ return [FIRAuthErrorUtils invalidMessagePayloadErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidRecipientEmailErrorMessage]) {
+ return [FIRAuthErrorUtils invalidRecipientEmailErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidPhoneNumberErrorMessage]) {
+ return [FIRAuthErrorUtils invalidPhoneNumberErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidSessionInfoErrorMessage]) {
+ return [FIRAuthErrorUtils invalidVerificationIDErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidVerificationCodeErrorMessage]) {
+ return [FIRAuthErrorUtils invalidVerificationCodeErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kSessionExpiredErrorMessage]) {
+ return [FIRAuthErrorUtils sessionExpiredErrorWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kMissingAppCredentialErrorMessage]) {
+ return [FIRAuthErrorUtils missingAppCredentialWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kInvalidAppCredentialErrorMessage]) {
+ return [FIRAuthErrorUtils invalidAppCredentialWithMessage:serverDetailErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kQuoutaExceededErrorMessage]) {
+ return [FIRAuthErrorUtils quotaExceededErrorWithMessage:serverErrorMessage];
+ }
+
+ if ([shortErrorMessage isEqualToString:kAppNotVerifiedErrorMessage]) {
+ return [FIRAuthErrorUtils appNotVerifiedErrorWithMessage:serverErrorMessage];
+ }
+
+ // In this case we handle an error that might be specified in the underlying errors dictionary,
+ // the error message in determined based on the @c reason key in the dictionary.
+ if (errorDictionary[kErrorsKey]) {
+ // Check for underlying error with reason = keyInvalid;
+ id underlyingErrors = errorDictionary[kErrorsKey];
+ if ([underlyingErrors isKindOfClass:[NSArray class]]) {
+ NSArray *underlyingErrorsArray = (NSArray *)underlyingErrors;
+ for (id underlyingError in underlyingErrorsArray) {
+ if ([underlyingError isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *underlyingErrorDictionary = (NSDictionary *)underlyingError;
+ NSString *reason = underlyingErrorDictionary[kReasonKey];
+ if ([reason hasPrefix:kInvalidKeyReasonValue]) {
+ return [FIRAuthErrorUtils invalidAPIKeyError];
+ }
+ if ([reason isEqualToString:kAppNotAuthorizedReasonValue]) {
+ return [FIRAuthErrorUtils appNotAuthorizedError];
+ }
+ }
+ }
+ }
+ }
+ return nil;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRAuthRPCRequest.h b/Firebase/Auth/Source/RPCs/FIRAuthRPCRequest.h
new file mode 100644
index 0000000..ddad3cb
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRAuthRPCRequest.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @protocol FIRAuthRPCRequest
+ @brief The generic interface for an RPC request needed by @c FIRAuthBackend.
+ */
+@protocol FIRAuthRPCRequest <NSObject>
+
+/** @fn requestURL
+ @brief Gets the request's full URL.
+ */
+- (NSURL *)requestURL;
+
+/** @fn UnencodedHTTPRequestBodyWithError:
+ @brief Creates unencoded HTTP body representing the request.
+ @param error An out field for an error which occurred constructing the request.
+ @return The HTTP body data representing the request before any encoding, or nil for error.
+ */
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRAuthRPCResponse.h b/Firebase/Auth/Source/RPCs/FIRAuthRPCResponse.h
new file mode 100644
index 0000000..c130f3f
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRAuthRPCResponse.h
@@ -0,0 +1,49 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @protocol FIRAuthRPCResponse
+ @brief The generic interface for an RPC response needed by @c FIRAuthBackend.
+ */
+@protocol FIRAuthRPCResponse <NSObject>
+
+/** @fn setFieldsWithDictionary:error:
+ @brief Sets the response instance from the decoded JSON response.
+ @param dictionary The dictionary decoded from HTTP JSON response.
+ @param error An out field for an error which occurred constructing the request.
+ @return Whether the operation was successful or not.
+ */
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error;
+
+@optional
+
+/** @fn clientErrorWithshortErrorMessage:detailErrorMessage
+ @brief This optional method allows response classes to create client errors given a short error
+ message and a detail error message from the server.
+ @param shortErrorMessage The short error message from the server.
+ @param detailErrorMessage The detailed error message from the server.
+ @return A client error, if any.
+ */
+- (nullable NSError *)clientErrorWithShortErrorMessage:(NSString *)shortErrorMessage
+ detailErrorMessage:(NSString *)detailErrorMessage;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.h b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.h
new file mode 100644
index 0000000..bb28826
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRCreateAuthURIRequest
+ @brief Represents the parameters for the createAuthUri endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri
+ */
+@interface FIRCreateAuthURIRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property identifier
+ @brief The email or federated ID of the user.
+ */
+@property(nonatomic, copy) NSString *identifier;
+
+/** @property continueURI
+ @brief The URI to which the IDP redirects the user after the federated login flow.
+ */
+@property(nonatomic, copy) NSString *continueURI;
+
+/** @property openIDRealm
+ @brief Optional realm for OpenID protocol. The sub string "scheme://domain:port" of the param
+ "continueUri" is used if this is not set.
+ */
+@property(nonatomic, copy, nullable) NSString *openIDRealm;
+
+/** @property providerID
+ @brief The IdP ID. For white listed IdPs it's a short domain name e.g. google.com, aol.com,
+ live.net and yahoo.com. For other OpenID IdPs it's the OP identifier.
+ */
+@property(nonatomic, copy, nullable) NSString *providerID;
+
+/** @property clientID
+ @brief The relying party OAuth client ID.
+ */
+@property(nonatomic, copy, nullable) NSString *clientID;
+
+/** @property context
+ @brief The opaque value used by the client to maintain context info between the authentication
+ request and the IDP callback.
+ */
+@property(nonatomic, copy, nullable) NSString *context;
+
+/** @property appID
+ @brief The iOS client application's bundle identifier.
+ */
+@property(nonatomic, copy, nullable) NSString *appID;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithIdentifier:continueURI:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithIdentifier:continueURI:APIKey:
+ @brief Designated initializer.
+ @param identifier The email or federated ID of the user.
+ @param continueURI The URI to which the IDP redirects the user after the federated login flow.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithIdentifier:(NSString *)identifier
+ continueURI:(NSString *)continueURI
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m
new file mode 100644
index 0000000..6d2b9e9
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m
@@ -0,0 +1,95 @@
+/*
+ * 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 "FIRCreateAuthURIRequest.h"
+
+/** @var kCreateAuthURIEndpoint
+ @brief The "createAuthUri" endpoint.
+ */
+static NSString *const kCreateAuthURIEndpoint = @"createAuthUri";
+
+/** @var kProviderIDKey
+ @brief The key for the "providerId" value in the request.
+ */
+static NSString *const kProviderIDKey = @"providerId";
+
+/** @var kIdentifierKey
+ @brief The key for the "identifier" value in the request.
+ */
+static NSString *const kIdentifierKey = @"identifier";
+
+/** @var kContinueURIKey
+ @brief The key for the "continueUri" value in the request.
+ */
+static NSString *const kContinueURIKey = @"continueUri";
+
+/** @var kOpenIDRealmKey
+ @brief The key for the "openidRealm" value in the request.
+ */
+static NSString *const kOpenIDRealmKey = @"openidRealm";
+
+/** @var kClientIDKey
+ @brief The key for the "clientId" value in the request.
+ */
+static NSString *const kClientIDKey = @"clientId";
+
+/** @var kContextKey
+ @brief The key for the "context" value in the request.
+ */
+static NSString *const kContextKey = @"context";
+
+/** @var kAppIDKey
+ @brief The key for the "appId" value in the request.
+ */
+static NSString *const kAppIDKey = @"appId";
+
+@implementation FIRCreateAuthURIRequest
+
+- (nullable instancetype)initWithIdentifier:(NSString *)identifier
+ continueURI:(NSString *)continueURI
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kCreateAuthURIEndpoint APIKey:APIKey];
+ if (self) {
+ _identifier = [identifier copy];
+ _continueURI = [continueURI copy];
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [@{
+ kIdentifierKey : _identifier,
+ kContinueURIKey : _continueURI
+ } mutableCopy];
+ if (_providerID) {
+ postBody[kProviderIDKey] = _providerID;
+ }
+ if (_openIDRealm) {
+ postBody[kOpenIDRealmKey] = _openIDRealm;
+ }
+ if (_clientID) {
+ postBody[kClientIDKey] = _clientID;
+ }
+ if (_context) {
+ postBody[kContextKey] = _context;
+ }
+ if (_appID) {
+ postBody[kAppIDKey] = _appID;
+ }
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.h
new file mode 100644
index 0000000..9f6cbae
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRCreateAuthURIResponse
+ @brief Represents the parameters for the createAuthUri endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri
+ */
+@interface FIRCreateAuthURIResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property authUri
+ @brief The URI used by the IDP to authenticate the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *authURI;
+
+/** @property registered
+ @brief Whether the user is registered if the identifier is an email.
+ */
+@property(nonatomic, assign, readonly) BOOL registered;
+
+/** @property providerId
+ @brief The provider ID of the auth URI.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *providerID;
+
+/** @property forExistingProvider
+ @brief True if the authUri is for user's existing provider.
+ */
+@property(nonatomic, assign, readonly) BOOL forExistingProvider;
+
+/** @property allProviders
+ @brief A list of provider IDs the passed @c identifier could use to sign in with.
+ */
+@property(nonatomic, copy, readonly, nullable) NSArray<NSString *> *allProviders;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m
new file mode 100644
index 0000000..7a38cca
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.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 "FIRCreateAuthURIResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRCreateAuthURIResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _providerID = [dictionary[@"providerId"] copy];
+ _authURI = [dictionary[@"authUri"] copy];
+ _registered = [dictionary[@"registered"] boolValue];
+ _forExistingProvider = [dictionary[@"forExistingProvider"] boolValue];
+ _allProviders = [dictionary[@"allProviders"] copy];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.h b/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.h
new file mode 100644
index 0000000..1751e54
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.h
@@ -0,0 +1,48 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRDeleteAccountRequest
+ @brief Represents the parameters for the deleteAccount endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/deleteAccount
+ */
+@interface FIRDeleteAccountRequest : FIRIdentityToolkitRequest<FIRAuthRPCRequest>
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithAPIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param APIKey The client's API Key.
+ @param localID The local ID.
+ @param accessToken The access token.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ localID:(NSString *)localID
+ accessToken:(NSString *)accessToken NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m b/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m
new file mode 100644
index 0000000..9105ba0
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m
@@ -0,0 +1,65 @@
+/*
+ * 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 "FIRDeleteAccountRequest.h"
+
+/** @var kCreateAuthURIEndpoint
+ @brief The "deleteAccount" endpoint.
+ */
+static NSString *const kDeleteAccountEndpoint = @"deleteAccount";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kLocalIDKey
+ @brief The key for the "localID" value in the request.
+ */
+static NSString *const kLocalIDKey = @"localId";
+
+@implementation FIRDeleteAccountRequest {
+ /** @var _accessToken
+ @brief The STS Access Token of the authenticated user.
+ */
+ NSString *_accessToken;
+
+ /** @var _localID
+ @brief The localID of the user.
+ */
+ NSString *_localID;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ localID:(nonnull NSString *)localID
+ accessToken:(nonnull NSString *)accessToken {
+ self = [super initWithEndpoint:kDeleteAccountEndpoint APIKey:APIKey];
+ if (self) {
+ _localID = [localID copy];
+ _accessToken = [accessToken copy];
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ postBody[kIDTokenKey] = _accessToken;
+ postBody[kLocalIDKey] = _localID;
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h
new file mode 100644
index 0000000..59226d6
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h
@@ -0,0 +1,26 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+/** @class FIRDeleteAccountResponse
+ @brief Represents the response from the deleteAccount endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/deleteAccount
+ */
+@interface FIRDeleteAccountResponse : NSObject<FIRAuthRPCResponse>
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m
new file mode 100644
index 0000000..01eb0a5
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m
@@ -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 "FIRDeleteAccountResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRDeleteAccountResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.h b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.h
new file mode 100644
index 0000000..b45b933
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRGetAccountInfoRequest
+ @brief Represents the parameters for the getAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo
+ */
+@interface FIRGetAccountInfoRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property accessToken
+ @brief The STS Access Token for the authenticated user.
+ */
+@property(nonatomic, copy) NSString *accessToken;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithAPIKey:IDToken:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:accessToken:
+ @brief Designated initializer.
+ @param APIKey The client's API Key.
+ @param accessToken The Access Token of the authenticated user.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ accessToken:(NSString *)accessToken NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m
new file mode 100644
index 0000000..5c73086
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m
@@ -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 "FIRGetAccountInfoRequest.h"
+
+/** @var kGetAccountInfoEndpoint
+ @brief The "getAccountInfo" endpoint.
+ */
+static NSString *const kGetAccountInfoEndpoint = @"getAccountInfo";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+@implementation FIRGetAccountInfoRequest
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ accessToken:(NSString *)accessToken {
+ self = [super initWithEndpoint:kGetAccountInfoEndpoint APIKey:APIKey];
+ if (self) {
+ _accessToken = [accessToken copy];
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ return @{
+ kIDTokenKey : _accessToken
+ };
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.h b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.h
new file mode 100644
index 0000000..009f3c1
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.h
@@ -0,0 +1,146 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRGetAccountInfoResponseProviderUserInfo
+ @brief Represents the provider user info part of the response from the getAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo
+ */
+@interface FIRGetAccountInfoResponseProviderUserInfo : NSObject
+
+/** @property providerID
+ @brief The ID of the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *providerID;
+
+/** @property displayName
+ @brief The user's display name at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property photoURL
+ @brief The user's photo URL at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
+
+/** @property federatedID
+ @brief The user's identifier at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *federatedID;
+
+/** @property email
+ @brief The user's email at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *email;
+
+/** @property phoneNumber
+ @brief A phone number associated with the user.
+ */
+@property(nonatomic, readonly, nullable) NSString *phoneNumber;
+
+/** @fn init
+ @brief Please use initWithDictionary:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param dictionary The provider user info data from endpoint.
+ */
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary NS_DESIGNATED_INITIALIZER;
+
+@end
+
+/** @class FIRGetAccountInfoResponseUser
+ @brief Represents the firebase user info part of the response from the getAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo
+ */
+@interface FIRGetAccountInfoResponseUser : NSObject
+
+/** @property localID
+ @brief The ID of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *localID;
+
+/** @property email
+ @brief The email or the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *email;
+
+/** @property emailVerified
+ @brief Whether the email has been verified.
+ */
+@property(nonatomic, assign, readonly) BOOL emailVerified;
+
+/** @property displayName
+ @brief The display name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property photoURL
+ @brief The user's photo URL.
+ */
+@property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
+
+/** @property providerUserInfo
+ @brief The user's profiles at the associated identity providers.
+ */
+@property(nonatomic, strong, readonly, nullable)
+ NSArray<FIRGetAccountInfoResponseProviderUserInfo *> *providerUserInfo;
+
+/** @property passwordHash
+ @brief Information about user's password.
+ @remarks This is not necessarily the hash of user's actual password.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *passwordHash;
+
+/** @property phoneNumber
+ @brief A phone number associated with the user.
+ */
+@property(nonatomic, readonly, nullable) NSString *phoneNumber;
+
+/** @fn init
+ @brief Please use initWithDictionary:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param dictionary The provider user info data from endpoint.
+ */
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary NS_DESIGNATED_INITIALIZER;
+
+@end
+
+/** @class FIRGetAccountInfoResponse
+ @brief Represents the response from the setAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo
+ */
+@interface FIRGetAccountInfoResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property providerUserInfo
+ @brief The requested users' profiles.
+ */
+@property(nonatomic, strong, readonly, nullable) NSArray<FIRGetAccountInfoResponseUser *> *users;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m
new file mode 100644
index 0000000..ba10746
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m
@@ -0,0 +1,94 @@
+/*
+ * 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 "FIRGetAccountInfoResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+/** @var kErrorKey
+ @brief The key for the "error" value in JSON responses from the server.
+ */
+static NSString *const kErrorKey = @"error";
+
+@implementation FIRGetAccountInfoResponseProviderUserInfo
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+ self = [super init];
+ if (self) {
+ _providerID = [dictionary[@"providerId"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ NSString *photoURL = dictionary[@"photoUrl"];
+ if (photoURL) {
+ _photoURL = [NSURL URLWithString:photoURL];
+ }
+ _federatedID = [dictionary[@"federatedId"] copy];
+ _email = [dictionary[@"email"] copy];
+ _phoneNumber = [dictionary[@"phoneNumber"] copy];
+ }
+ return self;
+}
+
+@end
+
+@implementation FIRGetAccountInfoResponseUser
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+ self = [super init];
+ if (self) {
+ NSArray<NSDictionary *> *providerUserInfoData = dictionary[@"providerUserInfo"];
+ if (providerUserInfoData) {
+ NSMutableArray<FIRGetAccountInfoResponseProviderUserInfo *> *providerUserInfoArray =
+ [NSMutableArray arrayWithCapacity:providerUserInfoData.count];
+ for (NSDictionary *dictionary in providerUserInfoData) {
+ [providerUserInfoArray addObject:
+ [[FIRGetAccountInfoResponseProviderUserInfo alloc] initWithDictionary:dictionary]];
+ }
+ _providerUserInfo = [providerUserInfoArray copy];
+ }
+ _localID = [dictionary[@"localId"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ _email = [dictionary[@"email"] copy];
+ NSString *photoURL = dictionary[@"photoUrl"];
+ if (photoURL) {
+ _photoURL = [NSURL URLWithString:photoURL];
+ }
+ _emailVerified = [dictionary[@"emailVerified"] boolValue];
+ _passwordHash = [dictionary[@"passwordHash"] copy];
+ _phoneNumber = [dictionary[@"phoneNumber"] copy];
+ }
+ return self;
+}
+
+@end
+
+@implementation FIRGetAccountInfoResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ NSArray<NSDictionary *> *usersData = dictionary[@"users"];
+ // The client side never sends a getAccountInfo request with multiple localID, so only one user
+ // data is expected in the response.
+ if (![usersData isKindOfClass:[NSArray class]] || usersData.count != 1) {
+ if (error) {
+ *error = [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:dictionary];
+ }
+ return NO;
+ }
+ _users = @[ [[FIRGetAccountInfoResponseUser alloc] initWithDictionary:usersData.firstObject] ];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h
new file mode 100644
index 0000000..08ab495
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.h
@@ -0,0 +1,87 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @enum FIRGetOOBConfirmationCodeRequestType
+ @brief Types of OOB Confirmation Code requests.
+ */
+typedef NS_ENUM(NSInteger, FIRGetOOBConfirmationCodeRequestType) {
+ /** @var FIRGetOOBConfirmationCodeRequestTypePasswordReset
+ @brief Requests a password reset code.
+ */
+ FIRGetOOBConfirmationCodeRequestTypePasswordReset,
+
+ /** @var FIRGetOOBConfirmationCodeRequestTypeVerifyEmail
+ @brief Requests an email verification code.
+ */
+ FIRGetOOBConfirmationCodeRequestTypeVerifyEmail,
+};
+
+/** @enum FIRGetOOBConfirmationCodeRequest
+ @brief Represents the parameters for the getOOBConfirmationCode endpoint.
+ */
+@interface FIRGetOOBConfirmationCodeRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property requestType
+ @brief The types of OOB Confirmation Code to request.
+ */
+@property(nonatomic, assign, readonly) FIRGetOOBConfirmationCodeRequestType requestType;
+
+/** @property email
+ @brief The email of the user.
+ @remarks For password reset.
+ */
+@property(nonatomic, copy, nullable, readonly) NSString *email;
+
+/** @property accessToken
+ @brief The STS Access Token of the authenticated user.
+ @remarks For email change.
+ */
+@property(nonatomic, copy, nullable, readonly) NSString *accessToken;
+
+/** @fn passwordResetRequestWithEmail:APIKey:
+ @brief Creates a password reset request.
+ @param email The user's email address.
+ @param APIKey The client's API Key.
+ @return A password reset request.
+ */
++ (nullable FIRGetOOBConfirmationCodeRequest *)passwordResetRequestWithEmail:(NSString *)email
+ APIKey:(NSString *)APIKey;
+
+/** @fn verifyEmailRequestWithAccessToken:APIKey:
+ @brief Creates a password reset request.
+ @param accessToken The user's STS Access Token.
+ @param APIKey The client's API Key.
+ @return A password reset request.
+ */
++ (nullable FIRGetOOBConfirmationCodeRequest *)
+ verifyEmailRequestWithAccessToken:(NSString *)accessToken APIKey:(NSString *)APIKey;
+
+/** @fn init
+ @brief Please use a factory method.
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m
new file mode 100644
index 0000000..b0523e4
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m
@@ -0,0 +1,135 @@
+/*
+ * 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 "FIRGetOOBConfirmationCodeRequest.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+#import "../Private/FIRAuth_Internal.h"
+
+/** @var kEndpoint
+ @brief The getOobConfirmationCode endpoint name.
+ */
+static NSString *const kEndpoint = @"getOobConfirmationCode";
+
+/** @var kRequestTypeKey
+ @brief The name of the required "requestType" property in the request.
+ */
+static NSString *const kRequestTypeKey = @"requestType";
+
+/** @var kEmailKey
+ @brief The name of the "email" property in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kPasswordResetRequestTypeValue
+ @brief The value for the "PASSWORD_RESET" request type.
+ */
+static NSString *const kPasswordResetRequestTypeValue = @"PASSWORD_RESET";
+
+/** @var kVerifyEmailRequestTypeValue
+ @brief The value for the "VERIFY_EMAIL" request type.
+ */
+static NSString *const kVerifyEmailRequestTypeValue = @"VERIFY_EMAIL";
+
+@interface FIRGetOOBConfirmationCodeRequest ()
+
+/** @fn initWithRequestType:email:APIKey:
+ @brief Designated initializer.
+ @param requestType The types of OOB Confirmation Code to request.
+ @param email The email of the user.
+ @param accessToken The STS Access Token of the currently signed in user.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithRequestType:(FIRGetOOBConfirmationCodeRequestType)requestType
+ email:(nullable NSString *)email
+ accessToken:(nullable NSString *)accessToken
+ APIKey:(nullable NSString *)APIKey
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FIRGetOOBConfirmationCodeRequest
+
+/** @var requestTypeStringValueForRequestType:
+ @brief Returns the string equivilent for an @c FIRGetOOBConfirmationCodeRequestType value.
+ */
++ (NSString *)requestTypeStringValueForRequestType:
+ (FIRGetOOBConfirmationCodeRequestType)requestType {
+ switch (requestType) {
+ case FIRGetOOBConfirmationCodeRequestTypePasswordReset:
+ return kPasswordResetRequestTypeValue;
+ case FIRGetOOBConfirmationCodeRequestTypeVerifyEmail:
+ return kVerifyEmailRequestTypeValue;
+ // No default case so that we get a compiler warning if a new value was added to the enum.
+ }
+}
+
++ (FIRGetOOBConfirmationCodeRequest *)passwordResetRequestWithEmail:(NSString *)email
+ APIKey:(NSString *)APIKey {
+ return [[self alloc] initWithRequestType:FIRGetOOBConfirmationCodeRequestTypePasswordReset
+ email:email
+ accessToken:nil
+ APIKey:APIKey];
+}
+
++ (FIRGetOOBConfirmationCodeRequest *)
+ verifyEmailRequestWithAccessToken:(NSString *)accessToken APIKey:(NSString *)APIKey {
+ return [[self alloc] initWithRequestType:FIRGetOOBConfirmationCodeRequestTypeVerifyEmail
+ email:nil
+ accessToken:accessToken
+ APIKey:APIKey];
+}
+
+- (nullable instancetype)initWithRequestType:(FIRGetOOBConfirmationCodeRequestType)requestType
+ email:(nullable NSString *)email
+ accessToken:(nullable NSString *)accessToken
+ APIKey:(nullable NSString *)APIKey {
+ self = [super initWithEndpoint:kEndpoint APIKey:APIKey];
+ if (self) {
+ _requestType = requestType;
+ _email = email;
+ _accessToken = accessToken;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *body = [@{
+ kRequestTypeKey : [[self class] requestTypeStringValueForRequestType:_requestType]
+ } mutableCopy];
+
+ // For password reset requests, we only need an email address in addition to the already required
+ // fields.
+ if (_requestType == FIRGetOOBConfirmationCodeRequestTypePasswordReset) {
+ body[kEmailKey] = _email;
+ }
+
+ // For verify email requests, we only need an STS Access Token in addition to the already required
+ // fields.
+ if (_requestType == FIRGetOOBConfirmationCodeRequestTypeVerifyEmail) {
+ body[kIDTokenKey] = _accessToken;
+ }
+
+ return body;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.h b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.h
new file mode 100644
index 0000000..69aa458
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRGetOOBConfirmationCodeResponse
+ @brief Represents the response from the getOobConfirmationCode endpoint.
+ */
+@interface FIRGetOOBConfirmationCodeResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property OOBCode
+ @brief The OOB code returned by the server in some cases.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *OOBCode;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.m b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.m
new file mode 100644
index 0000000..d5fc1dd
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeResponse.m
@@ -0,0 +1,38 @@
+/*
+ * 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 "FIRGetOOBConfirmationCodeResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kOOBCodeKey
+ @brief The name of the field in the response JSON for the OOB code.
+ */
+static NSString *const kOOBCodeKey = @"oobCode";
+
+@implementation FIRGetOOBConfirmationCodeResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _OOBCode = [dictionary[kOOBCodeKey] copy];
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.h b/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.h
new file mode 100644
index 0000000..873788d
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.h
@@ -0,0 +1,57 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRIdentityToolkitRequest
+ @brief Represents a request to an identity toolkit endpoint.
+ */
+@interface FIRIdentityToolkitRequest : NSObject
+
+/** @property endpoint
+ @brief Gets the RPC's endpoint.
+ */
+@property(nonatomic, copy, readonly) NSString *endpoint;
+
+/** @property APIKey
+ @brief Gets the client's API key used for the request.
+ */
+@property(nonatomic, copy, readonly) NSString *APIKey;
+
+/** @fn init
+ @brief Please use initWithEndpoint:APIKey:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Designated initializer.
+ @param endpoint The endpoint name.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey
+ NS_DESIGNATED_INITIALIZER;
+
+/** @fn requestURL
+ @brief Gets the request's full URL.
+ */
+- (NSURL *)requestURL;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.m b/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.m
new file mode 100644
index 0000000..fb51a82
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRIdentityToolkitRequest.m
@@ -0,0 +1,57 @@
+/*
+ * 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 "FIRIdentityToolkitRequest.h"
+
+/** @var kAPIURLFormat
+ @brief URL format for server API calls.
+ */
+static NSString *const kAPIURLFormat = @"https://%@/identitytoolkit/v3/relyingparty/%@?key=%@";
+
+/** @var gAPIHost
+ @brief Host for server API calls.
+ */
+static NSString *gAPIHost = @"www.googleapis.com";
+
+@implementation FIRIdentityToolkitRequest
+
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey {
+ self = [super init];
+ if (self) {
+ _endpoint = [endpoint copy];
+ _APIKey = [APIKey copy];
+ }
+ return self;
+}
+
+- (NSURL *)requestURL {
+ NSString *URLString = [NSString stringWithFormat:kAPIURLFormat, gAPIHost, _endpoint, _APIKey];
+ NSURL *URL = [NSURL URLWithString:URLString];
+ return URL;
+}
+
+#pragma mark - Internal API for development
+
++ (NSString *)host {
+ return gAPIHost;
+}
+
++ (void)setHost:(NSString *)host {
+ gAPIHost = host;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.h b/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.h
new file mode 100644
index 0000000..66b03ad
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRResetPasswordRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property oobCode
+ @brief The oobCode sent in the request.
+ */
+@property(nonatomic, copy, readonly) NSString *oobCode;
+
+/** @property updatedPassword
+ @brief The new password sent in the request.
+ */
+@property(nonatomic, copy, readonly) NSString *updatedPassword;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithOOBCode:oobCode: instead.
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:oobCode:currentPassword:
+ @brief Designated initializer.
+ @param APIKey The client's API Key.
+ @param oobCode The OOB Code.
+ @param newPassword The new password.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ oobCode:(NSString *)oobCode
+ newPassword:(nullable NSString *)newPassword NS_DESIGNATED_INITIALIZER;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m b/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m
new file mode 100644
index 0000000..603aa00
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.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 "FIRResetPasswordRequest.h"
+
+/** @var kResetPasswordEndpoint
+ @brief The "resetPassword" endpoint.
+ */
+static NSString *const kResetPasswordEndpoint = @"resetPassword";
+
+/** @var kOOBCodeKey
+ @brief The "resetPassword" key.
+ */
+static NSString *const kOOBCodeKey = @"oobCode";
+
+/** @var kCurrentPasswordKey
+ @brief The "newPassword" key.
+ */
+static NSString *const kCurrentPasswordKey = @"newPassword";
+
+@implementation FIRResetPasswordRequest
+
+- (instancetype)initWithAPIKey:(NSString *)APIKey
+ oobCode:(NSString *)oobCode
+ newPassword:(NSString *)newPassword {
+ self = [super initWithEndpoint:kResetPasswordEndpoint APIKey:APIKey];
+ if (self) {
+ _oobCode = oobCode;
+ _updatedPassword = newPassword;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ postBody[kOOBCodeKey] = _oobCode;
+ if (_updatedPassword) {
+ postBody[kCurrentPasswordKey] = _updatedPassword;
+ }
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.h b/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.h
new file mode 100644
index 0000000..28eb5f4
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAuthResetPasswordResponse
+ @brief Represents the response from the resetPassword endpoint.
+ @remarks Possible error codes:
+ - FIRAuthErrorCodeWeakPassword
+ - FIRAuthErrorCodeUserDisabled
+ - FIRAuthErrorCodeOperationNotAllowed
+ - FIRAuthErrorCodeExpiredActionCode
+ - FIRAuthErrorCodeInvalidActionCode
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/resetPassword
+ */
+@interface FIRResetPasswordResponse : NSObject<FIRAuthRPCResponse>
+
+/** @property email
+ @brief The email address corresponding to the reset password request.
+ */
+@property(nonatomic, strong, readonly) NSString *email;
+
+/** @property verifiedEmail
+ @brief The verified email returned from the backend.
+ */
+@property(nonatomic, strong, readonly) NSString *verifiedEmail;
+
+/** @property requestType
+ @brief The tpye of request as returned by the backend.
+ */
+@property(nonatomic, strong, readonly) NSString *requestType;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m b/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m
new file mode 100644
index 0000000..d306f6c
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.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 "FIRResetPasswordResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRResetPasswordResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _email = [dictionary[@"email"] copy];
+ _requestType = [dictionary[@"requestType"] copy];
+ _verifiedEmail = [dictionary[@"newEmail"] copy];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.h b/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.h
new file mode 100644
index 0000000..44c16b1
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.h
@@ -0,0 +1,109 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @enum FIRSecureTokenRequestGrantType
+ @brief Represents the possible grant types for a token request.
+ */
+typedef NS_ENUM(NSUInteger, FIRSecureTokenRequestGrantType) {
+ /** @var FIRSecureTokenRequestGrantTypeAuthorizationCode
+ @brief Indicates an authorization code request.
+ @remarks Exchanges a Gitkit "ID Token" for an STS Access Token and Refresh Token.
+ */
+ FIRSecureTokenRequestGrantTypeAuthorizationCode,
+
+ /** @var FIRSecureTokenRequestGrantTypeRefreshToken
+ @brief Indicates an refresh token request.
+ @remarks Uses an existing Refresh Token to create a new Access Token.
+ */
+ FIRSecureTokenRequestGrantTypeRefreshToken,
+};
+
+/** @class FIRSecureTokenRequest
+ @brief Represents the parameters for the token endpoint.
+ */
+@interface FIRSecureTokenRequest : NSObject <FIRAuthRPCRequest>
+
+/** @property grantType
+ @brief The type of grant requested.
+ @see FIRSecureTokenRequestGrantType
+ */
+@property(nonatomic, assign, readonly) FIRSecureTokenRequestGrantType grantType;
+
+/** @property scope
+ @brief The scopes requested (a comma-delimited list of scope strings.)
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *scope;
+
+/** @property refreshToken
+ @brief The client's refresh token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *refreshToken;
+
+/** @property code
+ @brief The client's authorization code (legacy Gitkit "ID Token").
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *code;
+
+/** @property APIKey
+ @brief The client's API Key.
+ */
+@property(nonatomic, copy, readonly) NSString *APIKey;
+
+/** @fn authCodeRequestWithCode:
+ @brief Creates an authorization code request with the given code (legacy Gitkit "ID Token").
+ @param code The authorization code (legacy Gitkit "ID Token").
+ @param APIKey The client's API Key.
+ @return An authorization request.
+ */
++ (FIRSecureTokenRequest *)authCodeRequestWithCode:(NSString *)code APIKey:(NSString *)APIKey;
+
+/** @fn refreshRequestWithCode:
+ @brief Creates a refresh request with the given refresh token.
+ @param refreshToken The refresh token.
+ @param APIKey The client's API Key.
+ @return A refresh request.
+ */
++ (FIRSecureTokenRequest *)refreshRequestWithRefreshToken:(NSString *)refreshToken
+ APIKey:(NSString *)APIKey;
+
+/** @fn init
+ @brief Please use initWithGrantType:scope:refreshToken:code:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithGrantType:scope:refreshToken:code:APIKey:
+ @brief Designated initializer.
+ @param grantType The type of request.
+ @param scope The scopes requested.
+ @param refreshToken The client's refresh token (for refresh requests.)
+ @param code The client's authorization code (Gitkit ID Token) (for authorization code requests.)
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithGrantType:(FIRSecureTokenRequestGrantType)grantType
+ scope:(nullable NSString *)scope
+ refreshToken:(nullable NSString *)refreshToken
+ code:(nullable NSString *)code
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m b/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m
new file mode 100644
index 0000000..1983542
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m
@@ -0,0 +1,141 @@
+/*
+ * 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 "FIRSecureTokenRequest.h"
+
+/** @var kFIRSecureTokenServiceGetTokenURLFormat
+ @brief The format of the secure token service URLs. Requires string format substitution with
+ the client's API Key.
+ */
+static NSString *const kFIRSecureTokenServiceGetTokenURLFormat = @"https://%@/v1/token?key=%@";
+
+/** @var kFIRSecureTokenServiceGrantTypeRefreshToken
+ @brief The string value of the @c FIRSecureTokenRequestGrantTypeRefreshToken request type.
+ */
+static NSString *const kFIRSecureTokenServiceGrantTypeRefreshToken = @"refresh_token";
+
+/** @var kFIRSecureTokenServiceGrantTypeAuthorizationCode
+ @brief The string value of the @c FIRSecureTokenRequestGrantTypeAuthorizationCode request type.
+ */
+static NSString *const kFIRSecureTokenServiceGrantTypeAuthorizationCode = @"authorization_code";
+
+/** @var kGrantTypeKey
+ @brief The key for the "grantType" parameter in the request.
+ */
+static NSString *const kGrantTypeKey = @"grantType";
+
+/** @var kScopeKey
+ @brief The key for the "scope" parameter in the request.
+ */
+static NSString *const kScopeKey = @"scope";
+
+/** @var kRefreshTokenKey
+ @brief The key for the "refreshToken" parameter in the request.
+ */
+static NSString *const kRefreshTokenKey = @"refreshToken";
+
+/** @var kCodeKey
+ @brief The key for the "code" parameter in the request.
+ */
+static NSString *const kCodeKey = @"code";
+
+/** @var gAPIHost
+ @brief Host for server API calls.
+ */
+static NSString *gAPIHost = @"securetoken.googleapis.com";
+
+@implementation FIRSecureTokenRequest
+
++ (FIRSecureTokenRequest *)authCodeRequestWithCode:(NSString *)code APIKey:(NSString *)APIKey {
+ return [[self alloc] initWithGrantType:FIRSecureTokenRequestGrantTypeAuthorizationCode
+ scope:nil
+ refreshToken:nil
+ code:code
+ APIKey:APIKey];
+}
+
++ (FIRSecureTokenRequest *)refreshRequestWithRefreshToken:(NSString *)refreshToken
+ APIKey:(NSString *)APIKey {
+ return [[self alloc] initWithGrantType:FIRSecureTokenRequestGrantTypeRefreshToken
+ scope:nil
+ refreshToken:refreshToken
+ code:nil
+ APIKey:APIKey];
+}
+
+/** @fn grantTypeStringWithGrantType:
+ @brief Converts a @c FIRSecureTokenRequestGrantType to it's @c NSString equivilent.
+ */
++ (NSString *)grantTypeStringWithGrantType:(FIRSecureTokenRequestGrantType)grantType {
+ switch (grantType) {
+ case FIRSecureTokenRequestGrantTypeAuthorizationCode:
+ return kFIRSecureTokenServiceGrantTypeAuthorizationCode;
+ case FIRSecureTokenRequestGrantTypeRefreshToken:
+ return kFIRSecureTokenServiceGrantTypeRefreshToken;
+ // No Default case so we will notice if new grant types are added to the enum.
+ }
+}
+
+- (nullable instancetype)initWithGrantType:(FIRSecureTokenRequestGrantType)grantType
+ scope:(nullable NSString *)scope
+ refreshToken:(nullable NSString *)refreshToken
+ code:(nullable NSString *)code
+ APIKey:(NSString *)APIKey {
+ self = [super init];
+ if (self) {
+ _grantType = grantType;
+ _scope = [scope copy];
+ _refreshToken = [refreshToken copy];
+ _code = [code copy];
+ _APIKey = [APIKey copy];
+ }
+ return self;
+}
+
+- (NSURL *)requestURL {
+ NSString *URLString =
+ [NSString stringWithFormat:kFIRSecureTokenServiceGetTokenURLFormat, gAPIHost, _APIKey];
+ NSURL *URL = [NSURL URLWithString:URLString];
+ return URL;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [@{
+ kGrantTypeKey : [[self class] grantTypeStringWithGrantType:_grantType]
+ } mutableCopy];
+ if (_scope) {
+ postBody[kScopeKey] = _scope;
+ }
+ if (_refreshToken) {
+ postBody[kRefreshTokenKey] = _refreshToken;
+ }
+ if (_code) {
+ postBody[kCodeKey] = _code;
+ }
+ return postBody;
+}
+
+#pragma mark - Internal API for development
+
++ (NSString *)host {
+ return gAPIHost;
+}
+
++ (void)setHost:(NSString *)host {
+ gAPIHost = host;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.h b/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.h
new file mode 100644
index 0000000..0dd4a20
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.h
@@ -0,0 +1,50 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRSecureTokenResponse
+ @brief Represents the response from the token endpoint.
+ */
+@interface FIRSecureTokenResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token. (Possibly an updated one for refresh requests.)
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *refreshToken;
+
+/** @property accessToken
+ @brief The new access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *accessToken;
+
+/** @property IDToken
+ @brief The new ID Token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *IDToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m b/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m
new file mode 100644
index 0000000..8ff3dde
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.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 "FIRSecureTokenResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+/** @var kExpiresInKey
+ @brief The key for the number of seconds till the access token expires.
+ */
+static NSString *const kExpiresInKey = @"expires_in";
+
+/** @var kRefreshTokenKey
+ @brief The key for the refresh token.
+ */
+static NSString *const kRefreshTokenKey = @"refresh_token";
+
+/** @var kAccessTokenKey
+ @brief The key for the access token.
+ */
+static NSString *const kAccessTokenKey = @"access_token";
+
+/** @var kIDTokenKey
+ @brief The key for the "id_token" value in the response.
+ */
+static NSString *const kIDTokenKey = @"id_token";
+
+@implementation FIRSecureTokenResponse
+
+- (nullable NSString *)expectedKind {
+ return nil;
+}
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _refreshToken = dictionary[kRefreshTokenKey];
+ _accessToken = dictionary[kAccessTokenKey];
+ _IDToken = dictionary[kIDTokenKey];
+ if (!_accessToken.length) {
+ if (error) {
+ *error = [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:dictionary];
+ }
+ return NO;
+ }
+ id expiresIn = dictionary[kExpiresInKey];
+ if (![expiresIn isKindOfClass:[NSString class]]) {
+ if (error) {
+ *error = [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:dictionary];
+ }
+ return NO;
+ }
+
+ _approximateExpirationDate = [NSDate dateWithTimeIntervalSinceNow:[expiresIn doubleValue]];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.h b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.h
new file mode 100644
index 0000000..596fb8c
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.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 "FIRIdentityToolkitRequest.h"
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+@class FIRAuthAppCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSendVerificationCodeRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property phoneNumber
+ @brief The phone number to which the verification code should be sent.
+ */
+@property(nonatomic, strong, readonly) NSString *phoneNumber;
+
+/** @property appCredential
+ @brief The credential to prove the identity of the app in order to send the verification code.
+ */
+@property(nonatomic, strong, readonly) FIRAuthAppCredential *appCredential;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithPhoneNumber:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithPhoneNumber:APIKey:
+ @brief Designated initializer.
+ @param phoneNumber The phone number to which the verification code is to be sent.
+ @param appCredential The credential that proves the identity of the app.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithPhoneNumber:(NSString *)phoneNumber
+ appCredential:(FIRAuthAppCredential *)appCredential
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.m b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.m
new file mode 100644
index 0000000..f2212ad
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeRequest.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 "FIRSendVerificationCodeRequest.h"
+
+#import "../Private/FIRAuthAppCredential.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kSendVerificationCodeEndPoint
+ @brief The "sendVerificationCodeEnd" endpoint.
+ */
+static NSString *const kSendVerificationCodeEndPoint = @"sendVerificationCode";
+
+/** @var kPhoneNumberKey
+ @brief The key for the Phone Number parameter in the request.
+ */
+static NSString *const kPhoneNumberKey = @"phoneNumber";
+
+/** @var kReceiptKey
+ @brief The key for the receipt parameter in the request.
+ */
+static NSString *const kReceiptKey = @"iosReceipt";
+
+/** @var kSecretKey
+ @brief The key for the Secret parameter in the request.
+ */
+static NSString *const kSecretKey = @"iosSecret";
+
+@implementation FIRSendVerificationCodeRequest {
+}
+
+- (nullable instancetype)initWithPhoneNumber:(NSString *)phoneNumber
+ appCredential:(FIRAuthAppCredential *)appCredential
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kSendVerificationCodeEndPoint APIKey:APIKey];
+ if (self) {
+ _phoneNumber = [phoneNumber copy];
+ _appCredential = appCredential;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_phoneNumber) {
+ postBody[kPhoneNumberKey] = _phoneNumber;
+ }
+ if (_appCredential.receipt) {
+ postBody[kReceiptKey] = _appCredential.receipt;
+ }
+ if (_appCredential.secret) {
+ postBody[kSecretKey] = _appCredential.secret;
+ }
+ return postBody;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.h b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.h
new file mode 100644
index 0000000..1a49ec2
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSendVerificationCodeResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property verificationID
+ @brief Encrypted session information returned by the backend.
+ */
+@property(nonatomic, readonly) NSString *verificationID;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.m b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.m
new file mode 100644
index 0000000..9e47b6e
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSendVerificationCodeResponse.m
@@ -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 "FIRSendVerificationCodeResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRSendVerificationCodeResponse
+
+// TODO: remove when resolving b/37169084 .
+- (nullable NSString *)expectedKind {
+ return nil;
+}
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _verificationID = [dictionary[@"sessionInfo"] copy];
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.h b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.h
new file mode 100644
index 0000000..4816474
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.h
@@ -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.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+@class FIRGetAccountInfoResponse;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var FIRSetAccountInfoUserAttributeEmail
+ @brief Constant for email attribute used in "deleteAttributes".
+ */
+extern NSString *const FIRSetAccountInfoUserAttributeEmail;
+
+/** @var FIRSetAccountInfoUserAttributeDisplayName
+ @brief Constant for displayName attribute used in "deleteAttributes".
+ */
+extern NSString *const FIRSetAccountInfoUserAttributeDisplayName;
+
+/** @var FIRSetAccountInfoUserAttributeProvider
+ @brief Constant for provider attribute used in "deleteAttributes".
+ */
+extern NSString *const FIRSetAccountInfoUserAttributeProvider;
+
+/** @var FIRSetAccountInfoUserAttributePhotoURL
+ @brief Constant for photoURL attribute used in "deleteAttributes".
+ */
+extern NSString *const FIRSetAccountInfoUserAttributePhotoURL;
+
+/** @var FIRSetAccountInfoUserAttributePassword
+ @brief Constant for password attribute used in "deleteAttributes".
+ */
+extern NSString *const FIRSetAccountInfoUserAttributePassword;
+
+/** @class FIRSetAccountInfoRequest
+ @brief Represents the parameters for the setAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo
+ */
+@interface FIRSetAccountInfoRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property accessToken
+ @brief The STS Access Token of the authenticated user.
+ */
+@property(nonatomic, copy, nullable) NSString *accessToken;
+
+/** @property displayName
+ @brief The name of the user.
+ */
+@property(nonatomic, copy, nullable) NSString *displayName;
+
+/** @property localID
+ @brief The local ID of the user.
+ */
+@property(nonatomic, copy, nullable) NSString *localID;
+
+/** @property email
+ @brief The email of the user.
+ */
+@property(nonatomic, copy, nullable) NSString *email;
+
+/** @property photoURL
+ @brief The photoURL of the user.
+ */
+@property(nonatomic, copy, nullable) NSURL *photoURL;
+
+/** @property password
+ @brief The new password of the user.
+ */
+@property(nonatomic, copy, nullable) NSString *password;
+
+/** @property providers
+ @brief The associated identity providers of the user.
+ */
+@property(nonatomic, copy, nullable) NSArray<NSString *> *providers;
+
+/** @property OOBCode
+ @brief The out-of-band code of the change email request.
+ */
+@property(nonatomic, copy, nullable) NSString *OOBCode;
+
+/** @property emailVerified
+ @brief Whether to mark the email as verified or not.
+ */
+@property(nonatomic, assign) BOOL emailVerified;
+
+/** @property upgradeToFederatedLogin
+ @brief Whether to mark the user to upgrade to federated login.
+ */
+@property(nonatomic, assign) BOOL upgradeToFederatedLogin;
+
+/** @property captchaChallenge
+ @brief The captcha challenge.
+ */
+@property(nonatomic, copy, nullable) NSString *captchaChallenge;
+
+/** @property captchaResponse
+ @brief Response to the captcha.
+ */
+@property(nonatomic, copy, nullable) NSString *captchaResponse;
+
+/** @property deleteAttributes
+ @brief The list of user attributes to delete.
+ @remarks Every element of the list must be one of the predefined constant starts with
+ "FIRSetAccountInfoUserAttribute".
+ */
+@property(nonatomic, copy, nullable) NSArray<NSString *> *deleteAttributes;
+
+/** @property deleteProviders
+ @brief The list of identity providers to delete.
+ */
+@property(nonatomic, copy, nullable) NSArray<NSString *> *deleteProviders;
+
+/** @property returnSecureToken
+ @brief Whether the response should return access token and refresh token directly.
+ @remarks The default value is @c YES .
+ */
+@property(nonatomic, assign) BOOL returnSecureToken;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithAPIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m
new file mode 100644
index 0000000..b18d0dd
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m
@@ -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.
+ */
+
+#import "FIRSetAccountInfoRequest.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+#import "../Private/FIRAuth_Internal.h"
+#import "FIRGetAccountInfoResponse.h"
+
+NSString *const FIRSetAccountInfoUserAttributeEmail = @"EMAIL";
+
+NSString *const FIRSetAccountInfoUserAttributeDisplayName = @"DISPLAY_NAME";
+
+NSString *const FIRSetAccountInfoUserAttributeProvider = @"PROVIDER";
+
+NSString *const FIRSetAccountInfoUserAttributePhotoURL = @"PHOTO_URL";
+
+NSString *const FIRSetAccountInfoUserAttributePassword = @"PASSWORD";
+
+/** @var kCreateAuthURIEndpoint
+ @brief The "setAccountInfo" endpoint.
+ */
+static NSString *const kSetAccountInfoEndpoint = @"setAccountInfo";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kDisplayNameKey
+ @brief The key for the "displayName" value in the request.
+ */
+static NSString *const kDisplayNameKey = @"displayName";
+
+/** @var kLocalIDKey
+ @brief The key for the "localID" value in the request.
+ */
+static NSString *const kLocalIDKey = @"localId";
+
+/** @var kEmailKey
+ @brief The key for the "email" value in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kPasswordKey
+ @brief The key for the "password" value in the request.
+ */
+static NSString *const kPasswordKey = @"password";
+
+/** @var kPhotoURLKey
+ @brief The key for the "photoURL" value in the request.
+ */
+static NSString *const kPhotoURLKey = @"photoUrl";
+
+/** @var kProvidersKey
+ @brief The key for the "providers" value in the request.
+ */
+static NSString *const kProvidersKey = @"provider";
+
+/** @var kOOBCodeKey
+ @brief The key for the "OOBCode" value in the request.
+ */
+static NSString *const kOOBCodeKey = @"oobCode";
+
+/** @var kEmailVerifiedKey
+ @brief The key for the "emailVerified" value in the request.
+ */
+static NSString *const kEmailVerifiedKey = @"emailVerified";
+
+/** @var kUpgradeToFederatedLoginKey
+ @brief The key for the "upgradeToFederatedLogin" value in the request.
+ */
+static NSString *const kUpgradeToFederatedLoginKey = @"upgradeToFederatedLogin";
+
+/** @var kCaptchaChallengeKey
+ @brief The key for the "captchaChallenge" value in the request.
+ */
+static NSString *const kCaptchaChallengeKey = @"captchaChallenge";
+
+/** @var kCaptchaResponseKey
+ @brief The key for the "captchaResponse" value in the request.
+ */
+static NSString *const kCaptchaResponseKey = @"captchaResponse";
+
+/** @var kDeleteAttributesKey
+ @brief The key for the "deleteAttribute" value in the request.
+ */
+static NSString *const kDeleteAttributesKey = @"deleteAttribute";
+
+/** @var kDeleteProvidersKey
+ @brief The key for the "deleteProvider" value in the request.
+ */
+static NSString *const kDeleteProvidersKey = @"deleteProvider";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@implementation FIRSetAccountInfoRequest
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kSetAccountInfoEndpoint APIKey:APIKey];
+ if (self) {
+ _returnSecureToken = YES;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_accessToken) {
+ postBody[kIDTokenKey] = _accessToken;
+ }
+ if (_displayName) {
+ postBody[kDisplayNameKey] = _displayName;
+ }
+ if (_localID) {
+ postBody[kLocalIDKey] = _localID;
+ }
+ if (_email) {
+ postBody[kEmailKey] = _email;
+ }
+ if (_password) {
+ postBody[kPasswordKey] = _password;
+ }
+ if (_photoURL) {
+ postBody[kPhotoURLKey] = _photoURL.absoluteString;
+ }
+ if (_providers) {
+ postBody[kProvidersKey] = _providers;
+ }
+ if (_OOBCode) {
+ postBody[kOOBCodeKey] = _OOBCode;
+ }
+ if (_emailVerified) {
+ postBody[kEmailVerifiedKey] = @YES;
+ }
+ if (_upgradeToFederatedLogin) {
+ postBody[kUpgradeToFederatedLoginKey] = @YES;
+ }
+ if (_captchaChallenge) {
+ postBody[kCaptchaChallengeKey] = _captchaChallenge;
+ }
+ if (_captchaResponse) {
+ postBody[kCaptchaResponseKey] = _captchaResponse;
+ }
+ if (_deleteAttributes) {
+ postBody[kDeleteAttributesKey] = _deleteAttributes;
+ }
+ if (_deleteProviders) {
+ postBody[kDeleteProvidersKey] = _deleteProviders;
+ }
+ if (_returnSecureToken) {
+ postBody[kReturnSecureTokenKey] = @YES;
+ }
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.h b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.h
new file mode 100644
index 0000000..92895c0
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.h
@@ -0,0 +1,98 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRSetAccountInfoResponseProviderUserInfo
+ @brief Represents the provider user info part of the response from the setAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo
+ */
+@interface FIRSetAccountInfoResponseProviderUserInfo : NSObject
+
+/** @property providerID
+ @brief The ID of the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *providerID;
+
+/** @property displayName
+ @brief The user's display name at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property photoURL
+ @brief The user's photo URL at the identity provider.
+ */
+@property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
+
+/** @fn init
+ @brief Please use initWithDictionary:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param dictionary The provider user info data from endpoint.
+ */
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary NS_DESIGNATED_INITIALIZER;
+
+@end
+
+/** @class FIRSetAccountInfoResponse
+ @brief Represents the response from the setAccountInfo endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo
+ */
+@interface FIRSetAccountInfoResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property email
+ @brief The email or the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *email;
+
+/** @property displayName
+ @brief The display name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property providerUserInfo
+ @brief The user's profiles at the associated identity providers.
+ */
+@property(nonatomic, strong, readonly, nullable)
+ NSArray<FIRSetAccountInfoResponseProviderUserInfo *> *providerUserInfo;
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m
new file mode 100644
index 0000000..140ba25
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m
@@ -0,0 +1,61 @@
+/*
+ * 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 "FIRSetAccountInfoResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRSetAccountInfoResponseProviderUserInfo
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+ self = [super init];
+ if (self) {
+ _providerID = [dictionary[@"providerId"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ NSString *photoURL = dictionary[@"photoUrl"];
+ if (photoURL) {
+ _photoURL = [NSURL URLWithString:photoURL];
+ }
+ }
+ return self;
+}
+
+@end
+
+@implementation FIRSetAccountInfoResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _email = [dictionary[@"email"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ _IDToken = [dictionary[@"idToken"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ NSArray<NSDictionary *> *providerUserInfoData = dictionary[@"providerUserInfo"];
+ if (providerUserInfoData) {
+ NSMutableArray<FIRSetAccountInfoResponseProviderUserInfo *> *providerUserInfoArray =
+ [NSMutableArray arrayWithCapacity:providerUserInfoData.count];
+ for (NSDictionary *dictionary in providerUserInfoData) {
+ [providerUserInfoArray addObject:
+ [[FIRSetAccountInfoResponseProviderUserInfo alloc] initWithDictionary:dictionary]];
+ }
+ _providerUserInfo = [providerUserInfoArray copy];
+ }
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.h b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.h
new file mode 100644
index 0000000..46b47d5
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.h
@@ -0,0 +1,67 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSignUpNewUserRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property email
+ @brief The email of the user.
+ */
+@property(nonatomic, copy, nullable) NSString *email;
+
+/** @property password
+ @brief The password inputed by the user.
+ */
+@property(nonatomic, copy, nullable) NSString *password;
+
+/** @property displayName
+ @brief The password inputed by the user.
+ */
+@property(nonatomic, copy, nullable) NSString *displayName;
+
+/** @property returnSecureToken
+ @brief Whether the response should return access token and refresh token directly.
+ @remarks The default value is @c YES .
+ */
+@property(nonatomic, assign) BOOL returnSecureToken;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithEmail:password:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+/** @fn initWithAPIKey:
+ @brief initializer for anonymous sign-in.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey;
+
+/** @fn initWithAPIKey:email:password:
+ @brief Designated initializer.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ email:(nullable NSString *)email
+ password:(nullable NSString *)password
+ displayName:(nullable NSString *)displayName NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m
new file mode 100644
index 0000000..af60b11
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m
@@ -0,0 +1,82 @@
+/*
+ * 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 "FIRSignUpNewUserRequest.h"
+
+/** @var kSignupNewUserEndpoint
+ @brief The "SingupNewUserEndpoint" endpoint.
+ */
+static NSString *const kSignupNewUserEndpoint = @"signupNewUser";
+
+/** @var kEmailKey
+ @brief The key for the "email" value in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kPasswordKey
+ @brief The key for the "password" value in the request.
+ */
+static NSString *const kPasswordKey = @"password";
+
+/** @var kDisplayNameKey
+ @brief The key for the "kDisplayName" value in the request.
+ */
+static NSString *const kDisplayNameKey = @"displayName";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@implementation FIRSignUpNewUserRequest
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ email:(NSString *)email
+ password:(NSString *)password
+ displayName:(NSString *)displayName {
+ self = [super initWithEndpoint:kSignupNewUserEndpoint APIKey:APIKey];
+ if (self) {
+ _email = [email copy];
+ _password = [password copy];
+ _displayName = [displayName copy];
+ _returnSecureToken = YES;
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey{
+ self = [self initWithAPIKey:APIKey email:nil password:nil displayName:nil];
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_email) {
+ postBody[kEmailKey] = _email;
+ }
+ if (_password) {
+ postBody[kPasswordKey] = _password;
+ }
+ if (_displayName) {
+ postBody[kDisplayNameKey] = _displayName;
+ }
+ if (_returnSecureToken) {
+ postBody[kReturnSecureTokenKey] = @YES;
+ }
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.h b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.h
new file mode 100644
index 0000000..0d55939
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.h
@@ -0,0 +1,44 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSignUpNewUserResponse : NSObject<FIRAuthRPCResponse>
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m
new file mode 100644
index 0000000..4196d21
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.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 "FIRSignUpNewUserResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRSignUpNewUserResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _IDToken = [dictionary[@"idToken"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h
new file mode 100644
index 0000000..3202b47
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h
@@ -0,0 +1,100 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyAssertionRequest
+ @brief Represents the parameters for the verifyAssertion endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyAssertion
+ */
+@interface FIRVerifyAssertionRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property requestURI
+ @brief The URI to which the IDP redirects the user back. It may contain federated login result
+ params added by the IDP.
+ */
+@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(nonatomic, copy, nullable) NSString *pendingIDToken;
+
+/** @property accessToken
+ @brief The STS Access Token for the authenticated user, only needed for linking the user.
+ */
+@property(nonatomic, copy, nullable) NSString *accessToken;
+
+/** @property returnSecureToken
+ @brief Whether the response should return access token and refresh token directly.
+ @remarks The default value is @c YES .
+ */
+@property(nonatomic, assign) BOOL returnSecureToken;
+
+#pragma mark - Components of "postBody"
+
+/** @property providerID
+ @brief The ID of the IDP whose credentials are being presented to the endpoint.
+ */
+@property(nonatomic, copy, readonly) NSString *providerID;
+
+/** @property providerAccessToken
+ @brief An access token from the IDP.
+ */
+@property(nonatomic, copy, nullable) NSString *providerAccessToken;
+
+/** @property providerIDToken
+ @brief An ID Token from the IDP.
+ */
+@property(nonatomic, copy, nullable) NSString *providerIDToken;
+
+/** @property providerOAuthTokenSecret
+ @brief An OAuth client secret from the IDP.
+ */
+@property(nonatomic, copy, nullable) NSString *providerOAuthTokenSecret;
+
+/** @property inputEmail
+ @brief The originally entered email in the UI.
+ */
+@property(nonatomic, copy, nullable) NSString *inputEmail;
+
+/** @property autoCreate
+ @brief A flag that indicates whether or not the user should be automatically created.
+ */
+@property(nonatomic, assign) BOOL autoCreate;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithAPIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAPIKey:
+ @brief Designated initializer.
+ @param APIKey The client's API Key.
+ @param providerID The auth provider's ID.
+ */
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ providerID:(NSString *)providerID NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m
new file mode 100644
index 0000000..b31ae42
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m
@@ -0,0 +1,142 @@
+/*
+ * 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 "FIRVerifyAssertionRequest.h"
+
+#import <GoogleToolboxForMac/GTMNSData+zlib.h>
+#import <GoogleToolboxForMac/GTMNSDictionary+URLArguments.h>
+
+/** @var kVerifyAssertionEndpoint
+ @brief The "verifyAssertion" endpoint.
+ */
+static NSString *const kVerifyAssertionEndpoint = @"verifyAssertion";
+
+/** @var kProviderIDKey
+ @brief The key for the "providerId" value in the request.
+ */
+static NSString *const kProviderIDKey = @"providerId";
+
+/** @var kProviderIDTokenKey
+ @brief The key for the "id_token" value in the request.
+ */
+static NSString *const kProviderIDTokenKey = @"id_token";
+
+/** @var kProviderAccessTokenKey
+ @brief The key for the "access_token" value in the request.
+ */
+static NSString *const kProviderAccessTokenKey = @"access_token";
+
+/** @var kProviderOAuthTokenSecretKey
+ @brief The key for the "oauth_token_secret" value in the request.
+ */
+static NSString *const kProviderOAuthTokenSecretKey = @"oauth_token_secret";
+
+/** @var kIdentifierKey
+ @brief The key for the "identifier" value in the request.
+ */
+static NSString *const kIdentifierKey = @"identifier";
+
+/** @var kRequestURIKey
+ @brief The key for the "requestUri" value in the request.
+ */
+static NSString *const kRequestURIKey = @"requestUri";
+
+/** @var kPostBodyKey
+ @brief The key for the "postBody" value in the request.
+ */
+static NSString *const kPostBodyKey = @"postBody";
+
+/** @var kPendingIDTokenKey
+ @brief The key for the "pendingIdToken" value in the request.
+ */
+static NSString *const kPendingIDTokenKey = @"pendingIdToken";
+
+/** @var kAutoCreateKey
+ @brief The key for the "autoCreate" value in the request.
+ */
+static NSString *const kAutoCreateKey = @"autoCreate";
+
+/** @var kIDTokenKey
+ @brief The key for the "idToken" value in the request. This is actually the STS Access Token,
+ despite it's confusing (backwards compatiable) parameter name.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@implementation FIRVerifyAssertionRequest
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+ providerID:(nonnull NSString *)providerID{
+ self = [super initWithEndpoint:kVerifyAssertionEndpoint APIKey:APIKey];
+ if (self) {
+ _providerID = providerID;
+ _returnSecureToken = YES;
+ _autoCreate = YES;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [@{
+ kProviderIDKey : _providerID,
+ } mutableCopy];
+
+ if (_providerIDToken) {
+ postBody[kProviderIDTokenKey] = _providerIDToken;
+ }
+
+ if (_providerAccessToken) {
+ postBody[kProviderAccessTokenKey] = _providerAccessToken;
+ }
+
+ if (!_providerIDToken && !_providerAccessToken) {
+ [NSException raise:NSInvalidArgumentException
+ format:@"Either IDToken or accessToken must be supplied."];
+ }
+
+ if (_providerOAuthTokenSecret) {
+ postBody[kProviderOAuthTokenSecretKey] = _providerOAuthTokenSecret;
+ }
+
+ if (_inputEmail) {
+ postBody[kIdentifierKey] = _inputEmail;
+ }
+
+ NSMutableDictionary *body = [@{
+ kRequestURIKey : @"http://localhost", // Unused by server, but required
+ kPostBodyKey : [postBody gtm_httpArgumentsString]
+ } mutableCopy];
+
+ if (_pendingIDToken) {
+ body[kPendingIDTokenKey] = _pendingIDToken;
+ }
+ if (_accessToken) {
+ body[kIDTokenKey] = _accessToken;
+ }
+ if (_returnSecureToken) {
+ body[kReturnSecureTokenKey] = @YES;
+ }
+
+ body[kAutoCreateKey] = @(_autoCreate);
+
+ return body;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h
new file mode 100644
index 0000000..ce796ee
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h
@@ -0,0 +1,186 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyAssertionResponse
+ @brief Represents the response from the verifyAssertion endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyAssertion
+ */
+@interface FIRVerifyAssertionResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property federatedID
+ @brief The unique ID identifies the IdP account.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *federatedID;
+
+/** @property providerID
+ @brief The IdP ID. For white listed IdPs it's a short domain name e.g. google.com, aol.com,
+ live.net and yahoo.com. If the "providerId" param is set to OpenID OP identifer other than
+ the whilte listed IdPs the OP identifier is returned. If the "identifier" param is federated
+ ID in the createAuthUri request. The domain part of the federated ID is returned.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *providerID;
+
+/** @property localID
+ @brief The RP local ID if it's already been mapped to the IdP account identified by the
+ federated ID.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *localID;
+
+/** @property email
+ @brief The email returned by the IdP. NOTE: The federated login user may not own the email.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *email;
+
+/** @property inputEmail
+ @brief It's the identifier param in the createAuthUri request if the identifier is an email. It
+ can be used to check whether the user input email is different from the asserted email.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *inputEmail;
+
+/** @property originalEmail
+ @brief The original email stored in the mapping storage. It's returned when the federated ID is
+ associated to a different email.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *originalEmail;
+
+/** @property oauthRequestToken
+ @brief The user approved request token for the OpenID OAuth extension.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *oauthRequestToken;
+
+/** @property oauthScope
+ @brief The scope for the OpenID OAuth extension.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *oauthScope;
+
+/** @property firstName
+ @brief The first name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *firstName;
+
+/** @property lastName
+ @brief The last name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *lastName;
+
+/** @property fullName
+ @brief The full name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *fullName;
+
+/** @property nickName
+ @brief The nick name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *nickName;
+
+/** @property displayName
+ @brief The display name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+/** @property action
+ @brief The action code.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *action;
+
+/** @property language
+ @brief The language preference of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *language;
+
+/** @property timeZone
+ @brief The timezone of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *timeZone;
+
+/** @property photoURL
+ @brief The URI of the public accessible profile picture.
+ */
+@property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
+
+/** @property dateOfBirth
+ @brief The birth date of the IdP account.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *dateOfBirth;
+
+/** @property context
+ @brief The opaque value used by the client to maintain context info between the authentication
+ request and the IDP callback.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *context;
+
+/** @property verifiedProvider
+ @brief When action is 'map', contains the idps which can be used for confirmation.
+ */
+@property(nonatomic, strong, readonly, nullable) NSArray<NSString *> *verifiedProvider;
+
+/** @property needConfirmation
+ @brief Whether the assertion is from a non-trusted IDP and need account linking confirmation.
+ */
+@property(nonatomic, assign) BOOL needConfirmation;
+
+/** @property emailRecycled
+ @brief It's true if the email is recycled.
+ */
+@property(nonatomic, assign) BOOL emailRecycled;
+
+/** @property emailVerified
+ @brief The value is true if the IDP is also the email provider. It means the user owns the
+ email.
+ */
+@property(nonatomic, assign) BOOL emailVerified;
+
+/** @property isNewUser
+ @brief Flag indicating that the user signing in is a new user and not a returning user.
+ */
+@property(nonatomic, assign) BOOL isNewUser;
+
+/** @property profile
+ @brief Dictionary containing the additional IdP specific information.
+ */
+@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSObject *> *profile;
+
+/** @property username
+ @brief The name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *username;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m
new file mode 100644
index 0000000..6ae33c6
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m
@@ -0,0 +1,78 @@
+/*
+ * 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 "FIRVerifyAssertionResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRVerifyAssertionResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _federatedID = [dictionary[@"federatedId"] copy];
+ _providerID = [dictionary[@"providerId"] copy];
+ _localID = [dictionary[@"localId"] copy];
+ _emailRecycled = [dictionary[@"emailRecycled"] boolValue];
+ _emailVerified = [dictionary[@"emailVerified"] boolValue];
+ _email = [dictionary[@"email"] copy];
+ _inputEmail = [dictionary[@"inputEmail"] copy];
+ _originalEmail = [dictionary[@"originalEmail"] copy];
+ _oauthRequestToken = [dictionary[@"oauthRequestToken"] copy];
+ _oauthScope = [dictionary[@"oauthScope"] copy];
+ _firstName = [dictionary[@"firstName"] copy];
+ _lastName = [dictionary[@"lastName"] copy];
+ _fullName = [dictionary[@"fullName"] copy];
+ _nickName = [dictionary[@"nickName"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ _IDToken = [dictionary[@"idToken"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ _isNewUser = [dictionary[@"isNewUser"] boolValue];
+ id rawUserInfo = dictionary[@"rawUserInfo"];
+ if ([rawUserInfo isKindOfClass:[NSString class]]) {
+ NSData *data = [rawUserInfo dataUsingEncoding:NSUTF8StringEncoding];
+ rawUserInfo = [NSJSONSerialization JSONObjectWithData:data
+ options:NSJSONReadingMutableLeaves
+ error:nil];
+ }
+ if ([rawUserInfo isKindOfClass:[NSDictionary class]]) {
+ _profile = [[NSDictionary alloc] initWithDictionary:rawUserInfo
+ copyItems:YES];
+ }
+ _username = [dictionary[@"username"] copy];
+ _action = [dictionary[@"action"] copy];
+ _language = [dictionary[@"language"] copy];
+ _timeZone = [dictionary[@"timeZone"] copy];
+ _photoURL = dictionary[@"photoUrl"] ? [NSURL URLWithString:dictionary[@"photoUrl"]] : nil;
+ _dateOfBirth = [dictionary[@"dateOfBirth"] copy];
+ _context = [dictionary[@"context"] copy];
+ _needConfirmation = [dictionary[@"needConfirmation"] boolValue];
+ id verifiedProvider = dictionary[@"verifiedProvider"];
+ if ([verifiedProvider isKindOfClass:[NSString class]]) {
+ NSData *data = [verifiedProvider dataUsingEncoding:NSUTF8StringEncoding];
+ verifiedProvider = [NSJSONSerialization JSONObjectWithData:data
+ options:NSJSONReadingMutableLeaves
+ error:nil];
+ }
+ if ([verifiedProvider isKindOfClass:[NSArray class]]) {
+ _verifiedProvider = [[NSArray alloc] initWithArray:verifiedProvider
+ copyItems:YES];
+ }
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.h
new file mode 100644
index 0000000..b5da6b8
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.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 "FIRIdentityToolkitRequest.h"
+
+#import "FIRAuthRPCRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRVerifyClientRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property appToken
+ @brief The APNS device token.
+ */
+@property(nonatomic, readonly, nullable) NSString *appToken;
+
+/** @property isSandbox
+ @brief The flag that denotes if the appToken pertains to Sandbox or Production.
+ */
+@property(nonatomic, assign, readonly) BOOL isSandbox;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithAppToken:isSandbox: instead.
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithAppToken:isSandbox:
+ @brief Designated initializer.
+ @param appToken The APNS device token.
+ @param isSandbox The flag indicating whether or not the app token provided is for Sandbox or
+ Production.
+ */
+- (nullable instancetype)initWithAppToken:(NSString *)appToken
+ isSandbox:(BOOL)isSandbox
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.m
new file mode 100644
index 0000000..7b4b469
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyClientRequest.m
@@ -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 "FIRVerifyClientRequest.h"
+
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kVerifyClientEndpoint
+ @brief The endpoint for the verifyClient request.
+ */
+static NSString *const kVerifyClientEndpoint = @"verifyClient";
+
+/** @var kAppTokenKey
+ @brief The key for the appToken request paramenter.
+ */
+static NSString *const kAPPTokenKey = @"appToken";
+
+/** @var kIsSandboxKey
+ @brief The key for the isSandbox request parameter
+ */
+static NSString *const kIsSandboxKey = @"isSandbox";
+
+@implementation FIRVerifyClientRequest
+
+- (nullable instancetype)initWithAppToken:(NSString *)appToken
+ isSandbox:(BOOL)isSandbox
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kVerifyClientEndpoint APIKey:APIKey];
+ if (self) {
+ _appToken = appToken;
+ _isSandbox = isSandbox;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing _Nullable *)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_appToken) {
+ postBody[kAPPTokenKey] = _appToken;
+ }
+ if (_isSandbox) {
+ postBody[kIsSandboxKey] = @YES;
+ }
+ return postBody;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.h
new file mode 100644
index 0000000..794256a
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.h
@@ -0,0 +1,38 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRVerifyClientResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property receipt
+ @brief Receipt that the APNS token was successfully validated with APNS.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *receipt;
+
+/** @property suggestedTimeOut
+ @brief The date after which delivery of the silent push notification is considered to have
+ failed.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *suggestedTimeOutDate;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.m
new file mode 100644
index 0000000..c2477d2
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyClientResponse.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 "FIRVerifyClientResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRVerifyClientResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _receipt = dictionary[@"receipt"];
+ _suggestedTimeOutDate = [dictionary[@"suggestedTimeout"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"suggestedTimeout"] doubleValue]] : nil;
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.h
new file mode 100644
index 0000000..20f3f4d
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyCustomTokenRequest
+ @brief Represents the parameters for the verifyCustomToken endpoint.
+ */
+@interface FIRVerifyCustomTokenRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property token
+ @brief The self-signed token from the client's BYOAuth server.
+ */
+@property(nonatomic, copy, readonly) NSString *token;
+
+/** @property returnSecureToken
+ @brief Whether the response should return access token and refresh token directly.
+ @remarks The default value is @c YES .
+ */
+@property(nonatomic, assign) BOOL returnSecureToken;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithToken:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithToken:APIKey:
+ @brief Designated initializer.
+ @param token The self-signed token from the client's BYOAuth server.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithToken:(NSString *)token
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m
new file mode 100644
index 0000000..63d72d1
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.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 "FIRVerifyCustomTokenRequest.h"
+
+/** @var kVerifyCustomTokenEndpoint
+ @brief The "verifyPassword" endpoint.
+ */
+static NSString *const kVerifyCustomTokenEndpoint = @"verifyCustomToken";
+
+/** @var kTokenKey
+ @brief The key for the "token" value in the request.
+ */
+static NSString *const kTokenKey = @"token";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@implementation FIRVerifyCustomTokenRequest
+
+- (nullable instancetype)initWithToken:(NSString *)token
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kVerifyCustomTokenEndpoint APIKey:APIKey];
+ if (self) {
+ _token = [token copy];
+ _returnSecureToken = YES;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *body = [@{
+ kTokenKey : _token
+ } mutableCopy];
+ if (_returnSecureToken) {
+ body[kReturnSecureTokenKey] = @YES;
+ }
+ return body;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.h
new file mode 100644
index 0000000..b8c215c
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyCustomTokenResponse
+ @brief Represents the response from the verifyCustomToken endpoint.
+ */
+@interface FIRVerifyCustomTokenResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m
new file mode 100644
index 0000000..f86d94b
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.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 "FIRVerifyCustomTokenResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRVerifyCustomTokenResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _IDToken = [dictionary[@"idToken"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.h
new file mode 100644
index 0000000..ba54bce
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.h
@@ -0,0 +1,79 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyPasswordRequest
+ @brief Represents the parameters for the verifyPassword endpoint.
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyPassword
+ */
+@interface FIRVerifyPasswordRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property email
+ @brief The email of the user.
+ */
+@property(nonatomic, copy) NSString *email;
+
+/** @property password
+ @brief The password inputed by the user.
+ */
+@property(nonatomic, copy) NSString *password;
+
+/** @property pendingIDToken
+ @brief The GITKit token for the non-trusted IDP, which is to be confirmed by the user.
+ */
+@property(nonatomic, copy, nullable) NSString *pendingIDToken;
+
+/** @property captchaChallenge
+ @brief The captcha challenge.
+ */
+@property(nonatomic, copy, nullable) NSString *captchaChallenge;
+
+/** @property captchaResponse
+ @brief Response to the captcha.
+ */
+@property(nonatomic, copy, nullable) NSString *captchaResponse;
+
+/** @property returnSecureToken
+ @brief Whether the response should return access token and refresh token directly.
+ @remarks The default value is @c YES .
+ */
+@property(nonatomic, assign) BOOL returnSecureToken;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithEmail:password:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithEmail:password:APIKey:
+ @brief Designated initializer.
+ @param email The email of the user.
+ @param password The password inputed by the user.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithEmail:(NSString *)email
+ password:(NSString *)password
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m
new file mode 100644
index 0000000..7a9da8b
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m
@@ -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 "FIRVerifyPasswordRequest.h"
+
+/** @var kVerifyPasswordEndpoint
+ @brief The "verifyPassword" endpoint.
+ */
+static NSString *const kVerifyPasswordEndpoint = @"verifyPassword";
+
+/** @var kEmailKey
+ @brief The key for the "email" value in the request.
+ */
+static NSString *const kEmailKey = @"email";
+
+/** @var kPasswordKey
+ @brief The key for the "password" value in the request.
+ */
+static NSString *const kPasswordKey = @"password";
+
+/** @var kPendingIDTokenKey
+ @brief The key for the "pendingIdToken" value in the request.
+ */
+static NSString *const kPendingIDTokenKey = @"pendingIdToken";
+
+/** @var kCaptchaChallengeKey
+ @brief The key for the "captchaChallenge" value in the request.
+ */
+static NSString *const kCaptchaChallengeKey = @"captchaChallenge";
+
+/** @var kCaptchaResponseKey
+ @brief The key for the "captchaResponse" value in the request.
+ */
+static NSString *const kCaptchaResponseKey = @"captchaResponse";
+
+/** @var kReturnSecureTokenKey
+ @brief The key for the "returnSecureToken" value in the request.
+ */
+static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
+
+@implementation FIRVerifyPasswordRequest
+
+- (nullable instancetype)initWithEmail:(NSString *)email
+ password:(NSString *)password
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kVerifyPasswordEndpoint APIKey:APIKey];
+ if (self) {
+ _email = [email copy];
+ _password = [password copy];
+ _returnSecureToken = YES;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_email) {
+ postBody[kEmailKey] = _email;
+ }
+ if (_password) {
+ postBody[kPasswordKey] = _password;
+ }
+ if (_pendingIDToken) {
+ postBody[kPendingIDTokenKey] = _pendingIDToken;
+ }
+ if (_captchaChallenge) {
+ postBody[kCaptchaChallengeKey] = _captchaChallenge;
+ }
+ if (_captchaResponse) {
+ postBody[kCaptchaResponseKey] = _captchaResponse;
+ }
+ if (_returnSecureToken) {
+ postBody[kReturnSecureTokenKey] = @YES;
+ }
+ return postBody;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.h
new file mode 100644
index 0000000..bed13be
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRVerifyPasswordResponse
+ @brief Represents the response from the verifyPassword endpoint.
+ @remarks Possible error codes:
+ - FIRAuthInternalErrorCodeUserDisabled
+ - FIRAuthInternalErrorCodeEmailNotFound
+ @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyPassword
+ */
+@interface FIRVerifyPasswordResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property localID
+ @brief The RP local ID if it's already been mapped to the IdP account identified by the
+ federated ID.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *localID;
+
+/** @property email
+ @brief The email returned by the IdP. NOTE: The federated login user may not own the email.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *email;
+
+/** @property displayName
+ @brief The display name of the user.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *displayName;
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+/** @property photoURL
+ @brief The URI of the public accessible profile picture.
+ */
+@property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m
new file mode 100644
index 0000000..4d0ca10
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m
@@ -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 "FIRVerifyPasswordResponse.h"
+
+#import "../Private/FIRAuthErrorUtils.h"
+
+@implementation FIRVerifyPasswordResponse
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _localID = [dictionary[@"localId"] copy];
+ _email = [dictionary[@"email"] copy];
+ _displayName = [dictionary[@"displayName"] copy];
+ _IDToken = [dictionary[@"idToken"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ _photoURL = dictionary[@"photoUrl"] ? [NSURL URLWithString:dictionary[@"photoUrl"]] : nil;
+ return YES;
+}
+
+@end
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.h
new file mode 100644
index 0000000..06039b9
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.h
@@ -0,0 +1,78 @@
+/*
+ * 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 "FIRIdentityToolkitRequest.h"
+
+#import "FIRAuthRPCRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRVerifyPhoneNumberRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+/** @property verificationID
+ @brief The verification ID obtained from the response of @c sendVerificationCode.
+*/
+@property(nonatomic, readonly, nullable) NSString *verificationID;
+
+/** @property verificationCode
+ @brief The verification code provided by the user.
+*/
+@property(nonatomic, readonly, nullable) NSString *verificationCode;
+
+/** @property accessToken
+ @brief The STS Access Token for the authenticated user.
+ */
+@property(nonatomic, copy, nullable) NSString *accessToken;
+
+/** @var temporaryProof
+ @brief The a temporary proof code pertaining to this credentil, returned from the backend.
+ */
+@property(nonatomic, readonly, nonnull) NSString *temporaryProof;
+
+/** @var phoneNumber
+ @brief The a phone number pertaining to this credential, returned from the backend.
+ */
+@property(nonatomic, readonly, nonnull) NSString *phoneNumber;
+
+/** @fn initWithEndpoint:APIKey:
+ @brief Please use initWithPhoneNumber:APIKey:
+ */
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+ APIKey:(NSString *)APIKey NS_UNAVAILABLE;
+
+/** @fn initWithTemporaryProof:phoneNumberAPIKey
+ @brief Designated initializer.
+ @param temporaryProof The temporary proof sent by the backed.
+ @param phoneNumber The phone number associated with the credential to be signed in.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithTemporaryProof:(NSString *)temporaryProof
+ phoneNumber:(NSString *)phoneNumber
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+/** @fn initWithVerificationID:verificationCode:APIKey
+ @brief Designated initializer.
+ @param verificationID The verification ID obtained from the response of @c sendVerificationCode.
+ @param verificationCode The verification code provided by the user.
+ @param APIKey The client's API Key.
+ */
+- (nullable instancetype)initWithVerificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode
+ APIKey:(NSString *)APIKey NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.m
new file mode 100644
index 0000000..b3d1054
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberRequest.m
@@ -0,0 +1,97 @@
+/*
+ * 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 "FIRVerifyPhoneNumberRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kVerifyPhoneNumberEndPoint
+ @brief The "verifyPhoneNumber" endpoint.
+ */
+static NSString *const kVerifyPhoneNumberEndPoint = @"verifyPhoneNumber";
+
+/** @var kVerificationIDKey
+ @brief The key for the verification ID parameter in the request.
+ */
+static NSString *const kVerificationIDKey = @"sessionInfo";
+
+/** @var kVerificationCodeKey
+ @brief The key for the verification code parameter in the request.
+ */
+static NSString *const kVerificationCodeKey = @"code";
+
+/** @var kIDTokenKey
+ @brief The key for the "ID Token" value in the request.
+ */
+static NSString *const kIDTokenKey = @"idToken";
+
+/** @var kTemporaryProofKey
+ @brief The key for the temporary proof value in the request.
+ */
+static NSString *const kTemporaryProofKey = @"temporaryProof";
+
+/** @var kPhoneNumberKey
+ @brief The key for the phone number value in the request.
+ */
+static NSString *const kPhoneNumberKey = @"phoneNumber";
+
+@implementation FIRVerifyPhoneNumberRequest
+
+- (nullable instancetype)initWithTemporaryProof:(NSString *)temporaryProof
+ phoneNumber:(NSString *)phoneNumber
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kVerifyPhoneNumberEndPoint APIKey:APIKey];
+ if (self) {
+ _temporaryProof = [temporaryProof copy];
+ _phoneNumber = [phoneNumber copy];
+ }
+ return self;
+}
+
+- (nullable instancetype)initWithVerificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode
+ APIKey:(NSString *)APIKey {
+ self = [super initWithEndpoint:kVerifyPhoneNumberEndPoint APIKey:APIKey];
+ if (self) {
+ _verificationID = verificationID;
+ _verificationCode = verificationCode;
+ }
+ return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing _Nullable *)error {
+ NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+ if (_verificationID) {
+ postBody[kVerificationIDKey] = _verificationID;
+ }
+ if (_verificationCode) {
+ postBody[kVerificationCodeKey] = _verificationCode;
+ }
+ if (_accessToken) {
+ postBody[kIDTokenKey] = _accessToken;
+ }
+ if (_temporaryProof) {
+ postBody[kTemporaryProofKey] = _temporaryProof;
+ }
+ if (_phoneNumber) {
+ postBody[kPhoneNumberKey] = _phoneNumber;
+ }
+ return postBody;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.h
new file mode 100644
index 0000000..f8f4ba2
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRVerifyPhoneNumberResponse : NSObject <FIRAuthRPCResponse>
+
+/** @property IDToken
+ @brief Either an authorization code suitable for performing an STS token exchange, or the
+ access token from Secure Token Service, depending on whether @c returnSecureToken is set
+ on the request.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *IDToken;
+
+/** @property refreshToken
+ @brief The refresh token from Secure Token Service.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *refreshToken;
+
+/** @property localID
+ @brief The Firebear user ID.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *localID;
+
+/** @property phoneNumber
+ @brief The verified phone number.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *phoneNumber;
+
+/** @property temporaryProof
+ @brief The temporary proof code returned by the backend.
+ */
+@property(nonatomic, strong, readonly, nullable) NSString *temporaryProof;
+
+/** @property isNewUser
+ @brief Flag indicating that the user signing in is a new user and not a returning user.
+ */
+@property(nonatomic, assign) BOOL isNewUser;
+
+/** @property approximateExpirationDate
+ @brief The approximate expiration date of the access token.
+ */
+@property(nonatomic, copy, readonly, nullable) NSDate *approximateExpirationDate;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.m
new file mode 100644
index 0000000..acba2c2
--- /dev/null
+++ b/Firebase/Auth/Source/RPCs/FIRVerifyPhoneNumberResponse.m
@@ -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 "FIRVerifyPhoneNumberResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRVerifyPhoneNumberResponse
+
+- (nullable NSString *)expectedKind {
+ return nil;
+}
+
+- (BOOL)setWithDictionary:(NSDictionary *)dictionary
+ error:(NSError *_Nullable *_Nullable)error {
+ _IDToken = [dictionary[@"idToken"] copy];
+ _refreshToken = [dictionary[@"refreshToken"] copy];
+ _isNewUser = [dictionary[@"isNewUser"] boolValue];
+ _localID = [dictionary[@"localId"] copy];
+ _phoneNumber = [dictionary[@"phoneNumber"] copy];
+ _temporaryProof = [dictionary[@"temporaryProof"] copy];
+ _approximateExpirationDate = [dictionary[@"expiresIn"] isKindOfClass:[NSString class]] ?
+ [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/FIRAnalyticsConfiguration.h b/Firebase/Core/FIRAnalyticsConfiguration.h
new file mode 100644
index 0000000..f42eaf5
--- /dev/null
+++ b/Firebase/Core/FIRAnalyticsConfiguration.h
@@ -0,0 +1,54 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRCoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * This class provides configuration fields for Firebase Analytics.
+ */
+FIR_SWIFT_NAME(AnalyticsConfiguration)
+@interface FIRAnalyticsConfiguration : NSObject
+
+/**
+ * Returns the shared instance of FIRAnalyticsConfiguration.
+ */
++ (FIRAnalyticsConfiguration *)sharedInstance FIR_SWIFT_NAME(shared());
+
+/**
+ * Sets the minimum engagement time in seconds required to start a new session. The default value
+ * is 10 seconds.
+ */
+- (void)setMinimumSessionInterval:(NSTimeInterval)minimumSessionInterval;
+
+/**
+ * Sets the interval of inactivity in seconds that terminates the current session. The default
+ * value is 1800 seconds (30 minutes).
+ */
+- (void)setSessionTimeoutInterval:(NSTimeInterval)sessionTimeoutInterval;
+
+/**
+ * Sets whether analytics collection is enabled for this app on this device. This setting is
+ * persisted across app sessions. By default it is enabled.
+ */
+- (void)setAnalyticsCollectionEnabled:(BOOL)analyticsCollectionEnabled;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/FIRAnalyticsConfiguration.m b/Firebase/Core/FIRAnalyticsConfiguration.m
new file mode 100644
index 0000000..cec3771
--- /dev/null
+++ b/Firebase/Core/FIRAnalyticsConfiguration.m
@@ -0,0 +1,61 @@
+// 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 "FIRAnalyticsConfiguration.h"
+
+#import "Private/FIRAnalyticsConfiguration+Internal.h"
+
+@implementation FIRAnalyticsConfiguration
+
++ (FIRAnalyticsConfiguration *)sharedInstance {
+ static FIRAnalyticsConfiguration *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FIRAnalyticsConfiguration alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (void)postNotificationName:(NSString *)name value:(id)value {
+ if (!name.length || !value) {
+ return;
+ }
+ [[NSNotificationCenter defaultCenter] postNotificationName:name
+ object:self
+ userInfo:@{ name : value }];
+}
+
+- (void)setMinimumSessionInterval:(NSTimeInterval)minimumSessionInterval {
+ [self postNotificationName:kFIRAnalyticsConfigurationSetMinimumSessionIntervalNotification
+ value:@(minimumSessionInterval)];
+}
+
+- (void)setSessionTimeoutInterval:(NSTimeInterval)sessionTimeoutInterval {
+ [self postNotificationName:kFIRAnalyticsConfigurationSetSessionTimeoutIntervalNotification
+ value:@(sessionTimeoutInterval)];
+}
+
+- (void)setAnalyticsCollectionEnabled:(BOOL)analyticsCollectionEnabled {
+ // Persist the measurementEnabledState. Use FIRAnalyticsEnabledState values instead of YES/NO.
+ FIRAnalyticsEnabledState analyticsEnabledState =
+ analyticsCollectionEnabled ? kFIRAnalyticsEnabledStateSetYes : kFIRAnalyticsEnabledStateSetNo;
+ [[NSUserDefaults standardUserDefaults] setObject:@(analyticsEnabledState)
+ forKey:kFIRAPersistedConfigMeasurementEnabledStateKey];
+ [[NSUserDefaults standardUserDefaults] synchronize];
+
+ [self postNotificationName:kFIRAnalyticsConfigurationSetEnabledNotification
+ value:@(analyticsCollectionEnabled)];
+}
+
+@end
diff --git a/Firebase/Core/FIRApp.h b/Firebase/Core/FIRApp.h
new file mode 100644
index 0000000..7f1d0c7
--- /dev/null
+++ b/Firebase/Core/FIRApp.h
@@ -0,0 +1,126 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+#import "FIRCoreSwiftNameSupport.h"
+
+@class FIROptions;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** A block that takes a BOOL and has no return value. */
+typedef void (^FIRAppVoidBoolCallback)(BOOL success) FIR_SWIFT_NAME(FirebaseAppVoidBoolCallback);
+
+/**
+ * The entry point of Firebase SDKs.
+ *
+ * Initialize and configure FIRApp using +[FIRApp configure]
+ * or other customized ways as shown below.
+ *
+ * The logging system has two modes: default mode and debug mode. In default mode, only logs with
+ * log level Notice, Warning and Error will be sent to device. In debug mode, all logs will be sent
+ * to device. The log levels that Firebase uses are consistent with the ASL log levels.
+ *
+ * Enable debug mode by passing the -FIRDebugEnabled argument to the application. You can add this
+ * argument in the application's Xcode scheme. When debug mode is enabled via -FIRDebugEnabled,
+ * further executions of the application will also be in debug mode. In order to return to default
+ * mode, you must explicitly disable the debug mode with the application argument -FIRDebugDisabled.
+ *
+ * It is also possible to change the default logging level in code by calling setLoggerLevel: on
+ * the FIRConfiguration interface.
+ */
+FIR_SWIFT_NAME(FirebaseApp)
+@interface FIRApp : NSObject
+
+/**
+ * Configures a default Firebase app. Raises an exception if any configuration step fails. The
+ * default app is named "__FIRAPP_DEFAULT". This method should be called after the app is launched
+ * and before using Firebase services. This method is thread safe.
+ */
++ (void)configure;
+
+/**
+ * Configures the default Firebase app with the provided options. The default app is named
+ * "__FIRAPP_DEFAULT". Raises an exception if any configuration step fails. This method is thread
+ * safe.
+ *
+ * @param options The Firebase application options used to configure the service.
+ */
++ (void)configureWithOptions:(FIROptions *)options FIR_SWIFT_NAME(configure(options:));
+
+/**
+ * Configures a Firebase app with the given name and options. Raises an exception if any
+ * configuration step fails. This method is thread safe.
+ *
+ * @param name The application's name given by the developer. The name should should only contain
+ Letters, Numbers and Underscore.
+ * @param options The Firebase application options used to configure the services.
+ */
++ (void)configureWithName:(NSString *)name options:(FIROptions *)options
+ FIR_SWIFT_NAME(configure(name:options:));
+
+/**
+ * Returns the default app, or nil if the default app does not exist.
+ */
++ (nullable FIRApp *)defaultApp FIR_SWIFT_NAME(app());
+
+/**
+ * Returns a previously created FIRApp instance with the given name, or nil if no such app exists.
+ * This method is thread safe.
+ */
++ (nullable FIRApp *)appNamed:(NSString *)name FIR_SWIFT_NAME(app(name:));
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+/**
+ * Returns the set of all extant FIRApp instances, or nil if there are no FIRApp instances. This
+ * method is thread safe.
+ */
+@property(class, readonly, nullable) NSDictionary <NSString *, FIRApp *> *allApps;
+#else
+/**
+ * Returns the set of all extant FIRApp instances, or nil if there are no FIRApp instances. This
+ * method is thread safe.
+ */
++ (nullable NSDictionary <NSString *, FIRApp *> *)allApps FIR_SWIFT_NAME(allApps());
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+/**
+ * Cleans up the current FIRApp, freeing associated data and returning its name to the pool for
+ * future use. This method is thread safe.
+ */
+- (void)deleteApp:(FIRAppVoidBoolCallback)completion;
+
+/**
+ * FIRApp instances should not be initialized directly. Call +[FIRApp configure],
+ * +[FIRApp configureWithOptions:], or +[FIRApp configureWithNames:options:] directly.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Gets the name of this app.
+ */
+@property(nonatomic, copy, readonly) NSString *name;
+
+/**
+ * Gets a copy of the options for this app. These are non-modifiable.
+ */
+@property(nonatomic, copy, readonly) FIROptions *options;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/FIRApp.m b/Firebase/Core/FIRApp.m
new file mode 100644
index 0000000..86784a3
--- /dev/null
+++ b/Firebase/Core/FIRApp.m
@@ -0,0 +1,596 @@
+// 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 <sys/utsname.h>
+
+#import "FIRApp.h"
+#import "Private/FIRAppInternal.h"
+#import "Private/FIRBundleUtil.h"
+#import "FIRConfiguration.h"
+#import "Private/FIRLogger.h"
+#import "Private/FIROptionsInternal.h"
+
+NSString *const kFIRServiceAdMob = @"AdMob";
+NSString *const kFIRServiceAuth = @"Auth";
+NSString *const kFIRServiceCrash = @"Crash";
+NSString *const kFIRServiceDatabase = @"Database";
+NSString *const kFIRServiceDynamicLinks = @"DynamicLinks";
+NSString *const kFIRServiceInstanceID = @"InstanceID";
+NSString *const kFIRServiceInvites = @"Invites";
+NSString *const kFIRServiceMessaging = @"Messaging";
+NSString *const kFIRServiceMeasurement = @"Measurement";
+NSString *const kFIRServiceRemoteConfig = @"RemoteConfig";
+NSString *const kFIRServiceStorage = @"Storage";
+NSString *const kGGLServiceAnalytics = @"Analytics";
+NSString *const kGGLServiceSignIn = @"SignIn";
+
+NSString *const kFIRDefaultAppName = @"__FIRAPP_DEFAULT";
+NSString *const kFIRAppReadyToConfigureSDKNotification = @"FIRAppReadyToConfigureSDKNotification";
+NSString *const kFIRAppDeleteNotification = @"FIRAppDeleteNotification";
+NSString *const kFIRAppIsDefaultAppKey = @"FIRAppIsDefaultAppKey";
+NSString *const kFIRAppNameKey = @"FIRAppNameKey";
+NSString *const kFIRGoogleAppIDKey = @"FIRGoogleAppIDKey";
+
+NSString *const kFIRAppDiagnosticsNotification = @"FIRAppDiagnosticsNotification";
+
+NSString *const kFIRAppDiagnosticsConfigurationTypeKey = @"ConfigType";
+NSString *const kFIRAppDiagnosticsErrorKey = @"Error";
+NSString *const kFIRAppDiagnosticsFIRAppKey = @"FIRApp";
+NSString *const kFIRAppDiagnosticsSDKNameKey = @"SDKName";
+NSString *const kFIRAppDiagnosticsSDKVersionKey = @"SDKVersion";
+
+/**
+ * The URL to download plist files.
+ */
+static NSString *const kPlistURL = @"https://console.firebase.google.com/";
+
+@interface FIRApp ()
+
+@property(nonatomic) BOOL alreadySentConfigureNotification;
+
+@property(nonatomic) BOOL alreadySentDeleteNotification;
+
+@end
+
+@implementation FIRApp
+
+// This is necessary since our custom getter prevents `_options` from being created.
+@synthesize options = _options;
+
+static NSMutableDictionary *sAllApps;
+static FIRApp *sDefaultApp;
+
++ (void)configure {
+ FIROptions *options = [FIROptions defaultOptions];
+ if (!options) {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFIRAppDiagnosticsNotification
+ object:nil
+ userInfo:@{
+ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
+ kFIRAppDiagnosticsErrorKey : [FIRApp errorForMissingOptions]
+ }];
+ [NSException raise:kFirebaseCoreErrorDomain
+ format:@"[FIRApp configure] could not find a valid GoogleService-Info.plist in "
+ @"your project. Please download one from %@.",
+ kPlistURL];
+ }
+ [FIRApp configureDefaultAppWithOptions:options sendingNotifications:YES];
+}
+
++ (void)configureWithOptions:(FIROptions *)options {
+ if (!options) {
+ [NSException raise:kFirebaseCoreErrorDomain
+ format:@"Options is nil. Please pass a valid options."];
+ }
+ [FIRApp configureDefaultAppWithOptions:options sendingNotifications:YES];
+}
+
++ (void)configureWithoutSendingNotification {
+ FIROptions *options = [FIROptions defaultOptions];
+ if (!options) {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFIRAppDiagnosticsNotification
+ object:nil
+ userInfo:@{
+ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
+ kFIRAppDiagnosticsErrorKey : [FIRApp errorForMissingOptions]
+ }];
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Please check there is a valid "
+ @"GoogleService-Info.plist in the project."];
+ }
+ [FIRApp configureDefaultAppWithOptions:options sendingNotifications:NO];
+}
+
++ (void)configureDefaultAppWithOptions:(FIROptions *)options
+ sendingNotifications:(BOOL)sendNotifications {
+ if (sDefaultApp) {
+ // FIRApp sets up FirebaseAnalytics and does plist validation, but does not cause it
+ // to fire notifications. So, if the default app already exists, but has not sent out
+ // configuration notifications, then continue re-initializing it.
+ if (!sendNotifications || sDefaultApp.alreadySentConfigureNotification) {
+ [NSException raise:kFirebaseCoreErrorDomain
+ format:@"Default app has already been configured."];
+ }
+ }
+ @synchronized(self) {
+ FIRLogDebug(kFIRLoggerCore, @"I-COR000001", @"Configuring the default app.");
+ sDefaultApp = [[FIRApp alloc] initInstanceWithName:kFIRDefaultAppName options:options];
+ [FIRApp addAppToAppDictionary:sDefaultApp];
+ if (!sDefaultApp.alreadySentConfigureNotification && sendNotifications) {
+ [FIRApp sendNotificationsToSDKs:sDefaultApp];
+ sDefaultApp.alreadySentConfigureNotification = YES;
+ }
+ }
+}
+
++ (void)configureWithName:(NSString *)name options:(FIROptions *)options {
+ if (!name || !options) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Neither name nor options can be nil."];
+ }
+ if (name.length == 0) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Name cannot be empty."];
+ }
+ if ([name isEqualToString:kFIRDefaultAppName]) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Name cannot be __FIRAPP_DEFAULT."];
+ }
+ NSString *lowerCaseName = [name lowercaseString];
+ for (NSInteger charIndex = 0; charIndex < lowerCaseName.length; charIndex++) {
+ char character = [lowerCaseName characterAtIndex:charIndex];
+ if (!((character >= 'a' && character <= 'z')
+ || (character >= '0' && character <= '9')
+ || character == '_'
+ || character == '-')) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"App name should only contain Letters, "
+ @"Numbers, Underscores, and Dashes."];
+ }
+ }
+
+ if (sAllApps && sAllApps[name]) {
+ [NSException raise:kFirebaseCoreErrorDomain
+ format:@"App named %@ has already been configured.", name];
+ }
+
+ @synchronized(self) {
+ FIRLogDebug(kFIRLoggerCore, @"I-COR000002", @"Configuring app named %@", name);
+ FIRApp *app = [[FIRApp alloc] initInstanceWithName:name options:options];
+ [FIRApp addAppToAppDictionary:app];
+ if (!app.alreadySentConfigureNotification) {
+ [FIRApp sendNotificationsToSDKs:app];
+ app.alreadySentConfigureNotification = YES;
+ }
+ }
+}
+
++ (FIRApp *)defaultApp {
+ if (sDefaultApp) {
+ return sDefaultApp;
+ }
+ FIRLogError(kFIRLoggerCore, @"I-COR000003", @"The default Firebase app has not yet been "
+ @"configured. Add [FIRApp configure] to your application initialization. Read more: "
+ @"https://goo.gl/ctyzm8.");
+ return nil;
+}
+
++ (FIRApp *)appNamed:(NSString *)name {
+ @synchronized(self) {
+ if (sAllApps) {
+ FIRApp *app = sAllApps[name];
+ if (app) {
+ return app;
+ }
+ }
+ FIRLogError(kFIRLoggerCore, @"I-COR000004", @"App with name %@ does not exist.", name);
+ return nil;
+ }
+}
+
++ (NSDictionary *)allApps {
+ @synchronized(self) {
+ if (!sAllApps) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000005", @"No app has been configured yet.");
+ }
+ NSDictionary *dict = [NSDictionary dictionaryWithDictionary:sAllApps];
+ return dict;
+ }
+}
+
+// Public only for tests
++ (void)resetApps {
+ sDefaultApp = nil;
+ [sAllApps removeAllObjects];
+ sAllApps = nil;
+}
+
+- (void)deleteApp:(FIRAppVoidBoolCallback)completion {
+ @synchronized([self class]) {
+ if (sAllApps && sAllApps[self.name]) {
+ FIRLogDebug(kFIRLoggerCore, @"I-COR000006", @"Deleting app named %@", self.name);
+ [sAllApps removeObjectForKey:self.name];
+ if ([self.name isEqualToString:kFIRDefaultAppName]) {
+ sDefaultApp = nil;
+ }
+ if (!self.alreadySentDeleteNotification) {
+ NSDictionary *appInfoDict = @ {
+ kFIRAppNameKey : self.name
+ };
+ [[NSNotificationCenter defaultCenter] postNotificationName:kFIRAppDeleteNotification
+ object:[self class]
+ userInfo:appInfoDict];
+ self.alreadySentDeleteNotification = YES;
+ }
+ completion(YES);
+ } else {
+ FIRLogError(kFIRLoggerCore, @"I-COR000007", @"App does not exist.");
+ completion(NO);
+ }
+ }
+}
+
++ (void)addAppToAppDictionary:(FIRApp *)app {
+ if (!sAllApps) {
+ sAllApps = [NSMutableDictionary dictionary];
+ }
+ if ([app configureCore]) {
+ sAllApps[app.name] = app;
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFIRAppDiagnosticsNotification
+ object:nil
+ userInfo:@{
+ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
+ kFIRAppDiagnosticsFIRAppKey : app
+ }];
+ } else {
+ [NSException raise:kFirebaseCoreErrorDomain
+ format:@"Configuration fails. It may be caused by an invalid GOOGLE_APP_ID in "
+ @"GoogleService-Info.plist or set in the customized options."];
+ }
+}
+
+- (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options {
+ self = [super init];
+ if (self) {
+ _name = [name copy];
+ _options = [options copy];
+ _options.editingLocked = YES;
+
+ FIRApp *app = sAllApps[name];
+ _alreadySentConfigureNotification = app.alreadySentConfigureNotification;
+ _alreadySentDeleteNotification = app.alreadySentDeleteNotification;
+ }
+ return self;
+}
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(FIRTokenCallback)callback {
+ if (!_getTokenImplementation) {
+ callback(nil, nil);
+ return;
+ }
+
+ _getTokenImplementation(forceRefresh, callback);
+}
+
+- (BOOL)configureCore {
+ [self checkExpectedBundleID];
+ if (![self isAppIDValid]) {
+ if (_options.usingOptionsFromDefaultPlist) {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFIRAppDiagnosticsNotification
+ object:nil
+ userInfo:@{
+ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
+ kFIRAppDiagnosticsErrorKey : [FIRApp errorForInvalidAppID],
+ }];
+ }
+ return NO;
+ }
+
+ if (NSClassFromString(@"FIRAppIndexing") != nil) {
+ FIRLogDebug(kFIRLoggerCore, @"I-COR000024", @"Firebase App Indexing on iOS is deprecated. "
+ @"You don't need to take any action at this time. Learn more about Firebase App "
+ @"Indexing at https://firebase.google.com/docs/app-indexing/.");
+ }
+
+ // Initialize the Analytics once there is a valid options under default app. Analytics should
+ // always initialize first by itself before the other SDKs.
+ if ([self.name isEqualToString:kFIRDefaultAppName]) {
+ Class firAnalyticsClass = NSClassFromString(@"FIRAnalytics");
+ if (!firAnalyticsClass) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000022", @"Firebase Analytics is not available.");
+ } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+ SEL startWithConfigurationSelector = @selector(startWithConfiguration:options:);
+#pragma clang diagnostic pop
+ if ([firAnalyticsClass respondsToSelector:startWithConfigurationSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ [firAnalyticsClass performSelector:startWithConfigurationSelector
+ withObject:[FIRConfiguration sharedInstance].analyticsConfiguration
+ withObject:_options];
+#pragma clang diagnostic pop
+ }
+ }
+ }
+ return YES;
+}
+
+- (FIROptions *)options {
+ return [_options copy];
+}
+
+#pragma mark - private
+
++ (void)sendNotificationsToSDKs:(FIRApp *)app {
+ NSNumber *isDefaultApp = [NSNumber numberWithBool:(app == sDefaultApp)];
+ NSDictionary *appInfoDict = @ {
+ kFIRAppNameKey : app.name,
+ kFIRAppIsDefaultAppKey : isDefaultApp,
+ kFIRGoogleAppIDKey : app.options.googleAppID
+ };
+ [[NSNotificationCenter defaultCenter] postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:self
+ userInfo:appInfoDict];
+}
+
++ (NSError *)errorForMissingOptions {
+ NSDictionary *errorDict = @{
+ NSLocalizedDescriptionKey :
+ @"Unable to parse GoogleService-Info.plist in order to configure services.",
+ NSLocalizedRecoverySuggestionErrorKey :
+ @"Check formatting and location of GoogleService-Info.plist."
+ };
+ return FIRCreateError(kFirebaseCoreErrorDomain, FIRErrorCodeInvalidPlistFile, errorDict);
+}
+
++ (NSError *)errorForSubspecConfigurationFailureWithDomain:(NSString *)domain
+ errorCode:(FIRErrorCode)code
+ service:(NSString *)service
+ reason:(NSString *)reason {
+ NSString *description =
+ [NSString stringWithFormat:@"Configuration failed for service %@.", service];
+ NSDictionary *errorDict = @{
+ NSLocalizedDescriptionKey : description,
+ NSLocalizedFailureReasonErrorKey : reason
+ };
+ return FIRCreateError(domain, code, errorDict);
+}
+
++ (NSError *)errorForInvalidAppID {
+ NSDictionary *errorDict = @{
+ NSLocalizedDescriptionKey :
+ @"Unable to validate Google App ID",
+ NSLocalizedRecoverySuggestionErrorKey :
+ @"Check formatting and location of GoogleService-Info.plist or GoogleAppID set in the "
+ @"customized options."
+ };
+ return FIRCreateError(kFirebaseCoreErrorDomain, FIRErrorCodeInvalidAppID, errorDict);
+}
+
+- (void)checkExpectedBundleID {
+ NSArray *bundles = [FIRBundleUtil relevantBundles];
+ 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]) {
+ FIRLogInfo(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 "
+ @"using a customized options. To ensure that everything can be configured "
+ @"correctly, you may need to make the Bundle IDs consistent. To continue with this "
+ @"plist file, you may change your app's bundle identifier to '%@'. Or you can "
+ @"download a new configuration file that matches your bundle identifier from %@ "
+ @"and replace the current one.", kServiceInfoFileName, kServiceInfoFileType,
+ expectedBundleID, kPlistURL);
+ }
+}
+
+- (nullable NSString *)getUID {
+ if (!_getUIDImplementation) {
+ FIRLogWarning(kFIRLoggerCore, @"I-COR000025", @"FIRAuth getUID implementation wasn't set.");
+ return nil;
+ }
+ return _getUIDImplementation();
+}
+
+#pragma mark - private - App ID Validation
+
+/**
+ * Validates the format and fingerprint of the app ID contained in GOOGLE_APP_ID in the plist file.
+ * This is the main method for validating app ID.
+ *
+ * @return YES if the app ID fulfills the expected format and fingerprint, NO otherwise.
+ */
+- (BOOL)isAppIDValid {
+ NSString *appID = _options.googleAppID;
+ BOOL isValid = [FIRApp validateAppID:appID];
+ if (!isValid){
+ NSString *expectedBundleID = [self expectedBundleID];
+ FIRLogError(kFIRLoggerCore, @"I-COR000009", @"The GOOGLE_APP_ID either in the plist file "
+ @"'%@.%@' or the one set in the customized options is invalid. If you are using "
+ @"the plist file, use the iOS version of bundle identifier to download the file, "
+ @"and do not manually edit the GOOGLE_APP_ID. You may change your app's bundle "
+ @"identifier to '%@'. Or you can download a new configuration file that matches "
+ @"your bundle identifier from %@ and replace the current one.",
+ kServiceInfoFileName, kServiceInfoFileType, expectedBundleID, kPlistURL);
+
+ };
+ return isValid;
+}
+
++ (BOOL)validateAppID:(NSString *)appID {
+ // Failing validation only occurs when we are sure we are looking at a V2 app ID and it does not
+ // have a valid fingerprint, otherwise we just warn about the potential issue.
+ if (!appID.length) {
+ return NO;
+ }
+
+ // All app IDs must start with at least "<version number>:".
+ NSString *const versionPattern = @"^\\d+:";
+ NSRegularExpression *versionRegex =
+ [NSRegularExpression regularExpressionWithPattern:versionPattern options:0 error:NULL];
+ if (!versionRegex) {
+ return NO;
+ }
+
+ NSRange appIDRange = NSMakeRange(0, appID.length);
+ NSArray *versionMatches = [versionRegex matchesInString:appID options:0 range:appIDRange];
+ if (versionMatches.count != 1) {
+ return NO;
+ }
+
+ NSRange versionRange = [(NSTextCheckingResult *)versionMatches.firstObject range];
+ NSString *appIDVersion = [appID substringWithRange:versionRange];
+ NSArray *knownVersions = @[ @"1:" ];
+ if (![knownVersions containsObject:appIDVersion]) {
+ // Permit unknown yet properly formatted app ID versions.
+ return YES;
+ }
+
+ if (![FIRApp validateAppIDFormat:appID withVersion:appIDVersion]) {
+ return NO;
+ }
+
+ if (![FIRApp validateAppIDFingerprint:appID withVersion:appIDVersion]) {
+ return NO;
+ }
+
+ return YES;
+}
+
++ (NSString *)actualBundleID {
+ return [[NSBundle mainBundle] bundleIdentifier];
+}
+
+/**
+ * Validates that the format of the app ID string is what is expected based on the supplied version.
+ * The version must end in ":".
+ *
+ * For v1 app ids the format is expected to be
+ * '<version #>:<project number>:ios:<fingerprint of bundle id>'.
+ *
+ * This method does not verify that the contents of the app id are correct, just that they fulfill
+ * the expected format.
+ *
+ * @param appID Contents of GOOGLE_APP_ID from the plist file.
+ * @param version Indicates what version of the app id format this string should be.
+ * @return YES if provided string fufills the expected format, NO otherwise.
+ */
++ (BOOL)validateAppIDFormat:(NSString *)appID withVersion:(NSString *)version {
+ if (!appID.length || !version.length) {
+ return NO;
+ }
+
+ if (![version hasSuffix:@":"]) {
+ return NO;
+ }
+
+ if (![appID hasPrefix:version]) {
+ return NO;
+ }
+
+ NSString *const pattern = @"^\\d+:ios:[a-f0-9]+$";
+ NSRegularExpression *regex =
+ [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
+ if (!regex) {
+ return NO;
+ }
+
+ NSRange localRange = NSMakeRange(version.length, appID.length - version.length);
+ NSUInteger numberOfMatches = [regex numberOfMatchesInString:appID options:0 range:localRange];
+ if (numberOfMatches != 1) {
+ 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 ":".
+ *
+ * Note that the v1 hash algorithm is not permitted on the client and cannot be fully validated.
+ *
+ * @param appID Contents of GOOGLE_APP_ID from the plist file.
+ * @param version Indicates what version of the app id format this string should be.
+ * @return YES if provided string fufills the expected fingerprint and the version is known, NO
+ * 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
+ // the version of the app ID.
+ NSArray *components = [appID componentsSeparatedByString:@":"];
+ if (components.count != 4) {
+ return NO;
+ }
+
+ NSString *suppliedFingerprintString = components[3];
+ if (!suppliedFingerprintString.length) {
+ return NO;
+ }
+
+ uint64_t suppliedFingerprint;
+ NSScanner *scanner = [NSScanner scannerWithString:suppliedFingerprintString];
+ if (![scanner scanHexLongLong:&suppliedFingerprint]) {
+ return NO;
+ }
+
+ if ([version isEqual:@"1:"]) {
+ // The v1 hash algorithm is not permitted on the client so the actual hash cannot be validated.
+ return YES;
+ }
+
+ // Unknown version.
+ return NO;
+}
+
+- (NSString *)expectedBundleID {
+ return _options.bundleID;
+}
+
+// end App ID validation
+#pragma mark
+
+- (void)sendLogsWithServiceName:(NSString *)serviceName
+ version:(NSString *)version
+ error:(NSError *)error{
+ NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithDictionary:@{
+ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeSDK),
+ kFIRAppDiagnosticsSDKNameKey : serviceName,
+ kFIRAppDiagnosticsSDKVersionKey : version,
+ kFIRAppDiagnosticsFIRAppKey : self
+ }];
+ if (error) {
+ userInfo[kFIRAppDiagnosticsErrorKey] = error;
+ }
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFIRAppDiagnosticsNotification
+ object:nil
+ userInfo:userInfo];
+}
+
+@end
diff --git a/Firebase/Core/FIRAppAssociationRegistration.m b/Firebase/Core/FIRAppAssociationRegistration.m
new file mode 100644
index 0000000..a36396d
--- /dev/null
+++ b/Firebase/Core/FIRAppAssociationRegistration.m
@@ -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 "Private/FIRAppAssociationRegistration.h"
+
+#import <objc/runtime.h>
+
+@implementation FIRAppAssociationRegistration
+
++ (nullable id)registeredObjectWithHost:(id)host
+ key:(NSString *)key
+ creationBlock:(id _Nullable (^)())creationBlock {
+ @synchronized(self) {
+ SEL dictKey = @selector(registeredObjectWithHost:key:creationBlock:);
+ NSMutableDictionary<NSString *, id> *objectsByKey = objc_getAssociatedObject(host, dictKey);
+ if (!objectsByKey) {
+ objectsByKey = [[NSMutableDictionary alloc] init];
+ objc_setAssociatedObject(host, dictKey, objectsByKey, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ }
+ id obj = objectsByKey[key];
+ NSValue *creationBlockBeingCalled = [NSValue valueWithPointer:dictKey];
+ if (obj) {
+ if ([creationBlockBeingCalled isEqual:obj]) {
+ [NSException raise:@"Reentering registeredObjectWithHost:key:creationBlock: not allowed"
+ format:@"host: %@ key: %@", host, key];
+ }
+ return obj;
+ }
+ objectsByKey[key] = creationBlockBeingCalled;
+ obj = creationBlock();
+ objectsByKey[key] = obj;
+ return obj;
+ }
+}
+
+@end
diff --git a/Firebase/Core/FIRAppEnvironmentUtil.m b/Firebase/Core/FIRAppEnvironmentUtil.m
new file mode 100644
index 0000000..b88b432
--- /dev/null
+++ b/Firebase/Core/FIRAppEnvironmentUtil.m
@@ -0,0 +1,207 @@
+// 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 "Private/FIRAppEnvironmentUtil.h"
+
+#import <dlfcn.h>
+#import <mach-o/dyld.h>
+#import <sys/utsname.h>
+
+/// The encryption info struct and constants are missing from the iPhoneSimulator SDK, but not from
+/// the iPhoneOS or Mac OS X SDKs. Since one doesn't ever ship a Simulator binary, we'll just
+/// provide the definitions here.
+#if TARGET_IPHONE_SIMULATOR && !defined(LC_ENCRYPTION_INFO)
+#define LC_ENCRYPTION_INFO 0x21
+struct encryption_info_command {
+ uint32_t cmd;
+ uint32_t cmdsize;
+ uint32_t cryptoff;
+ uint32_t cryptsize;
+ uint32_t cryptid;
+};
+#endif
+
+@implementation FIRAppEnvironmentUtil
+
+/// The file name of the sandbox receipt. This is available on iOS >= 8.0
+static NSString *const kFIRAIdentitySandboxReceiptFileName = @"sandboxReceipt";
+
+/// The following copyright from Landon J. Fuller applies to the isAppEncrypted function.
+///
+/// Copyright (c) 2017 Landon J. Fuller <landon@landonf.org>
+/// 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 <a href="http://iphonedevwiki.net/index.php/Crack_prevention">iPhone Dev Wiki
+/// Crack Prevention</a>:
+/// 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 <a href="http://landonf.org/2009/02/index.html">Landon Fuller's blog</a>
+static BOOL isAppEncrypted() {
+ const struct mach_header *executableHeader = NULL;
+ for (uint32_t i = 0; i < _dyld_image_count(); i++) {
+ const struct mach_header *header = _dyld_get_image_header(i);
+ if (header && header->filetype == MH_EXECUTE) {
+ executableHeader = header;
+ break;
+ }
+ }
+
+ if (!executableHeader) {
+ return NO;
+ }
+
+ BOOL is64bit = (executableHeader->magic == MH_MAGIC_64);
+ uintptr_t cursor = (uintptr_t)executableHeader +
+ (is64bit ? sizeof(struct mach_header_64) : sizeof(struct mach_header));
+ const struct segment_command *segmentCommand = NULL;
+ uint32_t i = 0;
+
+ while (i++ < executableHeader->ncmds) {
+ segmentCommand = (struct segment_command *)cursor;
+
+ if (!segmentCommand) {
+ continue;
+ }
+
+ if ((!is64bit && segmentCommand->cmd == LC_ENCRYPTION_INFO) ||
+ (is64bit && segmentCommand->cmd == LC_ENCRYPTION_INFO_64)) {
+ if (is64bit) {
+ struct encryption_info_command_64 *cryptCmd =
+ (struct encryption_info_command_64 *)segmentCommand;
+ return cryptCmd && cryptCmd->cryptid != 0;
+ } else {
+ struct encryption_info_command *cryptCmd = (struct encryption_info_command *)segmentCommand;
+ return cryptCmd && cryptCmd->cryptid != 0;
+ }
+ }
+ cursor += segmentCommand->cmdsize;
+ }
+
+ return NO;
+}
+
++ (BOOL)isFromAppStore {
+ static dispatch_once_t isEncryptedOnce;
+ static BOOL isEncrypted = NO;
+
+ dispatch_once(&isEncryptedOnce, ^{
+ isEncrypted = isAppEncrypted();
+ });
+
+ if ([FIRAppEnvironmentUtil isSimulator]) {
+ return NO;
+ }
+ if ([FIRAppEnvironmentUtil hasSCInfoFolder]) {
+ // When iTunes downloads a .ipa, it also gets a customized .sinf file which is added to the
+ // main SC_Info directory.
+ return YES;
+ }
+
+ // For iOS >= 8.0, iTunesMetadata.plist is moved outside of the sandbox. Any attempt to read
+ // the iTunesMetadata.plist outside of the sandbox will be rejected by Apple.
+ // If the app does not contain the sandboxReceipt file which means it is a TestFlight beta, and
+ // it does not contain the embedded.mobileprovision which is stripped out by Apple when the
+ // app is submitted to store, then it is highly likely that it is from Apple Store.
+ return isEncrypted && ![FIRAppEnvironmentUtil isAppStoreReceiptSandbox] &&
+ ![FIRAppEnvironmentUtil hasEmbeddedMobileProvision];
+}
+
++ (BOOL)isAppStoreReceiptSandbox {
+ NSURL *appStoreReceiptURL = [NSBundle mainBundle].appStoreReceiptURL;
+ NSString *appStoreReceiptFileName = appStoreReceiptURL.lastPathComponent;
+ return [appStoreReceiptFileName isEqualToString:kFIRAIdentitySandboxReceiptFileName];
+}
+
++ (BOOL)hasEmbeddedMobileProvision {
+ return [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"].length > 0;
+}
+
++ (BOOL)isSimulator {
+ NSString *platform = [FIRAppEnvironmentUtil deviceModel];
+ return [platform isEqual:@"x86_64"] || [platform isEqual:@"i386"];
+}
+
++ (NSString *)deviceModel {
+ static dispatch_once_t once;
+ static NSString *deviceModel;
+
+ dispatch_once(&once, ^{
+ struct utsname systemInfo;
+ if (uname(&systemInfo) == 0) {
+ deviceModel = [NSString stringWithUTF8String:systemInfo.machine];
+ }
+ });
+ return deviceModel;
+}
+
++ (NSString *)systemVersion {
+ return [UIDevice currentDevice].systemVersion;
+}
+
++ (BOOL)isAppExtension {
+ // Documented by <a href="https://goo.gl/RRB2Up">Apple</a>
+ BOOL appExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"];
+ return appExtension;
+}
+
++ (UIApplication *)sharedApplication {
+ if ([FIRAppEnvironmentUtil isAppExtension]) {
+ return nil;
+ }
+ id sharedApplication = nil;
+ Class uiApplicationClass = NSClassFromString(@"UIApplication");
+ if (uiApplicationClass &&
+ [uiApplicationClass respondsToSelector:(NSSelectorFromString(@"sharedApplication"))]) {
+ sharedApplication = [uiApplicationClass sharedApplication];
+ }
+ return sharedApplication;
+}
+
+#pragma mark - Helper methods
+
++ (BOOL)hasSCInfoFolder {
+ NSString *bundlePath = [NSBundle mainBundle].bundlePath;
+ NSString *scInfoPath = [bundlePath stringByAppendingPathComponent:@"SC_Info"];
+ return [[NSFileManager defaultManager] fileExistsAtPath:scInfoPath];
+}
+
+@end
diff --git a/Firebase/Core/FIRBundleUtil.m b/Firebase/Core/FIRBundleUtil.m
new file mode 100644
index 0000000..6c1d1e9
--- /dev/null
+++ b/Firebase/Core/FIRBundleUtil.m
@@ -0,0 +1,65 @@
+// 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 "Private/FIRBundleUtil.h"
+
+@implementation FIRBundleUtil
+
++ (NSArray *)relevantBundles {
+ return @[ [NSBundle mainBundle], [NSBundle bundleForClass:[self class]] ];
+}
+
++ (NSString *)optionsDictionaryPathWithResourceName:(NSString *)resourceName
+ andFileType:(NSString *)fileType
+ inBundles:(NSArray *)bundles {
+ // Loop through all bundles to find the config dict.
+ for (NSBundle *bundle in bundles) {
+ NSString *path = [bundle pathForResource:resourceName ofType:fileType];
+ // Use the first one we find.
+ if (path) {
+ return path;
+ }
+ }
+ return nil;
+}
+
++ (NSArray *)relevantURLSchemes {
+ NSMutableArray *result = [[NSMutableArray alloc] init];
+ for (NSBundle *bundle in [[self class] relevantBundles]) {
+ NSArray *urlTypes = [bundle objectForInfoDictionaryKey:@"CFBundleURLTypes"];
+ for (NSDictionary *urlType in urlTypes) {
+ [result addObjectsFromArray:urlType[@"CFBundleURLSchemes"]];
+ }
+ }
+ return result;
+}
+
++ (NSSet *)relevantBundleIdentifiers {
+ NSMutableSet *result = [[NSMutableSet alloc] init];
+ for (NSBundle *bundle in [[self class] relevantBundles]) {
+ [result addObject:[bundle bundleIdentifier]];
+ }
+ return result;
+}
+
++ (BOOL)hasBundleIdentifier:(NSString *)bundleIdentifier inBundles:(NSArray *)bundles {
+ for (NSBundle *bundle in bundles) {
+ if ([bundle.bundleIdentifier isEqualToString:bundleIdentifier]) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+@end
diff --git a/Firebase/Core/FIRConfiguration.h b/Firebase/Core/FIRConfiguration.h
new file mode 100644
index 0000000..496b211
--- /dev/null
+++ b/Firebase/Core/FIRConfiguration.h
@@ -0,0 +1,80 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRAnalyticsConfiguration.h"
+#import "FIRCoreSwiftNameSupport.h"
+#import "FIRLoggerLevel.h"
+
+/**
+ * The log levels used by FIRConfiguration.
+ */
+typedef NS_ENUM(NSInteger, FIRLogLevel) {
+ /** Error */
+ kFIRLogLevelError __deprecated = 0,
+ /** Warning */
+ kFIRLogLevelWarning __deprecated,
+ /** Info */
+ kFIRLogLevelInfo __deprecated,
+ /** Debug */
+ kFIRLogLevelDebug __deprecated,
+ /** Assert */
+ kFIRLogLevelAssert __deprecated,
+ /** Max */
+ kFIRLogLevelMax __deprecated = kFIRLogLevelAssert
+} DEPRECATED_MSG_ATTRIBUTE(
+ "Use -FIRDebugEnabled and -FIRDebugDisabled or setLoggerLevel. See FIRApp.h for more details.");
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * This interface provides global level properties that the developer can tweak, and the singleton
+ * of the Firebase Analytics configuration class.
+ */
+FIR_SWIFT_NAME(FirebaseConfiguration)
+@interface FIRConfiguration : NSObject
+
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+/** Returns the shared configuration object. */
+@property(class, nonatomic, readonly) FIRConfiguration *sharedInstance FIR_SWIFT_NAME(shared);
+#else
+/** Returns the shared configuration object. */
++ (FIRConfiguration *)sharedInstance FIR_SWIFT_NAME(shared());
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+/** The configuration class for Firebase Analytics. */
+@property(nonatomic, readwrite) FIRAnalyticsConfiguration *analyticsConfiguration;
+
+/** Global log level. Defaults to kFIRLogLevelError. */
+@property(nonatomic, readwrite, assign) FIRLogLevel logLevel DEPRECATED_MSG_ATTRIBUTE(
+ "Use -FIRDebugEnabled and -FIRDebugDisabled or setLoggerLevel. See FIRApp.h for more details.");
+
+/**
+ * Sets the logging level for internal Firebase logging. Firebase will only log messages
+ * that are logged at or below loggerLevel. The messages are logged both to the Xcode
+ * console and to the device's log. Note that if an app is running from AppStore, it will
+ * never log above FIRLoggerLevelNotice even if loggerLevel is set to a higher (more verbose)
+ * setting.
+ *
+ * @param loggerLevel The maximum logging level. The default level is set to FIRLoggerLevelNotice.
+ */
+- (void)setLoggerLevel:(FIRLoggerLevel)loggerLevel;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/FIRConfiguration.m b/Firebase/Core/FIRConfiguration.m
new file mode 100644
index 0000000..921aa48
--- /dev/null
+++ b/Firebase/Core/FIRConfiguration.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 "FIRConfiguration.h"
+
+extern void FIRSetLoggerLevel(FIRLoggerLevel loggerLevel);
+
+@implementation FIRConfiguration
+
++ (instancetype)sharedInstance {
+ static FIRConfiguration *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FIRConfiguration alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _analyticsConfiguration = [FIRAnalyticsConfiguration sharedInstance];
+ }
+ return self;
+}
+
+// This is deprecated, use setLoggerLevel instead.
+- (void)setLogLevel:(FIRLogLevel)logLevel {
+ NSAssert(logLevel <= kFIRLogLevelMax, @"Invalid log level, %ld", (long)logLevel);
+ _logLevel = logLevel;
+}
+
+- (void)setLoggerLevel:(FIRLoggerLevel)loggerLevel {
+ NSAssert(loggerLevel <= FIRLoggerLevelMax && loggerLevel >= FIRLoggerLevelMin,
+ @"Invalid logger level, %ld", (long)loggerLevel);
+ FIRSetLoggerLevel(loggerLevel);
+}
+
+
+@end
diff --git a/Firebase/Core/FIRCoreSwiftNameSupport.h b/Firebase/Core/FIRCoreSwiftNameSupport.h
new file mode 100644
index 0000000..f58bdd7
--- /dev/null
+++ b/Firebase/Core/FIRCoreSwiftNameSupport.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.
+ */
+
+#ifndef FIR_SWIFT_NAME
+
+#import <Foundation/Foundation.h>
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// // Wrap it in our own macro if it's a non-compatible SDK.
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+
+#endif // FIR_SWIFT_NAME
diff --git a/Firebase/Core/FIRErrors.m b/Firebase/Core/FIRErrors.m
new file mode 100644
index 0000000..3c7e39a
--- /dev/null
+++ b/Firebase/Core/FIRErrors.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 "Private/FIRErrors.h"
+
+NSString *const kFirebaseErrorDomain = @"com.firebase";
+NSString *const kFirebaseAdMobErrorDomain = @"com.firebase.admob";
+NSString *const kFirebaseAppInviteErrorDomain = @"com.firebase.appinvite";
+NSString *const kFirebaseAuthErrorDomain = @"com.firebase.auth";
+NSString *const kFirebaseCloudMessagingErrorDomain = @"com.firebase.cloudmessaging";
+NSString *const kFirebaseConfigErrorDomain = @"com.firebase.config";
+NSString *const kFirebaseCoreErrorDomain = @"com.firebase.core";
+NSString *const kFirebaseCrashReportingErrorDomain = @"com.firebase.crashreporting";
+NSString *const kFirebaseDatabaseErrorDomain = @"com.firebase.database";
+NSString *const kFirebaseDurableDeepLinkErrorDomain = @"com.firebase.durabledeeplink";
+NSString *const kFirebaseInstanceIDErrorDomain = @"com.firebase.instanceid";
+NSString *const kFirebasePerfErrorDomain = @"com.firebase.perf";
+NSString *const kFirebaseStorageErrorDomain = @"com.firebase.storage";
+
+NSError *FIRCreateError(NSString *domain, enum FIRErrorCode code, NSDictionary *userInfo) {
+ return [NSError errorWithDomain:domain code:code userInfo:userInfo];
+}
diff --git a/Firebase/Core/FIRLogger.m b/Firebase/Core/FIRLogger.m
new file mode 100644
index 0000000..0e3e325
--- /dev/null
+++ b/Firebase/Core/FIRLogger.m
@@ -0,0 +1,228 @@
+// 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 "Private/FIRLogger.h"
+
+#import "FIRLoggerLevel.h"
+#import "Private/FIRAppEnvironmentUtil.h"
+
+#include <asl.h>
+#include <assert.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/sysctl.h>
+
+FIRLoggerService kFIRLoggerABTesting = @"[Firebase/ABTesting]";
+FIRLoggerService kFIRLoggerAdMob = @"[Firebase/AdMob]";
+FIRLoggerService kFIRLoggerAnalytics = @"[Firebase/Analytics]";
+FIRLoggerService kFIRLoggerAuth = @"[Firebase/Auth]";
+FIRLoggerService kFIRLoggerCore = @"[Firebase/Core]";
+FIRLoggerService kFIRLoggerCrash = @"[Firebase/Crash]";
+FIRLoggerService kFIRLoggerDatabase = @"[Firebase/Database]";
+FIRLoggerService kFIRLoggerDynamicLinks = @"[Firebase/DynamicLinks]";
+FIRLoggerService kFIRLoggerInstanceID = @"[Firebase/InstanceID]";
+FIRLoggerService kFIRLoggerInvites = @"[Firebase/Invites]";
+FIRLoggerService kFIRLoggerMessaging = @"[Firebase/Messaging]";
+FIRLoggerService kFIRLoggerPerf = @"[Firebase/Performance]";
+FIRLoggerService kFIRLoggerRemoteConfig = @"[Firebase/RemoteConfig]";
+FIRLoggerService kFIRLoggerStorage = @"[Firebase/Storage]";
+
+/// Arguments passed on launch.
+NSString *const kFIRDisableDebugModeApplicationArgument = @"-FIRDebugDisabled";
+NSString *const kFIREnableDebugModeApplicationArgument = @"-FIRDebugEnabled";
+
+/// Key for the debug mode bit in NSUserDefaults.
+NSString *const kFIRPersistedDebugModeKey = @"/google/firebase/debug_mode";
+
+/// ASL client facility name used by FIRLogger.
+const char *kFIRLoggerASLClientFacilityName = "com.firebase.app.logger";
+
+/// Message format used by ASL client that matches format of NSLog.
+const char *kFIRLoggerCustomASLMessageFormat =
+ "$((Time)(J.3)) $(Sender)[$(PID)] <$((Level)(str))> $Message";
+
+static dispatch_once_t sFIRLoggerOnceToken;
+
+static aslclient sFIRLoggerClient;
+
+static dispatch_queue_t sFIRClientQueue;
+
+static BOOL sFIRLoggerDebugMode;
+
+// The sFIRAnalyticsDebugMode flag is here to support the -FIRDebugEnabled/-FIRDebugDisabled
+// flags used by Analytics. Users who use those flags expect Analytics to log verbosely,
+// while the rest of Firebase logs at the default level. This flag is introduced to support
+// that behavior.
+static BOOL sFIRAnalyticsDebugMode;
+
+static FIRLoggerLevel sFIRLoggerMaximumLevel;
+
+/// The regex pattern for the message code.
+static NSString *const kMessageCodePattern = @"^I-[A-Z]{3}[0-9]{6}$";
+static NSRegularExpression *sMessageCodeRegex;
+
+void FIRLoggerInitializeASL() {
+ dispatch_once(&sFIRLoggerOnceToken, ^{
+ // Initialize the ASL client handle.
+ sFIRLoggerClient = asl_open(NULL, kFIRLoggerASLClientFacilityName, ASL_OPT_STDERR);
+
+ // Set the filter used by system/device log. Initialize in default mode.
+ asl_set_filter(sFIRLoggerClient, ASL_FILTER_MASK_UPTO(ASL_LEVEL_NOTICE));
+ sFIRLoggerDebugMode = NO;
+ sFIRAnalyticsDebugMode = NO;
+ sFIRLoggerMaximumLevel = FIRLoggerLevelNotice;
+
+ NSArray *arguments = [NSProcessInfo processInfo].arguments;
+ NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
+ BOOL debugMode = [userDefaults boolForKey:kFIRPersistedDebugModeKey];
+
+ if ([arguments containsObject:kFIRDisableDebugModeApplicationArgument]) { // Default mode
+ [userDefaults removeObjectForKey:kFIRPersistedDebugModeKey];
+ } else if ([arguments containsObject:kFIREnableDebugModeApplicationArgument]
+ || debugMode) { // Debug mode
+ [userDefaults setBool:YES forKey:kFIRPersistedDebugModeKey];
+ asl_set_filter(sFIRLoggerClient, ASL_FILTER_MASK_UPTO(ASL_LEVEL_DEBUG));
+ sFIRLoggerDebugMode = YES;
+ }
+
+ // We should disable debug mode if we are running from App Store.
+ if (sFIRLoggerDebugMode && [FIRAppEnvironmentUtil isFromAppStore]) {
+ sFIRLoggerDebugMode = NO;
+ }
+
+ // Need to call asl_add_output_file so that the logs can appear in Xcode's console view. Set
+ // the ASL filter mask for this output file up to debug level so that all messages are viewable
+ // in the console.
+ asl_add_output_file(sFIRLoggerClient, STDERR_FILENO, kFIRLoggerCustomASLMessageFormat,
+ ASL_TIME_FMT_LCL, ASL_FILTER_MASK_UPTO(ASL_LEVEL_DEBUG), ASL_ENCODE_SAFE);
+
+ sFIRClientQueue = dispatch_queue_create("FIRLoggingClientQueue", DISPATCH_QUEUE_SERIAL);
+ dispatch_set_target_queue(sFIRClientQueue,
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
+
+ sMessageCodeRegex =
+ [NSRegularExpression regularExpressionWithPattern:kMessageCodePattern options:0 error:NULL];
+ });
+}
+
+void FIRSetAnalyticsDebugMode(BOOL analyticsDebugMode) {
+ FIRLoggerInitializeASL();
+ dispatch_async(sFIRClientQueue, ^{
+ // We should not enable debug mode if we are running from App Store.
+ if (analyticsDebugMode && [FIRAppEnvironmentUtil isFromAppStore]) {
+ return;
+ }
+ sFIRAnalyticsDebugMode = analyticsDebugMode;
+ asl_set_filter(sFIRLoggerClient, ASL_FILTER_MASK_UPTO(ASL_LEVEL_DEBUG));
+ });
+}
+
+void FIRSetLoggerLevel(FIRLoggerLevel loggerLevel) {
+ if (loggerLevel < FIRLoggerLevelMin || loggerLevel > FIRLoggerLevelMax) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000023", @"Invalid logger level, %ld", (long)loggerLevel);
+ return;
+ }
+ FIRLoggerInitializeASL();
+ dispatch_async(sFIRClientQueue, ^{
+ // We should not raise the logger level if we are running from App Store.
+ if (loggerLevel >= FIRLoggerLevelNotice && [FIRAppEnvironmentUtil isFromAppStore]) {
+ return;
+ }
+
+ sFIRLoggerMaximumLevel = loggerLevel;
+ asl_set_filter(sFIRLoggerClient, ASL_FILTER_MASK_UPTO(loggerLevel));
+ });
+}
+
+BOOL FIRIsLoggableLevel(FIRLoggerLevel loggerLevel, BOOL analyticsComponent) {
+ FIRLoggerInitializeASL();
+ if (sFIRLoggerDebugMode) {
+ return YES;
+ } else if (sFIRAnalyticsDebugMode && analyticsComponent) {
+ return YES;
+ }
+ return (BOOL)(loggerLevel <= sFIRLoggerMaximumLevel);
+}
+
+#ifdef DEBUG
+void FIRResetLogger() {
+ sFIRLoggerOnceToken = 0;
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:kFIRPersistedDebugModeKey];
+}
+
+aslclient getFIRLoggerClient() {
+ return sFIRLoggerClient;
+}
+
+dispatch_queue_t getFIRClientQueue() {
+ return sFIRClientQueue;
+}
+
+BOOL getFIRLoggerDebugMode() {
+ return sFIRLoggerDebugMode;
+}
+#endif
+
+void FIRLogBasic(FIRLoggerLevel level, FIRLoggerService service, NSString *messageCode,
+ NSString *message, va_list args_ptr) {
+ FIRLoggerInitializeASL();
+ BOOL canLog = level <= sFIRLoggerMaximumLevel;
+
+ if (sFIRLoggerDebugMode) {
+ canLog = YES;
+ } else if (sFIRAnalyticsDebugMode && [kFIRLoggerAnalytics isEqualToString:service]) {
+ canLog = YES;
+ }
+
+ if (!canLog) {
+ return;
+ }
+#ifdef DEBUG
+ NSCAssert(messageCode.length == 11, @"Incorrect message code length.");
+ NSRange messageCodeRange = NSMakeRange(0, messageCode.length);
+ NSUInteger numberOfMatches =
+ [sMessageCodeRegex numberOfMatchesInString:messageCode options:0 range:messageCodeRange];
+ NSCAssert(numberOfMatches == 1, @"Incorrect message code format.");
+#endif
+ NSString *logMsg = [[NSString alloc] initWithFormat:message arguments:args_ptr];
+ logMsg = [NSString stringWithFormat:@"%@[%@] %@", service, messageCode, logMsg];
+ dispatch_async(sFIRClientQueue, ^{
+ asl_log(sFIRLoggerClient, NULL, level, "%s", logMsg.UTF8String);
+ });
+}
+
+/**
+ * Generates the logging functions using macros.
+ *
+ * Calling FIRLogError(kFIRLoggerCore, @"I-COR000001", @"Configure %@ failed.", @"blah") shows:
+ * yyyy-mm-dd hh:mm:ss.SSS sender[PID] <Error> [Firebase/Core][I-COR000001] Configure blah failed.
+ * Calling FIRLogDebug(kFIRLoggerCore, @"I-COR000001", @"Configure succeed.") shows:
+ * yyyy-mm-dd hh:mm:ss.SSS sender[PID] <Debug> [Firebase/Core][I-COR000001] Configure succeed.
+ */
+#define FIR_LOGGING_FUNCTION(level) \
+void FIRLog##level(FIRLoggerService service, NSString *messageCode, NSString *message, ...) { \
+ va_list args_ptr; \
+ va_start(args_ptr, message); \
+ FIRLogBasic(FIRLoggerLevel##level, service, messageCode, message, args_ptr); \
+ va_end(args_ptr); \
+}
+
+FIR_LOGGING_FUNCTION(Error)
+FIR_LOGGING_FUNCTION(Warning)
+FIR_LOGGING_FUNCTION(Notice)
+FIR_LOGGING_FUNCTION(Info)
+FIR_LOGGING_FUNCTION(Debug)
+
+#undef FIR_MAKE_LOGGER
diff --git a/Firebase/Core/FIRLoggerLevel.h b/Firebase/Core/FIRLoggerLevel.h
new file mode 100644
index 0000000..fe0d47d
--- /dev/null
+++ b/Firebase/Core/FIRLoggerLevel.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 "FIRCoreSwiftNameSupport.h"
+
+/**
+ * The log levels used by internal logging.
+ */
+typedef NS_ENUM(NSInteger, FIRLoggerLevel) {
+ FIRLoggerLevelError = 3 /*ASL_LEVEL_ERR*/,
+ FIRLoggerLevelWarning = 4 /*ASL_LEVEL_WARNING*/,
+ FIRLoggerLevelNotice = 5 /*ASL_LEVEL_NOTICE*/,
+ FIRLoggerLevelInfo = 6 /*ASL_LEVEL_INFO*/,
+ FIRLoggerLevelDebug = 7 /*ASL_LEVEL_DEBUG*/,
+ FIRLoggerLevelMin = FIRLoggerLevelError,
+ FIRLoggerLevelMax = FIRLoggerLevelDebug
+} FIR_SWIFT_NAME(FirebaseLoggerLevel);
diff --git a/Firebase/Core/FIRMutableDictionary.m b/Firebase/Core/FIRMutableDictionary.m
new file mode 100644
index 0000000..1d6ef3a
--- /dev/null
+++ b/Firebase/Core/FIRMutableDictionary.m
@@ -0,0 +1,97 @@
+// 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 "Private/FIRMutableDictionary.h"
+
+@implementation FIRMutableDictionary {
+ /// The mutable dictionary.
+ NSMutableDictionary *_objects;
+
+ /// Serial synchronization queue. All reads should use dispatch_sync, while writes use
+ /// dispatch_async.
+ dispatch_queue_t _queue;
+}
+
+- (instancetype)init {
+ self = [super init];
+
+ if (self) {
+ _objects = [[NSMutableDictionary alloc] init];
+ _queue = dispatch_queue_create("FIRMutableDictionary", DISPATCH_QUEUE_SERIAL);
+ }
+
+ return self;
+}
+
+- (NSString *)description {
+ __block NSString *description;
+ dispatch_sync(_queue, ^{
+ description = _objects.description;
+ });
+ return description;
+}
+
+- (id)objectForKey:(id)key {
+ __block id object;
+ dispatch_sync(_queue, ^{
+ object = _objects[key];
+ });
+ return object;
+}
+
+- (void)setObject:(id)object forKey:(id<NSCopying>)key {
+ dispatch_async(_queue, ^{
+ _objects[key] = object;
+ });
+}
+
+- (void)removeObjectForKey:(id)key {
+ dispatch_async(_queue, ^{
+ [_objects removeObjectForKey:key];
+ });
+}
+
+- (void)removeAllObjects {
+ dispatch_async(_queue, ^{
+ [_objects removeAllObjects];
+ });
+}
+
+- (NSUInteger)count {
+ __block NSUInteger count;
+ dispatch_sync(_queue, ^{
+ count = _objects.count;
+ });
+ return count;
+}
+
+- (id)objectForKeyedSubscript:(id<NSCopying>)key {
+ // The method this calls is already synchronized.
+ return [self objectForKey:key];
+}
+
+- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key {
+ // The method this calls is already synchronized.
+ [self setObject:obj forKey:key];
+}
+
+- (NSDictionary *)dictionary {
+ __block NSDictionary *dictionary;
+ dispatch_sync(_queue, ^{
+ dictionary = [_objects copy];
+ });
+ return dictionary;
+}
+
+@end
diff --git a/Firebase/Core/FIRNetwork.m b/Firebase/Core/FIRNetwork.m
new file mode 100644
index 0000000..4926b2f
--- /dev/null
+++ b/Firebase/Core/FIRNetwork.m
@@ -0,0 +1,390 @@
+// 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 "Private/FIRNetwork.h"
+#import "Private/FIRNetworkMessageCode.h"
+
+#import "Private/FIRMutableDictionary.h"
+#import "Private/FIRNetworkConstants.h"
+#import "Private/FIRReachabilityChecker.h"
+#import "Private/FIRLogger.h"
+
+#import <GoogleToolboxForMac/GTMNSData+zlib.h>
+
+/// Constant string for request header Content-Encoding.
+static NSString *const kFIRNetworkContentCompressionKey = @"Content-Encoding";
+
+/// Constant string for request header Content-Encoding value.
+static NSString *const kFIRNetworkContentCompressionValue = @"gzip";
+
+/// Constant string for request header Content-Length.
+static NSString *const kFIRNetworkContentLengthKey = @"Content-Length";
+
+/// Constant string for request header Content-Type.
+static NSString *const kFIRNetworkContentTypeKey = @"Content-Type";
+
+/// Constant string for request header Content-Type value.
+static NSString *const kFIRNetworkContentTypeValue = @"application/x-www-form-urlencoded";
+
+/// Constant string for GET request method.
+static NSString *const kFIRNetworkGETRequestMethod = @"GET";
+
+/// Constant string for POST request method.
+static NSString *const kFIRNetworkPOSTRequestMethod = @"POST";
+
+/// Default constant string as a prefix for network logger.
+static NSString *const kFIRNetworkLogTag = @"Firebase/Network";
+
+@interface FIRNetwork ()<FIRReachabilityDelegate, FIRNetworkLoggerDelegate>
+@end
+
+@implementation FIRNetwork {
+ /// Network reachability.
+ FIRReachabilityChecker *_reachability;
+
+ /// The dictionary of requests by session IDs { NSString : id }.
+ FIRMutableDictionary *_requests;
+}
+
+- (instancetype)init {
+ return [self initWithReachabilityHost:kFIRNetworkReachabilityHost];
+}
+
+- (instancetype)initWithReachabilityHost:(NSString *)reachabilityHost {
+ self = [super init];
+ if (self) {
+ // Setup reachability.
+ _reachability =
+ [[FIRReachabilityChecker alloc] initWithReachabilityDelegate:self
+ loggerDelegate:self
+ withHost:reachabilityHost];
+ if (![_reachability start]) {
+ return nil;
+ }
+
+ _requests = [[FIRMutableDictionary alloc] init];
+ _timeoutInterval = kFIRNetworkTimeOutInterval;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ _reachability.reachabilityDelegate = nil;
+ [_reachability stop];
+}
+
+#pragma mark - External Methods
+
++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
+ completionHandler:(FIRNetworkSystemCompletionHandler)completionHandler {
+ [FIRNetworkURLSession handleEventsForBackgroundURLSessionID:sessionID
+ completionHandler:completionHandler];
+}
+
+- (NSString *)postURL:(NSURL *)url
+ payload:(NSData *)payload
+ queue:(dispatch_queue_t)queue
+ usingBackgroundSession:(BOOL)usingBackgroundSession
+ completionHandler:(FIRNetworkCompletionHandler)handler {
+ if (!url.absoluteString.length) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkInvalidURL queue:queue withHandler:handler];
+ return nil;
+ }
+
+ NSTimeInterval timeOutInterval = _timeoutInterval ?: kFIRNetworkTimeOutInterval;
+
+ NSMutableURLRequest *request =
+ [[NSMutableURLRequest alloc] initWithURL:url
+ cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
+ timeoutInterval:timeOutInterval];
+
+ if (!request) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkSessionTaskCreation
+ queue:queue
+ withHandler:handler];
+ return nil;
+ }
+
+ NSError *compressError = nil;
+ NSData *compressedData = [NSData gtm_dataByGzippingData:payload error:&compressError];
+ if (!compressedData || compressError) {
+ if (compressError || payload.length > 0) {
+ // If the payload is not empty but it fails to compress the payload, something has been wrong.
+ [self handleErrorWithCode:FIRErrorCodeNetworkPayloadCompression
+ queue:queue
+ withHandler:handler];
+ return nil;
+ }
+ compressedData = [[NSData alloc] init];
+ }
+
+ NSString *postLength = @(compressedData.length).stringValue;
+
+ // Set up the request with the compressed data.
+ [request setValue:postLength forHTTPHeaderField:kFIRNetworkContentLengthKey];
+ request.HTTPBody = compressedData;
+ request.HTTPMethod = kFIRNetworkPOSTRequestMethod;
+ [request setValue:kFIRNetworkContentTypeValue forHTTPHeaderField:kFIRNetworkContentTypeKey];
+ [request setValue:kFIRNetworkContentCompressionValue
+ forHTTPHeaderField:kFIRNetworkContentCompressionKey];
+
+ FIRNetworkURLSession *fetcher = [[FIRNetworkURLSession alloc] initWithNetworkLoggerDelegate:self];
+ fetcher.backgroundNetworkEnabled = usingBackgroundSession;
+
+ __weak FIRNetwork *weakSelf = self;
+ NSString *requestID = [fetcher
+ sessionIDFromAsyncPOSTRequest:request
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data,
+ NSString *sessionID, NSError *error) {
+ FIRNetwork *strongSelf = weakSelf;
+ if (!strongSelf) {
+ return;
+ }
+ dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
+ dispatch_async(queueToDispatch, ^{
+ if (sessionID.length) {
+ [strongSelf->_requests removeObjectForKey:sessionID];
+ }
+ if (handler) {
+ handler(response, data, error);
+ }
+ });
+ }];
+ if (!requestID) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkSessionTaskCreation
+ queue:queue
+ withHandler:handler];
+ return nil;
+ }
+
+ [self firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeNetwork000
+ message:@"Uploading data. Host"
+ context:url];
+ _requests[requestID] = fetcher;
+ return requestID;
+}
+
+- (NSString *)getURL:(NSURL *)url
+ headers:(NSDictionary *)headers
+ queue:(dispatch_queue_t)queue
+ usingBackgroundSession:(BOOL)usingBackgroundSession
+ completionHandler:(FIRNetworkCompletionHandler)handler {
+ if (!url.absoluteString.length) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkInvalidURL queue:queue withHandler:handler];
+ return nil;
+ }
+
+ NSTimeInterval timeOutInterval = _timeoutInterval ?: kFIRNetworkTimeOutInterval;
+ NSMutableURLRequest *request =
+ [[NSMutableURLRequest alloc] initWithURL:url
+ cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
+ timeoutInterval:timeOutInterval];
+
+ if (!request) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkSessionTaskCreation
+ queue:queue
+ withHandler:handler];
+ return nil;
+ }
+
+ request.HTTPMethod = kFIRNetworkGETRequestMethod;
+ request.allHTTPHeaderFields = headers;
+
+ FIRNetworkURLSession *fetcher = [[FIRNetworkURLSession alloc] initWithNetworkLoggerDelegate:self];
+ fetcher.backgroundNetworkEnabled = usingBackgroundSession;
+
+ __weak FIRNetwork *weakSelf = self;
+ NSString *requestID = [fetcher
+ sessionIDFromAsyncGETRequest:request
+ completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSString *sessionID,
+ NSError *error) {
+ FIRNetwork *strongSelf = weakSelf;
+ if (!strongSelf) {
+ return;
+ }
+ dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
+ dispatch_async(queueToDispatch, ^{
+ if (sessionID.length) {
+ [strongSelf->_requests removeObjectForKey:sessionID];
+ }
+ if (handler) {
+ handler(response, data, error);
+ }
+ });
+ }];
+
+ if (!requestID) {
+ [self handleErrorWithCode:FIRErrorCodeNetworkSessionTaskCreation
+ queue:queue
+ withHandler:handler];
+ return nil;
+ }
+
+ [self firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeNetwork001
+ message:@"Downloading data. Host"
+ context:url];
+ _requests[requestID] = fetcher;
+ return requestID;
+}
+
+- (BOOL)hasUploadInProgress {
+ return _requests.count > 0;
+}
+
+#pragma mark - Network Reachability
+
+/// Tells reachability delegate to call reachabilityDidChangeToStatus: to notify the network
+/// reachability has changed.
+- (void)reachability:(FIRReachabilityChecker *)reachability
+ statusChanged:(FIRReachabilityStatus)status {
+ _networkConnected = (status == kFIRReachabilityViaCellular || status == kFIRReachabilityViaWifi);
+ [_reachabilityDelegate reachabilityDidChange];
+}
+
+#pragma mark - Network logger delegate
+
+- (void)setLoggerDelegate:(id<FIRNetworkLoggerDelegate>)loggerDelegate {
+ // Explicitly check whether the delegate responds to the methods because conformsToProtocol does
+ // not work correctly even though the delegate does respond to the methods.
+ if (!loggerDelegate ||
+ ![loggerDelegate
+ respondsToSelector:@selector(firNetwork_logWithLevel:messageCode:message:contexts:)] ||
+ ![loggerDelegate
+ respondsToSelector:@selector(firNetwork_logWithLevel:messageCode:message:context:)] ||
+ ![loggerDelegate
+ respondsToSelector:@selector(firNetwork_logWithLevel:messageCode:message:)]) {
+ FIRLogError(kFIRLoggerAnalytics,
+ [NSString stringWithFormat:@"I-NET%06ld", (long)kFIRNetworkMessageCodeNetwork002],
+ @"Cannot set the network logger delegate: delegate does not conform to the network "
+ "logger protocol.");
+ return;
+ }
+ _loggerDelegate = loggerDelegate;
+}
+
+#pragma mark - Private methods
+
+/// Handles network error and calls completion handler with the error.
+- (void)handleErrorWithCode:(NSInteger)code
+ queue:(dispatch_queue_t)queue
+ withHandler:(FIRNetworkCompletionHandler)handler {
+ NSDictionary *userInfo = @{ kFIRNetworkErrorContext : @"Failed to create network request" };
+ NSError *error =
+ [[NSError alloc] initWithDomain:kFIRNetworkErrorDomain code:code userInfo:userInfo];
+ [self firNetwork_logWithLevel:kFIRNetworkLogLevelWarning
+ messageCode:kFIRNetworkMessageCodeNetwork002
+ message:@"Failed to create network request. Code, error"
+ contexts:@[ @(code), error ]];
+ if (handler) {
+ dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
+ dispatch_async(queueToDispatch, ^{
+ handler(nil, nil, error);
+ });
+ }
+}
+
+#pragma mark - Network logger
+
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message
+ contexts:(NSArray *)contexts {
+ // Let the delegate log the message if there is a valid logger delegate. Otherwise, just log
+ // errors/warnings/info messages to the console log.
+ if (_loggerDelegate) {
+ [_loggerDelegate firNetwork_logWithLevel:logLevel
+ messageCode:messageCode
+ message:message
+ contexts:contexts];
+ return;
+ }
+ if (_isDebugModeEnabled || logLevel == kFIRNetworkLogLevelError ||
+ logLevel == kFIRNetworkLogLevelWarning || logLevel == kFIRNetworkLogLevelInfo) {
+ NSString *formattedMessage = FIRStringWithLogMessage(message, logLevel, contexts);
+ NSLog(@"%@", formattedMessage);
+ FIRLogBasic((FIRLoggerLevel)logLevel, kFIRLoggerCore,
+ [NSString stringWithFormat:@"I-NET%06ld", (long)messageCode], formattedMessage,
+ NULL);
+ }
+}
+
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message
+ context:(id)context {
+ if (_loggerDelegate) {
+ [_loggerDelegate firNetwork_logWithLevel:logLevel
+ messageCode:messageCode
+ message:message
+ context:context];
+ return;
+ }
+ NSArray *contexts = context ? @[ context ] : @[];
+ [self firNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:contexts];
+}
+
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message {
+ if (_loggerDelegate) {
+ [_loggerDelegate firNetwork_logWithLevel:logLevel messageCode:messageCode message:message];
+ return;
+ }
+ [self firNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:@[]];
+}
+
+/// Returns a string for the given log level (e.g. kFIRNetworkLogLevelError -> @"ERROR").
+static NSString *FIRLogLevelDescriptionFromLogLevel(FIRNetworkLogLevel logLevel) {
+ static NSDictionary *levelNames = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ levelNames = @{
+ @(kFIRNetworkLogLevelError) : @"ERROR",
+ @(kFIRNetworkLogLevelWarning) : @"WARNING",
+ @(kFIRNetworkLogLevelInfo) : @"INFO",
+ @(kFIRNetworkLogLevelDebug) : @"DEBUG"
+ };
+ });
+ return levelNames[@(logLevel)];
+}
+
+/// Returns a formatted string to be used for console logging.
+static NSString *FIRStringWithLogMessage(NSString *message, FIRNetworkLogLevel logLevel,
+ NSArray *contexts) {
+ if (!message) {
+ message = @"(Message was nil)";
+ } else if (!message.length) {
+ message = @"(Message was empty)";
+ }
+ NSMutableString *result = [[NSMutableString alloc]
+ initWithFormat:@"<%@/%@> %@", kFIRNetworkLogTag, FIRLogLevelDescriptionFromLogLevel(logLevel),
+ message];
+
+ if (!contexts.count) {
+ return result;
+ }
+
+ NSMutableArray *formattedContexts = [[NSMutableArray alloc] init];
+ for (id item in contexts) {
+ [formattedContexts addObject:(item != [NSNull null] ? item : @"(nil)")];
+ }
+
+ [result appendString:@": "];
+ [result appendString:[formattedContexts componentsJoinedByString:@", "]];
+ return result;
+}
+
+@end
diff --git a/Firebase/Core/FIRNetworkConstants.m b/Firebase/Core/FIRNetworkConstants.m
new file mode 100644
index 0000000..7ba0e15
--- /dev/null
+++ b/Firebase/Core/FIRNetworkConstants.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 "Private/FIRNetworkConstants.h"
+
+@import Foundation;
+
+NSString *const kFIRNetworkBackgroundSessionConfigIDPrefix =
+ @"com.firebase.network.background-upload";
+NSString *const kFIRNetworkApplicationSupportSubdirectory = @"Firebase/Network";
+NSString *const kFIRNetworkTempDirectoryName = @"FIRNetworkTemporaryDirectory";
+const NSTimeInterval kFIRNetworkTempFolderExpireTime = 60 * 60; // 1 hour
+const NSTimeInterval kFIRNetworkTimeOutInterval = 60; // 1 minute.
+NSString *const kFIRNetworkReachabilityHost = @"app-measurement.com";
+NSString *const kFIRNetworkErrorContext = @"Context";
+
+const int kFIRNetworkHTTPStatusOK = 200;
+const int kFIRNetworkHTTPStatusNoContent = 204;
+const int kFIRNetworkHTTPStatusCodeMultipleChoices = 300;
+const int kFIRNetworkHTTPStatusCodeMovedPermanently = 301;
+const int kFIRNetworkHTTPStatusCodeFound = 302;
+const int kFIRNetworkHTTPStatusCodeNotModified = 304;
+const int kFIRNetworkHTTPStatusCodeMovedTemporarily = 307;
+const int kFIRNetworkHTTPStatusCodeNotFound = 404;
+const int kFIRNetworkHTTPStatusCodeCannotAcceptTraffic = 429;
+const int kFIRNetworkHTTPStatusCodeUnavailable = 503;
+
+NSString *const kFIRNetworkErrorDomain = @"com.firebase.network.ErrorDomain";
diff --git a/Firebase/Core/FIRNetworkURLSession.m b/Firebase/Core/FIRNetworkURLSession.m
new file mode 100644
index 0000000..2b17eb3
--- /dev/null
+++ b/Firebase/Core/FIRNetworkURLSession.m
@@ -0,0 +1,669 @@
+// 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 "Private/FIRNetworkURLSession.h"
+
+#import "Private/FIRMutableDictionary.h"
+#import "Private/FIRNetworkConstants.h"
+#import "Private/FIRNetworkMessageCode.h"
+#import "Private/FIRLogger.h"
+
+@implementation FIRNetworkURLSession {
+ /// The handler to be called when the request completes or error has occurs.
+ FIRNetworkURLSessionCompletionHandler _completionHandler;
+
+ /// Session ID generated randomly with a fixed prefix.
+ NSString *_sessionID;
+
+ /// The session configuration.
+ NSURLSessionConfiguration *_sessionConfig;
+
+ /// The path to the directory where all temporary files are stored before uploading.
+ NSURL *_networkDirectoryURL;
+
+ /// The downloaded data from fetching.
+ NSData *_downloadedData;
+
+ /// The path to the temporary file which stores the uploading data.
+ NSURL *_uploadingFileURL;
+
+ /// The current request.
+ NSURLRequest *_request;
+}
+
+#pragma mark - Init
+
+- (instancetype)initWithNetworkLoggerDelegate:(id<FIRNetworkLoggerDelegate>)networkLoggerDelegate {
+ self = [super init];
+ if (self) {
+ // Create URL to the directory where all temporary files to upload have to be stored.
+ NSArray *paths =
+ NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
+ NSString *applicationSupportDirectory = paths.firstObject;
+ NSArray *tempPathComponents = @[
+ applicationSupportDirectory,
+ kFIRNetworkApplicationSupportSubdirectory,
+ kFIRNetworkTempDirectoryName
+ ];
+ _networkDirectoryURL = [NSURL fileURLWithPathComponents:tempPathComponents];
+ _sessionID = [NSString stringWithFormat:@"%@-%@", kFIRNetworkBackgroundSessionConfigIDPrefix,
+ [[NSUUID UUID] UUIDString]];
+ _loggerDelegate = networkLoggerDelegate;
+ }
+ return self;
+}
+
+#pragma mark - External Methods
+
+#pragma mark - To be called from AppDelegate
+
++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
+ completionHandler:
+ (FIRNetworkSystemCompletionHandler)systemCompletionHandler {
+ // The session may not be FIRAnalytics background. Ignore those that do not have the prefix.
+ if (![sessionID hasPrefix:kFIRNetworkBackgroundSessionConfigIDPrefix]) {
+ return;
+ }
+ FIRNetworkURLSession *fetcher = [self fetcherWithSessionIdentifier:sessionID];
+ if (fetcher != nil) {
+ [fetcher addSystemCompletionHandler:systemCompletionHandler forSession:sessionID];
+ } else {
+ FIRLogError(kFIRLoggerCore,
+ [NSString stringWithFormat:@"I-NET%06ld", (long)kFIRNetworkMessageCodeNetwork003],
+ @"Failed to retrieve background session with ID %@ after app is relaunched.",
+ sessionID);
+ }
+}
+
+#pragma mark - External Methods
+
+/// Sends an async POST request using NSURLSession for iOS >= 7.0, and returns an ID of the
+/// connection.
+- (NSString *)sessionIDFromAsyncPOSTRequest:(NSURLRequest *)request
+ completionHandler:(FIRNetworkURLSessionCompletionHandler)handler {
+ // NSURLSessionUploadTask does not work with NSData in the background.
+ // To avoid this issue, write the data to a temporary file to upload it.
+ // Make a temporary file with the data subset.
+ _uploadingFileURL = [self temporaryFilePathWithSessionID:_sessionID];
+ NSError *writeError;
+ NSURLSessionUploadTask *postRequestTask;
+ NSURLSession *session;
+ BOOL didWriteFile = NO;
+
+ // Clean up the entire temp folder to avoid temp files that remain in case the previous session
+ // crashed and did not clean up.
+ [self maybeRemoveTempFilesAtURL:_networkDirectoryURL
+ expiringTime:kFIRNetworkTempFolderExpireTime];
+
+ // If there is no background network enabled, no need to write to file. This will allow default
+ // network session which runs on the foreground.
+ if (_backgroundNetworkEnabled && [self ensureTemporaryDirectoryExists]) {
+ didWriteFile = [request.HTTPBody writeToFile:_uploadingFileURL.path
+ options:NSDataWritingAtomic
+ error:&writeError];
+
+ if (writeError) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession000
+ message:@"Failed to write request data to file"
+ context:writeError];
+ }
+ }
+
+ if (didWriteFile) {
+ // Exclude this file from backing up to iTunes. There are conflicting reports that excluding
+ // directory from backing up does not excluding files of that directory from backing up.
+ [self excludeFromBackupForURL:_uploadingFileURL];
+
+ _sessionConfig = [self backgroundSessionConfigWithSessionID:_sessionID];
+ [self populateSessionConfig:_sessionConfig withRequest:request];
+ session = [NSURLSession sessionWithConfiguration:_sessionConfig
+ delegate:self
+ delegateQueue:[NSOperationQueue mainQueue]];
+ postRequestTask = [session uploadTaskWithRequest:request fromFile:_uploadingFileURL];
+ } else {
+ // If we cannot write to file, just send it in the foreground.
+ _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
+ [self populateSessionConfig:_sessionConfig withRequest:request];
+ _sessionConfig.URLCache = nil;
+ session = [NSURLSession sessionWithConfiguration:_sessionConfig
+ delegate:self
+ delegateQueue:[NSOperationQueue mainQueue]];
+ postRequestTask = [session uploadTaskWithRequest:request fromData:request.HTTPBody];
+ }
+
+ if (!session || !postRequestTask) {
+ NSError *error =
+ [[NSError alloc] initWithDomain:kFIRNetworkErrorDomain
+ code:FIRErrorCodeNetworkRequestCreation
+ userInfo:@{
+ kFIRNetworkErrorContext : @"Cannot create network session"
+ }];
+ [self callCompletionHandler:handler withResponse:nil data:nil error:error];
+ return nil;
+ }
+
+ // Save the session into memory.
+ NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIDToFetcherMap];
+ [sessionIdentifierToFetcherMap setObject:self forKey:_sessionID];
+
+ _request = [request copy];
+
+ // Store completion handler because background session does not accept handler block but custom
+ // delegate.
+ _completionHandler = [handler copy];
+ [postRequestTask resume];
+
+ return _sessionID;
+}
+
+/// Sends an async GET request using NSURLSession for iOS >= 7.0, and returns an ID of the session.
+- (NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request
+ completionHandler:(FIRNetworkURLSessionCompletionHandler)handler {
+ if (_backgroundNetworkEnabled) {
+ _sessionConfig = [self backgroundSessionConfigWithSessionID:_sessionID];
+ } else {
+ _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
+ }
+
+ [self populateSessionConfig:_sessionConfig withRequest:request];
+
+ // Do not cache the GET request.
+ _sessionConfig.URLCache = nil;
+
+ NSURLSession *session = [NSURLSession sessionWithConfiguration:_sessionConfig
+ delegate:self
+ delegateQueue:[NSOperationQueue mainQueue]];
+ NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
+
+ if (!session || !downloadTask) {
+ NSError *error =
+ [[NSError alloc] initWithDomain:kFIRNetworkErrorDomain
+ code:FIRErrorCodeNetworkRequestCreation
+ userInfo:@{
+ kFIRNetworkErrorContext : @"Cannot create network session"
+ }];
+ [self callCompletionHandler:handler withResponse:nil data:nil error:error];
+ return nil;
+ }
+
+ // Save the session into memory.
+ NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIDToFetcherMap];
+ [sessionIdentifierToFetcherMap setObject:self forKey:_sessionID];
+
+ _request = [request copy];
+
+ _completionHandler = [handler copy];
+ [downloadTask resume];
+
+ return _sessionID;
+}
+
+#pragma mark - NSURLSessionTaskDelegate
+
+/// Called by the NSURLSession once the download task is completed. The file is saved in the
+/// provided URL so we need to read the data and store into _downloadedData. Once the session is
+/// completed, URLSession:task:didCompleteWithError will be called and the completion handler will
+/// be called with the downloaded data.
+- (void)URLSession:(NSURLSession *)session
+ downloadTask:(NSURLSessionDownloadTask *)task
+ didFinishDownloadingToURL:(NSURL *)url {
+ if (!url.path) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession001
+ message:@"Unable to read downloaded data from empty temp path"];
+ _downloadedData = nil;
+ return;
+ }
+
+ NSError *error;
+ _downloadedData = [NSData dataWithContentsOfFile:url.path options:0 error:&error];
+
+ if (error) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession002
+ message:@"Cannot read the content of downloaded data"
+ context:error];
+ _downloadedData = nil;
+ }
+}
+
+- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeURLSession003
+ message:@"Background session finished"
+ context:session.configuration.identifier];
+ [self callSystemCompletionHandler:session.configuration.identifier];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didCompleteWithError:(NSError *)error {
+ // Avoid any chance of recursive behavior leading to it being used repeatedly.
+ FIRNetworkURLSessionCompletionHandler handler = _completionHandler;
+ _completionHandler = nil;
+
+ if (task.response) {
+ // The following assertion should always be true for HTTP requests, see https://goo.gl/gVLxT7.
+ NSAssert([task.response isKindOfClass:[NSHTTPURLResponse class]], @"URL response must be HTTP");
+
+ // The server responded so ignore the error created by the system.
+ error = nil;
+ } else if (!error) {
+ error =
+ [[NSError alloc] initWithDomain:kFIRNetworkErrorDomain
+ code:FIRErrorCodeNetworkInvalidResponse
+ userInfo:@{
+ kFIRNetworkErrorContext : @"Network Error: Empty network response"
+ }];
+ }
+
+ [self callCompletionHandler:handler
+ withResponse:(NSHTTPURLResponse *)task.response
+ data:_downloadedData
+ error:error];
+
+ // Remove the temp file to avoid trashing devices with lots of temp files.
+ [self removeTempItemAtURL:_uploadingFileURL];
+
+ // Try to clean up stale files again.
+ [self maybeRemoveTempFilesAtURL:_networkDirectoryURL
+ expiringTime:kFIRNetworkTempFolderExpireTime];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
+ NSURLCredential *credential))completionHandler {
+ // The handling is modeled after GTMSessionFetcher.
+ if ([challenge.protectionSpace.authenticationMethod
+ isEqualToString:NSURLAuthenticationMethodServerTrust]) {
+ SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
+ if (serverTrust == NULL) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeURLSession004
+ message:@"Received empty server trust for host. Host"
+ context:_request.URL];
+ completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+ return;
+ }
+ NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
+ if (!credential) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelWarning
+ messageCode:kFIRNetworkMessageCodeURLSession005
+ message:@"Unable to verify server identity. Host"
+ context:_request.URL];
+ completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
+ return;
+ }
+
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeURLSession006
+ message:@"Received SSL challenge for host. Host"
+ context:_request.URL];
+
+ void (^callback)(BOOL) = ^(BOOL allow) {
+ if (allow) {
+ completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
+ } else {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeURLSession007
+ message:@"Cancelling authentication challenge for host. Host"
+ context:_request.URL];
+ completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
+ }
+ };
+
+ // Retain the trust object to avoid a SecTrustEvaluate() crash on iOS 7.
+ CFRetain(serverTrust);
+
+ // Evaluate the certificate chain.
+ //
+ // The delegate queue may be the main thread. Trust evaluation could cause some
+ // blocking network activity, so we must evaluate async, as documented at
+ // https://developer.apple.com/library/ios/technotes/tn2232/
+ dispatch_queue_t evaluateBackgroundQueue =
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
+
+ dispatch_async(evaluateBackgroundQueue, ^{
+ SecTrustResultType trustEval = kSecTrustResultInvalid;
+ BOOL shouldAllow;
+ OSStatus trustError;
+
+ @synchronized([FIRNetworkURLSession class]) {
+ trustError = SecTrustEvaluate(serverTrust, &trustEval);
+ }
+
+ if (trustError != errSecSuccess) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession008
+ message:@"Cannot evaluate server trust. Error, host"
+ contexts:@[ @(trustError), _request.URL ]];
+ shouldAllow = NO;
+ } else {
+ // Having a trust level "unspecified" by the user is the usual result, described at
+ // https://developer.apple.com/library/mac/qa/qa1360
+ shouldAllow =
+ (trustEval == kSecTrustResultUnspecified || trustEval == kSecTrustResultProceed);
+ }
+
+ // Call the call back with the permission.
+ callback(shouldAllow);
+
+ CFRelease(serverTrust);
+ });
+ return;
+ }
+
+ // Default handling for other Auth Challenges.
+ completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+}
+
+#pragma mark - Internal Methods
+
+/// Stores system completion handler with session ID as key.
+- (void)addSystemCompletionHandler:(FIRNetworkSystemCompletionHandler)handler
+ forSession:(NSString *)identifier {
+ if (!handler) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession009
+ message:@"Cannot store nil system completion handler in network"];
+ return;
+ }
+
+ if (!identifier.length) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession010
+ message:@"Cannot store system completion handler with empty network "
+ "session identifier"];
+ return;
+ }
+
+ FIRMutableDictionary *systemCompletionHandlers =
+ [[self class] sessionIDToSystemCompletionHandlerDictionary];
+ if (systemCompletionHandlers[identifier]) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelWarning
+ messageCode:kFIRNetworkMessageCodeURLSession011
+ message:@"Got multiple system handlers for a single session ID"
+ context:identifier];
+ }
+
+ systemCompletionHandlers[identifier] = handler;
+}
+
+/// Calls the system provided completion handler with the session ID stored in the dictionary.
+/// The handler will be removed from the dictionary after being called.
+- (void)callSystemCompletionHandler:(NSString *)identifier {
+ FIRMutableDictionary *systemCompletionHandlers =
+ [[self class] sessionIDToSystemCompletionHandlerDictionary];
+ FIRNetworkSystemCompletionHandler handler = [systemCompletionHandlers objectForKey:identifier];
+
+ if (handler) {
+ [systemCompletionHandlers removeObjectForKey:identifier];
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ handler();
+ });
+ }
+}
+
+/// Sets or updates the session ID of this session.
+- (void)setSessionID:(NSString *)sessionID {
+ _sessionID = [sessionID copy];
+}
+
+/// Creates a background session configuration with the session ID using the supported method.
+- (NSURLSessionConfiguration *)backgroundSessionConfigWithSessionID:(NSString *)sessionID {
+#if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && \
+ MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) || \
+ (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && \
+ __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)
+ // iOS 8/10.10 builds require the new backgroundSessionConfiguration method name.
+ return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID];
+#elif (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && \
+ MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) || \
+ (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0)
+ // Do a runtime check to avoid a deprecation warning about using
+ // +backgroundSessionConfiguration: on iOS 8.
+ if ([NSURLSessionConfiguration
+ respondsToSelector:@selector(backgroundSessionConfigurationWithIdentifier:)]) {
+ // Running on iOS 8+/OS X 10.10+.
+ return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID];
+ } else {
+ // Running on iOS 7/OS X 10.9.
+ return [NSURLSessionConfiguration backgroundSessionConfiguration:sessionID];
+ }
+#else
+ // Building with an SDK earlier than iOS 8/OS X 10.10.
+ return [NSURLSessionConfiguration backgroundSessionConfiguration:sessionID];
+#endif
+}
+
+- (void)maybeRemoveTempFilesAtURL:(NSURL *)folderURL expiringTime:(NSTimeInterval)staleTime {
+ if (!folderURL.absoluteString.length) {
+ return;
+ }
+
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSError *error = nil;
+
+ NSArray *properties = @[ NSURLCreationDateKey ];
+ NSArray *directoryContent =
+ [fileManager contentsOfDirectoryAtURL:folderURL
+ includingPropertiesForKeys:properties
+ options:NSDirectoryEnumerationSkipsSubdirectoryDescendants
+ error:&error];
+ if (error && error.code != NSFileReadNoSuchFileError) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeURLSession012
+ message:@"Cannot get files from the temporary network folder. Error"
+ context:error];
+ return;
+ }
+
+ if (!directoryContent.count) {
+ return;
+ }
+
+ NSTimeInterval now = [NSDate date].timeIntervalSince1970;
+ for (NSURL *tempFile in directoryContent) {
+ NSDate *creationDate;
+ BOOL getCreationDate =
+ [tempFile getResourceValue:&creationDate forKey:NSURLCreationDateKey error:NULL];
+ if (!getCreationDate) {
+ continue;
+ }
+ NSTimeInterval creationTimeInterval = creationDate.timeIntervalSince1970;
+ if (fabs(now - creationTimeInterval) > staleTime) {
+ [self removeTempItemAtURL:tempFile];
+ }
+ }
+}
+
+/// Removes the temporary file written to disk for sending the request. It has to be cleaned up
+/// after the session is done.
+- (void)removeTempItemAtURL:(NSURL *)fileURL {
+ if (!fileURL.absoluteString.length) {
+ return;
+ }
+
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSError *error = nil;
+
+ if (![fileManager removeItemAtURL:fileURL error:&error] && error.code != NSFileNoSuchFileError) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession013
+ message:@"Failed to remove temporary uploading data file. Error"
+ context:error.localizedDescription];
+ }
+}
+
+/// Gets the fetcher with the session ID.
++ (instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
+ NSMapTable *sessionIdentifierToFetcherMap = [self sessionIDToFetcherMap];
+ FIRNetworkURLSession *session = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
+ if (!session && [sessionIdentifier hasPrefix:kFIRNetworkBackgroundSessionConfigIDPrefix]) {
+ session = [[FIRNetworkURLSession alloc] initWithNetworkLoggerDelegate:nil];
+ [session setSessionID:sessionIdentifier];
+ [sessionIdentifierToFetcherMap setObject:session forKey:sessionIdentifier];
+ }
+ return session;
+}
+
+/// Returns a map of the fetcher by session ID. Creates a map if it is not created.
++ (NSMapTable *)sessionIDToFetcherMap {
+ static NSMapTable *sessionIDToFetcherMap;
+
+ static dispatch_once_t sessionMapOnceToken;
+ dispatch_once(&sessionMapOnceToken, ^{
+ sessionIDToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
+ });
+ return sessionIDToFetcherMap;
+}
+
+/// Returns a map of system provided completion handler by session ID. Creates a map if it is not
+/// created.
++ (FIRMutableDictionary *)sessionIDToSystemCompletionHandlerDictionary {
+ static FIRMutableDictionary *systemCompletionHandlers;
+
+ static dispatch_once_t systemCompletionHandlerOnceToken;
+ dispatch_once(&systemCompletionHandlerOnceToken, ^{
+ systemCompletionHandlers = [[FIRMutableDictionary alloc] init];
+ });
+ return systemCompletionHandlers;
+}
+
+- (NSURL *)temporaryFilePathWithSessionID:(NSString *)sessionID {
+ NSString *tempName = [NSString stringWithFormat:@"FIRUpload_temp_%@", sessionID];
+ return [_networkDirectoryURL URLByAppendingPathComponent:tempName];
+}
+
+/// Makes sure that the directory to store temp files exists. If not, tries to create it and returns
+/// YES. If there is anything wrong, returns NO.
+- (BOOL)ensureTemporaryDirectoryExists {
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSError *error = nil;
+
+ // Create a temporary directory if it does not exist or was deleted.
+ if ([_networkDirectoryURL checkResourceIsReachableAndReturnError:&error]) {
+ return YES;
+ }
+
+ if (error && error.code != NSFileReadNoSuchFileError) {
+ [_loggerDelegate
+ firNetwork_logWithLevel:kFIRNetworkLogLevelWarning
+ messageCode:kFIRNetworkMessageCodeURLSession014
+ message:@"Error while trying to access Network temp folder. Error"
+ context:error];
+ }
+
+ NSError *writeError = nil;
+
+ [fileManager createDirectoryAtURL:_networkDirectoryURL
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&writeError];
+ if (writeError) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession015
+ message:@"Cannot create temporary directory. Error"
+ context:writeError];
+ return NO;
+ }
+
+ // Set the iCloud exclusion attribute on the Documents URL.
+ [self excludeFromBackupForURL:_networkDirectoryURL];
+
+ return YES;
+}
+
+- (void)excludeFromBackupForURL:(NSURL *)url {
+ if (!url.path) {
+ return;
+ }
+
+ // Set the iCloud exclusion attribute on the Documents URL.
+ NSError *preventBackupError = nil;
+ [url setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&preventBackupError];
+ if (preventBackupError) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession016
+ message:@"Cannot exclude temporary folder from iTunes backup"];
+ }
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ willPerformHTTPRedirection:(NSHTTPURLResponse *)response
+ newRequest:(NSURLRequest *)request
+ completionHandler:(void (^)(NSURLRequest *))completionHandler {
+ NSArray *nonAllowedRedirectionCodes = @[
+ @(kFIRNetworkHTTPStatusCodeFound),
+ @(kFIRNetworkHTTPStatusCodeMovedPermanently),
+ @(kFIRNetworkHTTPStatusCodeMovedTemporarily),
+ @(kFIRNetworkHTTPStatusCodeMultipleChoices)
+ ];
+
+ // Allow those not in the non allowed list to be followed.
+ if (![nonAllowedRedirectionCodes containsObject:@(response.statusCode)]) {
+ completionHandler(request);
+ return;
+ }
+
+ // Do not allow redirection if the response code is in the non-allowed list.
+ NSURLRequest *newRequest = request;
+
+ if (response) {
+ newRequest = nil;
+ }
+
+ completionHandler(newRequest);
+}
+
+#pragma mark - Helper Methods
+
+- (void)callCompletionHandler:(FIRNetworkURLSessionCompletionHandler)handler
+ withResponse:(NSHTTPURLResponse *)response
+ data:(NSData *)data
+ error:(NSError *)error {
+ if (error) {
+ [_loggerDelegate firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeURLSession017
+ message:@"Encounter network error. Code, error"
+ contexts:@[@(error.code), error]];
+ }
+
+ if (handler) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ handler(response, data, _sessionID, error);
+ });
+ }
+}
+
+- (void)populateSessionConfig:(NSURLSessionConfiguration *)sessionConfig
+ withRequest:(NSURLRequest *)request {
+ sessionConfig.HTTPAdditionalHeaders = request.allHTTPHeaderFields;
+ sessionConfig.timeoutIntervalForRequest = request.timeoutInterval;
+ sessionConfig.timeoutIntervalForResource = request.timeoutInterval;
+ sessionConfig.requestCachePolicy = request.cachePolicy;
+}
+
+@end
diff --git a/Firebase/Core/FIROptions.h b/Firebase/Core/FIROptions.h
new file mode 100644
index 0000000..5bae59c
--- /dev/null
+++ b/Firebase/Core/FIROptions.h
@@ -0,0 +1,131 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRCoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * This class provides constant fields of Google APIs.
+ */
+FIR_SWIFT_NAME(FirebaseOptions)
+@interface FIROptions : NSObject<NSCopying>
+
+/**
+ * Returns the default options.
+ */
++ (nullable FIROptions *)defaultOptions FIR_SWIFT_NAME(defaultOptions());
+
+/**
+ * An iOS API key used for authenticating requests from your app, e.g.
+ * @"AIzaSyDdVgKwhZl0sTTTLZ7iTmt1r3N2cJLnaDk", used to identify your app to Google servers.
+ */
+@property(nonatomic, copy, nullable) NSString *APIKey FIR_SWIFT_NAME(apiKey);
+
+/**
+ * The bundle ID for the application. Defaults to `[[NSBundle mainBundle] bundleID]` when not set
+ * manually or in a plist.
+ */
+@property(nonatomic, copy) NSString *bundleID;
+
+/**
+ * The OAuth2 client ID for iOS application used to authenticate Google users, for example
+ * @"12345.apps.googleusercontent.com", used for signing in with Google.
+ */
+@property(nonatomic, copy, nullable) NSString *clientID;
+
+/**
+ * The tracking ID for Google Analytics, e.g. @"UA-12345678-1", used to configure Google Analytics.
+ */
+@property(nonatomic, copy, nullable) NSString *trackingID;
+
+/**
+ * The Project Number from the Google Developer's console, for example @"012345678901", used to
+ * configure Google Cloud Messaging.
+ */
+@property(nonatomic, copy) NSString *GCMSenderID FIR_SWIFT_NAME(gcmSenderID);
+
+/**
+ * The Project ID from the Firebase console, for example @"abc-xyz-123".
+ */
+@property(nonatomic, copy, nullable) NSString *projectID;
+
+/**
+ * The Android client ID used in Google AppInvite when an iOS app has its Android version, for
+ * example @"12345.apps.googleusercontent.com".
+ */
+@property(nonatomic, copy, nullable) NSString *androidClientID;
+
+/**
+ * The Google App ID that is used to uniquely identify an instance of an app.
+ */
+@property(nonatomic, copy) NSString *googleAppID;
+
+/**
+ * The database root URL, e.g. @"http://abc-xyz-123.firebaseio.com".
+ */
+@property(nonatomic, copy, nullable) NSString *databaseURL;
+
+/**
+ * The URL scheme used to set up Durable Deep Link service.
+ */
+@property(nonatomic, copy, nullable) NSString *deepLinkURLScheme;
+
+/**
+ * The Google Cloud Storage bucket name, e.g. @"abc-xyz-123.storage.firebase.com".
+ */
+@property(nonatomic, copy, nullable) NSString *storageBucket;
+
+/**
+ * Initializes a customized instance of FIROptions with keys. googleAppID, bundleID and GCMSenderID
+ * are required. Other keys may required for configuring specific services.
+ */
+- (instancetype)initWithGoogleAppID:(NSString *)googleAppID
+ bundleID:(NSString *)bundleID
+ GCMSenderID:(NSString *)GCMSenderID
+ APIKey:(NSString *)APIKey
+ clientID:(NSString *)clientID
+ trackingID:(NSString *)trackingID
+ androidClientID:(NSString *)androidClientID
+ databaseURL:(NSString *)databaseURL
+ storageBucket:(NSString *)storageBucket
+ deepLinkURLScheme:(NSString *)deepLinkURLScheme
+ DEPRECATED_MSG_ATTRIBUTE("Use `-[FIROptions initWithGoogleAppID:gcmSenderID:]` and "
+ "properties instead.");
+
+/**
+ * Initializes a customized instance of FIROptions from the file at the given plist file path.
+ * For example,
+ * NSString *filePath =
+ * [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"];
+ * FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:filePath];
+ * Returns nil if the plist file does not exist or is invalid.
+ */
+- (nullable instancetype)initWithContentsOfFile:(NSString *)plistPath;
+
+/**
+ * Initializes a customized instance of FIROptions with required fields. Use the mutable properties
+ * to modify fields for configuring specific services.
+ */
+- (instancetype)initWithGoogleAppID:(NSString *)googleAppID
+ GCMSenderID:(NSString *)GCMSenderID
+ FIR_SWIFT_NAME(init(googleAppID:gcmSenderID:));
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/FIROptions.m b/Firebase/Core/FIROptions.m
new file mode 100644
index 0000000..6e19c82
--- /dev/null
+++ b/Firebase/Core/FIROptions.m
@@ -0,0 +1,427 @@
+// 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 "Private/FIRBundleUtil.h"
+#import "Private/FIRErrors.h"
+#import "Private/FIRLogger.h"
+#import "Private/FIROptionsInternal.h"
+
+// Keys for the strings in the plist file.
+NSString *const kFIRAPIKey = @"API_KEY";
+NSString *const kFIRTrackingID = @"TRACKING_ID";
+NSString *const kFIRGoogleAppID = @"GOOGLE_APP_ID";
+NSString *const kFIRClientID = @"CLIENT_ID";
+NSString *const kFIRGCMSenderID = @"GCM_SENDER_ID";
+NSString *const kFIRAndroidClientID = @"ANDROID_CLIENT_ID";
+NSString *const kFIRDatabaseURL = @"DATABASE_URL";
+NSString *const kFIRStorageBucket = @"STORAGE_BUCKET";
+// The key to locate the expected bundle identifier in the plist file.
+NSString *const kFIRBundleID = @"BUNDLE_ID";
+// The key to locate the project identifier in the plist file.
+NSString *const kFIRProjectID = @"PROJECT_ID";
+
+NSString *const kFIRIsMeasurementEnabled = @"IS_MEASUREMENT_ENABLED";
+NSString *const kFIRIsAnalyticsCollectionEnabled = @"FIREBASE_ANALYTICS_COLLECTION_ENABLED";
+NSString *const kFIRIsAnalyticsCollectionDeactivated = @"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED";
+
+NSString *const kFIRIsAnalyticsEnabled = @"IS_ANALYTICS_ENABLED";
+NSString *const kFIRIsSignInEnabled = @"IS_SIGNIN_ENABLED";
+
+// Library version ID.
+NSString *const kFIRLibraryVersionID =
+ @"4" // Major version (one or more digits)
+ @"00" // Minor version (exactly 2 digits)
+ @"00" // Build number (exactly 2 digits)
+ @"000"; // Fixed "000"
+// Plist file name.
+NSString *const kServiceInfoFileName = @"GoogleService-Info";
+// Plist file type.
+NSString *const kServiceInfoFileType = @"plist";
+
+// Exception raised from attempting to modify a FIROptions after it's been copied to a FIRApp.
+NSString *const kFIRExceptionBadModification =
+ @"Attempted to modify options after it's set on FIRApp. Please modify all properties before "
+ @"initializing FIRApp.";
+
+@interface FIROptions ()
+
+/**
+ * This property maintains the actual configuration key-value pairs.
+ */
+@property(nonatomic, readwrite) NSMutableDictionary *optionsDictionary;
+
+/**
+ * Combination of analytics options from both the main plist and the GoogleService-info.plist.
+ * Values which are present in the main plist override values from the GoogleService-info.plist.
+ */
+@property(nonatomic, readonly) NSDictionary *analyticsOptionsDictionary;
+
+@end
+
+@implementation FIROptions {
+ /// Backing variable for self.analyticsOptionsDictionary.
+ NSDictionary *_analyticsOptionsDictionary;
+ dispatch_once_t _createAnalyticsOptionsDictionaryOnce;
+}
+
+static FIROptions *sDefaultOptions = nil;
+static NSDictionary *sDefaultOptionsDictionary = nil;
+
+#pragma mark - Public only for internal class methods
+
++ (FIROptions *)defaultOptions {
+ if (sDefaultOptions != nil) {
+ return sDefaultOptions;
+ }
+
+ NSDictionary *defaultOptionsDictionary = [self defaultOptionsDictionary];
+ if (defaultOptionsDictionary == nil) {
+ return nil;
+ }
+
+ sDefaultOptions =
+ [[FIROptions alloc] initInternalWithOptionsDictionary:defaultOptionsDictionary];
+ return sDefaultOptions;
+}
+
+#pragma mark - Private class methods
+
++ (NSDictionary *)defaultOptionsDictionary {
+ if (sDefaultOptionsDictionary != nil) {
+ return sDefaultOptionsDictionary;
+ }
+ NSString *plistFilePath = [FIROptions plistFilePathWithName:kServiceInfoFileName];
+ if (plistFilePath == nil) {
+ return nil;
+ }
+ sDefaultOptionsDictionary = [NSDictionary dictionaryWithContentsOfFile:plistFilePath];
+ if (sDefaultOptionsDictionary == nil) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000011", @"The configuration file is not a dictionary: "
+ @"'%@.%@'.", kServiceInfoFileName, kServiceInfoFileType);
+ }
+ return sDefaultOptionsDictionary;
+}
+
+// Returns the path of the plist file with a given file name.
++ (NSString *)plistFilePathWithName:(NSString *)fileName {
+ NSArray *bundles = [FIRBundleUtil relevantBundles];
+ NSString *plistFilePath =
+ [FIRBundleUtil optionsDictionaryPathWithResourceName:fileName
+ andFileType:kServiceInfoFileType
+ inBundles:bundles];
+ if (plistFilePath == nil) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000012", @"Could not locate configuration file: '%@.%@'.",
+ fileName, kServiceInfoFileType);
+ }
+ return plistFilePath;
+}
+
++ (void)resetDefaultOptions {
+ sDefaultOptions = nil;
+ sDefaultOptionsDictionary = nil;
+}
+
+#pragma mark - Private instance methods
+
+- (instancetype)initInternalWithOptionsDictionary:(NSDictionary *)optionsDictionary {
+ self = [super init];
+ if (self) {
+ _optionsDictionary = [optionsDictionary mutableCopy];
+ _usingOptionsFromDefaultPlist = YES;
+ }
+ return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ FIROptions *newOptions = [[[self class] allocWithZone:zone] init];
+ if (newOptions) {
+ newOptions.optionsDictionary = self.optionsDictionary;
+ newOptions.deepLinkURLScheme = self.deepLinkURLScheme;
+ newOptions.editingLocked = self.isEditingLocked;
+ newOptions.usingOptionsFromDefaultPlist = self.usingOptionsFromDefaultPlist;
+ }
+ return newOptions;
+}
+
+#pragma mark - Public instance methods
+
+- (instancetype)initWithGoogleAppID:(NSString *)googleAppID
+ bundleID:(NSString *)bundleID
+ GCMSenderID:(NSString *)GCMSenderID
+ APIKey:(NSString *)APIKey
+ clientID:(NSString *)clientID
+ trackingID:(NSString *)trackingID
+ androidClientID:(NSString *)androidClientID
+ databaseURL:(NSString *)databaseURL
+ storageBucket:(NSString *)storageBucket
+ deepLinkURLScheme:(NSString *)deepLinkURLScheme {
+ self = [super init];
+ if (self) {
+ if (!googleAppID) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Please specify a valid Google App ID."];
+ } else if (!GCMSenderID) {
+ [NSException raise:kFirebaseCoreErrorDomain format:@"Please specify a valid GCM Sender ID."];
+ }
+
+ // `bundleID` is a required property, default to the main `bundleIdentifier` if it's `nil`.
+ if (!bundleID) {
+ bundleID = [[NSBundle mainBundle] bundleIdentifier];
+ }
+
+ NSMutableDictionary *mutableOptionsDict = [NSMutableDictionary dictionary];
+ [mutableOptionsDict setValue:googleAppID forKey:kFIRGoogleAppID];
+ [mutableOptionsDict setValue:bundleID forKey:kFIRBundleID];
+ [mutableOptionsDict setValue:GCMSenderID forKey:kFIRGCMSenderID];
+ [mutableOptionsDict setValue:APIKey forKey:kFIRAPIKey];
+ [mutableOptionsDict setValue:clientID forKey:kFIRClientID];
+ [mutableOptionsDict setValue:trackingID forKey:kFIRTrackingID];
+ [mutableOptionsDict setValue:androidClientID forKey:kFIRAndroidClientID];
+ [mutableOptionsDict setValue:databaseURL forKey:kFIRDatabaseURL];
+ [mutableOptionsDict setValue:storageBucket forKey:kFIRStorageBucket];
+ self.optionsDictionary = mutableOptionsDict;
+ self.deepLinkURLScheme = deepLinkURLScheme;
+ }
+ return self;
+}
+
+- (instancetype)initWithContentsOfFile:(NSString *)plistPath {
+ self = [super init];
+ if (self) {
+ if (plistPath == nil) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000013", @"The plist file path is nil.");
+ return nil;
+ }
+ _optionsDictionary = [[NSDictionary dictionaryWithContentsOfFile:plistPath] mutableCopy];
+ if (_optionsDictionary == nil) {
+ FIRLogError(kFIRLoggerCore, @"I-COR000014", @"The configuration file at %@ does not exist or "
+ @"is not a well-formed plist file.", plistPath);
+ return nil;
+ }
+ // TODO: Do we want to validate the dictionary here? It says we do that already in
+ // the public header.
+ }
+ return self;
+}
+
+- (instancetype)initWithGoogleAppID:(NSString *)googleAppID
+ GCMSenderID:(NSString *)GCMSenderID {
+ self = [super init];
+ if (self) {
+ NSMutableDictionary *mutableOptionsDict = [NSMutableDictionary dictionary];
+ [mutableOptionsDict setValue:googleAppID forKey:kFIRGoogleAppID];
+ [mutableOptionsDict setValue:GCMSenderID forKey:kFIRGCMSenderID];
+ [mutableOptionsDict setValue:[[NSBundle mainBundle] bundleIdentifier] forKey:kFIRBundleID];
+ self.optionsDictionary = mutableOptionsDict;
+ }
+ return self;
+}
+
+- (NSString *)APIKey {
+ return self.optionsDictionary[kFIRAPIKey];
+}
+
+- (void)setAPIKey:(NSString *)APIKey {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRAPIKey] = [APIKey copy];
+}
+
+- (NSString *)clientID {
+ return self.optionsDictionary[kFIRClientID];
+}
+
+- (void)setClientID:(NSString *)clientID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRClientID] = [clientID copy];
+}
+
+- (NSString *)trackingID {
+ return self.optionsDictionary[kFIRTrackingID];
+}
+
+- (void)setTrackingID:(NSString *)trackingID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRTrackingID] = [trackingID copy];
+}
+
+- (NSString *)GCMSenderID {
+ return self.optionsDictionary[kFIRGCMSenderID];
+}
+
+- (void)setGCMSenderID:(NSString *)GCMSenderID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRGCMSenderID] = [GCMSenderID copy];
+}
+
+- (NSString *)projectID {
+ return self.optionsDictionary[kFIRProjectID];
+}
+
+- (void)setProjectID:(NSString *)projectID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRProjectID] = [projectID copy];
+}
+
+- (NSString *)androidClientID {
+ return self.optionsDictionary[kFIRAndroidClientID];
+}
+
+- (void)setAndroidClientID:(NSString *)androidClientID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRAndroidClientID] = [androidClientID copy];
+}
+
+- (NSString *)googleAppID {
+ return self.optionsDictionary[kFIRGoogleAppID];
+}
+
+- (void)setGoogleAppID:(NSString *)googleAppID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRGoogleAppID] = [googleAppID copy];
+}
+
+- (NSString *)libraryVersionID {
+ return kFIRLibraryVersionID;
+}
+
+- (void)setLibraryVersionID:(NSString *)libraryVersionID {
+ _optionsDictionary[kFIRLibraryVersionID] = [libraryVersionID copy];
+}
+
+- (NSString *)databaseURL {
+ return self.optionsDictionary[kFIRDatabaseURL];
+}
+
+- (void)setDatabaseURL:(NSString *)databaseURL {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRDatabaseURL] = [databaseURL copy];
+}
+
+- (NSString *)storageBucket {
+ return self.optionsDictionary[kFIRStorageBucket];
+}
+
+- (void)setStorageBucket:(NSString *)storageBucket {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRStorageBucket] = [storageBucket copy];
+}
+
+- (void)setDeepLinkURLScheme:(NSString *)deepLinkURLScheme {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _deepLinkURLScheme = [deepLinkURLScheme copy];
+}
+
+- (NSString *)bundleID {
+ return self.optionsDictionary[kFIRBundleID];
+}
+
+- (void)setBundleID:(NSString *)bundleID {
+ if (self.isEditingLocked) {
+ [NSException raise:kFirebaseCoreErrorDomain format:kFIRExceptionBadModification];
+ }
+
+ _optionsDictionary[kFIRBundleID] = [bundleID copy];
+}
+
+#pragma mark - Internal instance methods
+
+- (NSDictionary *)analyticsOptionsDictionary {
+ dispatch_once(&_createAnalyticsOptionsDictionaryOnce, ^{
+ NSMutableDictionary *tempAnalyticsOptions = [[NSMutableDictionary alloc] init];
+ NSDictionary *mainInfoDictionary = [NSBundle mainBundle].infoDictionary;
+ NSArray *measurementKeys = @[ kFIRIsMeasurementEnabled,
+ kFIRIsAnalyticsCollectionEnabled,
+ kFIRIsAnalyticsCollectionDeactivated ];
+ for (NSString *key in measurementKeys) {
+ id value = mainInfoDictionary[key] ?: self.optionsDictionary[key] ?: nil;
+ if (!value) {
+ continue;
+ }
+ tempAnalyticsOptions[key] = value;
+ }
+ _analyticsOptionsDictionary = tempAnalyticsOptions;
+ });
+ return _analyticsOptionsDictionary;
+}
+
+/**
+ * Whether or not Measurement was enabled. Measurement is enabled unless explicitly disabled in
+ * GoogleService-Info.plist. This uses the old plist flag IS_MEASUREMENT_ENABLED, which should still
+ * be supported.
+ */
+- (BOOL)isMeasurementEnabled {
+ if (self.isAnalyticsCollectionDeactivated) {
+ return NO;
+ }
+ if (!self.analyticsOptionsDictionary[kFIRIsMeasurementEnabled]) {
+ return YES; // Enable Measurement by default when the key is not in the dictionary.
+ }
+ return [self.analyticsOptionsDictionary[kFIRIsMeasurementEnabled] boolValue];
+}
+
+- (BOOL)isAnalyticsCollectionEnabled {
+ if (self.isAnalyticsCollectionDeactivated) {
+ return NO;
+ }
+ if (!self.analyticsOptionsDictionary[kFIRIsAnalyticsCollectionEnabled]) {
+ return self.isMeasurementEnabled; // Fall back to older plist flag.
+ }
+ return [self.analyticsOptionsDictionary[kFIRIsAnalyticsCollectionEnabled] boolValue];
+}
+
+- (BOOL)isAnalyticsCollectionDeactivated {
+ if (!self.analyticsOptionsDictionary[kFIRIsAnalyticsCollectionDeactivated]) {
+ return NO; // Analytics Collection is not deactivated when the key is not in the dictionary.
+ }
+ return [self.analyticsOptionsDictionary[kFIRIsAnalyticsCollectionDeactivated] boolValue];
+}
+
+- (BOOL)isAnalyticsEnabled {
+ return [self.optionsDictionary[kFIRIsAnalyticsEnabled] boolValue];
+}
+
+- (BOOL)isSignInEnabled {
+ return [self.optionsDictionary[kFIRIsSignInEnabled] boolValue];
+}
+
+@end
diff --git a/Firebase/Core/FIRReachabilityChecker.m b/Firebase/Core/FIRReachabilityChecker.m
new file mode 100644
index 0000000..66b6547
--- /dev/null
+++ b/Firebase/Core/FIRReachabilityChecker.m
@@ -0,0 +1,245 @@
+// 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 "Private/FIRReachabilityChecker.h"
+#import "Private/FIRReachabilityChecker+Internal.h"
+
+#import "Private/FIRNetwork.h"
+#import "Private/FIRNetworkMessageCode.h"
+#import "Private/FIRLogger.h"
+
+static void ReachabilityCallback(SCNetworkReachabilityRef reachability,
+ SCNetworkReachabilityFlags flags, void *info);
+
+static const struct FIRReachabilityApi kFIRDefaultReachabilityApi = {
+ SCNetworkReachabilityCreateWithName,
+ SCNetworkReachabilitySetCallback,
+ SCNetworkReachabilityScheduleWithRunLoop,
+ SCNetworkReachabilityUnscheduleFromRunLoop,
+ CFRelease,
+};
+
+static NSString *const kFIRReachabilityUnknownStatus = @"Unknown";
+static NSString *const kFIRReachabilityConnectedStatus = @"Connected";
+static NSString *const kFIRReachabilityDisconnectedStatus = @"Disconnected";
+
+@interface FIRReachabilityChecker ()
+
+@property(nonatomic, assign) const struct FIRReachabilityApi *reachabilityApi;
+@property(nonatomic, assign) FIRReachabilityStatus reachabilityStatus;
+@property(nonatomic, copy) NSString *host;
+@property(nonatomic, assign) SCNetworkReachabilityRef reachability;
+
+@end
+
+@implementation FIRReachabilityChecker
+
+@synthesize reachabilityApi = reachabilityApi_;
+@synthesize reachability = reachability_;
+
+- (const struct FIRReachabilityApi *)reachabilityApi {
+ return reachabilityApi_;
+}
+
+- (void)setReachabilityApi:(const struct FIRReachabilityApi *)reachabilityApi {
+ if (reachability_) {
+ NSString *message = @"Cannot change reachability API while reachability is running. "
+ @"Call stop first.";
+ [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeReachabilityChecker000
+ message:message];
+ return;
+ }
+ reachabilityApi_ = reachabilityApi;
+}
+
+@synthesize reachabilityStatus = reachabilityStatus_;
+@synthesize host = host_;
+@synthesize reachabilityDelegate = reachabilityDelegate_;
+@synthesize loggerDelegate = loggerDelegate_;
+
+- (BOOL)isActive {
+ return reachability_ != nil;
+}
+
+- (void)setReachabilityDelegate:(id<FIRReachabilityDelegate>)reachabilityDelegate {
+ if (reachabilityDelegate &&
+ (![(NSObject *)reachabilityDelegate conformsToProtocol:@protocol(FIRReachabilityDelegate)])) {
+ FIRLogError(
+ kFIRLoggerCore,
+ [NSString stringWithFormat:@"I-NET%06ld",
+ (long)kFIRNetworkMessageCodeReachabilityChecker005],
+ @"Reachability delegate doesn't conform to Reachability protocol.");
+ return;
+ }
+ reachabilityDelegate_ = reachabilityDelegate;
+}
+
+- (void)setLoggerDelegate:(id<FIRNetworkLoggerDelegate>)loggerDelegate {
+ if (loggerDelegate &&
+ (![(NSObject *)loggerDelegate conformsToProtocol:@protocol(FIRNetworkLoggerDelegate)])) {
+ FIRLogError(
+ kFIRLoggerCore,
+ [NSString stringWithFormat:@"I-NET%06ld",
+ (long)kFIRNetworkMessageCodeReachabilityChecker006],
+ @"Reachability delegate doesn't conform to Logger protocol.");
+ return;
+ }
+ loggerDelegate_ = loggerDelegate;
+}
+
+- (instancetype)initWithReachabilityDelegate:(id<FIRReachabilityDelegate>)reachabilityDelegate
+ loggerDelegate:(id<FIRNetworkLoggerDelegate>)loggerDelegate
+ withHost:(NSString *)host {
+ self = [super init];
+
+ [self setLoggerDelegate:loggerDelegate];
+
+ if (!host || !host.length) {
+ [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeReachabilityChecker001
+ message:@"Invalid host specified"];
+ return nil;
+ }
+ if (self) {
+ [self setReachabilityDelegate:reachabilityDelegate];
+ reachabilityApi_ = &kFIRDefaultReachabilityApi;
+ reachabilityStatus_ = kFIRReachabilityUnknown;
+ host_ = [host copy];
+ reachability_ = nil;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ reachabilityDelegate_ = nil;
+ loggerDelegate_ = nil;
+ [self stop];
+}
+
+- (BOOL)start {
+ if (!reachability_) {
+ reachability_ = reachabilityApi_->createWithNameFn(kCFAllocatorDefault, [host_ UTF8String]);
+ if (!reachability_) {
+ return NO;
+ }
+ SCNetworkReachabilityContext context = {
+ 0, /* version */
+ (__bridge void *)(self), /* info (passed as last parameter to reachability callback) */
+ NULL, /* retain */
+ NULL, /* release */
+ NULL /* copyDescription */
+ };
+ if (!reachabilityApi_->setCallbackFn(reachability_, ReachabilityCallback, &context) ||
+ !reachabilityApi_->scheduleWithRunLoopFn(reachability_, CFRunLoopGetMain(),
+ kCFRunLoopCommonModes)) {
+ reachabilityApi_->releaseFn(reachability_);
+ reachability_ = nil;
+ [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError
+ messageCode:kFIRNetworkMessageCodeReachabilityChecker002
+ message:@"Failed to start reachability handle"];
+ return NO;
+ }
+ }
+ [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeReachabilityChecker003
+ message:@"Monitoring the network status"];
+ return YES;
+}
+
+- (void)stop {
+ if (reachability_) {
+ reachabilityStatus_ = kFIRReachabilityUnknown;
+ reachabilityApi_->unscheduleFromRunLoopFn(reachability_, CFRunLoopGetMain(),
+ kCFRunLoopCommonModes);
+ reachabilityApi_->releaseFn(reachability_);
+ reachability_ = nil;
+ }
+}
+
+- (FIRReachabilityStatus)statusForFlags:(SCNetworkReachabilityFlags)flags {
+ FIRReachabilityStatus status = kFIRReachabilityNotReachable;
+ // If the Reachable flag is not set, we definitely don't have connectivity.
+ if (flags & kSCNetworkReachabilityFlagsReachable) {
+ // Reachable flag is set. Check further flags.
+ if (!(flags & kSCNetworkReachabilityFlagsConnectionRequired)) {
+ // Connection required flag is not set, so we have connectivity.
+ status = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? kFIRReachabilityViaCellular
+ : kFIRReachabilityViaWifi;
+ } else if ((flags & (kSCNetworkReachabilityFlagsConnectionOnDemand |
+ kSCNetworkReachabilityFlagsConnectionOnTraffic)) &&
+ !(flags & kSCNetworkReachabilityFlagsInterventionRequired)) {
+ // If the connection on demand or connection on traffic flag is set, and user intervention
+ // is not required, we have connectivity.
+ status = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? kFIRReachabilityViaCellular
+ : kFIRReachabilityViaWifi;
+ }
+ }
+ return status;
+}
+
+- (void)reachabilityFlagsChanged:(SCNetworkReachabilityFlags)flags {
+ FIRReachabilityStatus status = [self statusForFlags:flags];
+ if (reachabilityStatus_ != status) {
+ NSString *reachabilityStatusString;
+ if (status == kFIRReachabilityUnknown) {
+ reachabilityStatusString = kFIRReachabilityUnknownStatus;
+ } else {
+ reachabilityStatusString = (status == kFIRReachabilityNotReachable)
+ ? kFIRReachabilityDisconnectedStatus
+ : kFIRReachabilityConnectedStatus;
+ }
+ [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
+ messageCode:kFIRNetworkMessageCodeReachabilityChecker004
+ message:@"Network status has changed. Code, status"
+ contexts:@[ @(status), reachabilityStatusString ]];
+ reachabilityStatus_ = status;
+ [reachabilityDelegate_ reachability:self statusChanged:reachabilityStatus_];
+ }
+}
+
+@end
+
+static void ReachabilityCallback(SCNetworkReachabilityRef reachability,
+ SCNetworkReachabilityFlags flags, void *info) {
+ FIRReachabilityChecker *checker = (__bridge FIRReachabilityChecker *)info;
+ [checker reachabilityFlagsChanged:flags];
+}
+
+// This function used to be at the top of the file, but it was moved here
+// as a workaround for a suspected compiler bug. When compiled in Release mode
+// and run on an iOS device with WiFi disabled, the reachability code crashed
+// when calling SCNetworkReachabilityScheduleWithRunLoop, or shortly thereafter.
+// After unsuccessfully trying to diagnose the cause of the crash, it was
+// discovered that moving this function to the end of the file magically fixed
+// the crash. If you are going to edit this file, exercise caution and make sure
+// to test thoroughly with an iOS device under various network conditions.
+const NSString *FIRReachabilityStatusString(FIRReachabilityStatus status) {
+ switch (status) {
+ case kFIRReachabilityUnknown:
+ return @"Reachability Unknown";
+
+ case kFIRReachabilityNotReachable:
+ return @"Not reachable";
+
+ case kFIRReachabilityViaWifi:
+ return @"Reachable via Wifi";
+
+ case kFIRReachabilityViaCellular:
+ return @"Reachable via Cellular Data";
+
+ default:
+ return [NSString stringWithFormat:@"Invalid reachability status %d", (int)status];
+ }
+}
diff --git a/Firebase/Core/FIRURLSchemeUtil.m b/Firebase/Core/FIRURLSchemeUtil.m
new file mode 100644
index 0000000..8dbecae
--- /dev/null
+++ b/Firebase/Core/FIRURLSchemeUtil.m
@@ -0,0 +1,43 @@
+// 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 "Private/FIRURLSchemeUtil.h"
+#import "Private/FIRLogger.h"
+
+/**
+ * Regular expression to match the URL scheme for Google sign-in.
+ */
+static NSString *const kFIRGoogleSignInURLSchemePattern =
+@"^com\\.googleusercontent\\.apps\\.\\d+-\\w+$";
+
+BOOL fir_areURLSchemesValidForGoogleSignIn(NSArray *urlSchemes) {
+ BOOL hasReversedClientID = NO;
+ for (NSString *urlScheme in urlSchemes) {
+ if (!hasReversedClientID) {
+ NSRange range = [urlScheme rangeOfString:kFIRGoogleSignInURLSchemePattern
+ options:NSRegularExpressionSearch];
+ if (range.location != NSNotFound) {
+ hasReversedClientID = YES;
+ }
+ }
+ }
+ if (hasReversedClientID) {
+ return YES;
+ }
+ if (!hasReversedClientID) {
+ FIRLogInfo(kFIRLoggerCore, @"I-COR000021", @"A reversed client ID should be added as a URL "
+ @"scheme to enable Google sign-in.");
+ }
+ return NO;
+}
diff --git a/Firebase/Core/FirebaseCore.h b/Firebase/Core/FirebaseCore.h
new file mode 100644
index 0000000..fa26f69
--- /dev/null
+++ b/Firebase/Core/FirebaseCore.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 "FIRAnalyticsConfiguration.h"
+#import "FIRApp.h"
+#import "FIRConfiguration.h"
+#import "FIRLoggerLevel.h"
+#import "FIROptions.h"
diff --git a/Firebase/Core/FirebaseCore.podspec b/Firebase/Core/FirebaseCore.podspec
new file mode 100644
index 0000000..f513367
--- /dev/null
+++ b/Firebase/Core/FirebaseCore.podspec
@@ -0,0 +1,35 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseCore'
+ s.version = '4.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]'
+ s.public_header_files =
+ 'FirebaseCore.h',
+ 'FIRAnalyticsConfiguration.h',
+ 'FIRApp.h',
+ 'FIRConfiguration.h',
+ 'FIRLoggerLevel.h',
+ 'FIROptions.h',
+ 'FIRCoreSwiftNameSupport.h'
+
+ s.dependency 'GoogleToolboxForMac/NSData+zlib', '~> 2.1'
+end
diff --git a/Firebase/Core/Private/FIRAnalyticsConfiguration+Internal.h b/Firebase/Core/Private/FIRAnalyticsConfiguration+Internal.h
new file mode 100644
index 0000000..6c57a0f
--- /dev/null
+++ b/Firebase/Core/Private/FIRAnalyticsConfiguration+Internal.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 "../FIRAnalyticsConfiguration.h"
+
+/// Values stored in analyticsEnabledState. Never alter these constants since they must match with
+/// values persisted to disk.
+typedef NS_ENUM(int64_t, FIRAnalyticsEnabledState) {
+ // 0 is the default value for keys not found stored in persisted config, so it cannot represent
+ // kFIRAnalyticsEnabledStateSetNo. It must represent kFIRAnalyticsEnabledStateNotSet.
+ kFIRAnalyticsEnabledStateNotSet = 0,
+ kFIRAnalyticsEnabledStateSetYes = 1,
+ kFIRAnalyticsEnabledStateSetNo = 2,
+};
+
+/// The user defaults key for the persisted measurementEnabledState value. FIRAPersistedConfig reads
+/// measurementEnabledState using this same key.
+static NSString *const kFIRAPersistedConfigMeasurementEnabledStateKey =
+ @"/google/measurement/measurement_enabled_state";
+
+static NSString *const kFIRAnalyticsConfigurationSetEnabledNotification =
+ @"FIRAnalyticsConfigurationSetEnabledNotification";
+static NSString *const kFIRAnalyticsConfigurationSetMinimumSessionIntervalNotification =
+ @"FIRAnalyticsConfigurationSetMinimumSessionIntervalNotification";
+static NSString *const kFIRAnalyticsConfigurationSetSessionTimeoutIntervalNotification =
+ @"FIRAnalyticsConfigurationSetSessionTimeoutIntervalNotification";
diff --git a/Firebase/Core/Private/FIRAppAssociationRegistration.h b/Firebase/Core/Private/FIRAppAssociationRegistration.h
new file mode 100644
index 0000000..3d697a7
--- /dev/null
+++ b/Firebase/Core/Private/FIRAppAssociationRegistration.h
@@ -0,0 +1,48 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FIRAppAssociationRegistration
+ @brief Manages object associations as a singleton-dependent: At most one object is
+ registered for any given host/key pair, and the object shall be created on-the-fly when
+ asked for.
+ */
+@interface FIRAppAssociationRegistration<ObjectType> : NSObject
+
+/** @fn registeredObjectWithHost:key:creationBlock:
+ @brief Retrieves the registered object with a particular host and key.
+ @param host The host object.
+ @param key The key to specify the registered object on the host.
+ @param creationBlock The block to return the object to be registered if not already.
+ The block is executed immediately before this method returns if it is executed at all.
+ It can also be executed multiple times across different method invocations if previous
+ execution of the block returns @c nil.
+ @return The registered object for the host/key pair, or @c nil if no object is registered
+ and @c creationBlock returns @c nil.
+ @remarks The method is thread-safe but non-reentrant in the sense that attempting to call this
+ method again within the @c creationBlock with the same host/key pair raises an exception.
+ The registered object is retained by the host.
+ */
++ (nullable ObjectType)registeredObjectWithHost:(id)host
+ key:(NSString *)key
+ creationBlock:(ObjectType _Nullable (^)())creationBlock;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/Private/FIRAppEnvironmentUtil.h b/Firebase/Core/Private/FIRAppEnvironmentUtil.h
new file mode 100644
index 0000000..ba4696c
--- /dev/null
+++ b/Firebase/Core/Private/FIRAppEnvironmentUtil.h
@@ -0,0 +1,48 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import <UIKit/UIKit.h>
+
+@interface FIRAppEnvironmentUtil : NSObject
+
+/// Indicates whether the app is from Apple Store or not. Returns NO if the app is on simulator,
+/// development environment or sideloaded.
++ (BOOL)isFromAppStore;
+
+/// Indicates whether the app is a Testflight app. Returns YES if the app has sandbox receipt.
+/// Returns NO otherwise.
++ (BOOL)isAppStoreReceiptSandbox;
+
+/// Indicates whether the app is on simulator or not at runtime depending on the device
+/// architecture.
++ (BOOL)isSimulator;
+
+/// The current device model. Returns an empty string if device model cannot be retrieved.
++ (NSString *)deviceModel;
+
+/// The current operating system version. Returns an empty string if the system version cannot be
+/// retrieved.
++ (NSString *)systemVersion;
+
+/// Indicates whether it is running inside an extension or an app.
++ (BOOL)isAppExtension;
+
+/// Returns the [UIApplication sharedApplication] if it is running on an app, not an extension.
++ (UIApplication *)sharedApplication;
+
+@end
diff --git a/Firebase/Core/Private/FIRAppInternal.h b/Firebase/Core/Private/FIRAppInternal.h
new file mode 100644
index 0000000..11b3bf6
--- /dev/null
+++ b/Firebase/Core/Private/FIRAppInternal.h
@@ -0,0 +1,140 @@
+/*
+ * 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 "FIRApp.h"
+#import "FIRErrors.h"
+
+/**
+ * The internal interface to FIRApp. This is meant for first-party integrators, who need to receive
+ * FIRApp notifications, log info about the success or failure of their configuration, and access
+ * other internal functionality of FIRApp.
+ *
+ * TODO(b/28296561): Restructure this header.
+ */
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSInteger, FIRConfigType) {
+ FIRConfigTypeCore = 1,
+ FIRConfigTypeSDK = 2,
+};
+
+/**
+ * Names of services provided by Firebase.
+ */
+extern NSString *const kFIRServiceAdMob;
+extern NSString *const kFIRServiceAuth;
+extern NSString *const kFIRServiceCrash;
+extern NSString *const kFIRServiceDatabase;
+extern NSString *const kFIRServiceDynamicLinks;
+extern NSString *const kFIRServiceInstanceID;
+extern NSString *const kFIRServiceInvites;
+extern NSString *const kFIRServiceMessaging;
+extern NSString *const kFIRServiceMeasurement;
+extern NSString *const kFIRServiceRemoteConfig;
+extern NSString *const kFIRServiceStorage;
+
+/**
+ * Names of services provided by the Google pod, but logged by the Firebase pod.
+ */
+extern NSString *const kGGLServiceAnalytics;
+extern NSString *const kGGLServiceSignIn;
+
+extern NSString *const kFIRDefaultAppName;
+extern NSString *const kFIRAppReadyToConfigureSDKNotification;
+extern NSString *const kFIRAppDeleteNotification;
+extern NSString *const kFIRAppIsDefaultAppKey;
+extern NSString *const kFIRAppNameKey;
+extern NSString *const kFIRGoogleAppIDKey;
+
+/** @typedef FIRTokenCallback
+ @brief The type of block which gets called when a token is ready.
+ */
+typedef void (^FIRTokenCallback)(NSString *_Nullable token, NSError *_Nullable error);
+
+/** @typedef FIRAppGetTokenImplementation
+ @brief The type of block which can provide an implementation for the @c getTokenWithCallback:
+ method.
+ @param forceRefresh Forces the token to be refreshed.
+ @param callback The block which should be invoked when the async call completes.
+ */
+typedef void (^FIRAppGetTokenImplementation)(BOOL forceRefresh, FIRTokenCallback callback);
+
+/** @typedef FIRAppGetUID
+ @brief The type of block which can provide an implementation for the @c getUID method.
+ */
+typedef NSString *_Nullable (^FIRAppGetUIDImplementation)();
+
+@interface FIRApp ()
+
+/** @property getTokenImplementation
+ @brief Gets or sets the block to use for the implementation of
+ @c getTokenForcingRefresh:withCallback:
+ */
+@property(nonatomic, copy) FIRAppGetTokenImplementation getTokenImplementation;
+
+/** @property getUIDImplementation
+ @brief Gets or sets the block to use for the implementation of @c getUID.
+ */
+@property(nonatomic, copy) FIRAppGetUIDImplementation getUIDImplementation;
+
+/**
+ * Creates an error for failing to configure a subspec service. This method is called by each
+ * FIRApp notification listener.
+ */
++ (NSError *)errorForSubspecConfigurationFailureWithDomain:(NSString *)domain
+ errorCode:(FIRErrorCode)code
+ service:(NSString *)service
+ reason:(NSString *)reason;
+
+/**
+ * Used by each SDK to send logs about SDK configuration status to Clearcut.
+ */
+- (void)sendLogsWithServiceName:(NSString *)serviceName
+ version:(NSString *)version
+ error:(NSError *)error;
+
+/**
+ * Can be used by the unit tests in eack SDK to reset FIRApp. This method is thread unsafe.
+ */
++ (void)resetApps;
+
+/**
+ * Can be used by the unit tests in each SDK to set customized options.
+ */
+- (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options;
+
+/** @fn getTokenForcingRefresh:withCallback:
+ @brief Retrieves the Firebase authentication token, possibly refreshing it.
+ @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason
+ other than an expiration.
+ @param callback The block to invoke when the token is available.
+ */
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(FIRTokenCallback)callback;
+
+/**
+ * Exposed for use by the Google pod. Configures the default app without sending notifications to
+ * other SDKs. Otherwise, behaves exactly like +configure.
+ */
++ (void)configureWithoutSendingNotification;
+
+/**
+ * Expose the UID of the current user for Firestore.
+ */
+- (nullable NSString *)getUID;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/Private/FIRBundleUtil.h b/Firebase/Core/Private/FIRBundleUtil.h
new file mode 100644
index 0000000..4bfef8d
--- /dev/null
+++ b/Firebase/Core/Private/FIRBundleUtil.h
@@ -0,0 +1,57 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+/**
+ * This class provides utilities for accessing resources in bundles.
+ */
+@interface FIRBundleUtil : NSObject
+
+/**
+ * Finds all relevant bundles, starting with [NSBundle mainBundle].
+ */
++ (NSArray *)relevantBundles;
+
+/**
+ * Reads the options dictionary from one of the provided bundles.
+ *
+ * @param resourceName The resource name, e.g. @"GoogleService-Info".
+ * @param fileType The file type (extension), e.g. @"plist".
+ * @param bundles The bundles to expect, in priority order. See also
+ * +[FIRBundleUtil relevantBundles].
+ */
++ (NSString *)optionsDictionaryPathWithResourceName:(NSString *)resourceName
+ andFileType:(NSString *)fileType
+ inBundles:(NSArray *)bundles;
+
+/**
+ * Finds URL schemes defined in all relevant bundles, starting with those from
+ * [NSBundle mainBundle].
+ */
++ (NSArray *)relevantURLSchemes;
+
+/**
+ * Finds bundle identifiers in all relevant bundles, starting with those from [NSBundle mainBundle].
+ */
++ (NSSet *)relevantBundleIdentifiers;
+
+/**
+ * Checks if the bundle identifier exists in the given bundles.
+ */
++ (BOOL)hasBundleIdentifier:(NSString *)bundleIdentifier inBundles:(NSArray *)bundles;
+
+@end
diff --git a/Firebase/Core/Private/FIRErrorCode.h b/Firebase/Core/Private/FIRErrorCode.h
new file mode 100644
index 0000000..01d3c56
--- /dev/null
+++ b/Firebase/Core/Private/FIRErrorCode.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.
+ */
+
+/** Error codes in Firebase error domain. */
+typedef NS_ENUM(NSInteger, FIRErrorCode) {
+ /**
+ * Unknown error.
+ */
+ FIRErrorCodeUnknown = 0,
+ /**
+ * Loading data from the GoogleService-Info.plist file failed. This is a fatal error and should
+ * not be ignored. Further calls to the API will fail and/or possibly cause crashes.
+ */
+ FIRErrorCodeInvalidPlistFile = -100,
+
+ /**
+ * Validating the Google App ID format failed.
+ */
+ FIRErrorCodeInvalidAppID = -101,
+
+ /**
+ * Error code for failing to configure a specific service.
+ */
+ FIRErrorCodeAdMobFailed = -110,
+ FIRErrorCodeAppInviteFailed = -112,
+ FIRErrorCodeCloudMessagingFailed = -113,
+ FIRErrorCodeConfigFailed = -114,
+ FIRErrorCodeDatabaseFailed = -115,
+ FIRErrorCodeCrashReportingFailed = -118,
+ FIRErrorCodeDurableDeepLinkFailed = -119,
+ FIRErrorCodeAuthFailed = -120,
+ FIRErrorCodeInstanceIDFailed = -121,
+ FIRErrorCodeStorageFailed = -123,
+
+ /**
+ * Error codes returned by Dynamic Links
+ */
+ FIRErrorCodeDynamicLinksStrongMatchNotAvailable = -124,
+ FIRErrorCodeDynamicLinksManualRetrievalNotEnabled = -125,
+ FIRErrorCodeDynamicLinksPendingLinkOnlyAvailableAtFirstLaunch = -126,
+ FIRErrorCodeDynamicLinksPendingLinkRetrievalAlreadyRunning = -127,
+};
diff --git a/Firebase/Core/Private/FIRErrors.h b/Firebase/Core/Private/FIRErrors.h
new file mode 100644
index 0000000..9a03575
--- /dev/null
+++ b/Firebase/Core/Private/FIRErrors.h
@@ -0,0 +1,43 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#include "FIRErrorCode.h"
+
+extern NSString *const kFirebaseErrorDomain;
+extern NSString *const kFirebaseAdMobErrorDomain;
+extern NSString *const kFirebaseAppInviteErrorDomain;
+extern NSString *const kFirebaseAuthErrorDomain;
+extern NSString *const kFirebaseCloudMessagingErrorDomain;
+extern NSString *const kFirebaseConfigErrorDomain;
+extern NSString *const kFirebaseCoreErrorDomain;
+extern NSString *const kFirebaseCrashReportingErrorDomain;
+extern NSString *const kFirebaseDatabaseErrorDomain;
+extern NSString *const kFirebaseDurableDeepLinkErrorDomain;
+extern NSString *const kFirebaseInstanceIDErrorDomain;
+extern NSString *const kFirebasePerfErrorDomain;
+extern NSString *const kFirebaseStorageErrorDomain;
+
+/**
+ * Factory for a NSError in the Firebase error domain.
+ *
+ * @param domain Domain of Firebase error.
+ * @param code Error code that NSError should have.
+ * @param userInfo User info that NSError should have.
+ * @return An NSError in the Firebase domain.
+ */
+extern NSError *FIRCreateError(NSString *domain, FIRErrorCode code, NSDictionary *userInfo);
diff --git a/Firebase/Core/Private/FIRLogger.h b/Firebase/Core/Private/FIRLogger.h
new file mode 100644
index 0000000..2206c0a
--- /dev/null
+++ b/Firebase/Core/Private/FIRLogger.h
@@ -0,0 +1,115 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRLoggerLevel.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The Firebase services used in Firebase logger.
+ */
+typedef NSString *const FIRLoggerService;
+
+extern FIRLoggerService kFIRLoggerABTesting;
+extern FIRLoggerService kFIRLoggerAdMob;
+extern FIRLoggerService kFIRLoggerAnalytics;
+extern FIRLoggerService kFIRLoggerAuth;
+extern FIRLoggerService kFIRLoggerCore;
+extern FIRLoggerService kFIRLoggerCrash;
+extern FIRLoggerService kFIRLoggerDatabase;
+extern FIRLoggerService kFIRLoggerDynamicLinks;
+extern FIRLoggerService kFIRLoggerInstanceID;
+extern FIRLoggerService kFIRLoggerInvites;
+extern FIRLoggerService kFIRLoggerMessaging;
+extern FIRLoggerService kFIRLoggerPerf;
+extern FIRLoggerService kFIRLoggerRemoteConfig;
+extern FIRLoggerService kFIRLoggerStorage;
+
+/**
+ * Enables or disables Analytics debug mode.
+ * If set to YES, the logging level for Analytics will be set to FIRLoggerLevelDebug.
+ * Enabling the debug mode has no effect if the app is running from App Store.
+ * (required) analytics debug mode flag.
+ */
+void FIRSetAnalyticsDebugMode(BOOL analyticsDebugMode);
+
+/**
+ * Changes the default logging level of FIRLoggerLevelNotice to a user-specified level.
+ * The default level cannot be set above FIRLoggerLevelNotice if the app is running from App Store.
+ * (required) log level (one of the FIRLoggerLevel enum values).
+ */
+void FIRSetLoggerLevel(FIRLoggerLevel loggerLevel);
+
+/**
+ * Checks if the specified logger level is loggable given the current settings.
+ * (required) log level (one of the FIRLoggerLevel enum values).
+ * (required) whether or not this function is called from the Analytics component.
+ */
+BOOL FIRIsLoggableLevel(FIRLoggerLevel loggerLevel, BOOL analyticsComponent);
+
+/**
+ * Logs a message to the Xcode console and the device log. If running from AppStore, will
+ * not log any messages with a level higher than FIRLoggerLevelNotice to avoid log spamming.
+ * (required) log level (one of the FIRLoggerLevel enum values).
+ * (required) service name of type FIRLoggerService.
+ * (required) message code starting with "I-" which means iOS, followed by a capitalized
+ * three-character service identifier and a six digit integer message ID that is unique
+ * within the service.
+ * An example of the message code is @"I-COR000001".
+ * (required) message string which can be a format string.
+ * (optional) variable arguments list obtained from calling va_start, used when message is a format
+ * string.
+ */
+extern void FIRLogBasic(FIRLoggerLevel level,
+ FIRLoggerService service,
+ NSString *messageCode,
+ NSString *message,
+// On 64-bit simulators, va_list is not a pointer, so cannot be marked nullable
+// See: http://stackoverflow.com/q/29095469
+#if __LP64__ && TARGET_IPHONE_SIMULATOR
+ va_list args_ptr
+#else
+ va_list _Nullable args_ptr
+#endif
+ );
+
+/**
+ * The following functions accept the following parameters in order:
+ * (required) service name of type FIRLoggerService.
+ * (required) message code starting from "I-" which means iOS, followed by a capitalized
+ * three-character service identifier and a six digit integer message ID that is unique
+ * within the service.
+ * An example of the message code is @"I-COR000001".
+ * See go/firebase-log-proposal for details.
+ * (required) message string which can be a format string.
+ * (optional) the list of arguments to substitute into the format string.
+ * Example usage:
+ * FIRLogError(kFIRLoggerCore, @"I-COR000001", @"Configuration of %@ failed.", app.name);
+ */
+extern void FIRLogError(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
+ NS_FORMAT_FUNCTION(3, 4);
+extern void FIRLogWarning(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
+ NS_FORMAT_FUNCTION(3, 4);
+extern void FIRLogNotice(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
+ NS_FORMAT_FUNCTION(3, 4);
+extern void FIRLogInfo(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
+ NS_FORMAT_FUNCTION(3, 4);
+extern void FIRLogDebug(FIRLoggerService service, NSString *messageCode, NSString *message, ...)
+ NS_FORMAT_FUNCTION(3, 4);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Core/Private/FIRMutableDictionary.h b/Firebase/Core/Private/FIRMutableDictionary.h
new file mode 100644
index 0000000..ebe2d33
--- /dev/null
+++ b/Firebase/Core/Private/FIRMutableDictionary.h
@@ -0,0 +1,46 @@
+/*
+ * 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 Foundation;
+
+/// A mutable dictionary that provides atomic accessor and mutators.
+@interface FIRMutableDictionary : NSObject
+
+/// Returns an object given a key in the dictionary or nil if not found.
+- (id)objectForKey:(id)key;
+
+/// Updates the object given its key or adds it to the dictionary if it is not in the dictionary.
+- (void)setObject:(id)object forKey:(id<NSCopying>)key;
+
+/// Removes the object given its session ID from the dictionary.
+- (void)removeObjectForKey:(id)key;
+
+/// Removes all objects.
+- (void)removeAllObjects;
+
+/// Returns the number of current objects in the dictionary.
+- (NSUInteger)count;
+
+/// Returns an object given a key in the dictionary or nil if not found.
+- (id)objectForKeyedSubscript:(id<NSCopying>)key;
+
+/// Updates the object given its key or adds it to the dictionary if it is not in the dictionary.
+- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key;
+
+/// Returns the immutable dictionary.
+- (NSDictionary *)dictionary;
+
+@end
diff --git a/Firebase/Core/Private/FIRNetwork.h b/Firebase/Core/Private/FIRNetwork.h
new file mode 100644
index 0000000..aac0bca
--- /dev/null
+++ b/Firebase/Core/Private/FIRNetwork.h
@@ -0,0 +1,87 @@
+/*
+ * 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 Foundation;
+
+#import "FIRNetworkConstants.h"
+#import "FIRNetworkLoggerProtocol.h"
+#import "FIRNetworkURLSession.h"
+
+/// Delegate protocol for FIRNetwork events.
+@protocol FIRNetworkReachabilityDelegate
+
+/// Tells the delegate to handle events when the network reachability changes to connected or not
+/// connected.
+- (void)reachabilityDidChange;
+
+@end
+
+/// The Network component that provides network status and handles network requests and responses.
+/// This is not thread safe.
+///
+/// NOTE:
+/// User must add FIRAnalytics handleEventsForBackgroundURLSessionID:completionHandler to the
+/// AppDelegate application:handleEventsForBackgroundURLSession:completionHandler:
+@interface FIRNetwork : NSObject
+
+/// Indicates if network connectivity is available.
+@property(nonatomic, readonly, getter=isNetworkConnected) BOOL networkConnected;
+
+/// Indicates if there are any uploads in progress.
+@property(nonatomic, readonly, getter=hasUploadInProgress) BOOL uploadInProgress;
+
+/// An optional delegate that can be used in the event when network reachability changes.
+@property(nonatomic, weak) id<FIRNetworkReachabilityDelegate> reachabilityDelegate;
+
+/// An optional delegate that can be used to log messages, warnings or errors that occur in the
+/// network operations.
+@property(nonatomic, weak) id<FIRNetworkLoggerDelegate> loggerDelegate;
+
+/// Indicates whether the logger should display debug messages.
+@property(nonatomic, assign) BOOL isDebugModeEnabled;
+
+/// The time interval in seconds for the network request to timeout.
+@property(nonatomic, assign) NSTimeInterval timeoutInterval;
+
+/// Initializes with the default reachability host.
+- (instancetype)init;
+
+/// Initializes with a custom reachability host.
+- (instancetype)initWithReachabilityHost:(NSString *)reachabilityHost;
+
+/// Handles events when background session with the given ID has finished.
++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
+ completionHandler:(FIRNetworkSystemCompletionHandler)completionHandler;
+
+/// Compresses and sends a POST request with the provided data to the URL. The session will be
+/// background session if usingBackgroundSession is YES. Otherwise, the POST session is default
+/// session. Returns a session ID or nil if an error occurs.
+- (NSString *)postURL:(NSURL *)url
+ payload:(NSData *)payload
+ queue:(dispatch_queue_t)queue
+ usingBackgroundSession:(BOOL)usingBackgroundSession
+ completionHandler:(FIRNetworkCompletionHandler)handler;
+
+/// Sends a GET request with the provided data to the URL. The session will be background session
+/// if usingBackgroundSession is YES. Otherwise, the GET session is default session. Returns a
+/// session ID or nil if an error occurs.
+- (NSString *)getURL:(NSURL *)url
+ headers:(NSDictionary *)headers
+ queue:(dispatch_queue_t)queue
+ usingBackgroundSession:(BOOL)usingBackgroundSession
+ completionHandler:(FIRNetworkCompletionHandler)handler;
+
+@end
diff --git a/Firebase/Core/Private/FIRNetworkConstants.h b/Firebase/Core/Private/FIRNetworkConstants.h
new file mode 100644
index 0000000..5878088
--- /dev/null
+++ b/Firebase/Core/Private/FIRNetworkConstants.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 Foundation;
+
+/// Error codes in Firebase Network error domain.
+/// Note: these error codes should never change. It would make it harder to decode the errors if
+/// we inadvertently altered any of these codes in a future SDK version.
+typedef NS_ENUM(NSInteger, FIRNetworkErrorCode) {
+ /// Unknown error.
+ FIRNetworkErrorCodeUnknown = 0,
+ /// Error occurs when the request URL is invalid.
+ FIRErrorCodeNetworkInvalidURL = 1,
+ /// Error occurs when request cannot be constructed.
+ FIRErrorCodeNetworkRequestCreation = 2,
+ /// Error occurs when payload cannot be compressed.
+ FIRErrorCodeNetworkPayloadCompression = 3,
+ /// Error occurs when session task cannot be created.
+ FIRErrorCodeNetworkSessionTaskCreation = 4,
+ /// Error occurs when there is no response.
+ FIRErrorCodeNetworkInvalidResponse = 5
+};
+
+#pragma mark - Network constants
+
+/// The prefix of the ID of the background session.
+extern NSString *const kFIRNetworkBackgroundSessionConfigIDPrefix;
+
+/// The sub directory to store the files of data that is being uploaded in the background.
+extern NSString *const kFIRNetworkApplicationSupportSubdirectory;
+
+/// Name of the temporary directory that stores files for background uploading.
+extern NSString *const kFIRNetworkTempDirectoryName;
+
+/// The period when the temporary uploading file can stay.
+extern const NSTimeInterval kFIRNetworkTempFolderExpireTime;
+
+/// The default network request timeout interval.
+extern const NSTimeInterval kFIRNetworkTimeOutInterval;
+
+/// The host to check the reachability of the network.
+extern NSString *const kFIRNetworkReachabilityHost;
+
+/// The key to get the error context of the UserInfo.
+extern NSString *const kFIRNetworkErrorContext;
+
+#pragma mark - Network Status Code
+
+extern const int kFIRNetworkHTTPStatusOK;
+extern const int kFIRNetworkHTTPStatusNoContent;
+extern const int kFIRNetworkHTTPStatusCodeMultipleChoices;
+extern const int kFIRNetworkHTTPStatusCodeMovedPermanently;
+extern const int kFIRNetworkHTTPStatusCodeFound;
+extern const int kFIRNetworkHTTPStatusCodeNotModified;
+extern const int kFIRNetworkHTTPStatusCodeMovedTemporarily;
+extern const int kFIRNetworkHTTPStatusCodeNotFound;
+extern const int kFIRNetworkHTTPStatusCodeCannotAcceptTraffic;
+extern const int kFIRNetworkHTTPStatusCodeUnavailable;
+
+#pragma mark - Error Domain
+
+extern NSString *const kFIRNetworkErrorDomain;
diff --git a/Firebase/Core/Private/FIRNetworkLoggerProtocol.h b/Firebase/Core/Private/FIRNetworkLoggerProtocol.h
new file mode 100644
index 0000000..7b7d094
--- /dev/null
+++ b/Firebase/Core/Private/FIRNetworkLoggerProtocol.h
@@ -0,0 +1,50 @@
+/*
+ * 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 Foundation;
+
+#import "FIRNetworkMessageCode.h"
+#import "../FIRLoggerLevel.h"
+
+/// The log levels used by FIRNetworkLogger.
+typedef NS_ENUM(NSInteger, FIRNetworkLogLevel) {
+ kFIRNetworkLogLevelError = FIRLoggerLevelError,
+ kFIRNetworkLogLevelWarning = FIRLoggerLevelWarning,
+ kFIRNetworkLogLevelInfo = FIRLoggerLevelInfo,
+ kFIRNetworkLogLevelDebug = FIRLoggerLevelDebug,
+};
+
+@protocol FIRNetworkLoggerDelegate<NSObject>
+
+@required
+/// Tells the delegate to log a message with an array of contexts and the log level.
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message
+ contexts:(NSArray *)contexts;
+
+/// Tells the delegate to log a message with a context and the log level.
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message
+ context:(id)context;
+
+/// Tells the delegate to log a message with the log level.
+- (void)firNetwork_logWithLevel:(FIRNetworkLogLevel)logLevel
+ messageCode:(FIRNetworkMessageCode)messageCode
+ message:(NSString *)message;
+
+@end
diff --git a/Firebase/Core/Private/FIRNetworkMessageCode.h b/Firebase/Core/Private/FIRNetworkMessageCode.h
new file mode 100644
index 0000000..30f562f
--- /dev/null
+++ b/Firebase/Core/Private/FIRNetworkMessageCode.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.
+ */
+
+// Make sure these codes do not overlap with any contained in the FIRAMessageCode enum.
+typedef NS_ENUM(NSInteger, FIRNetworkMessageCode) {
+ // FIRNetwork.m
+ kFIRNetworkMessageCodeNetwork000 = 900000, // I-NET900000
+ kFIRNetworkMessageCodeNetwork001 = 900001, // I-NET900001
+ kFIRNetworkMessageCodeNetwork002 = 900002, // I-NET900002
+ kFIRNetworkMessageCodeNetwork003 = 900003, // I-NET900003
+ // FIRNetworkURLSession.m
+ kFIRNetworkMessageCodeURLSession000 = 901000, // I-NET901000
+ kFIRNetworkMessageCodeURLSession001 = 901001, // I-NET901001
+ kFIRNetworkMessageCodeURLSession002 = 901002, // I-NET901002
+ kFIRNetworkMessageCodeURLSession003 = 901003, // I-NET901003
+ kFIRNetworkMessageCodeURLSession004 = 901004, // I-NET901004
+ kFIRNetworkMessageCodeURLSession005 = 901005, // I-NET901005
+ kFIRNetworkMessageCodeURLSession006 = 901006, // I-NET901006
+ kFIRNetworkMessageCodeURLSession007 = 901007, // I-NET901007
+ kFIRNetworkMessageCodeURLSession008 = 901008, // I-NET901008
+ kFIRNetworkMessageCodeURLSession009 = 901009, // I-NET901009
+ kFIRNetworkMessageCodeURLSession010 = 901010, // I-NET901010
+ kFIRNetworkMessageCodeURLSession011 = 901011, // I-NET901011
+ kFIRNetworkMessageCodeURLSession012 = 901012, // I-NET901012
+ kFIRNetworkMessageCodeURLSession013 = 901013, // I-NET901013
+ kFIRNetworkMessageCodeURLSession014 = 901014, // I-NET901014
+ kFIRNetworkMessageCodeURLSession015 = 901015, // I-NET901015
+ kFIRNetworkMessageCodeURLSession016 = 901016, // I-NET901016
+ kFIRNetworkMessageCodeURLSession017 = 901017, // I-NET901017
+ kFIRNetworkMessageCodeURLSession018 = 901018, // I-NET901018
+ // FIRReachabilityChecker.m
+ kFIRNetworkMessageCodeReachabilityChecker000 = 902000, // I-NET902000
+ kFIRNetworkMessageCodeReachabilityChecker001 = 902001, // I-NET902001
+ kFIRNetworkMessageCodeReachabilityChecker002 = 902002, // I-NET902002
+ kFIRNetworkMessageCodeReachabilityChecker003 = 902003, // I-NET902003
+ kFIRNetworkMessageCodeReachabilityChecker004 = 902004, // I-NET902004
+ kFIRNetworkMessageCodeReachabilityChecker005 = 902005, // I-NET902005
+ kFIRNetworkMessageCodeReachabilityChecker006 = 902006, // I-NET902006
+};
diff --git a/Firebase/Core/Private/FIRNetworkURLSession.h b/Firebase/Core/Private/FIRNetworkURLSession.h
new file mode 100644
index 0000000..d146de2
--- /dev/null
+++ b/Firebase/Core/Private/FIRNetworkURLSession.h
@@ -0,0 +1,57 @@
+/*
+ * 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 Foundation;
+
+#import "FIRNetworkLoggerProtocol.h"
+
+typedef void (^FIRNetworkCompletionHandler)(NSHTTPURLResponse *response, NSData *data,
+ NSError *error);
+typedef void (^FIRNetworkURLSessionCompletionHandler)(NSHTTPURLResponse *response, NSData *data,
+ NSString *sessionID, NSError *error);
+typedef void (^FIRNetworkSystemCompletionHandler)(void);
+
+/// The protocol that uses NSURLSession for iOS >= 7.0 to handle requests and responses.
+@interface FIRNetworkURLSession
+ : NSObject<NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate>
+
+/// Indicates whether the background network is enabled. Default value is NO.
+@property(nonatomic, getter=isBackgroundNetworkEnabled) BOOL backgroundNetworkEnabled;
+
+/// The logger delegate to log message, errors or warnings that occur during the network operations.
+@property(nonatomic, weak) id<FIRNetworkLoggerDelegate> loggerDelegate;
+
+/// Calls the system provided completion handler after the background session is finished.
++ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
+ completionHandler:(FIRNetworkSystemCompletionHandler)completionHandler;
+
+/// Initializes with logger delegate.
+- (instancetype)initWithNetworkLoggerDelegate:(id<FIRNetworkLoggerDelegate>)networkLoggerDelegate
+ NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/// Sends an asynchronous POST request and calls the provided completion handler when the request
+/// completes or when errors occur, and returns an ID of the session/connection.
+- (NSString *)sessionIDFromAsyncPOSTRequest:(NSURLRequest *)request
+ completionHandler:(FIRNetworkURLSessionCompletionHandler)handler;
+
+/// Sends an asynchronous GET request and calls the provided completion handler when the request
+/// completes or when errors occur, and returns an ID of the session.
+- (NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request
+ completionHandler:(FIRNetworkURLSessionCompletionHandler)handler;
+
+@end
diff --git a/Firebase/Core/Private/FIROptionsInternal.h b/Firebase/Core/Private/FIROptionsInternal.h
new file mode 100644
index 0000000..2b30248
--- /dev/null
+++ b/Firebase/Core/Private/FIROptionsInternal.h
@@ -0,0 +1,108 @@
+/*
+ * 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 "../FIROptions.h"
+
+/**
+ * Keys for the strings in the plist file.
+ */
+extern NSString *const kFIRAPIKey;
+extern NSString *const kFIRTrackingID;
+extern NSString *const kFIRGoogleAppID;
+extern NSString *const kFIRClientID;
+extern NSString *const kFIRGCMSenderID;
+extern NSString *const kFIRAndroidClientID;
+extern NSString *const kFIRDatabaseURL;
+extern NSString *const kFIRStorageBucket;
+extern NSString *const kFIRBundleID;
+extern NSString *const kFIRProjectID;
+
+/**
+ * Keys for the plist file name
+ */
+extern NSString *const kServiceInfoFileName;
+
+extern NSString *const kServiceInfoFileType;
+
+/**
+ * This header file exposes the initialization of FIROptions to internal use.
+ */
+@interface FIROptions ()
+
+/**
+ * resetDefaultOptions and initInternalWithOptionsDictionary: are exposed only for unit tests.
+ */
++ (void)resetDefaultOptions;
+
+/**
+ * Initializes the options with dictionary. The above strings are the keys of the dictionary.
+ * This is the designated initializer.
+ */
+- (instancetype)initInternalWithOptionsDictionary:(NSDictionary *)serviceInfoDictionary;
+
+/**
+ * defaultOptions and defaultOptionsDictionary are exposed in order to be used in FIRApp and
+ * other first party services.
+ */
++ (FIROptions *)defaultOptions;
+
++ (NSDictionary *)defaultOptionsDictionary;
+
+/**
+ * Whether or not Analytics Collection was enabled. Analytics Collection is enabled unless
+ * explicitly disabled in GoogleService-Info.plist.
+ */
+@property(nonatomic, readonly) BOOL isAnalyticsCollectionEnabled;
+
+/**
+ * Whether or not Analytics Collection was completely disabled. If YES, then
+ * isAnalyticsCollectionEnabled will be NO.
+ */
+@property(nonatomic, readonly) BOOL isAnalyticsCollectionDeactivated;
+
+/**
+ * The version ID of the client library, e.g. @"1100000".
+ */
+@property(nonatomic, readonly, copy) NSString *libraryVersionID;
+
+/**
+ * The flag indicating whether this object was constructed with the values in the default plist
+ * file.
+ */
+@property(nonatomic) BOOL usingOptionsFromDefaultPlist;
+
+/**
+ * Whether or not Measurement was enabled. Measurement is enabled unless explicitly disabled in
+ * GoogleService-Info.plist.
+ */
+@property(nonatomic, readonly) BOOL isMeasurementEnabled;
+
+/**
+ * Whether or not Analytics was enabled in the developer console.
+ */
+@property(nonatomic, readonly) BOOL isAnalyticsEnabled;
+
+/**
+ * Whether or not SignIn was enabled in the developer console.
+ */
+@property(nonatomic, readonly) BOOL isSignInEnabled;
+
+/**
+ * Whether or not editing is locked. This should occur after FIROptions has been set on a FIRApp.
+ */
+@property(nonatomic, getter=isEditingLocked) BOOL editingLocked;
+
+@end
diff --git a/Firebase/Core/Private/FIRReachabilityChecker+Internal.h b/Firebase/Core/Private/FIRReachabilityChecker+Internal.h
new file mode 100644
index 0000000..f82d103
--- /dev/null
+++ b/Firebase/Core/Private/FIRReachabilityChecker+Internal.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 "FIRReachabilityChecker.h"
+
+typedef SCNetworkReachabilityRef (*FIRReachabilityCreateWithNameFn)(CFAllocatorRef allocator,
+ const char *host);
+
+typedef Boolean (*FIRReachabilitySetCallbackFn)(SCNetworkReachabilityRef target,
+ SCNetworkReachabilityCallBack callback,
+ SCNetworkReachabilityContext *context);
+typedef Boolean (*FIRReachabilityScheduleWithRunLoopFn)(SCNetworkReachabilityRef target,
+ CFRunLoopRef runLoop,
+ CFStringRef runLoopMode);
+typedef Boolean (*FIRReachabilityUnscheduleFromRunLoopFn)(SCNetworkReachabilityRef target,
+ CFRunLoopRef runLoop,
+ CFStringRef runLoopMode);
+
+typedef void (*FIRReachabilityReleaseFn)(CFTypeRef cf);
+
+struct FIRReachabilityApi {
+ FIRReachabilityCreateWithNameFn createWithNameFn;
+ FIRReachabilitySetCallbackFn setCallbackFn;
+ FIRReachabilityScheduleWithRunLoopFn scheduleWithRunLoopFn;
+ FIRReachabilityUnscheduleFromRunLoopFn unscheduleFromRunLoopFn;
+ FIRReachabilityReleaseFn releaseFn;
+};
+
+@interface FIRReachabilityChecker (Internal)
+
+- (const struct FIRReachabilityApi *)reachabilityApi;
+- (void)setReachabilityApi:(const struct FIRReachabilityApi *)reachabilityApi;
+
+@end
diff --git a/Firebase/Core/Private/FIRReachabilityChecker.h b/Firebase/Core/Private/FIRReachabilityChecker.h
new file mode 100644
index 0000000..105cd3d
--- /dev/null
+++ b/Firebase/Core/Private/FIRReachabilityChecker.h
@@ -0,0 +1,83 @@
+/*
+ * 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 Foundation;
+@import SystemConfiguration;
+
+/// Reachability Status
+typedef enum {
+ kFIRReachabilityUnknown, ///< Have not yet checked or been notified whether host is reachable.
+ kFIRReachabilityNotReachable, ///< Host is not reachable.
+ kFIRReachabilityViaWifi, ///< Host is reachable via Wifi.
+ kFIRReachabilityViaCellular, ///< Host is reachable via cellular.
+} FIRReachabilityStatus;
+
+const NSString *FIRReachabilityStatusString(FIRReachabilityStatus status);
+
+@class FIRReachabilityChecker;
+@protocol FIRNetworkLoggerDelegate;
+
+/// Google Analytics iOS Reachability Checker.
+@protocol FIRReachabilityDelegate
+@required
+/// Called when network status has changed.
+- (void)reachability:(FIRReachabilityChecker *)reachability
+ statusChanged:(FIRReachabilityStatus)status;
+@end
+
+/// Google Analytics iOS Network Status Checker.
+@interface FIRReachabilityChecker : NSObject
+
+/// The last known reachability status, or FIRReachabilityStatusUnknown if the
+/// checker is not active.
+@property(nonatomic, readonly) FIRReachabilityStatus reachabilityStatus;
+/// The host to which reachability status is to be checked.
+@property(nonatomic, copy, readonly) NSString *host;
+/// The delegate to be notified of reachability status changes.
+@property(nonatomic, weak) id<FIRReachabilityDelegate> reachabilityDelegate;
+/// The delegate to be notified to log messages.
+@property(nonatomic, weak) id<FIRNetworkLoggerDelegate> loggerDelegate;
+/// `YES` if the reachability checker is active, `NO` otherwise.
+@property(nonatomic, readonly) BOOL isActive;
+
+/// Initialize the reachability checker. Note that you must call start to begin checking for and
+/// receiving notifications about network status changes.
+///
+/// @param reachabilityDelegate The delegate to be notified when reachability status to host
+/// changes.
+///
+/// @param loggerDelegate The delegate to send log messages to.
+///
+/// @param host The name of the host.
+///
+- (instancetype)initWithReachabilityDelegate:(id<FIRReachabilityDelegate>)reachabilityDelegate
+ loggerDelegate:(id<FIRNetworkLoggerDelegate>)loggerDelegate
+ withHost:(NSString *)host;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/// Start checking for reachability to the specified host. This has no effect if the status
+/// checker is already checking for connectivity.
+///
+/// @return `YES` if initiating status checking was successful or the status checking has already
+/// been initiated, `NO` otherwise.
+- (BOOL)start;
+
+/// Stop checking for reachability to the specified host. This has no effect if the status
+/// checker is not checking for connectivity.
+- (void)stop;
+
+@end
diff --git a/Firebase/Core/Private/FIRURLSchemeUtil.h b/Firebase/Core/Private/FIRURLSchemeUtil.h
new file mode 100644
index 0000000..d4fa961
--- /dev/null
+++ b/Firebase/Core/Private/FIRURLSchemeUtil.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 <Foundation/Foundation.h>
+
+/**
+ * Checks whether the URL schemes declared for Google SignIn are valid.
+ *
+ * @param urlSchemes The URL schemes to validate.
+ * @return YES if the schemes are valid; NO otherwise.
+ */
+extern BOOL fir_areURLSchemesValidForGoogleSignIn(NSArray *urlSchemes);
diff --git a/Firebase/Database/Api/FIRDataEventType.h b/Firebase/Database/Api/FIRDataEventType.h
new file mode 100644
index 0000000..fccc98a
--- /dev/null
+++ b/Firebase/Database/Api/FIRDataEventType.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.
+ */
+
+#ifndef Firebase_FIRDataEventType_h
+#define Firebase_FIRDataEventType_h
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+
+/**
+ * This enum is the set of events that you can observe at a Firebase Database location.
+ */
+typedef NS_ENUM(NSInteger, FIRDataEventType) {
+ /// A new child node is added to a location.
+ FIRDataEventTypeChildAdded,
+ /// A child node is removed from a location.
+ FIRDataEventTypeChildRemoved,
+ /// A child node at a location changes.
+ FIRDataEventTypeChildChanged,
+ /// A child node moves relative to the other child nodes at a location.
+ FIRDataEventTypeChildMoved,
+ /// Any data changes at a location or, recursively, at any child node.
+ FIRDataEventTypeValue
+} FIR_SWIFT_NAME(DataEventType);
+
+#endif
diff --git a/Firebase/Database/Api/FIRDataSnapshot.h b/Firebase/Database/Api/FIRDataSnapshot.h
new file mode 100644
index 0000000..e615260
--- /dev/null
+++ b/Firebase/Database/Api/FIRDataSnapshot.h
@@ -0,0 +1,148 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDatabaseReference;
+
+/**
+ * A FIRDataSnapshot contains data from a Firebase Database location. Any time you read
+ * Firebase data, you receive the data as a FIRDataSnapshot.
+ *
+ * FIRDataSnapshots are passed to the blocks you attach with observeEventType:withBlock: or observeSingleEvent:withBlock:.
+ * They are efficiently-generated immutable copies of the data at a Firebase Database location.
+ * They can't be modified and will never change. To modify data at a location,
+ * use a FIRDatabaseReference (e.g. with setValue:).
+ */
+FIR_SWIFT_NAME(DataSnapshot)
+@interface FIRDataSnapshot : NSObject
+
+
+#pragma mark - Navigating and inspecting a snapshot
+
+/**
+ * Gets a FIRDataSnapshot for the location at the specified relative path.
+ * The relative path can either be a simple child key (e.g. 'fred')
+ * or a deeper slash-separated path (e.g. 'fred/name/first'). If the child
+ * location has no data, an empty FIRDataSnapshot is returned.
+ *
+ * @param childPathString A relative path to the location of child data.
+ * @return The FIRDataSnapshot for the child location.
+ */
+- (FIRDataSnapshot *)childSnapshotForPath:(NSString *)childPathString;
+
+
+/**
+ * Return YES if the specified child exists.
+ *
+ * @param childPathString A relative path to the location of a potential child.
+ * @return YES if data exists at the specified childPathString, else NO.
+ */
+- (BOOL) hasChild:(NSString *)childPathString;
+
+
+/**
+ * Return YES if the DataSnapshot has any children.
+ *
+ * @return YES if this snapshot has any children, else NO.
+ */
+- (BOOL) hasChildren;
+
+
+/**
+ * Return YES if the DataSnapshot contains a non-null value.
+ *
+ * @return YES if this snapshot contains a non-null value, else NO.
+ */
+- (BOOL) exists;
+
+
+#pragma mark - Data export
+
+/**
+ * Returns the raw value at this location, coupled with any metadata, such as priority.
+ *
+ * Priorities, where they exist, are accessible under the ".priority" key in instances of NSDictionary.
+ * For leaf locations with priorities, the value will be under the ".value" key.
+ */
+- (id __nullable) valueInExportFormat;
+
+
+#pragma mark - Properties
+
+/**
+ * Returns the contents of this data snapshot as native types.
+ *
+ * Data types returned:
+ * + NSDictionary
+ * + NSArray
+ * + NSNumber (also includes booleans)
+ * + NSString
+ *
+ * @return The data as a native object.
+ */
+@property (strong, readonly, nonatomic, nullable) id value;
+
+
+/**
+ * Gets the number of children for this DataSnapshot.
+ *
+ * @return An integer indicating the number of children.
+ */
+@property (readonly, nonatomic) NSUInteger childrenCount;
+
+
+/**
+ * Gets a FIRDatabaseReference for the location that this data came from.
+ *
+ * @return A FIRDatabaseReference instance for the location of this data.
+ */
+@property (nonatomic, readonly, strong) FIRDatabaseReference * ref;
+
+
+/**
+ * The key of the location that generated this FIRDataSnapshot.
+ *
+ * @return An NSString containing the key for the location of this FIRDataSnapshot.
+ */
+@property (strong, readonly, nonatomic) NSString* key;
+
+
+/**
+ * An iterator for snapshots of the child nodes in this snapshot.
+ * You can use the native for..in syntax:
+ *
+ * for (FIRDataSnapshot* child in snapshot.children) {
+ * ...
+ * }
+ *
+ * @return An NSEnumerator of the children.
+ */
+@property (strong, readonly, nonatomic) NSEnumerator* children;
+
+/**
+ * The priority of the data in this FIRDataSnapshot.
+ *
+ * @return The priority as a string, or nil if no priority was set.
+ */
+@property (strong, readonly, nonatomic, nullable) id priority;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDataSnapshot.m b/Firebase/Database/Api/FIRDataSnapshot.m
new file mode 100644
index 0000000..9559c38
--- /dev/null
+++ b/Firebase/Database/Api/FIRDataSnapshot.m
@@ -0,0 +1,101 @@
+/*
+ * 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 "FIRDataSnapshot.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FChildrenNode.h"
+#import "FValidation.h"
+#import "FTransformedEnumerator.h"
+#import "FIRDatabaseReference.h"
+
+@interface FIRDataSnapshot ()
+@property (nonatomic, strong) FIRDatabaseReference *ref;
+@end
+
+@implementation FIRDataSnapshot
+
+- (id)initWithRef:(FIRDatabaseReference *)ref indexedNode:(FIndexedNode *)node
+{
+ self = [super init];
+ if (self != nil) {
+ self->_ref = ref;
+ self->_node = node;
+ }
+ return self;
+}
+
+- (id) value {
+ return [self.node.node val];
+}
+
+- (id) valueInExportFormat {
+ return [self.node.node valForExport:YES];
+}
+
+- (FIRDataSnapshot *)childSnapshotForPath:(NSString *)childPathString {
+ [FValidation validateFrom:@"child:" validPathString:childPathString];
+ FPath* childPath = [[FPath alloc] initWith:childPathString];
+ FIRDatabaseReference * childRef = [self.ref child:childPathString];
+
+ id<FNode> childNode = [self.node.node getChild:childPath];
+ return [[FIRDataSnapshot alloc] initWithRef:childRef indexedNode:[FIndexedNode indexedNodeWithNode:childNode]];
+}
+
+- (BOOL) hasChild:(NSString *)childPathString {
+ [FValidation validateFrom:@"hasChild:" validPathString:childPathString];
+ FPath* childPath = [[FPath alloc] initWith:childPathString];
+ return ! [[self.node.node getChild:childPath] isEmpty];
+}
+
+- (id) priority {
+ id<FNode> priority = [self.node.node getPriority];
+ return priority.val;
+}
+
+
+- (BOOL) hasChildren {
+ if([self.node.node isLeafNode]) {
+ return false;
+ }
+ else {
+ return ![self.node.node isEmpty];
+ }
+}
+
+- (BOOL) exists {
+ return ![self.node.node isEmpty];
+}
+
+- (NSString *) key {
+ return [self.ref key];
+}
+
+- (NSUInteger) childrenCount {
+ return [self.node.node numChildren];
+}
+
+- (NSEnumerator *) children {
+ return [[FTransformedEnumerator alloc] initWithEnumerator:self.node.childEnumerator andTransform:^id(FNamedNode *node) {
+ FIRDatabaseReference *childRef = [self.ref child:node.name];
+ return [[FIRDataSnapshot alloc] initWithRef:childRef indexedNode:[FIndexedNode indexedNodeWithNode:node.node]];
+ }];
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"Snap (%@) %@", self.key, self.node.node];
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabase.h b/Firebase/Database/Api/FIRDatabase.h
new file mode 100644
index 0000000..e77ed31
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabase.h
@@ -0,0 +1,140 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FIRDatabaseReference.h"
+#import "FIRDatabaseSwiftNameSupport.h"
+
+@class FIRApp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The entry point for accessing a Firebase Database. You can get an instance by calling
+ * [FIRDatabase database]. To access a location in the database and read or write data,
+ * use [FIRDatabase reference].
+ */
+FIR_SWIFT_NAME(Database)
+@interface FIRDatabase : NSObject
+
+/**
+ * Gets the instance of FIRDatabase for the default FIRApp.
+ *
+ * @return A FIRDatabase instance.
+ */
++ (FIRDatabase *) database FIR_SWIFT_NAME(database());
+
+/**
+ * Gets an instance of FIRDatabase for a specific FIRApp.
+ *
+ * @param app The FIRApp to get a FIRDatabase for.
+ * @return A FIRDatabase instance.
+ */
++ (FIRDatabase *) databaseForApp:(FIRApp*)app FIR_SWIFT_NAME(database(app:));
+
+/** The FIRApp instance to which this FIRDatabase belongs. */
+@property (weak, readonly, nonatomic) FIRApp *app;
+
+/**
+ * Gets a FIRDatabaseReference for the root of your Firebase Database.
+ */
+- (FIRDatabaseReference *) reference;
+
+/**
+ * Gets a FIRDatabaseReference for the provided path.
+ *
+ * @param path Path to a location in your Firebase Database.
+ * @return A FIRDatabaseReference pointing to the specified path.
+ */
+- (FIRDatabaseReference *) referenceWithPath:(NSString *)path;
+
+/**
+ * Gets a FIRDatabaseReference for the provided URL. The URL must be a URL to a path
+ * within this Firebase Database. To create a FIRDatabaseReference to a different database,
+ * create a FIRApp} with a FIROptions object configured with the appropriate database URL.
+ *
+ * @param databaseUrl A URL to a path within your database.
+ * @return A FIRDatabaseReference for the provided URL.
+*/
+- (FIRDatabaseReference *) referenceFromURL:(NSString *)databaseUrl;
+
+/**
+ * The Firebase Database client automatically queues writes and sends them to the server at the earliest opportunity,
+ * depending on network connectivity. In some cases (e.g. offline usage) there may be a large number of writes
+ * waiting to be sent. Calling this method will purge all outstanding writes so they are abandoned.
+ *
+ * All writes will be purged, including transactions and onDisconnect writes. The writes will
+ * be rolled back locally, perhaps triggering events for affected event listeners, and the client will not
+ * (re-)send them to the Firebase Database backend.
+ */
+- (void)purgeOutstandingWrites;
+
+/**
+ * Shuts down our connection to the Firebase Database backend until goOnline is called.
+ */
+- (void)goOffline;
+
+/**
+ * Resumes our connection to the Firebase Database backend after a previous goOffline call.
+ */
+- (void)goOnline;
+
+/**
+ * The Firebase Database client will cache synchronized data and keep track of all writes you've
+ * initiated while your application is running. It seamlessly handles intermittent network
+ * connections and re-sends write operations when the network connection is restored.
+ *
+ * However by default your write operations and cached data are only stored in-memory and will
+ * be lost when your app restarts. By setting this value to `YES`, the data will be persisted
+ * to on-device (disk) storage and will thus be available again when the app is restarted
+ * (even when there is no network connectivity at that time). Note that this property must be
+ * set before creating your first Database reference and only needs to be called once per
+ * application.
+ *
+ */
+@property (nonatomic) BOOL persistenceEnabled FIR_SWIFT_NAME(isPersistenceEnabled);
+
+/**
+ * By default the Firebase Database client will use up to 10MB of disk space to cache data. If the cache grows beyond
+ * this size, the client will start removing data that hasn't been recently used. If you find that your application
+ * caches too little or too much data, call this method to change the cache size. This property must be set before
+ * creating your first FIRDatabaseReference and only needs to be called once per application.
+ *
+ * Note that the specified cache size is only an approximation and the size on disk may temporarily exceed it
+ * at times. Cache sizes smaller than 1 MB or greater than 100 MB are not supported.
+ */
+@property (nonatomic) NSUInteger persistenceCacheSizeBytes;
+
+/**
+ * Sets the dispatch queue on which all events are raised. The default queue is the main queue.
+ *
+ * Note that this must be set before creating your first Database reference.
+ */
+@property (nonatomic, strong) dispatch_queue_t callbackQueue;
+
+/**
+ * Enables verbose diagnostic logging.
+ *
+ * @param enabled YES to enable logging, NO to disable.
+ */
++ (void) setLoggingEnabled:(BOOL)enabled;
+
+/** Retrieve the Firebase Database SDK version. */
++ (NSString *) sdkVersion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDatabase.m b/Firebase/Database/Api/FIRDatabase.m
new file mode 100644
index 0000000..124b463
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabase.m
@@ -0,0 +1,268 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FIRDatabase.h"
+#import "FIRDatabase_Private.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FRepoManager.h"
+#import "FValidation.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FRepoInfo.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseReference_Private.h"
+
+/**
+ * This is a hack that defines all the methods we need from FIRApp/Options. At runtime we use reflection to get the
+ * default FIRApp instance if we need it. Since protocols don't carry any runtime information and selectors
+ * are invoked by name we can write code against this protocol as long as the method signatures don't change.
+ *
+ * TODO: Consider weak-linking the actual Firebase/Core framework or something.
+ */
+
+extern NSString *const kFIRDefaultAppName;
+
+@protocol FIROptionsLike <NSObject>
+@property(nonatomic, readonly, copy) NSString *databaseURL;
+@end
+
+@protocol FIRAppLike <NSObject>
+@property(nonatomic, readonly) id<FIROptionsLike> options;
+@property(nonatomic, copy, readonly) NSString *name;
+@end
+
+@interface FIRDatabase ()
+@property (nonatomic, strong) FRepoInfo *repoInfo;
+@property (nonatomic, strong) FIRDatabaseConfig *config;
+@property (nonatomic, strong) FRepo *repo;
+@end
+
+@implementation FIRDatabase
+
+// The STR and STR_EXPAND macro allow a numeric version passed to he compiler driver
+// with a -D to be treated as a string instead of an invalid floating point value.
+#define STR(x) STR_EXPAND(x)
+#define STR_EXPAND(x) #x
+static const char *FIREBASE_SEMVER = (const char *)STR(FIRDatabase_VERSION);
+
+/**
+ * A static NSMutableDictionary of FirebaseApp names to FirebaseDatabase instance. To ensure thread-
+ * safety, it should only be accessed in databaseForApp, which is synchronized.
+ *
+ * TODO: This serves a duplicate purpose as RepoManager. We should clean up.
+ * TODO: We should maybe be conscious of leaks and make this a weak map or similar
+ * but we have a lot of work to do to allow FirebaseDatabase/Repo etc. to be GC'd.
+ */
++ (NSMutableDictionary *)instances {
+ static dispatch_once_t pred = 0;
+ static NSMutableDictionary *instances;
+ dispatch_once(&pred, ^{
+ instances = [NSMutableDictionary dictionary];
+ });
+ return instances;
+}
+
++ (FIRDatabase *)database {
+ id<FIRAppLike> app = [FIRDatabase getDefaultApp];
+ if (app == nil) {
+ [NSException raise:@"FIRAppNotConfigured" format:@"Failed to get default FIRDatabase instance. Must call FIRApp.configure() before using FIRDatabase."];
+ }
+ return [FIRDatabase databaseForApp:(FIRApp*)app];
+}
+
++ (FIRDatabase *)databaseForApp:(id)app {
+ if (app == nil) {
+ [NSException raise:@"InvalidFIRApp" format:@"nil FIRApp instance passed to databaseForApp."];
+ }
+ NSMutableDictionary *instances = [self instances];
+ @synchronized (instances) {
+ id<FIRAppLike> appLike = (id<FIRAppLike>)app;
+ FIRDatabase *database = instances[appLike.name];
+ if (!database) {
+ NSString *databaseUrl = appLike.options.databaseURL;
+ if (databaseUrl == nil) {
+ [NSException raise:@"MissingDatabaseURL" format:@"Failed to get FIRDatabase instance: FIRApp object has no "
+ "databaseURL in its FirebaseOptions object."];
+ }
+
+ FParsedUrl *parsedUrl = [FUtilities parseUrl:databaseUrl];
+ if (![parsedUrl.path isEmpty]) {
+ [NSException raise:@"InvalidDatabaseURL" format:@"Configured Database URL '%@' is invalid. It should "
+ "point to the root of a Firebase Database but it includes a path: %@",
+ databaseUrl, [parsedUrl.path toString]];
+ }
+
+ id<FAuthTokenProvider> authTokenProvider = [FAuthTokenProvider authTokenProviderForApp:appLike];
+
+ // If this is the default app, don't set the session persistence key so that we use our
+ // default ("default") instead of the FIRApp default ("[DEFAULT]") so that we
+ // preserve the default location used by the legacy Firebase SDK.
+ NSString *sessionIdentifier = @"default";
+ if (![appLike.name isEqualToString:kFIRDefaultAppName]) {
+ sessionIdentifier = appLike.name;
+ }
+
+ FIRDatabaseConfig *config = [[FIRDatabaseConfig alloc] initWithSessionIdentifier:sessionIdentifier
+ authTokenProvider:authTokenProvider];
+ database = [[FIRDatabase alloc] initWithApp:appLike repoInfo:parsedUrl.repoInfo config:config];
+ instances[appLike.name] = database;
+ }
+
+ return database;
+ }
+}
+
++ (NSString *) buildVersion {
+ // TODO: Restore git hash when build moves back to git
+ return [NSString stringWithFormat:@"%s_%s", FIREBASE_SEMVER, __DATE__];
+}
+
++ (FIRDatabase *)createDatabaseForTests:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config {
+ FIRDatabase *db = [[FIRDatabase alloc] initWithApp:nil repoInfo:repoInfo config:config];
+ [db ensureRepo];
+ return db;
+}
+
+
++ (NSString *) sdkVersion {
+ return [NSString stringWithUTF8String:FIREBASE_SEMVER];
+}
+
++ (void) setLoggingEnabled:(BOOL)enabled {
+ [FUtilities setLoggingEnabled:enabled];
+ FFLog(@"I-RDB024001", @"BUILD Version: %@", [FIRDatabase buildVersion]);
+}
+
+
+- (id)initWithApp:(id <FIRAppLike>)appLike repoInfo:(FRepoInfo *)info config:(FIRDatabaseConfig *)config {
+ self = [super init];
+ if (self != nil) {
+ self->_repoInfo = info;
+ self->_config = config;
+ self->_app = (FIRApp*) appLike;
+ }
+ return self;
+}
+
+- (FIRDatabaseReference *)reference {
+ [self ensureRepo];
+
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:[FPath empty]];
+}
+
+- (FIRDatabaseReference *)referenceWithPath:(NSString *)path {
+ [self ensureRepo];
+
+ [FValidation validateFrom:@"referenceWithPath" validRootPathString:path];
+ FPath *childPath = [[FPath alloc] initWith:path];
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:childPath];
+}
+
+- (FIRDatabaseReference *)referenceFromURL:(NSString *)databaseUrl {
+ [self ensureRepo];
+
+ if (databaseUrl == nil) {
+ [NSException raise:@"InvalidDatabaseURL" format:@"Invalid nil url passed to referenceFromURL:"];
+ }
+ FParsedUrl *parsedUrl = [FUtilities parseUrl:databaseUrl];
+ [FValidation validateFrom:@"referenceFromURL:" validURL:parsedUrl];
+ if (![parsedUrl.repoInfo.host isEqualToString:_repoInfo.host]) {
+ [NSException raise:@"InvalidDatabaseURL" format:@"Invalid URL (%@) passed to getReference(). URL was expected "
+ "to match configured Database URL: %@", databaseUrl, [self reference].URL];
+ }
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:parsedUrl.path];
+}
+
+
+- (void)purgeOutstandingWrites {
+ [self ensureRepo];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo purgeOutstandingWrites];
+ });
+}
+
+- (void)goOnline {
+ [self ensureRepo];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo resume];
+ });
+}
+
+
+- (void)goOffline {
+ [self ensureRepo];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo interrupt];
+ });
+}
+
++ (id<FIRAppLike>) getDefaultApp {
+ Class appClass = NSClassFromString(@"FIRApp");
+ if (appClass == nil) {
+ [NSException raise:@"FailedToFindFIRApp" format:@"Failed to find FIRApp class."];
+ return nil;
+ } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+ return [appClass performSelector:@selector(defaultApp)];
+#pragma clang diagnostic pop
+ }
+}
+
+- (void)setPersistenceEnabled:(BOOL)persistenceEnabled {
+ [self assertUnfrozen:@"setPersistenceEnabled"];
+ self->_config.persistenceEnabled = persistenceEnabled;
+}
+
+- (BOOL)persistenceEnabled {
+ return self->_config.persistenceEnabled;
+}
+
+- (void)setPersistenceCacheSizeBytes:(NSUInteger)persistenceCacheSizeBytes {
+ [self assertUnfrozen:@"setPersistenceCacheSizeBytes"];
+ self->_config.persistenceCacheSizeBytes = persistenceCacheSizeBytes;
+}
+
+- (NSUInteger)persistenceCacheSizeBytes {
+ return self->_config.persistenceCacheSizeBytes;
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t)callbackQueue {
+ [self assertUnfrozen:@"setCallbackQueue"];
+ self->_config.callbackQueue = callbackQueue;
+}
+
+- (dispatch_queue_t)callbackQueue {
+ return self->_config.callbackQueue;
+}
+
+- (void) assertUnfrozen:(NSString*)methodName {
+ if (self.repo != nil) {
+ [NSException raise:@"FIRDatabaseAlreadyInUse" format:@"Calls to %@ must be made before any other usage of "
+ "FIRDatabase instance.", methodName];
+ }
+}
+
+- (void) ensureRepo {
+ if (self.repo == nil) {
+ self.repo = [FRepoManager createRepo:self.repoInfo config:self.config database:self];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabaseConfig.h b/Firebase/Database/Api/FIRDatabaseConfig.h
new file mode 100644
index 0000000..d41f3a8
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseConfig.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 <Foundation/Foundation.h>
+
+@protocol FAuthTokenProvider;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * TODO: Merge FIRDatabaseConfig into FIRDatabase.
+ */
+@interface FIRDatabaseConfig : NSObject
+
+- (id)initWithSessionIdentifier:(NSString *)identifier authTokenProvider:(id<FAuthTokenProvider>)authTokenProvider;
+
+/**
+ * By default the Firebase Database client will keep data in memory while your application is running, but not
+ * when it is restarted. By setting this value to YES, the data will be persisted to on-device (disk)
+ * storage and will thus be available again when the app is restarted (even when there is no network
+ * connectivity at that time). Note that this property must be set before creating your first FIRDatabaseReference
+ * and only needs to be called once per application.
+ *
+ * If your app uses Firebase Authentication, the client will automatically persist the user's authentication
+ * token across restarts, even without persistence enabled. But if the auth token expired while offline and
+ * you've enabled persistence, the client will pause write operations until you successfully re-authenticate
+ * (or explicitly unauthenticate) to prevent your writes from being sent unauthenticated and failing due to
+ * security rules.
+ */
+@property (nonatomic) BOOL persistenceEnabled;
+
+/**
+ * By default the Firebase Database client will use up to 10MB of disk space to cache data. If the cache grows beyond this size,
+ * the client will start removing data that hasn't been recently used. If you find that your application caches too
+ * little or too much data, call this method to change the cache size. This property must be set before creating
+ * your first FIRDatabaseReference and only needs to be called once per application.
+ *
+ * Note that the specified cache size is only an approximation and the size on disk may temporarily exceed it
+ * at times.
+ */
+@property (nonatomic) NSUInteger persistenceCacheSizeBytes;
+
+/**
+ * Sets the dispatch queue on which all events are raised. The default queue is the main queue.
+ */
+@property (nonatomic, strong) dispatch_queue_t callbackQueue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDatabaseConfig.m b/Firebase/Database/Api/FIRDatabaseConfig.m
new file mode 100644
index 0000000..f4639f9
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseConfig.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 "FIRApp.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FIRNoopAuthTokenProvider.h"
+#import "FAuthTokenProvider.h"
+
+@interface FIRDatabaseConfig (Private)
+
+@property (nonatomic, strong, readwrite) NSString *sessionIdentifier;
+
+@end
+
+@implementation FIRDatabaseConfig
+
+- (id)init {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create config objects!"];
+ return nil;
+}
+
+- (id)initWithSessionIdentifier:(NSString *)identifier authTokenProvider:(id<FAuthTokenProvider>)authTokenProvider {
+ self = [super init];
+ if (self != nil) {
+ self->_sessionIdentifier = identifier;
+ self->_callbackQueue = dispatch_get_main_queue();
+ self->_persistenceCacheSizeBytes = 10*1024*1024; // Default cache size is 10MB
+ self->_authTokenProvider = authTokenProvider;
+ }
+ return self;
+}
+
+- (void)assertUnfrozen {
+ if (self.isFrozen) {
+ [NSException raise:NSGenericException format:@"Can't modify config objects after they are in use for FIRDatabaseReferences."];
+ }
+}
+
+- (void)setAuthTokenProvider:(id<FAuthTokenProvider>)authTokenProvider {
+ [self assertUnfrozen];
+ self->_authTokenProvider = authTokenProvider;
+}
+
+- (void)setPersistenceEnabled:(BOOL)persistenceEnabled {
+ [self assertUnfrozen];
+ self->_persistenceEnabled = persistenceEnabled;
+}
+
+- (void)setPersistenceCacheSizeBytes:(NSUInteger)persistenceCacheSizeBytes {
+ [self assertUnfrozen];
+ // Can't be less than 1MB
+ if (persistenceCacheSizeBytes < 1024*1024) {
+ [NSException raise:NSInvalidArgumentException format:@"The minimum cache size must be at least 1MB"];
+ }
+ if (persistenceCacheSizeBytes > 100*1024*1024) {
+ [NSException raise:NSInvalidArgumentException format:@"Firebase Database currently doesn't support a cache size larger than 100MB"];
+ }
+ self->_persistenceCacheSizeBytes = persistenceCacheSizeBytes;
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t)callbackQueue {
+ [self assertUnfrozen];
+ self->_callbackQueue = callbackQueue;
+}
+
+- (void)freeze {
+ self->_isFrozen = YES;
+}
+
+// TODO: Only used for tests. Migrate to FIRDatabase and remove.
++ (FIRDatabaseConfig *)defaultConfig {
+ static dispatch_once_t onceToken;
+ static FIRDatabaseConfig *defaultConfig;
+ dispatch_once(&onceToken, ^{
+ defaultConfig = [FIRDatabaseConfig configForName:@"default"];
+ });
+ return defaultConfig;
+}
+
+// TODO: This is only used for tests. We should fix them to go through FIRDatabase and remove
+// this method and the sessionsConfigs dictionary (FIRDatabase automatically creates one config per app).
++ (FIRDatabaseConfig *)configForName:(NSString *)name {
+ NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:@"^[a-zA-Z0-9-_]+$" options:0 error:nil];
+ if ([expression numberOfMatchesInString:name options:0 range:NSMakeRange(0, name.length)] == 0) {
+ [NSException raise:NSInvalidArgumentException format:@"Name can only contain [a-zA-Z0-9-_]"];
+ }
+
+ static dispatch_once_t onceToken;
+ static NSMutableDictionary *sessionConfigs;
+ dispatch_once(&onceToken, ^{
+ sessionConfigs = [NSMutableDictionary dictionary];
+ });
+ @synchronized(sessionConfigs) {
+ if (!sessionConfigs[name]) {
+ id<FAuthTokenProvider> authTokenProvider = [FAuthTokenProvider authTokenProviderForApp:[FIRApp defaultApp]];
+ sessionConfigs[name] = [[FIRDatabaseConfig alloc] initWithSessionIdentifier:name
+ authTokenProvider:authTokenProvider];
+ }
+ return sessionConfigs[name];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabaseQuery.h b/Firebase/Database/Api/FIRDatabaseQuery.h
new file mode 100644
index 0000000..be4ad27
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseQuery.h
@@ -0,0 +1,315 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+#import "FIRDataEventType.h"
+#import "FIRDataSnapshot.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A FIRDatabaseHandle is used to identify listeners of Firebase Database events. These handles
+ * are returned by observeEventType: and and can later be passed to removeObserverWithHandle: to
+ * stop receiving updates.
+ */
+typedef NSUInteger FIRDatabaseHandle FIR_SWIFT_NAME(DatabaseHandle);
+
+/**
+ * A FIRDatabaseQuery instance represents a query over the data at a particular location.
+ *
+ * You create one by calling one of the query methods (queryOrderedByChild:, queryStartingAtValue:, etc.)
+ * on a FIRDatabaseReference. The query methods can be chained to further specify the data you are interested in
+ * observing
+ */
+FIR_SWIFT_NAME(DatabaseQuery)
+@interface FIRDatabaseQuery : NSObject
+
+
+#pragma mark - Attach observers to read data
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+#pragma mark - Detaching observers
+
+/**
+ * Detach a block previously attached with observeEventType:withBlock:.
+ *
+ * @param handle The handle returned by the call to observeEventType:withBlock: which we are trying to remove.
+ */
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle;
+
+
+/**
+ * Detach all blocks previously attached to this Firebase Database location with observeEventType:withBlock:
+ */
+- (void) removeAllObservers;
+
+/**
+ * By calling `keepSynced:YES` on a location, the data for that location will automatically be downloaded and
+ * kept in sync, even when no listeners are attached for that location. Additionally, while a location is kept
+ * synced, it will not be evicted from the persistent disk cache.
+ *
+ * @param keepSynced Pass YES to keep this location synchronized, pass NO to stop synchronization.
+*/
+ - (void) keepSynced:(BOOL)keepSynced;
+
+
+#pragma mark - Querying and limiting
+
+/**
+* queryLimitedToFirst: is used to generate a reference to a limited view of the data at this location.
+* The FIRDatabaseQuery instance returned by queryLimitedToFirst: will respond to at most the first limit child nodes.
+*
+* @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+* @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+*/
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit;
+
+
+/**
+* queryLimitedToLast: is used to generate a reference to a limited view of the data at this location.
+* The FIRDatabaseQuery instance returned by queryLimitedToLast: will respond to at most the last limit child nodes.
+*
+* @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+* @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+*/
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit;
+
+/**
+ * queryOrderBy: is used to generate a reference to a view of the data that's been sorted by the values of
+ * a particular child key. This method is intended to be used in combination with queryStartingAtValue:,
+ * queryEndingAtValue:, or queryEqualToValue:.
+ *
+ * @param key The child key to use in ordering data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, ordered by the values of the specified child key.
+*/
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)key;
+
+/**
+ * queryOrderedByKey: is used to generate a reference to a view of the data that's been sorted by child key.
+ * This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child keys.
+ */
+- (FIRDatabaseQuery *) queryOrderedByKey;
+
+/**
+ * queryOrderedByValue: is used to generate a reference to a view of the data that's been sorted by child value.
+ * This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child value.
+ */
+- (FIRDatabaseQuery *) queryOrderedByValue;
+
+/**
+ * queryOrderedByPriority: is used to generate a reference to a view of the data that's been sorted by child
+ * priority. This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child priorities.
+ */
+- (FIRDatabaseQuery *) queryOrderedByPriority;
+
+/**
+ * queryStartingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue: will respond to events at nodes with a value
+ * greater than or equal to startValue.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue;
+
+/**
+ * queryStartingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue:childKey will respond to events at nodes with a value
+ * greater than startValue, or equal to startValue and with a key greater than or equal to childKey. This is most
+ * useful when implementing pagination in a case where multiple nodes can match the startValue.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The lower bound, inclusive, for the key of nodes with value equal to startValue
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEndingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue: will respond to events at nodes with a value
+ * less than or equal to endValue.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue;
+
+/**
+ * queryEndingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue:childKey will respond to events at nodes with a value
+ * less than endValue, or equal to endValue and with a key less than or equal to childKey. This is most useful when
+ * implementing pagination in a case where multiple nodes can match the endValue.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The upper bound, inclusive, for the key of nodes with value equal to endValue
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEqualToValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue: will respond to events at nodes with a value equal
+ * to the supplied argument.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value;
+
+/**
+ * queryEqualToValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue:childKey will respond to events at nodes with a value
+ * equal to the supplied argument and with their key equal to childKey. There will be at most one node that matches
+ * because child keys are unique.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @param childKey The name of nodes with the right value
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value and the key.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value childKey:(nullable NSString *)childKey;
+
+
+#pragma mark - Properties
+
+/**
+* Gets a FIRDatabaseReference for the location of this query.
+*
+* @return A FIRDatabaseReference for the location of this query.
+*/
+@property (nonatomic, readonly, strong) FIRDatabaseReference * ref;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDatabaseQuery.m b/Firebase/Database/Api/FIRDatabaseQuery.m
new file mode 100644
index 0000000..bcb1733
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseQuery.m
@@ -0,0 +1,525 @@
+/*
+ * 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 "FIRDatabaseQuery.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FValidation.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FValueEventRegistration.h"
+#import "FChildEventRegistration.h"
+#import "FPath.h"
+#import "FKeyIndex.h"
+#import "FPathIndex.h"
+#import "FPriorityIndex.h"
+#import "FValueIndex.h"
+#import "FLeafNode.h"
+#import "FSnapshotUtilities.h"
+#import "FConstants.h"
+
+@implementation FIRDatabaseQuery
+
+@synthesize repo;
+@synthesize path;
+@synthesize queryParams;
+
+#define INVALID_QUERY_PARAM_ERROR @"InvalidQueryParameter"
+
+
++ (dispatch_queue_t)sharedQueue
+{
+ // We use this shared queue across all of the FQueries so things happen FIFO (as opposed to dispatch_get_global_queue(0, 0) which is concurrent)
+ static dispatch_once_t pred;
+ static dispatch_queue_t sharedDispatchQueue;
+
+ dispatch_once(&pred, ^{
+ sharedDispatchQueue = dispatch_queue_create("FirebaseWorker", NULL);
+ });
+
+ return sharedDispatchQueue;
+}
+
+- (id) initWithRepo:(FRepo *)theRepo path:(FPath *)thePath {
+ return [self initWithRepo:theRepo path:thePath params:nil orderByCalled:NO priorityMethodCalled:NO];
+}
+
+- (id) initWithRepo:(FRepo *)theRepo
+ path:(FPath *)thePath
+ params:(FQueryParams *)theParams
+ orderByCalled:(BOOL)orderByCalled
+priorityMethodCalled:(BOOL)priorityMethodCalled {
+ self = [super init];
+ if (self) {
+ self.repo = theRepo;
+ self.path = thePath;
+ if (!theParams) {
+ theParams = [FQueryParams defaultInstance];
+ }
+ if (![theParams isValid]) {
+ @throw [[NSException alloc] initWithName:@"InvalidArgumentError" reason:@"Queries are limited to two constraints" userInfo:nil];
+ }
+ self.queryParams = theParams;
+ self.orderByCalled = orderByCalled;
+ self.priorityMethodCalled = priorityMethodCalled;
+ }
+ return self;
+}
+
+- (FQuerySpec *)querySpec {
+ return [[FQuerySpec alloc] initWithPath:self.path params:self.queryParams];
+}
+
+- (void)validateQueryEndpointsForParams:(FQueryParams *)params {
+ if ([params.index isEqual:[FKeyIndex keyIndex]]) {
+ if ([params hasStart]) {
+ if (params.indexStartKey != [FUtilities minName]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryStartingAtValue:childKey: or queryEqualTo:andChildKey: in combination with queryOrderedByKey"];
+ }
+ if (![params.indexStartValue.val isKindOfClass:[NSString class]]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryStartingAtValue: with other types than string in combination with queryOrderedByKey"];
+ }
+ }
+ if ([params hasEnd]) {
+ if (params.indexEndKey != [FUtilities maxName]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryEndingAtValue:childKey: or queryEqualToValue:childKey: in combination with queryOrderedByKey"];
+ }
+ if (![params.indexEndValue.val isKindOfClass:[NSString class]]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryEndingAtValue: with other types than string in combination with queryOrderedByKey"];
+ }
+ }
+ } else if ([params.index isEqual:[FPriorityIndex priorityIndex]]) {
+ if (([params hasStart] && ![FValidation validatePriorityValue:params.indexStartValue.val]) ||
+ ([params hasEnd] && ![FValidation validatePriorityValue:params.indexEndValue.val])) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"When using queryOrderedByPriority, values provided to queryStartingAtValue:, queryEndingAtValue:, or queryEqualToValue: must be valid priorities."];
+ }
+ }
+}
+
+- (void)validateEqualToCall {
+ if ([self.queryParams hasStart]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot combine queryEqualToValue: and queryStartingAtValue:"];
+ }
+ if ([self.queryParams hasEnd]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot combine queryEqualToValue: and queryEndingAtValue:"];
+ }
+}
+
+- (void)validateNoPreviousOrderByCalled {
+ if (self.orderByCalled) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot use multiple queryOrderedBy calls!"];
+ }
+}
+
+- (void)validateIndexValueType:(id)type fromMethod:(NSString *)method {
+ if (type != nil &&
+ ![type isKindOfClass:[NSNumber class]] &&
+ ![type isKindOfClass:[NSString class]] &&
+ ![type isKindOfClass:[NSNull class]]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"You can only pass nil, NSString or NSNumber to %@", method];
+ }
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue {
+ return [self queryStartingAtInternal:startValue childKey:nil from:@"queryStartingAtValue:" priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue childKey:(NSString *)childKey {
+ if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:@"You must use queryStartingAtValue: instead of queryStartingAtValue:childKey: when using queryOrderedByKey:"
+ userInfo:nil];
+ }
+ return [self queryStartingAtInternal:startValue
+ childKey:childKey
+ from:@"queryStartingAtValue:childKey:"
+ priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtInternal:(id<FNode>)startValue
+ childKey:(NSString *)childKey
+ from:(NSString *)methodName
+ priorityMethod:(BOOL)priorityMethod {
+ [self validateIndexValueType:startValue fromMethod:methodName];
+ if (childKey != nil) {
+ [FValidation validateFrom:methodName validKey:childKey];
+ }
+ if ([self.queryParams hasStart]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR
+ format:@"Can't call %@ after queryStartingAtValue or queryEqualToValue was previously called", methodName];
+ }
+ id<FNode> startNode = [FSnapshotUtilities nodeFrom:startValue];
+ FQueryParams* params = [self.queryParams startAt:startNode childKey:childKey];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue {
+ return [self queryEndingAtInternal:endValue
+ childKey:nil
+ from:@"queryEndingAtValue:"
+ priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue childKey:(NSString *)childKey {
+ if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:@"You must use queryEndingAtValue: instead of queryEndingAtValue:childKey: when using queryOrderedByKey:"
+ userInfo:nil];
+ }
+
+ return [self queryEndingAtInternal:endValue
+ childKey:childKey
+ from:@"queryEndingAtValue:childKey:"
+ priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtInternal:(id)endValue
+ childKey:(NSString *)childKey
+ from:(NSString *)methodName
+ priorityMethod:(BOOL)priorityMethod {
+ [self validateIndexValueType:endValue fromMethod:methodName];
+ if (childKey != nil) {
+ [FValidation validateFrom:methodName validKey:childKey];
+ }
+ if ([self.queryParams hasEnd]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR
+ format:@"Can't call %@ after queryEndingAtValue or queryEqualToValue was previously called", methodName];
+ }
+ id<FNode> endNode = [FSnapshotUtilities nodeFrom:endValue];
+ FQueryParams* params = [self.queryParams endAt:endNode childKey:childKey];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value {
+ return [self queryEqualToInternal:value childKey:nil from:@"queryEqualToValue:" priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value childKey:(NSString *)childKey {
+ if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:@"You must use queryEqualToValue: instead of queryEqualTo:childKey: when using queryOrderedByKey:"
+ userInfo:nil];
+ }
+ return [self queryEqualToInternal:value childKey:childKey from:@"queryEqualToValue:childKey:" priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEqualToInternal:(id)value
+ childKey:(NSString *)childKey
+ from:(NSString *)methodName
+ priorityMethod:(BOOL)priorityMethod {
+ [self validateIndexValueType:value fromMethod:methodName];
+ if (childKey != nil) {
+ [FValidation validateFrom:methodName validKey:childKey];
+ }
+ if ([self.queryParams hasEnd] || [self.queryParams hasStart]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR
+ format:@"Can't call %@ after queryStartingAtValue, queryEndingAtValue or queryEqualToValue was previously called", methodName];
+ }
+ id<FNode> node = [FSnapshotUtilities nodeFrom:value];
+ FQueryParams* params = [[self.queryParams startAt:node childKey:childKey] endAt:node childKey:childKey];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
+}
+
+- (void)validateLimitRange:(NSUInteger)limit
+{
+ // No need to check for negative ranges, since limit is unsigned
+ if (limit == 0) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Limit can't be zero"];
+ }
+ if (limit >= 1l<<31) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Limit must be less than 2,147,483,648"];
+ }
+}
+
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit {
+ if (self.queryParams.limitSet) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't call queryLimitedToFirst: if a limit was previously set"];
+ }
+ [self validateLimitRange:limit];
+ FQueryParams* params = [self.queryParams limitToFirst:limit];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit {
+ if (self.queryParams.limitSet) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't call queryLimitedToLast: if a limit was previously set"];
+ }
+ [self validateLimitRange:limit];
+ FQueryParams* params = [self.queryParams limitToLast:limit];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)indexPathString {
+ if ([indexPathString isEqualToString:@"$key"] || [indexPathString isEqualToString:@".key"]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByKey: instead.", indexPathString]
+ userInfo:nil];
+ } else if ([indexPathString isEqualToString:@"$priority"] || [indexPathString isEqualToString:@".priority"]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByPriority: instead.", indexPathString]
+ userInfo:nil];
+ } else if ([indexPathString isEqualToString:@"$value"] || [indexPathString isEqualToString:@".value"]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByValue: instead.", indexPathString]
+ userInfo:nil];
+ }
+ [self validateNoPreviousOrderByCalled];
+
+ [FValidation validateFrom:@"queryOrderedByChild:" validPathString:indexPathString];
+ FPath *indexPath = [FPath pathWithString:indexPathString];
+ if (indexPath.isEmpty) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) with an empty path is invalid. Use queryOrderedByValue: instead."]
+ userInfo:nil];
+ }
+ id<FIndex> index = [[FPathIndex alloc] initWithPath:indexPath];
+
+ FQueryParams *params = [self.queryParams orderBy:index];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByKey {
+ [self validateNoPreviousOrderByCalled];
+ FQueryParams *params = [self.queryParams orderBy:[FKeyIndex keyIndex]];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByValue {
+ [self validateNoPreviousOrderByCalled];
+ FQueryParams *params = [self.queryParams orderBy:[FValueIndex valueIndex]];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByPriority {
+ [self validateNoPreviousOrderByCalled];
+ FQueryParams *params = [self.queryParams orderBy:[FPriorityIndex priorityIndex]];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *))block {
+ [FValidation validateFrom:@"observeEventType:withBlock:" knownEventType:eventType];
+ return [self observeEventType:eventType withBlock:block withCancelBlock:nil];
+}
+
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+ [FValidation validateFrom:@"observeEventType:andPreviousSiblingKeyWithBlock:" knownEventType:eventType];
+ return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [FValidation validateFrom:@"observeEventType:withBlock:withCancelBlock:" knownEventType:eventType];
+
+ if (eventType == FIRDataEventTypeValue) {
+ // Handle FIRDataEventTypeValue specially because they shouldn't have prevName callbacks
+ NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
+ [self observeValueEventWithHandle:handle withBlock:block cancelCallback:cancelBlock];
+ return handle;
+ } else {
+ // Wrap up the userCallback so we can treat everything as a callback that has a prevName
+ fbt_void_datasnapshot userCallback = [block copy];
+ return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ if (userCallback != nil) {
+ userCallback(snapshot);
+ }
+ } withCancelBlock:cancelBlock];
+ }
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [FValidation validateFrom:@"observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:" knownEventType:eventType];
+
+
+ if (eventType == FIRDataEventTypeValue) {
+ // TODO: This gets hit by observeSingleEventOfType. Need to fix.
+ /*
+ @throw [[NSException alloc] initWithName:@"InvalidEventTypeForObserver"
+ reason:@"(observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:) Cannot use observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock: with FIRDataEventTypeValue. Use observeEventType:withBlock:withCancelBlock: instead."
+ userInfo:nil];
+ */
+ }
+
+ NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
+ NSDictionary *callbacks = @{[NSNumber numberWithInteger:eventType]: [block copy]};
+ [self observeChildEventWithHandle:handle withCallbacks:callbacks cancelCallback:cancelBlock];
+
+ return handle;
+}
+
+// If we want to distinguish between value event listeners and child event listeners, like in the Java client, we can
+// consider exporting this. If we do, add argument validation. Otherwise, arguments are validated in the public-facing
+// portions of the API. Also, move the FIRDatabaseHandle logic.
+- (void)observeValueEventWithHandle:(FIRDatabaseHandle)handle withBlock:(fbt_void_datasnapshot)block cancelCallback:(fbt_void_nserror)cancelBlock {
+ // Note that we don't need to copy the callbacks here, FEventRegistration callback properties set to copy
+ FValueEventRegistration *registration = [[FValueEventRegistration alloc] initWithRepo:self.repo
+ handle:handle
+ callback:block
+ cancelCallback:cancelBlock];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo addEventRegistration:registration forQuery:self.querySpec];
+ });
+}
+
+// Note: as with the above method, we may wish to expose this at some point.
+- (void)observeChildEventWithHandle:(FIRDatabaseHandle)handle withCallbacks:(NSDictionary *)callbacks cancelCallback:(fbt_void_nserror)cancelBlock {
+ // Note that we don't need to copy the callbacks here, FEventRegistration callback properties set to copy
+ FChildEventRegistration *registration = [[FChildEventRegistration alloc] initWithRepo:self.repo
+ handle:handle
+ callbacks:callbacks
+ cancelCallback:cancelBlock];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo addEventRegistration:registration forQuery:self.querySpec];
+ });
+}
+
+
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle {
+ FValueEventRegistration *event = [[FValueEventRegistration alloc] initWithRepo:self.repo
+ handle:handle
+ callback:nil
+ cancelCallback:nil];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo removeEventRegistration:event forQuery:self.querySpec];
+ });
+}
+
+
+- (void) removeAllObservers {
+ [self removeObserverWithHandle:NSNotFound];
+}
+
+- (void)keepSynced:(BOOL)keepSynced {
+ if ([self.path.getFront isEqualToString:kDotInfoPrefix]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't keep query on .info tree synced (this already is the case)."];
+ }
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo keepQuery:self.querySpec synced:keepSynced];
+ });
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {
+
+ [self observeSingleEventOfType:eventType withBlock:block withCancelBlock:nil];
+}
+
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+
+ [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+
+ // XXX: user reported memory leak in method
+
+ // "When you copy a block, any references to other blocks from within that block are copied if necessary—an entire tree may be copied (from the top). If you have block variables and you reference a block from within the block, that block will be copied."
+ // http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1
+ // So... we don't need to do this since inside the on: we copy this block off the stack to the heap.
+ // __block fbt_void_datasnapshot userCallback = [callback copy];
+
+ [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ if (block != nil) {
+ block(snapshot);
+ }
+ } withCancelBlock:cancelBlock];
+}
+
+/**
+* Attaches a listener, waits for the first event, and then removes the listener
+*/
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+
+ // XXX: user reported memory leak in method
+
+ // "When you copy a block, any references to other blocks from within that block are copied if necessary—an entire tree may be copied (from the top). If you have block variables and you reference a block from within the block, that block will be copied."
+ // http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1
+ // So... we don't need to do this since inside the on: we copy this block off the stack to the heap.
+ // __block fbt_void_datasnapshot userCallback = [callback copy];
+
+ __block FIRDatabaseHandle handle;
+ __block BOOL firstCall = YES;
+
+ fbt_void_datasnapshot_nsstring callback = [block copy];
+ fbt_void_datasnapshot_nsstring wrappedCallback = ^(FIRDataSnapshot *snap, NSString* prevName) {
+ if (firstCall) {
+ firstCall = NO;
+ [self removeObserverWithHandle:handle];
+ callback(snap, prevName);
+ }
+ };
+
+ fbt_void_nserror cancelCallback = [cancelBlock copy];
+ handle = [self observeEventType:eventType andPreviousSiblingKeyWithBlock:wrappedCallback withCancelBlock:^(NSError* error){
+
+ [self removeObserverWithHandle:handle];
+
+ if (cancelCallback) {
+ cancelCallback(error);
+ }
+ }];
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"(%@ %@)", self.path, self.queryParams.description];
+}
+
+- (FIRDatabaseReference *) ref {
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:self.path];
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h b/Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h
new file mode 100644
index 0000000..529adf4
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseSwiftNameSupport.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.
+ */
+
+#ifndef FIR_SWIFT_NAME
+
+#import <Foundation/Foundation.h>
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// // Wrap it in our own macro if it's a non-compatible SDK.
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+
+#endif // FIR_SWIFT_NAME \ No newline at end of file
diff --git a/Firebase/Database/Api/FIRMutableData.h b/Firebase/Database/Api/FIRMutableData.h
new file mode 100644
index 0000000..5c26024
--- /dev/null
+++ b/Firebase/Database/Api/FIRMutableData.h
@@ -0,0 +1,130 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A FIRMutableData instance is populated with data from a Firebase Database location.
+ * When you are using runTransactionBlock:, you will be given an instance containing the current
+ * data at that location. Your block will be responsible for updating that instance to the data
+ * you wish to save at that location, and then returning using [FIRTransactionResult successWithValue:].
+ *
+ * To modify the data, set its value property to any of the native types support by Firebase Database:
+ *
+ * + NSNumber (includes BOOL)
+ * + NSDictionary
+ * + NSArray
+ * + NSString
+ * + nil / NSNull to remove the data
+ *
+ * Note that changes made to a child FIRMutableData instance will be visible to the parent.
+ */
+FIR_SWIFT_NAME(MutableData)
+@interface FIRMutableData : NSObject
+
+
+#pragma mark - Inspecting and navigating the data
+
+
+/**
+ * Returns boolean indicating whether this mutable data has children.
+ *
+ * @return YES if this data contains child nodes.
+ */
+- (BOOL) hasChildren;
+
+
+/**
+ * Indicates whether this mutable data has a child at the given path.
+ *
+ * @param path A path string, consisting either of a single segment, like 'child', or multiple segments, 'a/deeper/child'
+ * @return YES if this data contains a child at the specified relative path
+ */
+- (BOOL) hasChildAtPath:(NSString *)path;
+
+
+/**
+ * Used to obtain a FIRMutableData instance that encapsulates the data at the given relative path.
+ * Note that changes made to the child will be visible to the parent.
+ *
+ * @param path A path string, consisting either of a single segment, like 'child', or multiple segments, 'a/deeper/child'
+ * @return A FIRMutableData instance containing the data at the given path
+ */
+- (FIRMutableData *)childDataByAppendingPath:(NSString *)path;
+
+
+#pragma mark - Properties
+
+
+/**
+ * To modify the data contained by this instance of FIRMutableData, set this to any of the native types supported by Firebase Database:
+ *
+ * + NSNumber (includes BOOL)
+ * + NSDictionary
+ * + NSArray
+ * + NSString
+ * + nil / NSNull to remove the data
+ *
+ * Note that setting this value will override the priority at this location.
+ *
+ * @return The current data at this location as a native object
+ */
+@property (strong, nonatomic, nullable) id value;
+
+
+/**
+ * Set this property to update the priority of the data at this location. Can be set to the following types:
+ *
+ * + NSNumber
+ * + NSString
+ * + nil / NSNull to remove the priority
+ *
+ * @return The priority of the data at this location
+ */
+@property (strong, nonatomic, nullable) id priority;
+
+
+/**
+ * @return The number of child nodes at this location
+ */
+@property (readonly, nonatomic) NSUInteger childrenCount;
+
+
+/**
+ * Used to iterate over the children at this location. You can use the native for .. in syntax:
+ *
+ * for (FIRMutableData* child in data.children) {
+ * ...
+ * }
+ *
+ * Note that this enumerator operates on an immutable copy of the child list. So, you can modify the instance
+ * during iteration, but the new additions will not be visible until you get a new enumerator.
+ */
+@property (readonly, nonatomic, strong) NSEnumerator* children;
+
+
+/**
+ * @return The key name of this node, or nil if it is the top-most location
+ */
+@property (readonly, nonatomic, strong, nullable) NSString* key;
+
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRMutableData.m b/Firebase/Database/Api/FIRMutableData.m
new file mode 100644
index 0000000..7e10dcd
--- /dev/null
+++ b/Firebase/Database/Api/FIRMutableData.m
@@ -0,0 +1,134 @@
+/*
+ * 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 "FIRMutableData.h"
+#import "FIRMutableData_Private.h"
+#import "FSnapshotHolder.h"
+#import "FSnapshotUtilities.h"
+#import "FChildrenNode.h"
+#import "FTransformedEnumerator.h"
+#import "FNamedNode.h"
+#import "FIndexedNode.h"
+
+@interface FIRMutableData ()
+
+- (id) initWithPrefixPath:(FPath *)path andSnapshotHolder:(FSnapshotHolder *)snapshotHolder;
+
+@property (strong, nonatomic) FSnapshotHolder* data;
+@property (strong, nonatomic) FPath* prefixPath;
+
+@end
+
+@implementation FIRMutableData
+
+@synthesize data;
+@synthesize prefixPath;
+
+- (id) initWithNode:(id<FNode>)node {
+ FSnapshotHolder* holder = [[FSnapshotHolder alloc] init];
+ FPath* path = [FPath empty];
+ [holder updateSnapshot:path withNewSnapshot:node];
+ return [self initWithPrefixPath:path andSnapshotHolder:holder];
+}
+
+- (id) initWithPrefixPath:(FPath *)path andSnapshotHolder:(FSnapshotHolder *)snapshotHolder {
+ self = [super init];
+ if (self) {
+ self.prefixPath = path;
+ self.data = snapshotHolder;
+ }
+ return self;
+}
+
+- (FIRMutableData *)childDataByAppendingPath:(NSString *)path {
+ FPath* wholePath = [self.prefixPath childFromString:path];
+ return [[FIRMutableData alloc] initWithPrefixPath:wholePath andSnapshotHolder:self.data];
+}
+
+- (FIRMutableData *) parent {
+ if ([self.prefixPath isEmpty]) {
+ return nil;
+ } else {
+ FPath* path = [self.prefixPath parent];
+ return [[FIRMutableData alloc] initWithPrefixPath:path andSnapshotHolder:self.data];
+ }
+}
+
+- (void) setValue:(id)aValue {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:aValue withValidationFrom:@"setValue:"];
+ [self.data updateSnapshot:self.prefixPath withNewSnapshot:node];
+}
+
+- (void) setPriority:(id)aPriority {
+ id<FNode> node = [self.data getNode:self.prefixPath];
+ id<FNode> pri = [FSnapshotUtilities nodeFrom:aPriority];
+ node = [node updatePriority:pri];
+ [self.data updateSnapshot:self.prefixPath withNewSnapshot:node];
+}
+
+- (id) value {
+ return [[self.data getNode:self.prefixPath] val];
+}
+
+- (id) priority {
+ return [[[self.data getNode:self.prefixPath] getPriority] val];
+}
+
+- (BOOL) hasChildren {
+ id<FNode> node = [self.data getNode:self.prefixPath];
+ return ![node isLeafNode] && ![(FChildrenNode*)node isEmpty];
+}
+
+- (BOOL) hasChildAtPath:(NSString *)path {
+ id<FNode> node = [self.data getNode:self.prefixPath];
+ FPath* childPath = [[FPath alloc] initWith:path];
+ return ![[node getChild:childPath] isEmpty];
+}
+
+- (NSUInteger) childrenCount {
+ return [[self.data getNode:self.prefixPath] numChildren];
+}
+
+- (NSString *) key {
+ return [self.prefixPath getBack];
+}
+
+- (id<FNode>) nodeValue {
+ return [self.data getNode:self.prefixPath];
+}
+
+- (NSEnumerator *) children {
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:self.nodeValue];
+ return [[FTransformedEnumerator alloc] initWithEnumerator:[indexedNode childEnumerator] andTransform:^id(FNamedNode *node) {
+ FPath* childPath = [self.prefixPath childFromString:node.name];
+ FIRMutableData * childData = [[FIRMutableData alloc] initWithPrefixPath:childPath andSnapshotHolder:self.data];
+ return childData;
+ }];
+}
+
+- (BOOL) isEqualToData:(FIRMutableData *)other {
+ return self.data == other.data && [[self.prefixPath description] isEqualToString:[other.prefixPath description]];
+}
+
+- (NSString *) description {
+ if (self.key == nil) {
+ return [NSString stringWithFormat:@"FIRMutableData (top-most transaction) %@ %@", self.key, self.value];
+ } else {
+ return [NSString stringWithFormat:@"FIRMutableData (%@) %@", self.key, self.value];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRServerValue.h b/Firebase/Database/Api/FIRServerValue.h
new file mode 100644
index 0000000..f5eadd5
--- /dev/null
+++ b/Firebase/Database/Api/FIRServerValue.h
@@ -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 "FIRDatabaseSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Placeholder values you may write into Firebase Database as a value or priority
+ * that will automatically be populated by the Firebase Database server.
+ */
+FIR_SWIFT_NAME(ServerValue)
+@interface FIRServerValue : NSObject
+
+/**
+ * Placeholder value for the number of milliseconds since the Unix epoch
+ */
++ (NSDictionary *) timestamp;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRServerValue.m b/Firebase/Database/Api/FIRServerValue.m
new file mode 100644
index 0000000..14bb745
--- /dev/null
+++ b/Firebase/Database/Api/FIRServerValue.m
@@ -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 "FIRDatabaseReference.h"
+#import "FIRServerValue.h"
+
+@implementation FIRServerValue
+
++ (NSDictionary *) timestamp {
+ static NSDictionary *timestamp = nil;
+ if (timestamp == nil) {
+ timestamp = @{ @".sv": @"timestamp" };
+ }
+ return timestamp;
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRTransactionResult.h b/Firebase/Database/Api/FIRTransactionResult.h
new file mode 100644
index 0000000..3c2d39a
--- /dev/null
+++ b/Firebase/Database/Api/FIRTransactionResult.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 <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+#import "FIRMutableData.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Used for runTransactionBlock:. An FIRTransactionResult instance is a container for the results of the transaction.
+ */
+FIR_SWIFT_NAME(TransactionResult)
+@interface FIRTransactionResult : NSObject
+
+/**
+ * Used for runTransactionBlock:. Indicates that the new value should be saved at this location
+ *
+ * @param value A FIRMutableData instance containing the new value to be set
+ * @return An FIRTransactionResult instance that can be used as a return value from the block given to runTransactionBlock:
+ */
++ (FIRTransactionResult *)successWithValue:(FIRMutableData *)value;
+
+
+/**
+ * Used for runTransactionBlock:. Indicates that the current transaction should no longer proceed.
+ *
+ * @return An FIRTransactionResult instance that can be used as a return value from the block given to runTransactionBlock:
+ */
++ (FIRTransactionResult *) abort;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRTransactionResult.m b/Firebase/Database/Api/FIRTransactionResult.m
new file mode 100644
index 0000000..8afc5b7
--- /dev/null
+++ b/Firebase/Database/Api/FIRTransactionResult.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 "FIRTransactionResult.h"
+#import "FIRTransactionResult_Private.h"
+
+@implementation FIRTransactionResult
+
+@synthesize update;
+@synthesize isSuccess;
+
++ (FIRTransactionResult *)successWithValue:(FIRMutableData *)value {
+ FIRTransactionResult * result = [[FIRTransactionResult alloc] init];
+ result.isSuccess = YES;
+ result.update = value;
+ return result;
+}
+
++ (FIRTransactionResult *) abort {
+ FIRTransactionResult * result = [[FIRTransactionResult alloc] init];
+ result.isSuccess = NO;
+ result.update = nil;
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/Api/FirebaseDatabase.h b/Firebase/Database/Api/FirebaseDatabase.h
new file mode 100644
index 0000000..e52f5d6
--- /dev/null
+++ b/Firebase/Database/Api/FirebaseDatabase.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.
+ */
+
+#ifndef FirebaseDatabase_h
+#define FirebaseDatabase_h
+
+#import "FIRDatabase.h"
+#import "FIRDatabaseQuery.h"
+#import "FIRDatabaseReference.h"
+#import "FIRDataEventType.h"
+#import "FIRDataSnapshot.h"
+#import "FIRMutableData.h"
+#import "FIRServerValue.h"
+#import "FIRTransactionResult.h"
+
+#endif /* FirebaseDatabase_h */
diff --git a/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h b/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h
new file mode 100644
index 0000000..4ff285b
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "FIndexedNode.h"
+#import "FTypedefs_Private.h"
+
+@interface FIRDataSnapshot ()
+
+// in _Private for testing purposes
+@property (nonatomic, strong) FIndexedNode *node;
+
+- (id)initWithRef:(FIRDatabaseReference *)ref indexedNode:(FIndexedNode *)node;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h b/Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h
new file mode 100644
index 0000000..3a10fe3
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "FRepo.h"
+#import "FPath.h"
+#import "FRepoManager.h"
+#import "FTypedefs_Private.h"
+#import "FQueryParams.h"
+#import "FIRDatabaseQuery.h"
+
+@interface FIRDatabaseQuery ()
+
++ (dispatch_queue_t)sharedQueue;
+
+- (id) initWithRepo:(FRepo *)repo path:(FPath *)path;
+- (id) initWithRepo:(FRepo *)repo
+ path:(FPath *)path
+ params:(FQueryParams *)params
+ orderByCalled:(BOOL)orderByCalled
+priorityMethodCalled:(BOOL)priorityMethodCalled;
+
+@property (nonatomic, strong) FRepo* repo;
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) FQueryParams *queryParams;
+@property (nonatomic) BOOL orderByCalled;
+@property (nonatomic) BOOL priorityMethodCalled;
+
+- (FQuerySpec *)querySpec;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRDatabaseReference_Private.h b/Firebase/Database/Api/Private/FIRDatabaseReference_Private.h
new file mode 100644
index 0000000..cb28feb
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDatabaseReference_Private.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 "FIRDatabaseReference.h"
+#import "FTypedefs_Private.h"
+#import "FIRDatabaseConfig.h"
+#import "FRepo.h"
+
+@interface FIRDatabaseReference ()
+
+- (id)initWithConfig:(FIRDatabaseConfig *)config;
+- (id)initWithRepo:(FRepo *)repo path:(FPath *)path;
+
+// TODO: Update tests to not use this.
++ (FIRDatabaseConfig *)defaultConfig;
+@end
diff --git a/Firebase/Database/Api/Private/FIRDatabase_Private.h b/Firebase/Database/Api/Private/FIRDatabase_Private.h
new file mode 100644
index 0000000..5b7f8cc
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDatabase_Private.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 "FIRDatabase.h"
+
+@class FRepo;
+@class FRepoInfo;
+@class FIRDatabaseConfig;
+
+@interface FIRDatabase ()
+
++ (NSString *) buildVersion;
++ (FIRDatabase *) createDatabaseForTests:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRMutableData_Private.h b/Firebase/Database/Api/Private/FIRMutableData_Private.h
new file mode 100644
index 0000000..ee3aa96
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRMutableData_Private.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "FIRMutableData.h"
+#import "FNode.h"
+
+@interface FIRMutableData ()
+
+- (id) initWithNode:(id<FNode>)node;
+- (id<FNode>) nodeValue;
+- (BOOL) isEqualToData:(FIRMutableData *)other;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRTransactionResult_Private.h b/Firebase/Database/Api/Private/FIRTransactionResult_Private.h
new file mode 100644
index 0000000..82290f2
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRTransactionResult_Private.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 "FIRTransactionResult.h"
+#import "FIRMutableData.h"
+
+@interface FIRTransactionResult ()
+
+@property (nonatomic) BOOL isSuccess;
+@property (nonatomic, strong) FIRMutableData * update;
+
+@end
diff --git a/Firebase/Database/Api/Private/FTypedefs_Private.h b/Firebase/Database/Api/Private/FTypedefs_Private.h
new file mode 100644
index 0000000..73f4c9a
--- /dev/null
+++ b/Firebase/Database/Api/Private/FTypedefs_Private.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.
+ */
+
+#ifndef __FTYPEDEFS_PRIVATE__
+#define __FTYPEDEFS_PRIVATE__
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSInteger, FTransactionStatus) {
+ FTransactionInitializing, // 0
+ FTransactionRun, // 1
+ FTransactionSent, // 2
+ FTransactionCompleted, // 3
+ FTransactionSentNeedsAbort, // 4
+ FTransactionNeedsAbort // 5
+};
+
+@protocol FNode;
+@class FPath;
+@class FIRTransactionResult;
+@class FIRMutableData;
+@class FIRDataSnapshot;
+@class FCompoundHash;
+
+typedef void (^fbt_void_nserror_bool_datasnapshot) (NSError* error, BOOL committed, FIRDataSnapshot * snapshot);
+typedef FIRTransactionResult * (^fbt_transactionresult_mutabledata) (FIRMutableData * currentData);
+typedef void (^fbt_void_path_node) (FPath*, id<FNode>);
+typedef void (^fbt_void_nsstring) (NSString *);
+typedef BOOL (^fbt_bool_nsstring_node) (NSString *, id<FNode>);
+typedef void (^fbt_void_path_node_marray) (FPath *, id<FNode>, NSMutableArray *);
+typedef BOOL (^fbt_bool_void) (void);
+typedef void (^fbt_void_nsstring_nsstring)(NSString *str1, NSString* str2);
+typedef void (^fbt_void_nsstring_nserror)(NSString *str, NSError* error);
+typedef BOOL (^fbt_bool_path)(FPath *str);
+typedef void (^fbt_void_id)(id data);
+typedef NSString* (^fbt_nsstring_void) (void);
+typedef FCompoundHash* (^fbt_compoundhash_void) (void);
+typedef NSArray* (^fbt_nsarray_nsstring_id)(NSString *status, id Data);
+typedef NSArray* (^fbt_nsarray_nsstring)(NSString *status);
+
+// WWDC 2012 session 712 starting in page 83 for saving blocks in properties (use @property (strong) type name).
+
+#endif
diff --git a/Firebase/Database/Constants/FConstants.h b/Firebase/Database/Constants/FConstants.h
new file mode 100644
index 0000000..e97a8a1
--- /dev/null
+++ b/Firebase/Database/Constants/FConstants.h
@@ -0,0 +1,190 @@
+/*
+ * 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 Firebase_FConstants_h
+#define Firebase_FConstants_h
+
+#import <Foundation/Foundation.h>
+
+#pragma mark -
+#pragma mark Wire Protocol Envelope Constants
+
+FOUNDATION_EXPORT NSString *const kFWPRequestType;
+FOUNDATION_EXPORT NSString *const kFWPRequestTypeData;
+FOUNDATION_EXPORT NSString *const kFWPRequestDataPayload;
+FOUNDATION_EXPORT NSString *const kFWPRequestNumber;
+FOUNDATION_EXPORT NSString *const kFWPRequestPayloadBody;
+FOUNDATION_EXPORT NSString *const kFWPRequestError;
+FOUNDATION_EXPORT NSString *const kFWPRequestAction;
+FOUNDATION_EXPORT NSString *const kFWPResponseForRNData;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionStatus;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionStatusOk;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionStatusDataStale;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionData;
+FOUNDATION_EXPORT NSString *const kFWPResponseDataWarnings;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerAction;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerPayloadBody;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdate;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataMerge;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataRangeMerge;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerAuthRevoked;
+FOUNDATION_EXPORT NSString *const kFWPASyncServerListenCancelled;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerSecurityDebug;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateBodyPath; // {“a”: “d”, “b”: {“p”: “/”, “d”: “<data>”}}
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateBodyData;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateStartPath;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateEndPath;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateRangeMerge;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateBodyTag;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataQueries;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerEnvelopeType;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerEnvelopeData;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessage;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageType;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageData;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataMessage;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHello;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloTimestamp;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloVersion;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloConnectedHost;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloSession;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageShutdown;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageReset;
+
+#pragma mark -
+#pragma mark Wire Protocol Payload Constants
+
+FOUNDATION_EXPORT NSString *const kFWPRequestActionPut;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionMerge;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionTaggedListen;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionTaggedUnlisten;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionListen; // {"t": "d", "d": {"r": 1, "a": "l", "b": { "p": "/" } } }
+FOUNDATION_EXPORT NSString *const kFWPRequestActionUnlisten;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionStats;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionDisconnectPut;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionDisconnectMerge;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionDisconnectCancel;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionAuth;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionUnauth;
+FOUNDATION_EXPORT NSString *const kFWPRequestCredential;
+FOUNDATION_EXPORT NSString *const kFWPRequestPath;
+FOUNDATION_EXPORT NSString *const kFWPRequestCounters;
+FOUNDATION_EXPORT NSString *const kFWPRequestQueries;
+FOUNDATION_EXPORT NSString *const kFWPRequestTag;
+FOUNDATION_EXPORT NSString *const kFWPRequestData;
+FOUNDATION_EXPORT NSString *const kFWPRequestHash;
+FOUNDATION_EXPORT NSString *const kFWPRequestCompoundHash;
+FOUNDATION_EXPORT NSString *const kFWPRequestCompoundHashPaths;
+FOUNDATION_EXPORT NSString *const kFWPRequestCompoundHashHashes;
+FOUNDATION_EXPORT NSString *const kFWPRequestStatus;
+
+#pragma mark -
+#pragma mark Websock Transport Constants
+
+FOUNDATION_EXPORT NSString *const kWireProtocolVersionParam;
+FOUNDATION_EXPORT NSString *const kWebsocketProtocolVersion;
+FOUNDATION_EXPORT NSString *const kWebsocketServerKillPacket;
+FOUNDATION_EXPORT const int kWebsocketMaxFrameSize;
+FOUNDATION_EXPORT NSUInteger const kWebsocketKeepaliveInterval;
+FOUNDATION_EXPORT NSUInteger const kWebsocketConnectTimeout;
+
+FOUNDATION_EXPORT float const kPersistentConnReconnectMinDelay;
+FOUNDATION_EXPORT float const kPersistentConnReconnectMaxDelay;
+FOUNDATION_EXPORT float const kPersistentConnReconnectMultiplier;
+FOUNDATION_EXPORT float const kPersistentConnSuccessfulConnectionEstablishedDelay;
+
+#pragma mark -
+#pragma mark Query / QueryParams constants
+
+FOUNDATION_EXPORT NSString *const kQueryDefault;
+FOUNDATION_EXPORT NSString *const kQueryDefaultObject;
+FOUNDATION_EXPORT NSString *const kViewManagerDictConstView;
+FOUNDATION_EXPORT NSString *const kFQPIndexStartValue;
+FOUNDATION_EXPORT NSString *const kFQPIndexStartName;
+FOUNDATION_EXPORT NSString *const kFQPIndexEndValue;
+FOUNDATION_EXPORT NSString *const kFQPIndexEndName;
+FOUNDATION_EXPORT NSString *const kFQPLimit;
+FOUNDATION_EXPORT NSString *const kFQPViewFrom;
+FOUNDATION_EXPORT NSString *const kFQPViewFromLeft;
+FOUNDATION_EXPORT NSString *const kFQPViewFromRight;
+FOUNDATION_EXPORT NSString *const kFQPIndex;
+
+#pragma mark -
+#pragma mark Interrupt Reasons
+
+FOUNDATION_EXPORT NSString *const kFInterruptReasonServerKill;
+FOUNDATION_EXPORT NSString *const kFInterruptReasonWaitingForOpen;
+FOUNDATION_EXPORT NSString *const kFInterruptReasonRepoInterrupt;
+FOUNDATION_EXPORT NSString *const kFInterruptReasonAuthExpired;
+
+#pragma mark -
+#pragma mark Payload constants
+
+FOUNDATION_EXPORT NSString *const kPayloadPriority;
+FOUNDATION_EXPORT NSString *const kPayloadValue;
+FOUNDATION_EXPORT NSString *const kPayloadMetadataPrefix;
+
+#pragma mark -
+#pragma mark ServerValue constants
+
+FOUNDATION_EXPORT NSString *const kServerValueSubKey;
+FOUNDATION_EXPORT NSString *const kServerValuePriority;
+
+#pragma mark -
+#pragma mark .info/ constants
+
+FOUNDATION_EXPORT NSString *const kDotInfoPrefix;
+FOUNDATION_EXPORT NSString *const kDotInfoConnected;
+FOUNDATION_EXPORT NSString *const kDotInfoServerTimeOffset;
+
+#pragma mark -
+#pragma mark ObjectiveC to JavaScript type constants
+
+FOUNDATION_EXPORT NSString *const kJavaScriptObject;
+FOUNDATION_EXPORT NSString *const kJavaScriptString;
+FOUNDATION_EXPORT NSString *const kJavaScriptBoolean;
+FOUNDATION_EXPORT NSString *const kJavaScriptNumber;
+FOUNDATION_EXPORT NSString *const kJavaScriptNull;
+FOUNDATION_EXPORT NSString *const kJavaScriptTrue;
+FOUNDATION_EXPORT NSString *const kJavaScriptFalse;
+
+#pragma mark -
+#pragma mark Error handling constants
+
+FOUNDATION_EXPORT NSString *const kFErrorDomain;
+FOUNDATION_EXPORT NSUInteger const kFAuthError;
+FOUNDATION_EXPORT NSString *const kFErrorWriteCanceled;
+
+#pragma mark -
+#pragma mark Validation Constants
+
+FOUNDATION_EXPORT NSUInteger const kFirebaseMaxObjectDepth;
+FOUNDATION_EXPORT const unsigned int kFirebaseMaxLeafSize;
+
+#pragma mark -
+#pragma mark Transaction Constants
+
+FOUNDATION_EXPORT NSUInteger const kFTransactionMaxRetries;
+FOUNDATION_EXPORT NSString *const kFTransactionTooManyRetries;
+FOUNDATION_EXPORT NSString *const kFTransactionNoData;
+FOUNDATION_EXPORT NSString *const kFTransactionSet;
+FOUNDATION_EXPORT NSString *const kFTransactionDisconnect;
+
+#endif
diff --git a/Firebase/Database/Constants/FConstants.m b/Firebase/Database/Constants/FConstants.m
new file mode 100644
index 0000000..e492ba1
--- /dev/null
+++ b/Firebase/Database/Constants/FConstants.m
@@ -0,0 +1,183 @@
+/*
+ * 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 "FConstants.h"
+
+#pragma mark -
+#pragma mark Wire Protocol Envelope Constants
+
+NSString *const kFWPRequestType = @"t";
+NSString *const kFWPRequestTypeData = @"d";
+NSString *const kFWPRequestDataPayload = @"d";
+NSString *const kFWPRequestNumber = @"r";
+NSString *const kFWPRequestPayloadBody = @"b";
+NSString *const kFWPRequestError = @"error";
+NSString *const kFWPRequestAction = @"a";
+NSString *const kFWPResponseForRNData = @"b";
+NSString *const kFWPResponseForActionStatus = @"s";
+NSString *const kFWPResponseForActionStatusOk = @"ok";
+NSString *const kFWPResponseForActionStatusDataStale = @"datastale";
+NSString *const kFWPResponseForActionData = @"d";
+NSString *const kFWPResponseDataWarnings = @"w";
+NSString *const kFWPAsyncServerAction = @"a";
+NSString *const kFWPAsyncServerPayloadBody = @"b";
+NSString *const kFWPAsyncServerDataUpdate = @"d";
+NSString *const kFWPAsyncServerDataMerge = @"m";
+NSString *const kFWPAsyncServerDataRangeMerge = @"rm";
+NSString *const kFWPAsyncServerAuthRevoked = @"ac";
+NSString *const kFWPASyncServerListenCancelled = @"c";
+NSString *const kFWPAsyncServerSecurityDebug = @"sd";
+NSString *const kFWPAsyncServerDataUpdateBodyPath = @"p"; // {“a”: “d”, “b”: {“p”: “/”, “d”: “<data>”}}
+NSString *const kFWPAsyncServerDataUpdateBodyData = @"d";
+NSString *const kFWPAsyncServerDataUpdateStartPath = @"s";
+NSString *const kFWPAsyncServerDataUpdateEndPath = @"e";
+NSString *const kFWPAsyncServerDataUpdateRangeMerge = @"m";
+NSString *const kFWPAsyncServerDataUpdateBodyTag = @"t";
+NSString *const kFWPAsyncServerDataQueries = @"q";
+
+NSString *const kFWPAsyncServerEnvelopeType = @"t";
+NSString *const kFWPAsyncServerEnvelopeData = @"d";
+NSString *const kFWPAsyncServerControlMessage = @"c";
+NSString *const kFWPAsyncServerControlMessageType = @"t";
+NSString *const kFWPAsyncServerControlMessageData = @"d";
+NSString *const kFWPAsyncServerDataMessage = @"d";
+
+NSString *const kFWPAsyncServerHello = @"h";
+NSString *const kFWPAsyncServerHelloTimestamp = @"ts";
+NSString *const kFWPAsyncServerHelloVersion = @"v";
+NSString *const kFWPAsyncServerHelloConnectedHost = @"h";
+NSString *const kFWPAsyncServerHelloSession = @"s";
+
+NSString *const kFWPAsyncServerControlMessageShutdown = @"s";
+NSString *const kFWPAsyncServerControlMessageReset = @"r";
+
+#pragma mark -
+#pragma mark Wire Protocol Payload Constants
+
+NSString *const kFWPRequestActionPut = @"p";
+NSString *const kFWPRequestActionMerge = @"m";
+NSString *const kFWPRequestActionListen = @"l"; // {"t": "d", "d": {"r": 1, "a": "l", "b": { "p": "/" } } }
+NSString *const kFWPRequestActionUnlisten = @"u";
+NSString *const kFWPRequestActionStats = @"s";
+NSString *const kFWPRequestActionTaggedListen = @"q";
+NSString *const kFWPRequestActionTaggedUnlisten = @"n";
+NSString *const kFWPRequestActionDisconnectPut = @"o";
+NSString *const kFWPRequestActionDisconnectMerge = @"om";
+NSString *const kFWPRequestActionDisconnectCancel = @"oc";
+NSString *const kFWPRequestActionAuth = @"auth";
+NSString *const kFWPRequestActionUnauth = @"unauth";
+NSString *const kFWPRequestCredential = @"cred";
+NSString *const kFWPRequestPath = @"p";
+NSString *const kFWPRequestCounters = @"c";
+NSString *const kFWPRequestQueries = @"q";
+NSString *const kFWPRequestTag = @"t";
+NSString *const kFWPRequestData = @"d";
+NSString *const kFWPRequestHash = @"h";
+NSString *const kFWPRequestCompoundHash = @"ch";
+NSString *const kFWPRequestCompoundHashPaths = @"ps";
+NSString *const kFWPRequestCompoundHashHashes = @"hs";
+NSString *const kFWPRequestStatus = @"s";
+
+#pragma mark -
+#pragma mark Websock Transport Constants
+
+NSString *const kWireProtocolVersionParam = @"v";
+NSString *const kWebsocketProtocolVersion = @"5";
+NSString *const kWebsocketServerKillPacket = @"kill";
+const int kWebsocketMaxFrameSize = 16384;
+NSUInteger const kWebsocketKeepaliveInterval = 45;
+NSUInteger const kWebsocketConnectTimeout = 30;
+
+float const kPersistentConnReconnectMinDelay = 1.0;
+float const kPersistentConnReconnectMaxDelay = 30.0;
+float const kPersistentConnReconnectMultiplier = 1.3f;
+float const kPersistentConnSuccessfulConnectionEstablishedDelay = 30.0;
+
+#pragma mark -
+#pragma mark Query constants
+
+NSString *const kQueryDefault = @"default";
+NSString *const kQueryDefaultObject = @"{}";
+NSString *const kViewManagerDictConstView = @"view";
+NSString *const kFQPIndexStartValue = @"sp";
+NSString *const kFQPIndexStartName = @"sn";
+NSString *const kFQPIndexEndValue = @"ep";
+NSString *const kFQPIndexEndName = @"en";
+NSString *const kFQPLimit = @"l";
+NSString *const kFQPViewFrom = @"vf";
+NSString *const kFQPViewFromLeft = @"l";
+NSString *const kFQPViewFromRight = @"r";
+NSString *const kFQPIndex = @"i";
+
+#pragma mark -
+#pragma mark Interrupt Reasons
+
+NSString *const kFInterruptReasonServerKill = @"server_kill";
+NSString *const kFInterruptReasonWaitingForOpen = @"waiting_for_open";
+NSString *const kFInterruptReasonRepoInterrupt = @"repo_interrupt";
+
+#pragma mark -
+#pragma mark Payload constants
+
+NSString *const kPayloadPriority = @".priority";
+NSString *const kPayloadValue = @".value";
+NSString *const kPayloadMetadataPrefix = @".";
+
+#pragma mark -
+#pragma mark ServerValue constants
+
+NSString *const kServerValueSubKey = @".sv";
+NSString *const kServerValuePriority = @"timestamp";
+
+#pragma mark -
+#pragma mark .info/ constants
+
+NSString *const kDotInfoPrefix = @".info";
+NSString *const kDotInfoConnected = @"connected";
+NSString *const kDotInfoServerTimeOffset = @"serverTimeOffset";
+
+#pragma mark -
+#pragma mark ObjectiveC to JavaScript type constants
+
+NSString *const kJavaScriptObject = @"object";
+NSString *const kJavaScriptString = @"string";
+NSString *const kJavaScriptBoolean = @"boolean";
+NSString *const kJavaScriptNumber = @"number";
+NSString *const kJavaScriptNull = @"null";
+NSString *const kJavaScriptTrue = @"true";
+NSString *const kJavaScriptFalse = @"false";
+
+#pragma mark -
+#pragma mark Error handling constants
+
+NSString *const kFErrorDomain = @"com.firebase";
+NSUInteger const kFAuthError = 1;
+NSString *const kFErrorWriteCanceled = @"write_canceled";
+
+#pragma mark -
+#pragma mark Validation Constants
+
+NSUInteger const kFirebaseMaxObjectDepth = 1000;
+const unsigned int kFirebaseMaxLeafSize = 1024 * 1024 * 10; // 10 MB
+
+#pragma mark -
+#pragma mark Transaction Constants
+
+NSUInteger const kFTransactionMaxRetries = 25;
+NSString *const kFTransactionTooManyRetries = @"maxretry";
+NSString *const kFTransactionNoData = @"nodata";
+NSString *const kFTransactionSet = @"set";
+NSString *const kFTransactionDisconnect = @"disconnect";
diff --git a/Firebase/Database/Core/FCompoundHash.h b/Firebase/Database/Core/FCompoundHash.h
new file mode 100644
index 0000000..cd5240e
--- /dev/null
+++ b/Firebase/Database/Core/FCompoundHash.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FNode.h"
+
+
+@interface FCompoundHashBuilder : NSObject
+
+- (FPath *)currentPath;
+
+@end
+
+
+typedef BOOL (^FCompoundHashSplitStrategy) (FCompoundHashBuilder *builder);
+
+
+@interface FCompoundHash : NSObject
+
+@property (nonatomic, strong, readonly) NSArray *posts;
+@property (nonatomic, strong, readonly) NSArray *hashes;
+
++ (FCompoundHash *)fromNode:(id<FNode>)node;
++ (FCompoundHash *)fromNode:(id<FNode>)node splitStrategy:(FCompoundHashSplitStrategy)strategy;
+
+@end
diff --git a/Firebase/Database/Core/FCompoundHash.m b/Firebase/Database/Core/FCompoundHash.m
new file mode 100644
index 0000000..b4f72cd
--- /dev/null
+++ b/Firebase/Database/Core/FCompoundHash.m
@@ -0,0 +1,236 @@
+/*
+ * 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 "FCompoundHash.h"
+#import "FLeafNode.h"
+#import "FStringUtilities.h"
+#import "FSnapshotUtilities.h"
+#import "FChildrenNode.h"
+
+@interface FCompoundHashBuilder ()
+
+@property (nonatomic, strong) FCompoundHashSplitStrategy splitStrategy;
+
+@property (nonatomic, strong) NSMutableArray *currentPaths;
+@property (nonatomic, strong) NSMutableArray *currentHashes;
+
+@end
+
+@implementation FCompoundHashBuilder {
+
+ // NOTE: We use the existence of this to know if we've started building a range (i.e. encountered a leaf node).
+ NSMutableString *optHashValueBuilder;
+
+ // The current path as a stack. This is used in combination with currentPathDepth to simultaneously store the
+ // last leaf node path. The depth is changed when descending and ascending, at the same time the current key
+ // is set for the current depth. Because the keys are left unchanged for ascending the path will also contain
+ // the path of the last visited leaf node (using lastLeafDepth elements)
+ NSMutableArray *currentPath;
+ NSInteger lastLeafDepth;
+ NSInteger currentPathDepth;
+
+ BOOL needsComma;
+}
+
+- (instancetype)initWithSplitStrategy:(FCompoundHashSplitStrategy)strategy {
+ self = [super init];
+ if (self != nil) {
+ self->_splitStrategy = strategy;
+ self->optHashValueBuilder = nil;
+ self->currentPath = [NSMutableArray array];
+ self->lastLeafDepth = -1;
+ self->currentPathDepth = 0;
+ self->needsComma = YES;
+ self->_currentPaths = [NSMutableArray array];
+ self->_currentHashes = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (BOOL)isBuildingRange {
+ return self->optHashValueBuilder != nil;
+}
+
+- (NSUInteger)currentHashLength {
+ return self->optHashValueBuilder.length;
+}
+
+- (FPath *)currentPath {
+ return [self currentPathWithDepth:self->currentPathDepth];
+}
+
+- (FPath *)currentPathWithDepth:(NSInteger)depth {
+ NSArray *pieces = [self->currentPath subarrayWithRange:NSMakeRange(0, depth)];
+ return [[FPath alloc] initWithPieces:pieces andPieceNum:0];
+}
+
+- (void)enumerateCurrentPathToDepth:(NSInteger)depth withBlock:(void (^) (NSString *key))block {
+ for (NSInteger i = 0; i < depth; i++) {
+ block(self->currentPath[i]);
+ }
+}
+
+- (void)appendKey:(NSString *)key toString:(NSMutableString *)string {
+ [FSnapshotUtilities appendHashV2RepresentationForString:key toString:string];
+}
+
+- (void)ensureRange {
+ if (![self isBuildingRange]) {
+ optHashValueBuilder = [NSMutableString string];
+ [optHashValueBuilder appendString:@"("];
+ [self enumerateCurrentPathToDepth:self->currentPathDepth withBlock:^(NSString *key) {
+ [self appendKey:key toString:self->optHashValueBuilder];
+ [self->optHashValueBuilder appendString:@":("];
+ }];
+ self->needsComma = NO;
+ }
+}
+
+- (void)processLeaf:(FLeafNode *)leafNode {
+ [self ensureRange];
+
+ self->lastLeafDepth = self->currentPathDepth;
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:leafNode
+ toString:self->optHashValueBuilder
+ hashVersion:FDataHashVersionV2];
+ self->needsComma = YES;
+ if (self.splitStrategy(self)) {
+ [self endRange];
+ }
+}
+
+- (void)startChild:(NSString *)key {
+ [self ensureRange];
+
+ if (self->needsComma) {
+ [self->optHashValueBuilder appendString:@","];
+ }
+ [self appendKey:key toString:self->optHashValueBuilder];
+ [self->optHashValueBuilder appendString:@":("];
+ if (self->currentPathDepth == currentPath.count) {
+ [self->currentPath addObject:key];
+ } else {
+ self->currentPath[self->currentPathDepth] = key;
+ }
+ self->currentPathDepth++;
+ self->needsComma = NO;
+}
+
+- (void)endChild {
+ self->currentPathDepth--;
+ if ([self isBuildingRange]) {
+ [self->optHashValueBuilder appendString:@")"];
+ }
+ self->needsComma = YES;
+}
+
+- (void)finishHashing {
+ NSAssert(self->currentPathDepth == 0, @"Can't finish hashing in the middle of processing a child");
+ if ([self isBuildingRange] ) {
+ [self endRange];
+ }
+
+ // Always close with the empty hash for the remaining range to allow simple appending
+ [self.currentHashes addObject:@""];
+}
+
+- (void)endRange {
+ NSAssert([self isBuildingRange], @"Can't end range without starting a range!");
+ // Add closing parenthesis for current depth
+ for (NSUInteger i = 0; i < currentPathDepth; i++) {
+ [self->optHashValueBuilder appendString:@")"];
+ }
+ [self->optHashValueBuilder appendString:@")"];
+
+ FPath *lastLeafPath = [self currentPathWithDepth:self->lastLeafDepth];
+ NSString *hash = [FStringUtilities base64EncodedSha1:self->optHashValueBuilder];
+ [self.currentHashes addObject:hash];
+ [self.currentPaths addObject:lastLeafPath];
+
+ self->optHashValueBuilder = nil;
+}
+
+@end
+
+
+@interface FCompoundHash ()
+
+@property (nonatomic, strong, readwrite) NSArray *posts;
+@property (nonatomic, strong, readwrite) NSArray *hashes;
+
+@end
+
+@implementation FCompoundHash
+
+- (id)initWithPosts:(NSArray *)posts hashes:(NSArray *)hashes {
+ self = [super init];
+ if (self != nil) {
+ if (posts.count != hashes.count - 1) {
+ [NSException raise:NSInvalidArgumentException format:@"Number of posts need to be n-1 for n hashes in FCompoundHash"];
+ }
+ self.posts = posts;
+ self.hashes = hashes;
+ }
+ return self;
+}
+
++ (FCompoundHashSplitStrategy)simpleSizeSplitStrategyForNode:(id<FNode>)node {
+ NSUInteger estimatedSize = [FSnapshotUtilities estimateSerializedNodeSize:node];
+
+ // Splits for
+ // 1k -> 512 (2 parts)
+ // 5k -> 715 (7 parts)
+ // 100k -> 3.2k (32 parts)
+ // 500k -> 7k (71 parts)
+ // 5M -> 23k (228 parts)
+ NSUInteger splitThreshold = MAX(512, (NSUInteger)sqrt(estimatedSize * 100));
+
+ return ^BOOL(FCompoundHashBuilder *builder) {
+ // Never split on priorities
+ return [builder currentHashLength] > splitThreshold && ![[[builder currentPath] getBack] isEqualToString:@".priority"];
+ };
+}
+
++ (FCompoundHash *)fromNode:(id<FNode>)node {
+ return [FCompoundHash fromNode:node splitStrategy:[FCompoundHash simpleSizeSplitStrategyForNode:node]];
+}
+
++ (FCompoundHash *)fromNode:(id<FNode>)node splitStrategy:(FCompoundHashSplitStrategy)strategy {
+ if ([node isEmpty]) {
+ return [[FCompoundHash alloc] initWithPosts:@[] hashes:@[@""]];
+ } else {
+ FCompoundHashBuilder *builder = [[FCompoundHashBuilder alloc] initWithSplitStrategy:strategy];
+ [FCompoundHash processNode:node builder:builder];
+ [builder finishHashing];
+ return [[FCompoundHash alloc] initWithPosts:builder.currentPaths hashes:builder.currentHashes];
+ }
+}
+
++ (void)processNode:(id<FNode>)node builder:(FCompoundHashBuilder *)builder {
+ if ([node isLeafNode]) {
+ [builder processLeaf:node];
+ } else {
+ NSAssert(![node isEmpty], @"Can't calculate hash on empty node!");
+ FChildrenNode *childrenNode = (FChildrenNode *)node;
+ [childrenNode enumerateChildrenAndPriorityUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [builder startChild:key];
+ [self processNode:node builder:builder];
+ [builder endChild];
+ }];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Core/FListenProvider.h b/Firebase/Database/Core/FListenProvider.h
new file mode 100644
index 0000000..7a41754
--- /dev/null
+++ b/Firebase/Database/Core/FListenProvider.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 "FTypedefs_Private.h"
+
+@class FQuerySpec;
+@protocol FSyncTreeHash;
+
+typedef NSArray* (^fbt_startListeningBlock)(FQuerySpec *query,
+ NSNumber *tagId,
+ id<FSyncTreeHash> hash,
+ fbt_nsarray_nsstring onComplete);
+typedef void (^fbt_stopListeningBlock)(FQuerySpec *query, NSNumber *tagId);
+
+@interface FListenProvider : NSObject
+
+@property (nonatomic, copy) fbt_startListeningBlock startListening;
+@property (nonatomic, copy) fbt_stopListeningBlock stopListening;
+
+@end
diff --git a/Firebase/Database/Core/FListenProvider.m b/Firebase/Database/Core/FListenProvider.m
new file mode 100644
index 0000000..7a49609
--- /dev/null
+++ b/Firebase/Database/Core/FListenProvider.m
@@ -0,0 +1,26 @@
+/*
+ * 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 "FListenProvider.h"
+#import "FIRDatabaseQuery.h"
+
+
+@implementation FListenProvider
+
+@synthesize startListening;
+@synthesize stopListening;
+
+@end
diff --git a/Firebase/Database/Core/FPersistentConnection.h b/Firebase/Database/Core/FPersistentConnection.h
new file mode 100644
index 0000000..412c874
--- /dev/null
+++ b/Firebase/Database/Core/FPersistentConnection.h
@@ -0,0 +1,78 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FConnection.h"
+#import "FRepoInfo.h"
+#import "FTypedefs.h"
+#import "FTypedefs_Private.h"
+
+@protocol FPersistentConnectionDelegate;
+@protocol FSyncTreeHash;
+@class FQuerySpec;
+@class FIRDatabaseConfig;
+
+@interface FPersistentConnection : NSObject <FConnectionDelegate>
+
+@property (nonatomic, weak) id <FPersistentConnectionDelegate> delegate;
+@property (nonatomic) BOOL pauseWrites;
+
+- (id)initWithRepoInfo:(FRepoInfo *)repoInfo
+ dispatchQueue:(dispatch_queue_t)queue
+ config:(FIRDatabaseConfig *)config;
+
+- (void)open;
+
+- (void) putData:(id)data forPath:(NSString *)pathString withHash:(NSString *)hash withCallback:(fbt_void_nsstring_nsstring)onComplete;
+- (void) mergeData:(id)data forPath:(NSString *)pathString withCallback:(fbt_void_nsstring_nsstring)onComplete;
+
+- (void) listen:(FQuerySpec *)query
+ tagId:(NSNumber *)tagId
+ hash:(id<FSyncTreeHash>)hash
+ onComplete:(fbt_void_nsstring)onComplete;
+
+- (void) unlisten:(FQuerySpec *)query tagId:(NSNumber *)tagId;
+- (void) refreshAuthToken:(NSString *)token;
+- (void) onDisconnectPutData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback;
+- (void) onDisconnectMergeData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback;
+- (void) onDisconnectCancelPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback;
+- (void) ackPuts;
+- (void) purgeOutstandingWrites;
+
+- (void) interruptForReason:(NSString *)reason;
+- (void) resumeForReason:(NSString *)reason;
+- (BOOL) isInterruptedForReason:(NSString *)reason;
+
+// FConnection delegate methods
+- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID;
+- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FConnection *)fconnection withReason:(FDisconnectReason)reason;
+- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason;
+
+// Testing methods
+- (NSDictionary *) dumpListens;
+
+@end
+
+@protocol FPersistentConnectionDelegate <NSObject>
+
+- (void)onDataUpdate:(FPersistentConnection *)fpconnection forPath:(NSString *)pathString message:(id)message isMerge:(BOOL)isMerge tagId:(NSNumber *)tagId;
+- (void)onRangeMerge:(NSArray *)ranges forPath:(NSString *)path tagId:(NSNumber *)tag;
+- (void)onConnect:(FPersistentConnection *)fpconnection;
+- (void)onDisconnect:(FPersistentConnection *)fpconnection;
+- (void)onServerInfoUpdate:(FPersistentConnection *)fpconnection updates:(NSDictionary *)updates;
+
+@end
diff --git a/Firebase/Database/Core/FPersistentConnection.m b/Firebase/Database/Core/FPersistentConnection.m
new file mode 100644
index 0000000..0eb1f9f
--- /dev/null
+++ b/Firebase/Database/Core/FPersistentConnection.m
@@ -0,0 +1,945 @@
+/*
+ * 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 <SystemConfiguration/SystemConfiguration.h>
+#import <netinet/in.h>
+#import <dlfcn.h>
+#import "FIRDatabaseReference.h"
+#import "FPersistentConnection.h"
+#import "FConstants.h"
+#import "FAtomicNumber.h"
+#import "FQueryParams.h"
+#import "FTupleOnDisconnect.h"
+#import "FTupleCallbackStatus.h"
+#import "FQuerySpec.h"
+#import "FIndex.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FSnapshotUtilities.h"
+#import "FRangeMerge.h"
+#import "FCompoundHash.h"
+#import "FSyncTree.h"
+#import "FIRRetryHelper.h"
+#import "FAuthTokenProvider.h"
+#import "FUtilities.h"
+
+@interface FOutstandingQuery : NSObject
+
+@property (nonatomic, strong) FQuerySpec* query;
+@property (nonatomic, strong) NSNumber *tagId;
+@property (nonatomic, strong) id<FSyncTreeHash> syncTreeHash;
+@property (nonatomic, copy) fbt_void_nsstring onComplete;
+
+@end
+
+@implementation FOutstandingQuery
+
+@end
+
+
+@interface FOutstandingPut : NSObject
+
+@property (nonatomic, strong) NSString *action;
+@property (nonatomic, strong) NSDictionary *request;
+@property (nonatomic, copy) fbt_void_nsstring_nsstring onCompleteBlock;
+@property (nonatomic) BOOL sent;
+
+@end
+
+@implementation FOutstandingPut
+
+@end
+
+
+typedef enum {
+ ConnectionStateDisconnected,
+ ConnectionStateGettingToken,
+ ConnectionStateConnecting,
+ ConnectionStateAuthenticating,
+ ConnectionStateConnected
+} ConnectionState;
+
+@interface FPersistentConnection () {
+ ConnectionState connectionState;
+ BOOL firstConnection;
+ NSTimeInterval reconnectDelay;
+ NSTimeInterval lastConnectionAttemptTime;
+ NSTimeInterval lastConnectionEstablishedTime;
+ SCNetworkReachabilityRef reachability;
+}
+
+- (int) getNextRequestNumber;
+- (void) onDataPushWithAction:(NSString *)action andBody:(NSDictionary *)body;
+- (void) handleTimestamp:(NSNumber *)timestamp;
+- (void) sendOnDisconnectAction:(NSString *)action forPath:(NSString *)pathString withData:(id)data andCallback:(fbt_void_nsstring_nsstring)callback;
+
+@property (nonatomic, strong) FConnection* realtime;
+@property (nonatomic, strong) NSMutableDictionary* listens;
+@property (nonatomic, strong) NSMutableDictionary* outstandingPuts;
+@property (nonatomic, strong) NSMutableArray* onDisconnectQueue;
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+@property (nonatomic, strong) FAtomicNumber* putCounter;
+@property (nonatomic, strong) FAtomicNumber* requestNumber;
+@property (nonatomic, strong) NSMutableDictionary* requestCBHash;
+@property (nonatomic, strong) FIRDatabaseConfig *config;
+@property (nonatomic) NSUInteger unackedListensCount;
+@property (nonatomic, strong) NSMutableArray *putsToAck;
+@property (nonatomic, strong) dispatch_queue_t dispatchQueue;
+@property (nonatomic, strong) NSString* lastSessionID;
+@property (nonatomic, strong) NSMutableSet *interruptReasons;
+@property (nonatomic, strong) FIRRetryHelper *retryHelper;
+@property (nonatomic, strong) id<FAuthTokenProvider> authTokenProvider;
+@property (nonatomic, strong) NSString *authToken;
+@property (nonatomic) BOOL forceAuthTokenRefresh;
+@property (nonatomic) NSUInteger currentFetchTokenAttempt;
+
+@end
+
+
+@implementation FPersistentConnection
+
+- (id)initWithRepoInfo:(FRepoInfo *)repoInfo dispatchQueue:(dispatch_queue_t)dispatchQueue config:(FIRDatabaseConfig *)config {
+ self = [super init];
+ if (self) {
+ self->_config = config;
+ self->_repoInfo = repoInfo;
+ self->_dispatchQueue = dispatchQueue;
+ self->_authTokenProvider = config.authTokenProvider;
+ NSAssert(self->_authTokenProvider != nil, @"Expected auth token provider");
+ self.interruptReasons = [NSMutableSet set];
+
+ self.listens = [[NSMutableDictionary alloc] init];
+ self.outstandingPuts = [[NSMutableDictionary alloc] init];
+ self.onDisconnectQueue = [[NSMutableArray alloc] init];
+ self.putCounter = [[FAtomicNumber alloc] init];
+ self.requestNumber = [[FAtomicNumber alloc] init];
+ self.requestCBHash = [[NSMutableDictionary alloc] init];
+ self.unackedListensCount = 0;
+ self.putsToAck = [NSMutableArray array];
+ connectionState = ConnectionStateDisconnected;
+ firstConnection = YES;
+ reconnectDelay = kPersistentConnReconnectMinDelay;
+
+ self->_retryHelper = [[FIRRetryHelper alloc] initWithDispatchQueue:dispatchQueue
+ minRetryDelayAfterFailure:kPersistentConnReconnectMinDelay
+ maxRetryDelay:kPersistentConnReconnectMaxDelay
+ retryExponent:kPersistentConnReconnectMultiplier
+ jitterFactor:0.7];
+
+ [self setupNotifications];
+ // Make sure we don't actually connect until open is called
+ [self interruptForReason:kFInterruptReasonWaitingForOpen];
+ }
+ // nb: The reason establishConnection isn't called here like the JS version is because
+ // callers need to set the delegate first. The ctor can be modified to accept the delegate
+ // but that deviates from normal ios conventions. After the delegate has been set, the caller
+ // is responsible for calling establishConnection:
+ return self;
+}
+
+- (void) dealloc {
+ if (reachability) {
+ // Unschedule the notifications
+ SCNetworkReachabilitySetDispatchQueue(reachability, NULL);
+ CFRelease(reachability);
+ }
+}
+
+#pragma mark -
+#pragma mark Public methods
+
+- (void) open {
+ [self resumeForReason:kFInterruptReasonWaitingForOpen];
+}
+
+/**
+* Note that the listens dictionary has a type of Map[String (pathString), Map[FQueryParams, FOutstandingQuery]]
+*
+* This means, for each path we care about, there are sets of queryParams that correspond to an FOutstandingQuery object.
+* There can be multiple sets at a path since we overlap listens for a short time while adding or removing a query from a
+* location in the tree.
+*/
+- (void) listen:(FQuerySpec *)query
+ tagId:(NSNumber *)tagId
+ hash:(id<FSyncTreeHash>)hash
+ onComplete:(fbt_void_nsstring)onComplete {
+ FFLog(@"I-RDB034001", @"Listen called for %@", query);
+
+ NSAssert(self.listens[query] == nil, @"listen() called twice for the same query");
+ NSAssert(query.isDefault || !query.loadsAllData, @"listen called for non-default but complete query");
+ FOutstandingQuery* outstanding = [[FOutstandingQuery alloc] init];
+ outstanding.query = query;
+ outstanding.tagId = tagId;
+ outstanding.syncTreeHash = hash;
+ outstanding.onComplete = onComplete;
+ [self.listens setObject:outstanding forKey:query];
+ if ([self connected]) {
+ [self sendListen:outstanding];
+ }
+}
+
+- (void) putData:(id)data forPath:(NSString *)pathString withHash:(NSString *)hash withCallback:(fbt_void_nsstring_nsstring)onComplete {
+ [self putInternal:data forAction:kFWPRequestActionPut forPath:pathString withHash:hash withCallback:onComplete];
+}
+
+- (void) mergeData:(id)data forPath:(NSString *)pathString withCallback:(fbt_void_nsstring_nsstring)onComplete {
+ [self putInternal:data forAction:kFWPRequestActionMerge forPath:pathString withHash:nil withCallback:onComplete];
+}
+
+- (void) onDisconnectPutData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback {
+ if ([self canSendWrites]) {
+ [self sendOnDisconnectAction:kFWPRequestActionDisconnectPut forPath:[path description] withData:data andCallback:callback];
+ } else {
+ FTupleOnDisconnect* tuple = [[FTupleOnDisconnect alloc] init];
+ tuple.pathString = [path description];
+ tuple.action = kFWPRequestActionDisconnectPut;
+ tuple.data = data;
+ tuple.onComplete = callback;
+ [self.onDisconnectQueue addObject:tuple];
+ }
+}
+
+- (void) onDisconnectMergeData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback {
+ if ([self canSendWrites]) {
+ [self sendOnDisconnectAction:kFWPRequestActionDisconnectMerge forPath:[path description] withData:data andCallback:callback];
+ } else {
+ FTupleOnDisconnect* tuple = [[FTupleOnDisconnect alloc] init];
+ tuple.pathString = [path description];
+ tuple.action = kFWPRequestActionDisconnectMerge;
+ tuple.data = data;
+ tuple.onComplete = callback;
+ [self.onDisconnectQueue addObject:tuple];
+ }
+}
+
+- (void) onDisconnectCancelPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback {
+ if ([self canSendWrites]) {
+ [self sendOnDisconnectAction:kFWPRequestActionDisconnectCancel forPath:[path description] withData:[NSNull null] andCallback:callback];
+ } else {
+ FTupleOnDisconnect* tuple = [[FTupleOnDisconnect alloc] init];
+ tuple.pathString = [path description];
+ tuple.action = kFWPRequestActionDisconnectCancel;
+ tuple.data = [NSNull null];
+ tuple.onComplete = callback;
+ [self.onDisconnectQueue addObject:tuple];
+ }
+}
+
+- (void) unlisten:(FQuerySpec *)query tagId:(NSNumber *)tagId {
+ FPath *path = query.path;
+ FFLog(@"I-RDB034002", @"Unlistening for %@", query);
+
+ NSArray *outstanding = [self removeListen:query];
+ if (outstanding.count > 0 && [self connected]) {
+ [self sendUnlisten:path queryParams:query.params tagId:tagId];
+ }
+}
+
+- (void) refreshAuthToken:(NSString *)token {
+ self.authToken = token;
+ if ([self connected]) {
+ if (token != nil) {
+ [self sendAuthAndRestoreStateAfterComplete:NO];
+ } else {
+ [self sendUnauth];
+ }
+ }
+}
+
+#pragma mark -
+#pragma mark Connection status
+
+- (BOOL)connected {
+ return self->connectionState == ConnectionStateAuthenticating || self->connectionState == ConnectionStateConnected;
+}
+
+- (BOOL)canSendWrites {
+ return self->connectionState == ConnectionStateConnected;
+}
+
+#pragma mark -
+#pragma mark FConnection delegate methods
+
+- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID {
+ FFLog(@"I-RDB034003", @"On ready");
+ lastConnectionEstablishedTime = [[NSDate date] timeIntervalSince1970];
+ [self handleTimestamp:timestamp];
+
+ if (firstConnection) {
+ [self sendConnectStats];
+ }
+
+ [self restoreAuth];
+ firstConnection = NO;
+ self.lastSessionID = sessionID;
+ dispatch_async(self.dispatchQueue, ^{
+ [self.delegate onConnect:self];
+ });
+}
+
+- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message {
+ if (message[kFWPRequestNumber] != nil) {
+ // this is a response to a request we sent
+ NSNumber* rn = [NSNumber numberWithInt:[[message objectForKey:kFWPRequestNumber] intValue]];
+ if ([self.requestCBHash objectForKey:rn]) {
+ void (^callback)(NSDictionary*) = [self.requestCBHash objectForKey:rn];
+ [self.requestCBHash removeObjectForKey:rn];
+
+ if (callback) {
+ //dispatch_async(self.dispatchQueue, ^{
+ callback([message objectForKey:kFWPResponseForRNData]);
+ //});
+ }
+ }
+ } else if (message[kFWPRequestError] != nil) {
+ NSString* error = [message objectForKey:kFWPRequestError];
+ @throw [[NSException alloc] initWithName:@"FirebaseDatabaseServerError" reason:error userInfo:nil];
+ } else if (message[kFWPAsyncServerAction] != nil) {
+ // this is a server push of some sort
+ NSString* action = [message objectForKey:kFWPAsyncServerAction];
+ NSDictionary* body = [message objectForKey:kFWPAsyncServerPayloadBody];
+ [self onDataPushWithAction:action andBody:body];
+ }
+}
+
+- (void)onDisconnect:(FConnection *)fconnection withReason:(FDisconnectReason)reason {
+ FFLog(@"I-RDB034004", @"Got on disconnect due to %s", (reason == DISCONNECT_REASON_SERVER_RESET) ? "server_reset" : "other");
+ connectionState = ConnectionStateDisconnected;
+ // Drop the realtime connection
+ self.realtime = nil;
+ [self cancelSentTransactions];
+ [self.requestCBHash removeAllObjects];
+ self.unackedListensCount = 0;
+ if ([self shouldReconnect]) {
+ NSTimeInterval timeSinceLastConnectSucceeded = [[NSDate date] timeIntervalSince1970] - lastConnectionEstablishedTime;
+ BOOL lastConnectionWasSuccessful;
+ if (lastConnectionEstablishedTime > 0) {
+ lastConnectionWasSuccessful = timeSinceLastConnectSucceeded > kPersistentConnSuccessfulConnectionEstablishedDelay;
+ } else {
+ lastConnectionWasSuccessful = NO;
+ }
+
+ if (reason == DISCONNECT_REASON_SERVER_RESET || lastConnectionWasSuccessful) {
+ [self.retryHelper signalSuccess];
+ }
+ [self tryScheduleReconnect];
+ }
+ lastConnectionEstablishedTime = 0;
+ [self.delegate onDisconnect:self];
+}
+
+- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason {
+ FFWarn(@"I-RDB034005", @"Firebase Database connection was forcefully killed by the server. Will not attempt reconnect. Reason: %@", reason);
+ [self interruptForReason:kFInterruptReasonServerKill];
+}
+
+#pragma mark -
+#pragma mark Connection handling methods
+
+- (void) interruptForReason:(NSString *)reason {
+ FFLog(@"I-RDB034006", @"Connection interrupted for: %@", reason);
+
+ [self.interruptReasons addObject:reason];
+ if (self.realtime) {
+ // Will call onDisconnect and set the connection state to Disconnected
+ [self.realtime close];
+ self.realtime = nil;
+ } else {
+ [self.retryHelper cancel];
+ self->connectionState = ConnectionStateDisconnected;
+ }
+ // Reset timeouts
+ [self.retryHelper signalSuccess];
+}
+
+- (void) resumeForReason:(NSString *)reason {
+ FFLog(@"I-RDB034007", @"Connection no longer interrupted for: %@", reason);
+ [self.interruptReasons removeObject:reason];
+
+ if ([self shouldReconnect] && connectionState == ConnectionStateDisconnected) {
+ [self tryScheduleReconnect];
+ }
+}
+
+- (BOOL) shouldReconnect {
+ return self.interruptReasons.count == 0;
+}
+
+- (BOOL) isInterruptedForReason:(NSString *)reason {
+ return [self.interruptReasons containsObject:reason];
+}
+
+#pragma mark -
+#pragma mark Private methods
+
+- (void) tryScheduleReconnect {
+ if ([self shouldReconnect]) {
+ NSAssert(self->connectionState == ConnectionStateDisconnected,
+ @"Not in disconnected state: %d", self->connectionState);
+ BOOL forceRefresh = self.forceAuthTokenRefresh;
+ self.forceAuthTokenRefresh = NO;
+ FFLog(@"I-RDB034008", @"Scheduling connection attempt");
+ [self.retryHelper retry:^{
+ FFLog(@"I-RDB034009", @"Trying to fetch auth token");
+ NSAssert(self->connectionState == ConnectionStateDisconnected,
+ @"Not in disconnected state: %d", self->connectionState);
+ self->connectionState = ConnectionStateGettingToken;
+ self.currentFetchTokenAttempt++;
+ NSUInteger thisFetchTokenAttempt = self.currentFetchTokenAttempt;
+ [self.authTokenProvider fetchTokenForcingRefresh:forceRefresh withCallback:^(NSString *token, NSError *error) {
+ if (thisFetchTokenAttempt == self.currentFetchTokenAttempt) {
+ if (error != nil) {
+ self->connectionState = ConnectionStateDisconnected;
+ FFLog(@"I-RDB034010", @"Error fetching token: %@", error);
+ [self tryScheduleReconnect];
+ } else {
+ // Someone could have interrupted us while fetching the token,
+ // marking the connection as Disconnected
+ if (self->connectionState == ConnectionStateGettingToken) {
+ FFLog(@"I-RDB034011", @"Successfully fetched token, opening connection");
+ [self openNetworkConnectionWithToken:token];
+ } else {
+ NSAssert(self->connectionState == ConnectionStateDisconnected,
+ @"Expected connection state disconnected, but got %d", self->connectionState);
+ FFLog(@"I-RDB034012", @"Not opening connection after token refresh, because connection was set to disconnected.");
+ }
+ }
+ } else {
+ FFLog(@"I-RDB034013", @"Ignoring fetch token result, because this was not the latest attempt.");
+ }
+ }];
+ }];
+
+ }
+}
+
+- (void) openNetworkConnectionWithToken:(NSString *)token {
+ NSAssert(self->connectionState == ConnectionStateGettingToken,
+ @"Trying to open network connection while in wrong state: %d", self->connectionState);
+ self.authToken = token;
+ self->connectionState = ConnectionStateConnecting;
+ self.realtime = [[FConnection alloc] initWith:self.repoInfo
+ andDispatchQueue:self.dispatchQueue
+ lastSessionID:self.lastSessionID];
+ self.realtime.delegate = self;
+ [self.realtime open];
+}
+
+static void reachabilityCallback(SCNetworkReachabilityRef ref, SCNetworkReachabilityFlags flags, void* info) {
+ if (flags & kSCNetworkReachabilityFlagsReachable) {
+ FFLog(@"I-RDB034014", @"Network became reachable. Trigger a connection attempt");
+ FPersistentConnection* self = (__bridge FPersistentConnection *)info;
+ // Reset reconnect delay
+ [self.retryHelper signalSuccess];
+ if (self->connectionState == ConnectionStateDisconnected) {
+ [self tryScheduleReconnect];
+ }
+ } else {
+ FFLog(@"I-RDB034015", @"Network is not reachable");
+ }
+}
+
+- (void) enteringForeground {
+ dispatch_async(self.dispatchQueue, ^{
+ // Reset reconnect delay
+ [self.retryHelper signalSuccess];
+ if (self->connectionState == ConnectionStateDisconnected) {
+ [self tryScheduleReconnect];
+ }
+ });
+}
+
+- (void) setupNotifications {
+
+ NSString * const* foregroundConstant = (NSString * const *) dlsym(RTLD_DEFAULT, "UIApplicationWillEnterForegroundNotification");
+ if (foregroundConstant) {
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(enteringForeground)
+ name:*foregroundConstant
+ object:nil];
+ }
+ // An empty address is interpreted a generic internet access
+ struct sockaddr_in zeroAddress;
+ bzero(&zeroAddress, sizeof(zeroAddress));
+ zeroAddress.sin_len = sizeof(zeroAddress);
+ zeroAddress.sin_family = AF_INET;
+ reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *)&zeroAddress);
+ SCNetworkReachabilityContext ctx = {0, (__bridge void *)(self), NULL, NULL, NULL};
+ if (SCNetworkReachabilitySetCallback(reachability, reachabilityCallback, &ctx)) {
+ SCNetworkReachabilitySetDispatchQueue(reachability, self.dispatchQueue);
+ } else {
+ FFLog(@"I-RDB034016", @"Failed to set up network reachability monitoring");
+ CFRelease(reachability);
+ reachability = NULL;
+ }
+}
+
+- (void) sendAuthAndRestoreStateAfterComplete:(BOOL)restoreStateAfterComplete {
+ NSAssert([self connected], @"Must be connected to send auth");
+ NSAssert(self.authToken != nil, @"Can't send auth if there is no credential");
+
+ NSDictionary* requestData = @{kFWPRequestCredential: self.authToken};
+ [self sendAction:kFWPRequestActionAuth body:requestData sensitive:YES callback:^(NSDictionary *data) {
+ self->connectionState = ConnectionStateConnected;
+ NSString* status = [data objectForKey:kFWPResponseForActionStatus];
+ id responseData = [data objectForKey:kFWPResponseForActionData];
+ if (responseData == nil) {
+ responseData = @"error";
+ }
+
+ BOOL statusOk = [status isEqualToString:kFWPResponseForActionStatusOk];
+ if (statusOk) {
+ if (restoreStateAfterComplete) {
+ [self restoreState];
+ }
+ } else {
+ self.authToken = nil;
+ self.forceAuthTokenRefresh = YES;
+ if ([status isEqualToString:@"expired_token"]) {
+ FFLog(@"I-RDB034017", @"Authentication failed: %@ (%@)", status, responseData);
+ } else {
+ FFWarn(@"I-RDB034018", @"Authentication failed: %@ (%@)", status, responseData);
+ }
+ [self.realtime close];
+ }
+ }];
+}
+
+- (void) sendUnauth {
+ [self sendAction:kFWPRequestActionUnauth body:@{} sensitive:NO callback:nil];
+}
+
+- (void) onAuthRevokedWithStatus:(NSString *)status andReason:(NSString *)reason {
+ // This might be for an earlier token than we just recently sent. But since we need to close the connection anyways,
+ // we can set it to null here and we will refresh the token later on reconnect
+ if ([status isEqualToString:@"expired_token"]) {
+ FFLog(@"I-RDB034019", @"Auth token revoked: %@ (%@)", status, reason);
+ } else {
+ FFWarn(@"I-RDB034020", @"Auth token revoked: %@ (%@)", status, reason);
+ }
+ self.authToken = nil;
+ self.forceAuthTokenRefresh = YES;
+ // Try reconnecting on auth revocation
+ [self.realtime close];
+}
+
+- (void) onListenRevoked:(FPath *)path {
+ NSArray *queries = [self removeAllListensAtPath:path];
+ for (FOutstandingQuery* query in queries) {
+ query.onComplete(@"permission_denied");
+ }
+}
+
+- (void) sendOnDisconnectAction:(NSString *)action forPath:(NSString *)pathString withData:(id)data andCallback:(fbt_void_nsstring_nsstring)callback {
+
+ NSDictionary* request = @{kFWPRequestPath: pathString, kFWPRequestData: data};
+ FFLog(@"I-RDB034021", @"onDisconnect %@: %@", action, request);
+
+ [self sendAction:action
+ body:request
+ sensitive:NO
+ callback:^(NSDictionary *data) {
+ NSString* status = [data objectForKey:kFWPResponseForActionStatus];
+ NSString* errorReason = [data objectForKey:kFWPResponseForActionData];
+ callback(status, errorReason);
+ }];
+}
+
+- (void) sendPut:(NSNumber *) index {
+ NSAssert([self canSendWrites], @"sendPut called when not able to send writes");
+ FOutstandingPut* put = self.outstandingPuts[index];
+ assert(put != nil);
+ fbt_void_nsstring_nsstring onComplete = put.onCompleteBlock;
+
+ // Do not async this block; copying the block insinde sendAction: doesn't happen in time (or something) so coredumps
+ put.sent = YES;
+ [self sendAction:put.action
+ body:put.request
+ sensitive:NO
+ callback:^(NSDictionary* data) {
+
+ FOutstandingPut *currentPut = self.outstandingPuts[index];
+ if (currentPut == put) {
+ [self.outstandingPuts removeObjectForKey:index];
+
+ if (onComplete != nil) {
+ NSString *status = [data objectForKey:kFWPResponseForActionStatus];
+ NSString *errorReason = [data objectForKey:kFWPResponseForActionData];
+ if (self.unackedListensCount == 0) {
+ onComplete(status, errorReason);
+ } else {
+ FTupleCallbackStatus *putToAck = [[FTupleCallbackStatus alloc] init];
+ putToAck.block = onComplete;
+ putToAck.status = status;
+ putToAck.errorReason = errorReason;
+ [self.putsToAck addObject:putToAck];
+ }
+ }
+ } else {
+ FFLog(@"I-RDB034022", @"Ignoring on complete for put %@ because it was already removed", index);
+ }
+ }];
+}
+
+- (void) sendUnlisten:(FPath *)path queryParams:(FQueryParams *)queryParams tagId:(NSNumber *)tagId {
+ FFLog(@"I-RDB034023", @"Unlisten on %@ for %@", path, queryParams);
+
+ NSMutableDictionary* request = [NSMutableDictionary dictionaryWithObjectsAndKeys:[path toString], kFWPRequestPath, nil];
+ if (tagId) {
+ [request setObject:queryParams.wireProtocolParams forKey:kFWPRequestQueries];
+ [request setObject:tagId forKey:kFWPRequestTag];
+ }
+
+ [self sendAction:kFWPRequestActionTaggedUnlisten
+ body:request
+ sensitive:NO
+ callback:nil];
+}
+
+- (void) putInternal:(id)data forAction:(NSString *)action forPath:(NSString *)pathString withHash:(NSString *)hash withCallback:(fbt_void_nsstring_nsstring)onComplete {
+
+ NSMutableDictionary *request = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ pathString, kFWPRequestPath,
+ data, kFWPRequestData, nil];
+ if(hash) {
+ [request setObject:hash forKey:kFWPRequestHash];
+ }
+
+ FOutstandingPut *put = [[FOutstandingPut alloc] init];
+ put.action = action;
+ put.request = request;
+ put.onCompleteBlock = onComplete;
+ put.sent = NO;
+
+ NSNumber* index = [self.putCounter getAndIncrement];
+ self.outstandingPuts[index] = put;
+
+ if ([self canSendWrites]) {
+ FFLog(@"I-RDB034024", @"Was connected, and added as index: %@", index);
+ [self sendPut:index];
+ }
+ else {
+ FFLog(@"I-RDB034025", @"Wasn't connected or writes paused, so added to outstanding puts only. Path: %@", pathString);
+ }
+}
+
+- (void) sendListen:(FOutstandingQuery *)listenSpec {
+ FQuerySpec *query = listenSpec.query;
+ FFLog(@"I-RDB034026", @"Listen for %@", query);
+ NSMutableDictionary *request = [NSMutableDictionary dictionaryWithObject:[query.path toString] forKey:kFWPRequestPath];
+
+ // Only bother to send query if it's non-default
+ if (listenSpec.tagId != nil) {
+ [request setObject:[query.params wireProtocolParams] forKey:kFWPRequestQueries];
+ [request setObject:listenSpec.tagId forKey:kFWPRequestTag];
+ }
+
+ [request setObject:[listenSpec.syncTreeHash simpleHash] forKey:kFWPRequestHash];
+ if ([listenSpec.syncTreeHash includeCompoundHash]) {
+ FCompoundHash *compoundHash = [listenSpec.syncTreeHash compoundHash];
+ NSMutableArray *posts = [NSMutableArray array];
+ for (FPath *path in compoundHash.posts) {
+ [posts addObject:path.wireFormat];
+ }
+ request[kFWPRequestCompoundHash] = @{ kFWPRequestCompoundHashHashes: compoundHash.hashes,
+ kFWPRequestCompoundHashPaths: posts };
+ }
+
+ fbt_void_nsdictionary onResponse = ^(NSDictionary *response) {
+ FFLog(@"I-RDB034027", @"Listen response %@", response);
+ // warn in any case, even if the listener was removed
+ [self warnOnListenWarningsForQuery:query payload:response[kFWPResponseForActionData]];
+
+ FOutstandingQuery *currentListenSpec = self.listens[query];
+
+ // only trigger actions if the listen hasn't been removed (and maybe readded)
+ if (currentListenSpec == listenSpec) {
+ NSString *status = [response objectForKey:kFWPRequestStatus];
+ if (![status isEqualToString:@"ok"]) {
+ [self removeListen:query];
+ }
+
+ if (listenSpec.onComplete) {
+ listenSpec.onComplete(status);
+ }
+ }
+
+ self.unackedListensCount--;
+ NSAssert(self.unackedListensCount >= 0, @"unackedListensCount decremented to be negative.");
+ if (self.unackedListensCount == 0) {
+ [self ackPuts];
+ }
+ };
+
+ [self sendAction:kFWPRequestActionTaggedListen
+ body:request
+ sensitive:NO
+ callback:onResponse];
+
+ self.unackedListensCount++;
+}
+
+- (void) warnOnListenWarningsForQuery:(FQuerySpec *)query payload:(id)payload {
+ if (payload != nil && [payload isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *payloadDict = payload;
+ id warnings = payloadDict[kFWPResponseDataWarnings];
+ if (warnings != nil && [warnings isKindOfClass:[NSArray class]]) {
+ NSArray *warningsArr = warnings;
+ if ([warningsArr containsObject:@"no_index"]) {
+ NSString *indexSpec = [NSString stringWithFormat:@"\".indexOn\": \"%@\"", [query.params.index queryDefinition]];
+ NSString *indexPath = [query.path description];
+ FFWarn(@"I-RDB034028", @"Using an unspecified index. Consider adding %@ at %@ to your security rules for better performance", indexSpec, indexPath);
+ }
+ }
+ }
+}
+
+- (int) getNextRequestNumber {
+ return [[self.requestNumber getAndIncrement] intValue];
+}
+
+- (void)sendAction:(NSString *)action
+ body:(NSDictionary *)message
+ sensitive:(BOOL)sensitive
+ callback:(void (^)(NSDictionary* data))onMessage {
+ // Hold onto the onMessage callback for this request before firing it off
+ NSNumber* rn = [NSNumber numberWithInt:[self getNextRequestNumber]];
+ NSDictionary* msg = [NSDictionary dictionaryWithObjectsAndKeys:
+ rn, kFWPRequestNumber,
+ action, kFWPRequestAction,
+ message, kFWPRequestPayloadBody,
+ nil];
+
+ [self.realtime sendRequest:msg sensitive:sensitive];
+
+ if (onMessage) {
+ // Debug message without a callback; bump the rn, but don't hold onto the cb
+ [self.requestCBHash setObject:[onMessage copy] forKey:rn];
+ }
+}
+
+- (void) cancelSentTransactions {
+ NSMutableArray* toPrune = [[NSMutableArray alloc] init];
+ for (NSNumber* index in self.outstandingPuts) {
+ FOutstandingPut* put = self.outstandingPuts[index];
+ if (put.request[kFWPRequestHash] && put.sent) {
+ // This is a sent transaction put
+ put.onCompleteBlock(kFTransactionDisconnect, @"Client was disconnected while running a transaction");
+ [toPrune addObject:index];
+ }
+ }
+ for (NSNumber* index in toPrune) {
+ [self.outstandingPuts removeObjectForKey:index];
+ }
+}
+
+- (void) onDataPushWithAction:(NSString *)action andBody:(NSDictionary *)body {
+ FFLog(@"I-RDB034029", @"handleServerMessage: %@, %@", action, body);
+ id<FPersistentConnectionDelegate> delegate = self.delegate;
+ if ([action isEqualToString:kFWPAsyncServerDataUpdate] || [action isEqualToString:kFWPAsyncServerDataMerge]) {
+ BOOL isMerge = [action isEqualToString:kFWPAsyncServerDataMerge];
+
+ if ([body objectForKey:kFWPAsyncServerDataUpdateBodyPath] && [body objectForKey:kFWPAsyncServerDataUpdateBodyData]) {
+ NSString* path = [body objectForKey:kFWPAsyncServerDataUpdateBodyPath];
+ id payloadData = [body objectForKey:kFWPAsyncServerDataUpdateBodyData];
+ if (isMerge && [payloadData isKindOfClass:[NSDictionary class]] && [payloadData count] == 0) {
+ // ignore empty merge
+ } else {
+ [delegate onDataUpdate:self forPath:path message:payloadData isMerge:isMerge tagId:[body objectForKey:kFWPAsyncServerDataUpdateBodyTag]];
+ }
+ }
+ else {
+ FFLog(@"I-RDB034030", @"Malformed data response from server missing path or data: %@", body);
+ }
+ } else if ([action isEqualToString:kFWPAsyncServerDataRangeMerge]) {
+ NSString *path = body[kFWPAsyncServerDataUpdateBodyPath];
+ NSArray *ranges = body[kFWPAsyncServerDataUpdateBodyData];
+ NSNumber *tag = body[kFWPAsyncServerDataUpdateBodyTag];
+ NSMutableArray *rangeMerges = [NSMutableArray array];
+ for (NSDictionary *range in ranges) {
+ NSString *startString = range[kFWPAsyncServerDataUpdateStartPath];
+ NSString *endString = range[kFWPAsyncServerDataUpdateEndPath];
+ id updateData = range[kFWPAsyncServerDataUpdateRangeMerge];
+ id<FNode> updates = [FSnapshotUtilities nodeFrom:updateData];
+ FPath *start = (startString != nil) ? [[FPath alloc] initWith:startString] : nil;
+ FPath *end = (endString != nil) ? [[FPath alloc] initWith:endString] : nil;
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:start end:end updates:updates];
+ [rangeMerges addObject:merge];
+ }
+ [delegate onRangeMerge:rangeMerges forPath:path tagId:tag];
+ } else if ([action isEqualToString:kFWPAsyncServerAuthRevoked]) {
+ NSString* status = [body objectForKey:kFWPResponseForActionStatus];
+ NSString* reason = [body objectForKey:kFWPResponseForActionData];
+ [self onAuthRevokedWithStatus:status andReason:reason];
+ } else if ([action isEqualToString:kFWPASyncServerListenCancelled]) {
+ NSString* pathString = [body objectForKey:kFWPAsyncServerDataUpdateBodyPath];
+ [self onListenRevoked:[[FPath alloc] initWith:pathString]];
+ } else if ([action isEqualToString:kFWPAsyncServerSecurityDebug]) {
+ NSString* msg = [body objectForKey:@"msg"];
+ if (msg != nil) {
+ NSArray *msgs = [msg componentsSeparatedByString:@"\n"];
+ for (NSString* m in msgs) {
+ FFWarn(@"I-RDB034031", @"%@", m);
+ }
+ }
+ } else {
+ // TODO: revoke listens, auth, security debug
+ FFLog(@"I-RDB034032", @"Unsupported action from server: %@", action);
+ }
+}
+
+- (void) restoreAuth {
+ FFLog(@"I-RDB034033", @"Calling restore state");
+
+ NSAssert(self->connectionState == ConnectionStateConnecting,
+ @"Wanted to restore auth, but was in wrong state: %d", self->connectionState);
+ if (self.authToken == nil) {
+ FFLog(@"I-RDB034034", @"Not restoring auth because token is nil");
+ self->connectionState = ConnectionStateConnected;
+ [self restoreState];
+ } else {
+ FFLog(@"I-RDB034035", @"Restoring auth");
+ self->connectionState = ConnectionStateAuthenticating;
+ [self sendAuthAndRestoreStateAfterComplete:YES];
+ }
+}
+
+- (void) restoreState {
+ NSAssert(self->connectionState == ConnectionStateConnected,
+ @"Should be connected if we're restoring state, but we are: %d", self->connectionState);
+
+ [self.listens enumerateKeysAndObjectsUsingBlock:^(FQuerySpec *query, FOutstandingQuery *outstandingListen, BOOL *stop) {
+ FFLog(@"I-RDB034036", @"Restoring listen for %@", query);
+ [self sendListen:outstandingListen];
+ }];
+
+ NSArray* keys = [[self.outstandingPuts allKeys] sortedArrayUsingSelector:@selector(compare:)];
+ for(int i = 0; i < [keys count]; i++) {
+ if([self.outstandingPuts objectForKey:[keys objectAtIndex:i]] != nil) {
+ FFLog(@"I-RDB034037", @"Restoring put: %d", i);
+ [self sendPut:[keys objectAtIndex:i]];
+ }
+ else {
+ FFLog(@"I-RDB034038", @"Restoring put: skipped nil: %d", i);
+ }
+ }
+
+ for (FTupleOnDisconnect* tuple in self.onDisconnectQueue) {
+ [self sendOnDisconnectAction:tuple.action forPath:tuple.pathString withData:tuple.data andCallback:tuple.onComplete];
+ }
+ [self.onDisconnectQueue removeAllObjects];
+}
+
+- (NSArray *) removeListen:(FQuerySpec *)query {
+ NSAssert(query.isDefault || !query.loadsAllData, @"removeListen called for non-default but complete query");
+
+ FOutstandingQuery* outstanding = self.listens[query];
+ if (!outstanding) {
+ FFLog(@"I-RDB034039", @"Trying to remove listener for query %@ but no listener exists", query);
+ return @[];
+ } else {
+ [self.listens removeObjectForKey:query];
+ return @[outstanding];
+ }
+}
+
+- (NSArray *) removeAllListensAtPath:(FPath *)path {
+ FFLog(@"I-RDB034040", @"Removing all listens at path %@", path);
+ NSMutableArray *removed = [NSMutableArray array];
+ NSMutableArray *toRemove = [NSMutableArray array];
+ [self.listens enumerateKeysAndObjectsUsingBlock:^(FQuerySpec *spec, FOutstandingQuery *outstanding, BOOL *stop) {
+ if ([spec.path isEqual:path]) {
+ [removed addObject:outstanding];
+ [toRemove addObject:spec];
+ }
+ }];
+ [self.listens removeObjectsForKeys:toRemove];
+ return removed;
+}
+
+- (void) purgeOutstandingWrites {
+ // We might have unacked puts in our queue that we need to ack now before we send out any cancels...
+ [self ackPuts];
+ // Cancel in order
+ NSArray* keys = [[self.outstandingPuts allKeys] sortedArrayUsingSelector:@selector(compare:)];
+ for (NSNumber *key in keys) {
+ FOutstandingPut *put = self.outstandingPuts[key];
+ if (put.onCompleteBlock != nil) {
+ put.onCompleteBlock(kFErrorWriteCanceled, nil);
+ }
+ }
+ for (FTupleOnDisconnect *onDisconnect in self.onDisconnectQueue) {
+ if (onDisconnect.onComplete != nil) {
+ onDisconnect.onComplete(kFErrorWriteCanceled, nil);
+ }
+ }
+ [self.outstandingPuts removeAllObjects];
+ [self.onDisconnectQueue removeAllObjects];
+}
+
+- (void) ackPuts {
+ for (FTupleCallbackStatus *put in self.putsToAck) {
+ put.block(put.status, put.errorReason);
+ }
+ [self.putsToAck removeAllObjects];
+}
+
+- (void) handleTimestamp:(NSNumber *)timestamp {
+ FFLog(@"I-RDB034041", @"Handling timestamp: %@", timestamp);
+ double timestampDeltaMs = [timestamp doubleValue] - ([[NSDate date] timeIntervalSince1970] * 1000);
+ [self.delegate onServerInfoUpdate:self updates:@{kDotInfoServerTimeOffset: [NSNumber numberWithDouble:timestampDeltaMs]}];
+}
+
+- (void) sendStats:(NSDictionary *)stats {
+ if ([stats count] > 0) {
+ NSDictionary *request = @{ kFWPRequestCounters: stats };
+ [self sendAction:kFWPRequestActionStats body:request sensitive:NO callback:^(NSDictionary *data) {
+ NSString* status = [data objectForKey:kFWPResponseForActionStatus];
+ NSString* errorReason = [data objectForKey:kFWPResponseForActionData];
+ BOOL statusOk = [status isEqualToString:kFWPResponseForActionStatusOk];
+ if (!statusOk) {
+ FFLog(@"I-RDB034042", @"Failed to send stats: %@", errorReason);
+ }
+ }];
+ } else {
+ FFLog(@"I-RDB034043", @"Not sending stats because stats are empty");
+ }
+}
+
+- (void) sendConnectStats {
+ NSMutableDictionary *stats = [NSMutableDictionary dictionary];
+
+#if TARGET_OS_IPHONE
+ if (self.config.persistenceEnabled) {
+ stats[@"persistence.ios.enabled"] = @1;
+ }
+#else // this must be OSX then
+ if (self.config.persistenceEnabled) {
+ stats[@"persistence.osx.enabled"] = @1;
+ }
+#endif
+ NSString *sdkVersion = [[FIRDatabase sdkVersion] stringByReplacingOccurrencesOfString:@"." withString:@"-"];
+ NSString *sdkStatName = [NSString stringWithFormat:@"sdk.objc.%@", sdkVersion];
+ stats[sdkStatName] = @1;
+ FFLog(@"I-RDB034044", @"Sending first connection stats");
+ [self sendStats:stats];
+}
+
+- (NSDictionary *) dumpListens {
+ return self.listens;
+}
+
+@end
diff --git a/Firebase/Database/Core/FQueryParams.h b/Firebase/Database/Core/FQueryParams.h
new file mode 100644
index 0000000..e9728e7
--- /dev/null
+++ b/Firebase/Database/Core/FQueryParams.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 <Foundation/Foundation.h>
+
+@protocol FIndex, FNodeFilter, FNode;
+
+@interface FQueryParams : NSObject <NSCopying>
+
+@property (nonatomic, readonly) BOOL limitSet;
+@property (nonatomic, readonly) NSInteger limit;
+
+@property (nonatomic, strong, readonly) NSString *viewFrom;
+@property (nonatomic, strong, readonly) id<FNode> indexStartValue;
+@property (nonatomic, strong, readonly) NSString *indexStartKey;
+@property (nonatomic, strong, readonly) id<FNode> indexEndValue;
+@property (nonatomic, strong, readonly) NSString *indexEndKey;
+
+@property (nonatomic, strong, readonly) id<FIndex> index;
+
+- (BOOL)loadsAllData;
+- (BOOL)isDefault;
+- (BOOL)isValid;
+- (BOOL)hasAnchoredLimit;
+
+- (FQueryParams *) limitTo:(NSInteger) limit;
+- (FQueryParams *) limitToFirst:(NSInteger) newLimit;
+- (FQueryParams *) limitToLast:(NSInteger) newLimit;
+
+- (FQueryParams *) startAt:(id<FNode>)indexValue childKey:(NSString *)key;
+- (FQueryParams *) startAt:(id<FNode>)indexValue;
+- (FQueryParams *) endAt:(id<FNode>)indexValue childKey:(NSString *)key;
+- (FQueryParams *) endAt:(id<FNode>)indexValue;
+
+- (FQueryParams *) orderBy:(id<FIndex>) index;
+
++ (FQueryParams *) defaultInstance;
++ (FQueryParams *) fromQueryObject:(NSDictionary *)dict;
+
+- (BOOL)hasStart;
+- (BOOL)hasEnd;
+
+- (NSDictionary *) wireProtocolParams;
+- (BOOL) isViewFromLeft;
+- (id<FNodeFilter>) nodeFilter;
+@end
diff --git a/Firebase/Database/Core/FQueryParams.m b/Firebase/Database/Core/FQueryParams.m
new file mode 100644
index 0000000..7920358
--- /dev/null
+++ b/Firebase/Database/Core/FQueryParams.m
@@ -0,0 +1,372 @@
+/*
+ * 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 "FQueryParams.h"
+#import "FValidation.h"
+#import "FConstants.h"
+#import "FIndex.h"
+#import "FPriorityIndex.h"
+#import "FUtilities.h"
+#import "FNodeFilter.h"
+#import "FIndexedFilter.h"
+#import "FLimitedFilter.h"
+#import "FRangedFilter.h"
+#import "FNode.h"
+#import "FSnapshotUtilities.h"
+
+@interface FQueryParams ()
+
+@property (nonatomic, readwrite) BOOL limitSet;
+@property (nonatomic, readwrite) NSInteger limit;
+
+@property (nonatomic, strong, readwrite) NSString *viewFrom;
+/**
+* indexStartValue is anything you can store as a priority / value.
+*/
+@property (nonatomic, strong, readwrite) id<FNode> indexStartValue;
+@property (nonatomic, strong, readwrite) NSString *indexStartKey;
+/**
+* indexStartValue is anything you can store as a priority / value.
+*/
+@property (nonatomic, strong, readwrite) id<FNode> indexEndValue;
+@property (nonatomic, strong, readwrite) NSString *indexEndKey;
+
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+
+@end
+
+@implementation FQueryParams
+
++ (FQueryParams *) defaultInstance {
+ static FQueryParams *defaultParams = nil;
+ static dispatch_once_t defaultParamsToken;
+ dispatch_once(&defaultParamsToken, ^{
+ defaultParams = [[FQueryParams alloc] init];
+ });
+ return defaultParams;
+}
+
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ self->_limitSet = NO;
+ self->_limit = 0;
+
+ self->_viewFrom = nil;
+ self->_indexStartValue = nil;
+ self->_indexStartKey = nil;
+ self->_indexEndValue = nil;
+ self->_indexEndKey = nil;
+
+ self->_index = [FPriorityIndex priorityIndex];
+ }
+ return self;
+}
+
+/**
+* Only valid if hasStart is true
+*/
+- (id) indexStartValue {
+ NSAssert([self hasStart], @"Only valid if start has been set");
+ return _indexStartValue;
+}
+
+/**
+* Only valid if hasStart is true.
+* @return The starting key name for the range defined by these query parameters
+*/
+- (NSString *) indexStartKey {
+ NSAssert([self hasStart], @"Only valid if start has been set");
+ if (_indexStartKey == nil) {
+ return [FUtilities minName];
+ } else {
+ return _indexStartKey;
+ }
+}
+
+/**
+* Only valid if hasEnd is true.
+*/
+- (id) indexEndValue {
+ NSAssert([self hasEnd], @"Only valid if end has been set");
+ return _indexEndValue;
+}
+
+/**
+* Only valid if hasEnd is true.
+* @return The end key name for the range defined by these query parameters
+*/
+- (NSString *) indexEndKey {
+ NSAssert([self hasEnd], @"Only valid if end has been set");
+ if (_indexEndKey == nil) {
+ return [FUtilities maxName];
+ } else {
+ return _indexEndKey;
+ }
+}
+
+/**
+* @return true if a limit has been set and has been explicitly anchored
+*/
+- (BOOL) hasAnchoredLimit {
+ return self.limitSet && self.viewFrom != nil;
+}
+
+/**
+* Only valid to call if limitSet returns true
+*/
+- (NSInteger) limit {
+ NSAssert(self.limitSet, @"Only valid if limit has been set");
+ return _limit;
+}
+
+- (BOOL)hasStart {
+ return self->_indexStartValue != nil;
+}
+
+- (BOOL)hasEnd {
+ return self->_indexEndValue != nil;
+}
+
+- (id) copyWithZone:(NSZone *)zone {
+ // Immutable
+ return self;
+}
+
+- (id) mutableCopy {
+ FQueryParams* other = [[[self class] alloc] init];
+ // Maybe need to do extra copying here
+ other->_limitSet = _limitSet;
+ other->_limit = _limit;
+ other->_indexStartValue = _indexStartValue;
+ other->_indexStartKey = _indexStartKey;
+ other->_indexEndValue = _indexEndValue;
+ other->_indexEndKey = _indexEndKey;
+ other->_viewFrom = _viewFrom;
+ other->_index = _index;
+ return other;
+}
+
+- (FQueryParams *) limitTo:(NSInteger)newLimit {
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_limitSet = YES;
+ newParams->_limit = newLimit;
+ newParams->_viewFrom = nil;
+ return newParams;
+}
+
+- (FQueryParams *) limitToFirst:(NSInteger)newLimit {
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_limitSet = YES;
+ newParams->_limit = newLimit;
+ newParams->_viewFrom = kFQPViewFromLeft;
+ return newParams;
+}
+
+- (FQueryParams *) limitToLast:(NSInteger)newLimit {
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_limitSet = YES;
+ newParams->_limit = newLimit;
+ newParams->_viewFrom = kFQPViewFromRight;
+ return newParams;
+}
+
+- (FQueryParams *) startAt:(id<FNode>)indexValue childKey:(NSString *)key {
+ NSAssert([indexValue isLeafNode] || [indexValue isEmpty], nil);
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_indexStartValue = indexValue;
+ newParams->_indexStartKey = key;
+ return newParams;
+}
+
+- (FQueryParams *) startAt:(id<FNode>)indexValue {
+ return [self startAt:indexValue childKey:nil];
+}
+
+- (FQueryParams *) endAt:(id<FNode>)indexValue childKey:(NSString *)key {
+ NSAssert([indexValue isLeafNode] || [indexValue isEmpty], nil);
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_indexEndValue = indexValue;
+ newParams->_indexEndKey = key;
+ return newParams;
+}
+
+- (FQueryParams *) endAt:(id<FNode>)indexValue {
+ return [self endAt:indexValue childKey:nil];
+}
+
+- (FQueryParams *) orderBy:(id)newIndex {
+ FQueryParams *newParams = [self mutableCopy];
+ newParams->_index = newIndex;
+ return newParams;
+}
+
+- (NSDictionary *) wireProtocolParams {
+ NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
+ if ([self hasStart]) {
+ [dict setObject:[self.indexStartValue valForExport:YES] forKey:kFQPIndexStartValue];
+
+ // Don't use property as it will be [MIN-NAME]
+ if (self->_indexStartKey != nil) {
+ [dict setObject:self->_indexStartKey forKey:kFQPIndexStartName];
+ }
+ }
+
+ if ([self hasEnd]) {
+ [dict setObject:[self.indexEndValue valForExport:YES] forKey:kFQPIndexEndValue];
+
+ // Don't use property as it will be [MAX-NAME]
+ if (self->_indexEndKey != nil) {
+ [dict setObject:self->_indexEndKey forKey:kFQPIndexEndName];
+ }
+ }
+
+ if (self.limitSet) {
+ [dict setObject:[NSNumber numberWithInteger:self.limit] forKey:kFQPLimit];
+ NSString *vf = self.viewFrom;
+ if (vf == nil) {
+ // limit() rather than limitToFirst or limitToLast was called.
+ // This means that only one of startSet or endSet is true. Use them
+ // to calculate which side of the view to anchor to. If neither is set,
+ // Anchor to end
+ if ([self hasStart]) {
+ vf = kFQPViewFromLeft;
+ } else {
+ vf = kFQPViewFromRight;
+ }
+ }
+ [dict setObject:vf forKey:kFQPViewFrom];
+ }
+
+ // For now, priority index is the default, so we only specify if it's some other index.
+ if (![self.index isEqual:[FPriorityIndex priorityIndex]]) {
+ [dict setObject:[self.index queryDefinition] forKey:kFQPIndex];
+ }
+
+ return dict;
+}
+
++ (FQueryParams *)fromQueryObject:(NSDictionary *)dict {
+ if (dict.count == 0) {
+ return [FQueryParams defaultInstance];
+ }
+
+ FQueryParams *params = [[FQueryParams alloc] init];
+ if (dict[kFQPLimit] != nil) {
+ params->_limitSet = YES;
+ params->_limit = [dict[kFQPLimit] integerValue];
+ }
+
+ if (dict[kFQPIndexStartValue] != nil) {
+ params->_indexStartValue = [FSnapshotUtilities nodeFrom:dict[kFQPIndexStartValue]];
+ if (dict[kFQPIndexStartName] != nil) {
+ params->_indexStartKey = dict[kFQPIndexStartName];
+ }
+ }
+
+ if (dict[kFQPIndexEndValue] != nil) {
+ params->_indexEndValue = [FSnapshotUtilities nodeFrom:dict[kFQPIndexEndValue]];
+ if (dict[kFQPIndexEndName] != nil) {
+ params->_indexEndKey = dict[kFQPIndexEndName];
+ }
+ }
+
+ if (dict[kFQPViewFrom] != nil) {
+ NSString *viewFrom = dict[kFQPViewFrom];
+ if (![viewFrom isEqualToString:kFQPViewFromLeft] && ![viewFrom isEqualToString:kFQPViewFromRight]) {
+ [NSException raise:NSInvalidArgumentException format:@"Unknown view from paramter: %@", viewFrom];
+ }
+ params->_viewFrom = viewFrom;
+ }
+
+ NSString *index = dict[kFQPIndex];
+ if (index != nil) {
+ params->_index = [FIndex indexFromQueryDefinition:index];
+ }
+
+ return params;
+}
+
+- (BOOL) isViewFromLeft {
+ if (self.viewFrom != nil) {
+ // Not null, we can just check
+ return [self.viewFrom isEqualToString:kFQPViewFromLeft];
+ } else {
+ // If start is set, it's view from left. Otherwise not.
+ return self.hasStart;
+ }
+}
+
+- (id<FNodeFilter>) nodeFilter {
+ if (self.loadsAllData) {
+ return [[FIndexedFilter alloc] initWithIndex:self.index];
+ } else if (self.limitSet) {
+ return [[FLimitedFilter alloc] initWithQueryParams:self];
+ } else {
+ return [[FRangedFilter alloc] initWithQueryParams:self];
+ }
+}
+
+
+- (BOOL) isValid {
+ return !(self.hasStart && self.hasEnd && self.limitSet && !self.hasAnchoredLimit);
+}
+
+- (BOOL) loadsAllData {
+ return !(self.hasStart || self.hasEnd || self.limitSet);
+}
+
+- (BOOL) isDefault {
+ return [self loadsAllData] && [self.index isEqual:[FPriorityIndex priorityIndex]];
+}
+
+- (NSString *) description {
+ return [[self wireProtocolParams] description];
+}
+
+- (BOOL) isEqual:(id)obj {
+ if (self == obj) {
+ return YES;
+ }
+ if (![obj isKindOfClass:[self class]]) {
+ return NO;
+ }
+ FQueryParams *other = (FQueryParams *)obj;
+ if (self->_limitSet != other->_limitSet) return NO;
+ if (self->_limit != other->_limit) return NO;
+ if ((self->_index != other->_index) && ![self->_index isEqual:other->_index]) return NO;
+ if ((self->_indexStartKey != other->_indexStartKey) && ![self->_indexStartKey isEqualToString:other->_indexStartKey]) return NO;
+ if ((self->_indexStartValue != other->_indexStartValue) && ![self->_indexStartValue isEqual:other->_indexStartValue]) return NO;
+ if ((self->_indexEndKey != other->_indexEndKey) && ![self->_indexEndKey isEqualToString:other->_indexEndKey]) return NO;
+ if ((self->_indexEndValue != other->_indexEndValue) && ![self->_indexEndValue isEqual:other->_indexEndValue]) return NO;
+ if ([self isViewFromLeft] != [other isViewFromLeft]) return NO;
+
+ return YES;
+}
+
+- (NSUInteger) hash {
+ NSUInteger result = _limitSet ? _limit : 0;
+ result = 31 * result + ([self isViewFromLeft] ? 1231 : 1237);
+ result = 31 * result + [_indexStartKey hash];
+ result = 31 * result + [_indexStartValue hash];
+ result = 31 * result + [_indexEndKey hash];
+ result = 31 * result + [_indexEndValue hash];
+ result = 31 * result + [_index hash];
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/Core/FQuerySpec.h b/Firebase/Database/Core/FQuerySpec.h
new file mode 100644
index 0000000..49ed536
--- /dev/null
+++ b/Firebase/Database/Core/FQuerySpec.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 <Foundation/Foundation.h>
+
+#import "FQueryParams.h"
+#import "FPath.h"
+#import "FIndex.h"
+
+@interface FQuerySpec : NSObject<NSCopying>
+
+@property (nonatomic, strong, readonly) FPath* path;
+@property (nonatomic, strong, readonly) FQueryParams *params;
+
+- (id)initWithPath:(FPath *)path params:(FQueryParams *)params;
+
++ (FQuerySpec *)defaultQueryAtPath:(FPath *)path;
+
+- (id<FIndex>)index;
+- (BOOL)isDefault;
+- (BOOL)loadsAllData;
+
+@end
diff --git a/Firebase/Database/Core/FQuerySpec.m b/Firebase/Database/Core/FQuerySpec.m
new file mode 100644
index 0000000..24be433
--- /dev/null
+++ b/Firebase/Database/Core/FQuerySpec.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 "FQuerySpec.h"
+
+@interface FQuerySpec ()
+
+@property (nonatomic, strong, readwrite) FPath* path;
+@property (nonatomic, strong, readwrite) FQueryParams *params;
+
+
+@end
+
+@implementation FQuerySpec
+
+- (id)initWithPath:(FPath *)path params:(FQueryParams *)params {
+ self = [super init];
+ if (self != nil) {
+ self->_path = path;
+ self->_params = params;
+ }
+ return self;
+}
+
++ (FQuerySpec *)defaultQueryAtPath:(FPath *)path {
+ return [[FQuerySpec alloc] initWithPath:path params:[FQueryParams defaultInstance]];
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ // Immutable
+ return self;
+}
+
+- (id<FIndex>)index {
+ return self.params.index;
+}
+
+- (BOOL)isDefault {
+ return self.params.isDefault;
+}
+
+- (BOOL)loadsAllData {
+ return self.params.loadsAllData;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FQuerySpec class]]) {
+ return NO;
+ }
+
+ FQuerySpec *other = (FQuerySpec *)object;
+
+ if (![self.path isEqual:other.path]) {
+ return NO;
+ }
+
+ return [self.params isEqual:other.params];
+}
+
+- (NSUInteger)hash {
+ return self.path.hash * 31 + self.params.hash;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"FQuerySpec (path: %@, params: %@)", self.path, self.params];
+}
+
+@end
diff --git a/Firebase/Database/Core/FRangeMerge.h b/Firebase/Database/Core/FRangeMerge.h
new file mode 100644
index 0000000..8825e0e
--- /dev/null
+++ b/Firebase/Database/Core/FRangeMerge.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FNode.h"
+
+/**
+ * Applies a merge of a snap for a given interval of paths.
+ * Each leaf in the current node which the relative path lies *after* (the optional) start and lies *before or at*
+ * (the optional) end will be deleted. Each leaf in snap that lies in the interval will be added to the resulting node.
+ * Nodes outside of the range are ignored. nil for start and end are sentinel values that represent -infinity and
+ * +infinity respectively (aka includes any path).
+ * Priorities of children nodes are treated as leaf children of that node.
+ */
+@interface FRangeMerge : NSObject
+
+- (instancetype)initWithStart:(FPath *)start end:(FPath *)end updates:(id<FNode>)updates;
+
+- (id<FNode>)applyToNode:(id<FNode>)node;
+
+@end
diff --git a/Firebase/Database/Core/FRangeMerge.m b/Firebase/Database/Core/FRangeMerge.m
new file mode 100644
index 0000000..8bc67bf
--- /dev/null
+++ b/Firebase/Database/Core/FRangeMerge.m
@@ -0,0 +1,107 @@
+/*
+ * 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 "FRangeMerge.h"
+
+#import "FEmptyNode.h"
+
+@interface FRangeMerge ()
+
+@property (nonatomic, strong) FPath *optExclusiveStart;
+@property (nonatomic, strong) FPath *optInclusiveEnd;
+@property (nonatomic, strong) id<FNode> updates;
+
+@end
+
+@implementation FRangeMerge
+
+- (instancetype)initWithStart:(FPath *)start end:(FPath *)end updates:(id<FNode>)updates {
+ self = [super init];
+ if (self != nil) {
+ self->_optExclusiveStart = start;
+ self->_optInclusiveEnd = end;
+ self->_updates = updates;
+ }
+ return self;
+}
+
+- (id<FNode>)applyToNode:(id<FNode>)node {
+ return [self updateRangeInNode:[FPath empty] node:node updates:self.updates];
+}
+
+- (id<FNode>)updateRangeInNode:(FPath *)currentPath node:(id<FNode>)node updates:(id<FNode>)updates {
+ NSComparisonResult startComparison = (self.optExclusiveStart == nil) ? NSOrderedDescending : [currentPath compare:self.optExclusiveStart];
+ NSComparisonResult endComparison = (self.optInclusiveEnd == nil) ? NSOrderedAscending : [currentPath compare:self.optInclusiveEnd];
+ BOOL startInNode = self.optExclusiveStart != nil && [currentPath contains:self.optExclusiveStart];
+ BOOL endInNode = self.optInclusiveEnd != nil && [currentPath contains:self.optInclusiveEnd];
+ if (startComparison == NSOrderedDescending && endComparison == NSOrderedAscending && !endInNode) {
+ // child is completly contained
+ return updates;
+ } else if (startComparison == NSOrderedDescending && endInNode && [updates isLeafNode]) {
+ return updates;
+ } else if (startComparison == NSOrderedDescending && endComparison == NSOrderedSame) {
+ NSAssert(endInNode, @"End not in node");
+ NSAssert(![updates isLeafNode], @"Found leaf node update, this case should have been handled above.");
+ if ([node isLeafNode]) {
+ // Update node was not a leaf node, so we can delete it
+ return [FEmptyNode emptyNode];
+ } else {
+ // Unaffected by range, ignore
+ return node;
+ }
+ } else if (startInNode || endInNode) {
+ // There is a partial update we need to do, so collect all relevant children
+ NSMutableSet *allChildren = [NSMutableSet set];
+ [node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [allChildren addObject:key];
+ }];
+ [updates enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [allChildren addObject:key];
+ }];
+
+ __block id<FNode> newNode = node;
+ void (^action)(id, BOOL *) = ^void(NSString *key, BOOL *stop) {
+ id<FNode> currentChild = [node getImmediateChild:key];
+ id<FNode> updatedChild = [self updateRangeInNode:[currentPath childFromString:key]
+ node:currentChild
+ updates:[updates getImmediateChild:key]];
+ // Only need to update if the node changed
+ if (updatedChild != currentChild) {
+ newNode = [newNode updateImmediateChild:key withNewChild:updatedChild];
+ }
+ };
+
+ [allChildren enumerateObjectsUsingBlock:action];
+
+ // Add priority last, so the node is not empty when applying
+ if (!updates.getPriority.isEmpty || !node.getPriority.isEmpty) {
+ BOOL stop = NO;
+ action(@".priority", &stop);
+ }
+ return newNode;
+ } else {
+ // Unaffected by this range
+ NSAssert(endComparison == NSOrderedDescending || startComparison <= NSOrderedSame, @"Invalid range for update");
+ return node;
+ }
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"RangeMerge (optExclusiveStart = %@, optExclusiveEng = %@, updates = %@)",
+ self.optExclusiveStart, self.optInclusiveEnd, self.updates];
+}
+
+@end
diff --git a/Firebase/Database/Core/FRepo.h b/Firebase/Database/Core/FRepo.h
new file mode 100644
index 0000000..69ec6bf
--- /dev/null
+++ b/Firebase/Database/Core/FRepo.h
@@ -0,0 +1,76 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FRepoInfo.h"
+#import "FPersistentConnection.h"
+#import "FIRDataEventType.h"
+#import "FTupleUserCallback.h"
+
+@class FQuerySpec;
+@class FPersistence;
+@class FAuthenticationManager;
+@class FIRDatabaseConfig;
+@protocol FEventRegistration;
+@class FCompoundWrite;
+@protocol FClock;
+@class FIRDatabase;
+
+@interface FRepo : NSObject <FPersistentConnectionDelegate>
+
+@property (nonatomic, strong) FIRDatabaseConfig *config;
+
+- (id)initWithRepoInfo:(FRepoInfo *)info config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database;
+
+- (void) set:(FPath *)path withNode:(id)node withCallback:(fbt_void_nserror_ref)onComplete;
+- (void) update:(FPath *)path withNodes:(FCompoundWrite *)compoundWrite withCallback:(fbt_void_nserror_ref)callback;
+- (void) purgeOutstandingWrites;
+
+- (void) addEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query;
+- (void) removeEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query;
+- (void) keepQuery:(FQuerySpec *)query synced:(BOOL)synced;
+
+- (NSString*)name;
+- (NSTimeInterval)serverTime;
+
+- (void) onDataUpdate:(FPersistentConnection *)fpconnection forPath:(NSString *)pathString message:(id)message isMerge:(BOOL)isMerge tagId:(NSNumber *)tagId;
+- (void) onConnect:(FPersistentConnection *)fpconnection;
+- (void) onDisconnect:(FPersistentConnection *)fpconnection;
+
+// Disconnect methods
+- (void) onDisconnectCancel:(FPath *)path withCallback:(fbt_void_nserror_ref)callback;
+- (void) onDisconnectSet:(FPath *)path withNode:(id<FNode>)node withCallback:(fbt_void_nserror_ref)callback;
+- (void) onDisconnectUpdate:(FPath *)path withNodes:(FCompoundWrite *)compoundWrite withCallback:(fbt_void_nserror_ref)callback;
+
+// Connection Management.
+- (void) interrupt;
+- (void) resume;
+
+// Transactions
+- (void) startTransactionOnPath:(FPath *)path
+ update:(fbt_transactionresult_mutabledata)update
+ onComplete:(fbt_void_nserror_bool_datasnapshot)onComplete
+ withLocalEvents:(BOOL)applyLocally;
+
+// Testing methods
+- (NSDictionary *) dumpListens;
+- (void) dispose;
+- (void) setHijackHash:(BOOL)hijack;
+
+@property (nonatomic, strong, readonly) FAuthenticationManager *auth;
+@property (nonatomic, strong, readonly) FIRDatabase *database;
+
+@end
diff --git a/Firebase/Database/Core/FRepo.m b/Firebase/Database/Core/FRepo.m
new file mode 100644
index 0000000..06cc253
--- /dev/null
+++ b/Firebase/Database/Core/FRepo.m
@@ -0,0 +1,1116 @@
+/*
+ * 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 <dlfcn.h>
+#import "FRepo.h"
+#import "FSnapshotUtilities.h"
+#import "FConstants.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQuerySpec.h"
+#import "FTupleNodePath.h"
+#import "FRepo_Private.h"
+#import "FRepoManager.h"
+#import "FServerValues.h"
+#import "FTupleSetIdPath.h"
+#import "FSyncTree.h"
+#import "FEventRegistration.h"
+#import "FAtomicNumber.h"
+#import "FSyncTree.h"
+#import "FListenProvider.h"
+#import "FEventRaiser.h"
+#import "FSnapshotHolder.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FLevelDBStorageEngine.h"
+#import "FPersistenceManager.h"
+#import "FWriteRecord.h"
+#import "FCachePolicy.h"
+#import "FClock.h"
+#import "FIRDatabase_Private.h"
+#import "FTree.h"
+#import "FTupleTransaction.h"
+#import "FIRTransactionResult.h"
+#import "FIRTransactionResult_Private.h"
+#import "FIRMutableData.h"
+#import "FIRMutableData_Private.h"
+#import "FIRDataSnapshot.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FValueEventRegistration.h"
+#import "FEmptyNode.h"
+
+#ifdef TARGET_OS_IPHONE
+#import <UIKit/UIKit.h>
+#endif
+
+@interface FRepo()
+
+@property (nonatomic, strong) FOffsetClock *serverClock;
+@property (nonatomic, strong) FPersistenceManager* persistenceManager;
+@property (nonatomic, strong) FIRDatabase *database;
+@property (nonatomic, strong, readwrite) FAuthenticationManager *auth;
+@property (nonatomic, strong) FSyncTree *infoSyncTree;
+@property (nonatomic) NSInteger writeIdCounter;
+@property (nonatomic) BOOL hijackHash;
+@property (nonatomic, strong) FTree *transactionQueueTree;
+@property (nonatomic) BOOL loggedTransactionPersistenceWarning;
+
+/**
+* Test only. For load testing the server.
+*/
+@property (nonatomic, strong) id (^interceptServerDataCallback)(NSString *pathString, id data);
+@end
+
+
+@implementation FRepo
+
+- (id)initWithRepoInfo:(FRepoInfo*)info config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database {
+ self = [super init];
+ if (self) {
+ self.repoInfo = info;
+ self.config = config;
+ self.database = database;
+
+ // Access can occur outside of shared queue, so the clock needs to be initialized here
+ self.serverClock = [[FOffsetClock alloc] initWithClock:[FSystemClock clock] offset:0];
+
+ self.connection = [[FPersistentConnection alloc] initWithRepoInfo:self.repoInfo dispatchQueue:[FIRDatabaseQuery sharedQueue] config:self.config];
+
+ // Needs to be called before authentication manager is instantiated
+ self.eventRaiser = [[FEventRaiser alloc] initWithQueue:self.config.callbackQueue];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self deferredInit];
+ });
+ }
+ return self;
+}
+
+- (void)deferredInit {
+ // TODO: cleanup on dealloc
+ __weak FRepo *weakSelf = self;
+ [self.config.authTokenProvider listenForTokenChanges:^(NSString *token) {
+ [weakSelf.connection refreshAuthToken:token];
+ }];
+
+ // Open connection now so that by the time we are connected the deferred init has run
+ // This relies on the fact that all callbacks run on repos queue
+ self.connection.delegate = self;
+ [self.connection open];
+
+ self.dataUpdateCount = 0;
+ self.rangeMergeUpdateCount = 0;
+ self.interceptServerDataCallback = nil;
+
+ if (self.config.persistenceEnabled) {
+ NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", self.repoInfo.host, self.repoInfo.namespace];
+ NSString* persistencePrefix = [NSString stringWithFormat:@"%@/%@", self.config.sessionIdentifier, repoHashString];
+
+ id<FCachePolicy> cachePolicy = [[FLRUCachePolicy alloc] initWithMaxSize:self.config.persistenceCacheSizeBytes];
+
+ id<FStorageEngine> engine;
+ if (self.config.forceStorageEngine != nil) {
+ engine = self.config.forceStorageEngine;
+ } else {
+ FLevelDBStorageEngine *levelDBEngine = [[FLevelDBStorageEngine alloc] initWithPath:persistencePrefix];
+ // We need the repo info to run the legacy migration. Future migrations will be managed by the database itself
+ // Remove this once we are confident that no-one is using legacy migration anymore...
+ [levelDBEngine runLegacyMigration:self.repoInfo];
+ engine = levelDBEngine;
+ }
+
+ self.persistenceManager = [[FPersistenceManager alloc] initWithStorageEngine:engine cachePolicy:cachePolicy];
+ } else {
+ self.persistenceManager = nil;
+ }
+
+ [self initTransactions];
+
+ // A list of data pieces and paths to be set when this client disconnects
+ self.onDisconnect = [[FSparseSnapshotTree alloc] init];
+ self.infoData = [[FSnapshotHolder alloc] init];
+
+ FListenProvider *infoListenProvider = [[FListenProvider alloc] init];
+ infoListenProvider.startListening = ^(FQuerySpec *query,
+ NSNumber *tagId,
+ id<FSyncTreeHash> hash,
+ fbt_nsarray_nsstring onComplete) {
+ NSArray *infoEvents = @[];
+ FRepo *strongSelf = weakSelf;
+ id<FNode> node = [strongSelf.infoData getNode:query.path];
+ // This is possibly a hack, but we have different semantics for .info endpoints. We don't raise null events
+ // on initial data...
+ if (![node isEmpty]) {
+ infoEvents = [strongSelf.infoSyncTree applyServerOverwriteAtPath:query.path newData:node];
+ [strongSelf.eventRaiser raiseCallback:^{
+ onComplete(kFWPResponseForActionStatusOk);
+ }];
+ }
+ return infoEvents;
+ };
+ infoListenProvider.stopListening = ^(FQuerySpec *query, NSNumber *tagId) {};
+ self.infoSyncTree = [[FSyncTree alloc] initWithListenProvider:infoListenProvider];
+
+ FListenProvider *serverListenProvider = [[FListenProvider alloc] init];
+ serverListenProvider.startListening = ^(FQuerySpec *query,
+ NSNumber *tagId,
+ id<FSyncTreeHash> hash,
+ fbt_nsarray_nsstring onComplete) {
+ [weakSelf.connection listen:query tagId:tagId hash:hash onComplete:^(NSString *status) {
+ NSArray *events = onComplete(status);
+ [weakSelf.eventRaiser raiseEvents:events];
+ }];
+ // No synchronous events for network-backed sync trees
+ return @[];
+ };
+ serverListenProvider.stopListening = ^(FQuerySpec *query, NSNumber *tag) {
+ [weakSelf.connection unlisten:query tagId:tag];
+ };
+ self.serverSyncTree = [[FSyncTree alloc] initWithPersistenceManager:self.persistenceManager
+ listenProvider:serverListenProvider];
+
+ [self restoreWrites];
+
+ [self updateInfo:kDotInfoConnected withValue:@NO];
+
+ [self setupNotifications];
+}
+
+
+- (void) restoreWrites {
+ NSArray *writes = self.persistenceManager.userWrites;
+
+ NSDictionary *serverValues = [FServerValues generateServerValues:self.serverClock];
+ __block NSInteger lastWriteId = NSIntegerMin;
+ [writes enumerateObjectsUsingBlock:^(FWriteRecord *write, NSUInteger idx, BOOL *stop) {
+ NSInteger writeId = write.writeId;
+ fbt_void_nsstring_nsstring callback = ^(NSString *status, NSString *errorReason) {
+ [self warnIfWriteFailedAtPath:write.path status:status message:@"Persisted write"];
+ [self ackWrite:writeId rerunTransactionsAtPath:write.path status:status];
+ };
+ if (lastWriteId >= writeId) {
+ [NSException raise:NSInternalInconsistencyException format:@"Restored writes were not in order!"];
+ }
+ lastWriteId = writeId;
+ self.writeIdCounter = writeId + 1;
+ if ([write isOverwrite]) {
+ FFLog(@"I-RDB038001", @"Restoring overwrite with id %ld", (long)write.writeId);
+ [self.connection putData:[write.overwrite valForExport:YES]
+ forPath:[write.path toString]
+ withHash:nil
+ withCallback:callback];
+ id<FNode> resolved = [FServerValues resolveDeferredValueSnapshot:write.overwrite withServerValues:serverValues];
+ [self.serverSyncTree applyUserOverwriteAtPath:write.path newData:resolved writeId:writeId isVisible:YES];
+ } else {
+ FFLog(@"I-RDB038002", @"Restoring merge with id %ld", (long)write.writeId);
+ [self.connection mergeData:[write.merge valForExport:YES]
+ forPath:[write.path toString]
+ withCallback:callback];
+ FCompoundWrite *resolved = [FServerValues resolveDeferredValueCompoundWrite:write.merge withServerValues:serverValues];
+ [self.serverSyncTree applyUserMergeAtPath:write.path changedChildren:resolved writeId:writeId];
+ }
+ }];
+}
+
+- (NSString*)name {
+ return self.repoInfo.namespace;
+}
+
+- (NSString *) description {
+ return [self.repoInfo description];
+}
+
+- (void) interrupt {
+ [self.connection interruptForReason:kFInterruptReasonRepoInterrupt];
+}
+
+- (void) resume {
+ [self.connection resumeForReason:kFInterruptReasonRepoInterrupt];
+}
+
+// NOTE: Typically if you're calling this, you should be in an @autoreleasepool block to make sure that ARC kicks
+// in and cleans up things no longer referenced (i.e. pendingPutsDB).
+- (void) dispose {
+ [self.connection interruptForReason:kFInterruptReasonRepoInterrupt];
+
+ // We need to nil out any references to LevelDB, to make sure the
+ // LevelDB exclusive locks are released.
+ [self.persistenceManager close];
+}
+
+- (NSInteger) nextWriteId {
+ return self->_writeIdCounter++;
+}
+
+- (NSTimeInterval) serverTime {
+ return [self.serverClock currentTime];
+}
+
+- (void) set:(FPath *)path withNode:(id<FNode>)node withCallback:(fbt_void_nserror_ref)onComplete {
+ id value = [node valForExport:YES];
+ FFLog(@"I-RDB038003", @"Setting: %@ with %@ pri: %@", [path toString], [value description], [[node getPriority] val]);
+
+ // TODO: Optimize this behavior to either (a) store flag to skip resolving where possible and / or
+ // (b) store unresolved paths on JSON parse
+ NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock];
+ id<FNode> newNode = [FServerValues resolveDeferredValueSnapshot:node withServerValues:serverValues];
+
+ NSInteger writeId = [self nextWriteId];
+ [self.persistenceManager saveUserOverwrite:node atPath:path writeId:writeId];
+ NSArray *events = [self.serverSyncTree applyUserOverwriteAtPath:path newData:newNode writeId:writeId isVisible:YES];
+ [self.eventRaiser raiseEvents:events];
+
+ [self.connection putData:value forPath:[path toString] withHash:nil withCallback:^(NSString *status, NSString *errorReason) {
+ [self warnIfWriteFailedAtPath:path status:status message:@"setValue: or removeValue:"];
+ [self ackWrite:writeId rerunTransactionsAtPath:path status:status];
+ [self callOnComplete:onComplete withStatus:status errorReason:errorReason andPath:path];
+ }];
+
+ FPath* affectedPath = [self abortTransactionsAtPath:path error:kFTransactionSet];
+ [self rerunTransactionsForPath:affectedPath];
+}
+
+- (void) update:(FPath *)path withNodes:(FCompoundWrite *)nodes withCallback:(fbt_void_nserror_ref)callback {
+ NSDictionary *values = [nodes valForExport:YES];
+
+ FFLog(@"I-RDB038004", @"Updating: %@ with %@", [path toString], [values description]);
+ NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock];
+ FCompoundWrite *resolved = [FServerValues resolveDeferredValueCompoundWrite:nodes withServerValues:serverValues];
+
+ if (!resolved.isEmpty) {
+ NSInteger writeId = [self nextWriteId];
+ [self.persistenceManager saveUserMerge:nodes atPath:path writeId:writeId];
+ NSArray *events = [self.serverSyncTree applyUserMergeAtPath:path changedChildren:resolved writeId:writeId];
+ [self.eventRaiser raiseEvents:events];
+
+ [self.connection mergeData:values forPath:[path description] withCallback:^(NSString *status, NSString *errorReason) {
+ [self warnIfWriteFailedAtPath:path status:status message:@"updateChildValues:"];
+ [self ackWrite:writeId rerunTransactionsAtPath:path status:status];
+ [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path];
+ }];
+
+ [nodes enumerateWrites:^(FPath *childPath, id<FNode> node, BOOL *stop) {
+ FPath* pathFromRoot = [path child:childPath];
+ FFLog(@"I-RDB038005", @"Cancelling transactions at path: %@", pathFromRoot);
+ FPath *affectedPath = [self abortTransactionsAtPath:pathFromRoot error:kFTransactionSet];
+ [self rerunTransactionsForPath:affectedPath];
+ }];
+ } else {
+ FFLog(@"I-RDB038006", @"update called with empty data. Doing nothing");
+ // Do nothing, just call the callback
+ [self callOnComplete:callback withStatus:@"ok" errorReason:nil andPath:path];
+ }
+}
+
+- (void) onDisconnectCancel:(FPath *)path withCallback:(fbt_void_nserror_ref)callback {
+ [self.connection onDisconnectCancelPath:path withCallback:^(NSString *status, NSString *errorReason) {
+ BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk];
+ if (success) {
+ [self.onDisconnect forgetPath:path];
+ } else {
+ FFLog(@"I-RDB038007", @"cancelDisconnectOperations: at %@ failed: %@", path, status);
+ }
+
+ [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path];
+ }];
+}
+
+- (void) onDisconnectSet:(FPath *)path withNode:(id<FNode>)node withCallback:(fbt_void_nserror_ref)callback {
+ [self.connection onDisconnectPutData:[node valForExport:YES] forPath:path withCallback:^(NSString *status, NSString *errorReason) {
+ BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk];
+ if (success) {
+ [self.onDisconnect rememberData:node onPath:path];
+ } else {
+ FFWarn(@"I-RDB038008", @"onDisconnectSetValue: or onDisconnectRemoveValue: at %@ failed: %@", path, status);
+ }
+
+ [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path];
+ }];
+}
+
+- (void) onDisconnectUpdate:(FPath *)path withNodes:(FCompoundWrite *)nodes withCallback:(fbt_void_nserror_ref)callback {
+ if (!nodes.isEmpty) {
+ NSDictionary *values = [nodes valForExport:YES];
+
+ [self.connection onDisconnectMergeData:values forPath:path withCallback:^(NSString *status, NSString *errorReason) {
+ BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk];
+ if (success) {
+ [nodes enumerateWrites:^(FPath *relativePath, id<FNode> nodeUnresolved, BOOL *stop) {
+ FPath* childPath = [path child:relativePath];
+ [self.onDisconnect rememberData:nodeUnresolved onPath:childPath];
+ }];
+ } else {
+ FFWarn(@"I-RDB038009", @"onDisconnectUpdateChildValues: at %@ failed %@", path, status);
+ }
+
+ [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path];
+ }];
+ } else {
+ // Do nothing, just call the callback
+ [self callOnComplete:callback withStatus:@"ok" errorReason:nil andPath:path];
+ }
+}
+
+- (void) purgeOutstandingWrites {
+ FFLog(@"I-RDB038010", @"Purging outstanding writes");
+ NSArray *events = [self.serverSyncTree removeAllWrites];
+ [self.eventRaiser raiseEvents:events];
+ // Abort any transactions
+ [self abortTransactionsAtPath:[FPath empty] error:kFErrorWriteCanceled];
+ // Remove outstanding writes from connection
+ [self.connection purgeOutstandingWrites];
+}
+
+- (void) addEventRegistration:(id <FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query {
+ NSArray *events = nil;
+ if ([[query.path getFront] isEqualToString:kDotInfoPrefix]) {
+ events = [self.infoSyncTree addEventRegistration:eventRegistration forQuery:query];
+ } else {
+ events = [self.serverSyncTree addEventRegistration:eventRegistration forQuery:query];
+ }
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (void) removeEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query {
+ // These are guaranteed not to raise events, since we're not passing in a cancelError. However we can future-proof
+ // a little bit by handling the return values anyways.
+ FFLog(@"I-RDB038011", @"Removing event registration with hande: %lu", (unsigned long)eventRegistration.handle);
+ NSArray *events = nil;
+ if ([[query.path getFront] isEqualToString:kDotInfoPrefix]) {
+ events = [self.infoSyncTree removeEventRegistration:eventRegistration forQuery:query cancelError:nil];
+ } else {
+ events = [self.serverSyncTree removeEventRegistration:eventRegistration forQuery:query cancelError:nil];
+ }
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (void) keepQuery:(FQuerySpec *)query synced:(BOOL)synced {
+ NSAssert(![[query.path getFront] isEqualToString:kDotInfoPrefix], @"Can't keep .info tree synced!");
+ [self.serverSyncTree keepQuery:query synced:synced];
+}
+
+- (void) updateInfo:(NSString *) pathString withValue:(id)value {
+ // hack to make serverTimeOffset available in a threadsafe way. Property is marked as atomic
+ if ([pathString isEqualToString:kDotInfoServerTimeOffset]) {
+ NSTimeInterval offset = [(NSNumber *)value doubleValue]/1000.0;
+ self.serverClock = [[FOffsetClock alloc] initWithClock:[FSystemClock clock] offset:offset];
+ }
+
+ FPath* path = [[FPath alloc] initWith:[NSString stringWithFormat:@"%@/%@", kDotInfoPrefix, pathString]];
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:value];
+ [self.infoData updateSnapshot:path withNewSnapshot:newNode];
+ NSArray *events = [self.infoSyncTree applyServerOverwriteAtPath:path newData:newNode];
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (void) callOnComplete:(fbt_void_nserror_ref)onComplete withStatus:(NSString *)status errorReason:(NSString *)errorReason andPath:(FPath *)path {
+ if (onComplete) {
+ FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithRepo:self path:path];
+ BOOL statusOk = [status isEqualToString:kFWPResponseForActionStatusOk];
+ NSError* err = nil;
+ if (!statusOk) {
+ err = [FUtilities errorForStatus:status andReason:errorReason];
+ }
+ [self.eventRaiser raiseCallback:^{
+ onComplete(err, ref);
+ }];
+ }
+}
+
+- (void)ackWrite:(NSInteger)writeId rerunTransactionsAtPath:(FPath *)path status:(NSString *)status {
+ if ([status isEqualToString:kFErrorWriteCanceled]) {
+ // This write was already removed, we just need to ignore it...
+ } else {
+ BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk];
+ NSArray *clearEvents = [self.serverSyncTree ackUserWriteWithWriteId:writeId revert:!success persist:YES clock:self.serverClock];
+ if ([clearEvents count] > 0) {
+ [self rerunTransactionsForPath:path];
+ }
+ [self.eventRaiser raiseEvents:clearEvents];
+ }
+}
+
+- (void) warnIfWriteFailedAtPath:(FPath *)path status:(NSString *)status message:(NSString *)message {
+ if (!([status isEqualToString:kFWPResponseForActionStatusOk] || [status isEqualToString:kFErrorWriteCanceled])) {
+ FFWarn(@"I-RDB038012", @"%@ at %@ failed: %@", message, path, status);
+ }
+}
+
+#pragma mark -
+#pragma mark FPersistentConnectionDelegate methods
+
+- (void) onDataUpdate:(FPersistentConnection *)fpconnection forPath:(NSString *)pathString message:(id)data isMerge:(BOOL)isMerge tagId:(NSNumber *)tagId {
+ FFLog(@"I-RDB038013", @"onDataUpdateForPath: %@ withMessage: %@", pathString, data);
+
+ // For testing.
+ self.dataUpdateCount++;
+
+ FPath* path = [[FPath alloc] initWith:pathString];
+ data = self.interceptServerDataCallback ? self.interceptServerDataCallback(pathString, data) : data;
+ NSArray *events = nil;
+
+ if (tagId != nil) {
+ if (isMerge) {
+ NSDictionary *message = data;
+ FCompoundWrite *taggedChildren = [FCompoundWrite compoundWriteWithValueDictionary:message];
+ events = [self.serverSyncTree applyTaggedQueryMergeAtPath:path changedChildren:taggedChildren tagId:tagId];
+ } else {
+ id<FNode> taggedSnap = [FSnapshotUtilities nodeFrom:data];
+ events = [self.serverSyncTree applyTaggedQueryOverwriteAtPath:path newData:taggedSnap tagId:tagId];
+ }
+ } else if (isMerge) {
+ NSDictionary *message = data;
+ FCompoundWrite *changedChildren = [FCompoundWrite compoundWriteWithValueDictionary:message];
+ events = [self.serverSyncTree applyServerMergeAtPath:path changedChildren:changedChildren];
+ } else {
+ id<FNode> snap = [FSnapshotUtilities nodeFrom:data];
+ events = [self.serverSyncTree applyServerOverwriteAtPath:path newData:snap];
+ }
+
+ if ([events count] > 0) {
+ // Since we have a listener outstanding for each transaction, receiving any events
+ // is a proxy for some change having occurred.
+ [self rerunTransactionsForPath:path];
+ }
+
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (void)onRangeMerge:(NSArray *)ranges forPath:(NSString *)pathString tagId:(NSNumber *)tag {
+ FFLog(@"I-RDB038014", @"onRangeMerge: %@ => %@", pathString, ranges);
+
+ // For testing
+ self.rangeMergeUpdateCount++;
+
+ FPath* path = [[FPath alloc] initWith:pathString];
+ NSArray *events;
+ if (tag != nil) {
+ events = [self.serverSyncTree applyTaggedServerRangeMergeAtPath:path updates:ranges tagId:tag];
+ } else {
+ events = [self.serverSyncTree applyServerRangeMergeAtPath:path updates:ranges];
+ }
+ if (events.count > 0) {
+ // Since we have a listener outstanding for each transaction, receiving any events
+ // is a proxy for some change having occurred.
+ [self rerunTransactionsForPath:path];
+ }
+
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (void)onConnect:(FPersistentConnection *)fpconnection {
+ [self updateInfo:kDotInfoConnected withValue:@true];
+}
+
+- (void)onDisconnect:(FPersistentConnection *)fpconnection {
+ [self updateInfo:kDotInfoConnected withValue:@false];
+ [self runOnDisconnectEvents];
+}
+
+- (void)onServerInfoUpdate:(FPersistentConnection *)fpconnection updates:(NSDictionary *)updates {
+ for (NSString* key in updates) {
+ id val = [updates objectForKey:key];
+ [self updateInfo:key withValue:val];
+ }
+}
+
+- (void) setupNotifications {
+ NSString * const *backgroundConstant = (NSString * const *) dlsym(RTLD_DEFAULT, "UIApplicationDidEnterBackgroundNotification");
+ if (backgroundConstant) {
+ FFLog(@"I-RDB038015", @"Registering for background notification.");
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(didEnterBackground)
+ name:*backgroundConstant
+ object:nil];
+ } else {
+ FFLog(@"I-RDB038016", @"Skipped registering for background notification.");
+ }
+}
+
+- (void) didEnterBackground {
+ if (!self.config.persistenceEnabled)
+ return;
+
+ // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build.
+#if TARGET_OS_IPHONE
+ // The idea is to wait until any outstanding sets get written to disk. Since the sets might still be in our
+ // dispatch queue, we wait for the dispatch queue to catch up and for persistence to catch up.
+ // This may be undesirable though. The dispatch queue might just be processing a bunch of incoming data or
+ // something. We might want to keep track of whether there are any unpersisted sets or something.
+ FFLog(@"I-RDB038017", @"Entering background. Starting background task to finish work.");
+ Class uiApplicationClass = NSClassFromString(@"UIApplication");
+ assert(uiApplicationClass); // If we are here, we should be on iOS and UIApplication should be available.
+
+ UIApplication *application = [uiApplicationClass sharedApplication];
+ __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
+ [application endBackgroundTask:bgTask];
+ }];
+
+ NSDate *start = [NSDate date];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ NSTimeInterval finishTime = [start timeIntervalSinceNow]*-1;
+ FFLog(@"I-RDB038018", @"Background task completed. Queue time: %f", finishTime);
+ [application endBackgroundTask:bgTask];
+ });
+#endif
+}
+
+#pragma mark -
+#pragma mark Internal methods
+
+/**
+* Applies all the changes stored up in the onDisconnect tree
+*/
+- (void) runOnDisconnectEvents {
+ FFLog(@"I-RDB038019", @"Running onDisconnectEvents");
+ NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock];
+ FSparseSnapshotTree* resolvedTree = [FServerValues resolveDeferredValueTree:self.onDisconnect withServerValues:serverValues];
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+
+ [resolvedTree forEachTreeAtPath:[FPath empty] do:^(FPath *path, id<FNode> node) {
+ [events addObjectsFromArray:[self.serverSyncTree applyServerOverwriteAtPath:path newData:node]];
+ FPath* affectedPath = [self abortTransactionsAtPath:path error:kFTransactionSet];
+ [self rerunTransactionsForPath:affectedPath];
+ }];
+
+ self.onDisconnect = [[FSparseSnapshotTree alloc] init];
+ [self.eventRaiser raiseEvents:events];
+}
+
+- (NSDictionary *) dumpListens {
+ return [self.connection dumpListens];
+}
+
+#pragma mark -
+#pragma mark Transactions
+
+/**
+ * Setup the transaction data structures
+ */
+- (void) initTransactions {
+ self.transactionQueueTree = [[FTree alloc] init];
+ self.hijackHash = NO;
+ self.loggedTransactionPersistenceWarning = NO;
+}
+
+/**
+ * Creates a new transaction, add its to the transactions we're tracking, and sends it to the server if possible
+ */
+- (void) startTransactionOnPath:(FPath *)path update:(fbt_transactionresult_mutabledata)update onComplete:(fbt_void_nserror_bool_datasnapshot)onComplete withLocalEvents:(BOOL)applyLocally {
+ if (self.config.persistenceEnabled && !self.loggedTransactionPersistenceWarning) {
+ self.loggedTransactionPersistenceWarning = YES;
+ FFInfo(@"I-RDB038020", @"runTransactionBlock: usage detected while persistence is enabled. Please be aware that transactions "
+ @"*will not* be persisted across app restarts. "
+ @"See https://www.firebase.com/docs/ios/guide/offline-capabilities.html#section-handling-transactions-offline for more details.");
+ }
+
+ FIRDatabaseReference * watchRef = [[FIRDatabaseReference alloc] initWithRepo:self path:path];
+ // make sure we're listening on this node
+ // Note: we can't do this asynchronously. To preserve event ordering, it has to be done in this block.
+ // This is ok, this block is guaranteed to be our own event loop
+ NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
+ fbt_void_datasnapshot cb = ^(FIRDataSnapshot *snapshot) {};
+ FValueEventRegistration *registration = [[FValueEventRegistration alloc] initWithRepo:self
+ handle:handle
+ callback:cb
+ cancelCallback:nil];
+ [watchRef.repo addEventRegistration:registration forQuery:watchRef.querySpec];
+ fbt_void_void unwatcher = ^{ [watchRef removeObserverWithHandle:handle]; };
+
+ // Save all the data that represents this transaction
+ FTupleTransaction* transaction = [[FTupleTransaction alloc] init];
+ transaction.path = path;
+ transaction.update = update;
+ transaction.onComplete = onComplete;
+ transaction.status = FTransactionInitializing;
+ transaction.order = [FUtilities LUIDGenerator];
+ transaction.applyLocally = applyLocally;
+ transaction.retryCount = 0;
+ transaction.unwatcher = unwatcher;
+ transaction.currentWriteId = nil;
+ transaction.currentInputSnapshot = nil;
+ transaction.currentOutputSnapshotRaw = nil;
+ transaction.currentOutputSnapshotResolved = nil;
+
+ // Run transaction initially
+ id<FNode> currentState = [self latestStateAtPath:path excludeWriteIds:nil];
+ transaction.currentInputSnapshot = currentState;
+ FIRMutableData * mutableCurrent = [[FIRMutableData alloc] initWithNode:currentState];
+ FIRTransactionResult * result = transaction.update(mutableCurrent);
+
+ if (!result.isSuccess) {
+ // Abort the transaction
+ transaction.unwatcher();
+ transaction.currentOutputSnapshotRaw = nil;
+ transaction.currentOutputSnapshotResolved = nil;
+ if (transaction.onComplete) {
+ FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self path:transaction.path];
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:transaction.currentInputSnapshot];
+ FIRDataSnapshot *snap = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:indexedNode];
+ [self.eventRaiser raiseCallback:^{
+ transaction.onComplete(nil, NO, snap);
+ }];
+ }
+ } else {
+ // Note: different from js. We don't need to validate, FIRMutableData does validation.
+ // We also don't have to worry about priorities. Just mark as run and add to queue.
+ transaction.status = FTransactionRun;
+ FTree* queueNode = [self.transactionQueueTree subTree:transaction.path];
+ NSMutableArray* nodeQueue = [queueNode getValue];
+ if (nodeQueue == nil) {
+ nodeQueue = [[NSMutableArray alloc] init];
+ }
+ [nodeQueue addObject:transaction];
+ [queueNode setValue:nodeQueue];
+
+ // Update visibleData and raise events
+ // Note: We intentionally raise events after updating all of our transaction state, since the user could
+ // start new transactions from the event callbacks
+ NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock];
+ id<FNode> newValUnresolved = [result.update nodeValue];
+ id<FNode> newVal = [FServerValues resolveDeferredValueSnapshot:newValUnresolved withServerValues:serverValues];
+ transaction.currentOutputSnapshotRaw = newValUnresolved;
+ transaction.currentOutputSnapshotResolved = newVal;
+ transaction.currentWriteId = [NSNumber numberWithInteger:[self nextWriteId]];
+
+ NSArray *events = [self.serverSyncTree applyUserOverwriteAtPath:path newData:newVal
+ writeId:[transaction.currentWriteId integerValue]
+ isVisible:transaction.applyLocally];
+ [self.eventRaiser raiseEvents:events];
+
+ [self sendAllReadyTransactions];
+ }
+}
+
+/**
+ * @param writeIdsToExclude A specific set to exclude
+ */
+- (id<FNode>) latestStateAtPath:(FPath *)path excludeWriteIds:(NSArray *)writeIdsToExclude {
+ id<FNode> latestState = [self.serverSyncTree calcCompleteEventCacheAtPath:path excludeWriteIds:writeIdsToExclude];
+ return latestState ? latestState : [FEmptyNode emptyNode];
+}
+
+/**
+ * Sends any already-run transactions that aren't waiting for outstanding transactions to complete.
+ *
+ * Externally, call the version with no arguments.
+ * Internally, calls itself recursively with a particular transactionQueueTree node to recurse through the tree
+ */
+- (void) sendAllReadyTransactions {
+ FTree* node = self.transactionQueueTree;
+
+ [self pruneCompletedTransactionsBelowNode:node];
+ [self sendReadyTransactionsForTree:node];
+}
+
+- (void) sendReadyTransactionsForTree:(FTree *)node {
+ NSMutableArray* queue = [node getValue];
+ if (queue != nil) {
+ queue = [self buildTransactionQueueAtNode:node];
+ NSAssert([queue count] > 0, @"Sending zero length transaction queue");
+
+ NSUInteger notRunIndex = [queue indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
+ return ((FTupleTransaction*)obj).status != FTransactionRun;
+ }];
+
+ // If they're all run (and not sent), we can send them. Else, we must wait.
+ if (notRunIndex == NSNotFound) {
+ [self sendTransactionQueue:queue atPath:node.path];
+ }
+ } else if ([node hasChildren]) {
+ [node forEachChild:^(FTree *child) {
+ [self sendReadyTransactionsForTree:child];
+ }];
+ }
+}
+
+/**
+ * Given a list of run transactions, send them to the server and then handle the result (success or failure).
+ */
+- (void) sendTransactionQueue:(NSMutableArray *)queue atPath:(FPath *)path {
+ // Mark transactions as sent and bump the retry count
+ NSMutableArray *writeIdsToExclude = [[NSMutableArray alloc] init];
+ for (FTupleTransaction *transaction in queue) {
+ [writeIdsToExclude addObject:transaction.currentWriteId];
+ }
+ id<FNode> latestState = [self latestStateAtPath:path excludeWriteIds:writeIdsToExclude];
+ id<FNode> snapToSend = latestState;
+ NSString *latestHash = [latestState dataHash];
+ for (FTupleTransaction* transaction in queue) {
+ NSAssert(transaction.status == FTransactionRun, @"[FRepo sendTransactionQueue:] items in queue should all be run.");
+ FFLog(@"I-RDB038021", @"Transaction at %@ set to SENT", transaction.path);
+ transaction.status = FTransactionSent;
+ transaction.retryCount++;
+ FPath *relativePath = [FPath relativePathFrom:path to:transaction.path];
+ // If we've gotten to this point, the output snapshot must be defined.
+ snapToSend = [snapToSend updateChild:relativePath withNewChild:transaction.currentOutputSnapshotRaw];
+ }
+
+ id dataToSend = [snapToSend valForExport:YES];
+ NSString *pathToSend = [path description];
+ latestHash = self.hijackHash ? @"badhash" : latestHash;
+
+ // Send the put
+ [self.connection putData:dataToSend forPath:pathToSend withHash:latestHash withCallback:^(NSString *status, NSString *errorReason) {
+ FFLog(@"I-RDB038022", @"Transaction put response: %@ : %@", pathToSend, status);
+
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ if ([status isEqualToString:kFWPResponseForActionStatusOk]) {
+ // Queue up the callbacks and fire them after cleaning up all of our transaction state, since
+ // the callback could trigger more transactions or sets.
+ NSMutableArray *callbacks = [[NSMutableArray alloc] init];
+ for (FTupleTransaction *transaction in queue) {
+ transaction.status = FTransactionCompleted;
+ [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue]
+ revert:NO
+ persist:NO
+ clock:self.serverClock]];
+ if (transaction.onComplete) {
+ // We never unset the output snapshot, and given that this transaction is complete, it should be set
+ id <FNode> node = transaction.currentOutputSnapshotResolved;
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:node];
+ FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self path:transaction.path];
+ FIRDataSnapshot *snapshot = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:indexedNode];
+ fbt_void_void cb = ^{
+ transaction.onComplete(nil, YES, snapshot);
+ };
+ [callbacks addObject:[cb copy]];
+ }
+ transaction.unwatcher();
+ }
+
+ // Now remove the completed transactions.
+ [self pruneCompletedTransactionsBelowNode:[self.transactionQueueTree subTree:path]];
+ // There may be pending transactions that we can now send.
+ [self sendAllReadyTransactions];
+
+ // Finally, trigger onComplete callbacks
+ [self.eventRaiser raiseCallbacks:callbacks];
+ } else {
+ // transactions are no longer sent. Update their status appropriately.
+ if ([status isEqualToString:kFWPResponseForActionStatusDataStale]) {
+ for (FTupleTransaction *transaction in queue) {
+ if (transaction.status == FTransactionSentNeedsAbort) {
+ transaction.status = FTransactionNeedsAbort;
+ } else {
+ transaction.status = FTransactionRun;
+ }
+ }
+ } else {
+ FFWarn(@"I-RDB038023", @"runTransactionBlock: at %@ failed: %@", path, status);
+ for (FTupleTransaction *transaction in queue) {
+ transaction.status = FTransactionNeedsAbort;
+ [transaction setAbortStatus:status reason:errorReason];
+ }
+ }
+ }
+
+ [self rerunTransactionsForPath:path];
+ [self.eventRaiser raiseEvents:events];
+ }];
+}
+
+/**
+ * Finds all transactions dependent on the data at changed Path and reruns them.
+ *
+ * Should be called any time cached data changes.
+ *
+ * Return the highest path that was affected by rerunning transactions. This is the path at which events need to
+ * be raised for.
+ */
+- (FPath *) rerunTransactionsForPath:(FPath *)changedPath {
+ // For the common case that there are no transactions going on, skip all this!
+ if ([self.transactionQueueTree isEmpty]) {
+ return changedPath;
+ } else {
+ FTree* rootMostTransactionNode = [self getAncestorTransactionNodeForPath:changedPath];
+ FPath* path = rootMostTransactionNode.path;
+
+ NSArray* queue = [self buildTransactionQueueAtNode:rootMostTransactionNode];
+ [self rerunTransactionQueue:queue atPath:path];
+
+ return path;
+ }
+}
+
+/**
+ * Does all the work of rerunning transactions (as well as cleans up aborted transactions and whatnot).
+ */
+- (void) rerunTransactionQueue:(NSArray *)queue atPath:(FPath *)path {
+ if (queue.count == 0) {
+ return; // nothing to do
+ }
+
+ // Queue up the callbacks and fire them after cleaning up all of our transaction state, since
+ // the callback could trigger more transactions or sets.
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ NSMutableArray *callbacks = [[NSMutableArray alloc] init];
+
+ // Ignore, by default, all of the sets in this queue, since we're re-running all of them. However, we want to include
+ // the results of new sets triggered as part of this re-run, so we don't want to ignore a range, just these specific
+ // sets.
+ NSMutableArray *writeIdsToExclude = [[NSMutableArray alloc] init];
+ for (FTupleTransaction *transaction in queue) {
+ [writeIdsToExclude addObject:transaction.currentWriteId];
+ }
+
+ for (FTupleTransaction* transaction in queue) {
+ FPath* relativePath __unused = [FPath relativePathFrom:path to:transaction.path];
+ BOOL abortTransaction = NO;
+ NSAssert(relativePath != nil, @"[FRepo rerunTransactionsQueue:] relativePath should not be null.");
+
+ if (transaction.status == FTransactionNeedsAbort) {
+ abortTransaction = YES;
+ if (![transaction.abortStatus isEqualToString:kFErrorWriteCanceled]) {
+ NSArray *ackEvents = [self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue]
+ revert:YES
+ persist:NO
+ clock:self.serverClock];
+ [events addObjectsFromArray:ackEvents];
+ }
+ } else if (transaction.status == FTransactionRun) {
+ if (transaction.retryCount >= kFTransactionMaxRetries) {
+ abortTransaction = YES;
+ [transaction setAbortStatus:kFTransactionTooManyRetries reason:nil];
+ [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue]
+ revert:YES
+ persist:NO
+ clock:self.serverClock]];
+ } else {
+ // This code reruns a transaction
+ id<FNode> currentNode = [self latestStateAtPath:transaction.path excludeWriteIds:writeIdsToExclude];
+ transaction.currentInputSnapshot = currentNode;
+ FIRMutableData * mutableCurrent = [[FIRMutableData alloc] initWithNode:currentNode];
+ FIRTransactionResult * result = transaction.update(mutableCurrent);
+ if (result.isSuccess) {
+ NSNumber *oldWriteId = transaction.currentWriteId;
+ NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock];
+
+ id<FNode> newVal = [result.update nodeValue];
+ id<FNode> newValResolved = [FServerValues resolveDeferredValueSnapshot:newVal withServerValues:serverValues];
+
+ transaction.currentOutputSnapshotRaw = newVal;
+ transaction.currentOutputSnapshotResolved = newValResolved;
+
+ transaction.currentWriteId = [NSNumber numberWithInteger:[self nextWriteId]];
+ // Mutates writeIdsToExclude in place
+ [writeIdsToExclude removeObject:oldWriteId];
+ [events addObjectsFromArray:[self.serverSyncTree applyUserOverwriteAtPath:transaction.path
+ newData:transaction.currentOutputSnapshotResolved
+ writeId:[transaction.currentWriteId integerValue]
+ isVisible:transaction.applyLocally]];
+ [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[oldWriteId integerValue]
+ revert:YES
+ persist:NO
+ clock:self.serverClock]];
+ } else {
+ abortTransaction = YES;
+ // The user aborted the transaction. JS treats ths as a "nodata" abort, but it's not an error, so we don't send them an error.
+ [transaction setAbortStatus:nil reason:nil];
+ [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue]
+ revert:YES
+ persist:NO
+ clock:self.serverClock]];
+ }
+ }
+ }
+
+ [self.eventRaiser raiseEvents:events];
+ events = nil;
+
+ if (abortTransaction) {
+ // Abort
+ transaction.status = FTransactionCompleted;
+ transaction.unwatcher();
+ if (transaction.onComplete) {
+ FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithRepo:self path:transaction.path];
+ FIndexedNode *lastInput = [FIndexedNode indexedNodeWithNode:transaction.currentInputSnapshot];
+ FIRDataSnapshot * snap = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:lastInput];
+ fbt_void_void cb = ^{
+ // Unlike JS, no need to check for "nodata" because ObjC has abortError = nil
+ transaction.onComplete(transaction.abortError, NO, snap);
+ };
+ [callbacks addObject:[cb copy]];
+ }
+ }
+ }
+
+ // Note: unlike current js client, we don't need to preserve priority. Users can set priority via FIRMutableData
+
+ // Clean up completed transactions.
+ [self pruneCompletedTransactionsBelowNode:self.transactionQueueTree];
+
+ // Now fire callbacks, now that we're in a good, known state.
+ [self.eventRaiser raiseCallbacks:callbacks];
+
+ // Try to send the transaction result to the server
+ [self sendAllReadyTransactions];
+}
+
+- (FTree *) getAncestorTransactionNodeForPath:(FPath *)path {
+ FTree* transactionNode = self.transactionQueueTree;
+
+ while (![path isEmpty] && [transactionNode getValue] == nil) {
+ NSString* front = [path getFront];
+ transactionNode = [transactionNode subTree:[[FPath alloc] initWith:front]];
+ path = [path popFront];
+ }
+
+ return transactionNode;
+}
+
+- (NSMutableArray *) buildTransactionQueueAtNode:(FTree *)node {
+ NSMutableArray* queue = [[NSMutableArray alloc] init];
+ [self aggregateTransactionQueuesForNode:node andQueue:queue];
+
+ [queue sortUsingComparator:^NSComparisonResult(FTupleTransaction* obj1, FTupleTransaction* obj2) {
+ return [obj1.order compare:obj2.order];
+ }];
+
+ return queue;
+}
+
+- (void) aggregateTransactionQueuesForNode:(FTree *)node andQueue:(NSMutableArray *)queue {
+ NSArray* nodeQueue = [node getValue];
+ [queue addObjectsFromArray:nodeQueue];
+
+ [node forEachChild:^(FTree *child) {
+ [self aggregateTransactionQueuesForNode:child andQueue:queue];
+ }];
+}
+
+/**
+ * Remove COMPLETED transactions at or below this node in the transactionQueueTree
+ */
+- (void) pruneCompletedTransactionsBelowNode:(FTree *)node {
+ NSMutableArray* queue = [node getValue];
+ if (queue != nil) {
+ int i = 0;
+ // remove all of the completed transactions from the queue
+ while (i < queue.count) {
+ FTupleTransaction* transaction = [queue objectAtIndex:i];
+ if (transaction.status == FTransactionCompleted) {
+ [queue removeObjectAtIndex:i];
+ } else {
+ i++;
+ }
+ }
+ if (queue.count > 0) {
+ [node setValue:queue];
+ } else {
+ [node setValue:nil];
+ }
+ }
+
+ [node forEachChildMutationSafe:^(FTree *child) {
+ [self pruneCompletedTransactionsBelowNode:child];
+ }];
+}
+
+/**
+ * Aborts all transactions on ancestors or descendants of the specified path. Called when doing a setValue: or
+ * updateChildValues: since we consider them incompatible with transactions
+ *
+ * @param path path for which we want to abort related transactions.
+ */
+- (FPath *) abortTransactionsAtPath:(FPath *)path error:(NSString *)error {
+ // For the common case that there are no transactions going on, skip all this!
+ if ([self.transactionQueueTree isEmpty]) {
+ return path;
+ } else {
+ FPath* affectedPath = [self getAncestorTransactionNodeForPath:path].path;
+
+ FTree* transactionNode = [self.transactionQueueTree subTree:path];
+ [transactionNode forEachAncestor:^BOOL(FTree *ancestor) {
+ [self abortTransactionsAtNode:ancestor error:error];
+ return NO;
+ }];
+
+ [self abortTransactionsAtNode:transactionNode error:error];
+
+ [transactionNode forEachDescendant:^(FTree *child) {
+ [self abortTransactionsAtNode:child error:error];
+ }];
+
+ return affectedPath;
+ }
+}
+
+/**
+ * Abort transactions stored in this transactions queue node.
+ *
+ * @param node Node to abort transactions for.
+ */
+- (void) abortTransactionsAtNode:(FTree *)node error:(NSString *)error {
+ NSMutableArray* queue = [node getValue];
+ if (queue != nil) {
+
+ // Queue up the callbacks and fire them after cleaning up all of our transaction state, since
+ // can be immediately aborted and removed.
+ NSMutableArray* callbacks = [[NSMutableArray alloc] init];
+
+ // Go through queue. Any already-sent transactions must be marked for abort, while the unsent ones
+ // can be immediately aborted and removed
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ int lastSent = -1;
+ // Note: all of the sent transactions will be at the front of the queue, so safe to increment lastSent
+ for (FTupleTransaction* transaction in queue) {
+ if (transaction.status == FTransactionSentNeedsAbort) {
+ // No-op. already marked.
+ } else if (transaction.status == FTransactionSent) {
+ // Mark this transaction for abort when it returns
+ lastSent++;
+ transaction.status = FTransactionSentNeedsAbort;
+ [transaction setAbortStatus:error reason:nil];
+ } else {
+ // we can abort this immediately
+ transaction.unwatcher();
+ if ([error isEqualToString:kFTransactionSet]) {
+ [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue]
+ revert:YES
+ persist:NO
+ clock:self.serverClock]];
+ } else {
+ // If it was cancelled it was already removed from the sync tree, no need to ack
+ NSAssert([error isEqualToString:kFErrorWriteCanceled], nil);
+ }
+
+ if (transaction.onComplete) {
+ NSError* abortReason = [FUtilities errorForStatus:error andReason:nil];
+ FIRDataSnapshot * snapshot = nil;
+ fbt_void_void cb = ^{
+ transaction.onComplete(abortReason, NO, snapshot);
+ };
+ [callbacks addObject:[cb copy]];
+ }
+ }
+ }
+ if (lastSent == -1) {
+ // We're not waiting for any sent transactions. We can clear the queue.
+ [node setValue:nil];
+ } else {
+ // Remove the transactions we aborted
+ NSRange theRange;
+ theRange.location = lastSent + 1;
+ theRange.length = queue.count - theRange.location;
+ [queue removeObjectsInRange:theRange];
+ }
+
+ // Now fire the callbacks
+ [self.eventRaiser raiseEvents:events];
+ [self.eventRaiser raiseCallbacks:callbacks];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Core/FRepoInfo.h b/Firebase/Database/Core/FRepoInfo.h
new file mode 100644
index 0000000..dace937
--- /dev/null
+++ b/Firebase/Database/Core/FRepoInfo.h
@@ -0,0 +1,34 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@interface FRepoInfo : NSObject
+
+@property (nonatomic, readonly, strong) NSString* host;
+@property (nonatomic, readonly, strong) NSString* namespace;
+@property (nonatomic, strong) NSString* internalHost;
+@property (nonatomic, readonly) bool secure;
+
+- (id) initWithHost:(NSString*)host isSecure:(bool)secure withNamespace:(NSString*)namespace;
+
+- (NSString *) connectionURLWithLastSessionID:(NSString*)lastSessionID;
+- (NSString *) connectionURL;
+- (void) clearInternalHostCache;
+- (BOOL) isDemoHost;
+- (BOOL) isCustomHost;
+
+@end
diff --git a/Firebase/Database/Core/FRepoInfo.m b/Firebase/Database/Core/FRepoInfo.m
new file mode 100644
index 0000000..6b15fe5
--- /dev/null
+++ b/Firebase/Database/Core/FRepoInfo.m
@@ -0,0 +1,115 @@
+/*
+ * 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 "FRepoInfo.h"
+#import "FConstants.h"
+
+@interface FRepoInfo ()
+
+@property (nonatomic, strong) NSString *domain;
+
+@end
+
+
+@implementation FRepoInfo
+
+@synthesize namespace;
+@synthesize host;
+@synthesize internalHost;
+@synthesize secure;
+@synthesize domain;
+
+- (id) initWithHost:(NSString*)aHost isSecure:(bool)isSecure withNamespace:(NSString*)aNamespace {
+ self = [super init];
+ if (self) {
+ host = aHost;
+ domain = [host substringFromIndex:[host rangeOfString:@"."].location+1];
+ secure = isSecure;
+ namespace = aNamespace;
+
+ // Get cached internal host if it exists
+ NSString* internalHostKey = [NSString stringWithFormat:@"firebase:host:%@", self.host];
+ NSString* cachedInternalHost = [[NSUserDefaults standardUserDefaults] stringForKey:internalHostKey];
+ if (cachedInternalHost != nil) {
+ internalHost = cachedInternalHost;
+ } else {
+ internalHost = self.host;
+ }
+ }
+ return self;
+}
+
+- (NSString *)description {
+ // The namespace is encoded in the hostname, so we can just return this.
+ return [NSString stringWithFormat:@"http%@://%@", (self.secure ? @"s" : @""), self.host];
+}
+
+- (void) setInternalHost:(NSString *)newHost {
+ if (![internalHost isEqualToString:newHost]) {
+ internalHost = newHost;
+
+ // Cache the internal host so we don't need to redirect later on
+ NSString* internalHostKey = [NSString stringWithFormat:@"firebase:host:%@", self.host];
+ NSUserDefaults* cache = [NSUserDefaults standardUserDefaults];
+ [cache setObject:internalHost forKey:internalHostKey];
+ [cache synchronize];
+ }
+}
+
+- (void) clearInternalHostCache {
+ internalHost = self.host;
+
+ // Remove the cached entry
+ NSString* internalHostKey = [NSString stringWithFormat:@"firebase:host:%@", self.host];
+ NSUserDefaults* cache = [NSUserDefaults standardUserDefaults];
+ [cache removeObjectForKey:internalHostKey];
+ [cache synchronize];
+}
+
+- (BOOL) isDemoHost {
+ return [self.domain isEqualToString:@"firebaseio-demo.com"];
+}
+
+- (BOOL) isCustomHost {
+ return ![self.domain isEqualToString:@"firebaseio-demo.com"] && ![self.domain isEqualToString:@"firebaseio.com"];
+}
+
+
+- (NSString *) connectionURL {
+ return [self connectionURLWithLastSessionID:nil];
+}
+
+- (NSString *) connectionURLWithLastSessionID:(NSString*)lastSessionID {
+ NSString *scheme;
+ if (self.secure) {
+ scheme = @"wss";
+ } else {
+ scheme = @"ws";
+ }
+ NSString *url = [NSString stringWithFormat:@"%@://%@/.ws?%@=%@&ns=%@",
+ scheme,
+ self.internalHost,
+ kWireProtocolVersionParam,
+ kWebsocketProtocolVersion,
+ self.namespace];
+
+ if (lastSessionID != nil) {
+ url = [NSString stringWithFormat:@"%@&ls=%@", url, lastSessionID];
+ }
+ return url;
+}
+
+@end
diff --git a/Firebase/Database/Core/FRepoManager.h b/Firebase/Database/Core/FRepoManager.h
new file mode 100644
index 0000000..c492861
--- /dev/null
+++ b/Firebase/Database/Core/FRepoManager.h
@@ -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 <Foundation/Foundation.h>
+#import "FRepoInfo.h"
+#import "FRepo.h"
+#import "FIRDatabaseConfig.h"
+
+@interface FRepoManager : NSObject
+
++ (FRepo *) getRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config;
++ (FRepo *) createRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database;
++ (void) interruptAll;
++ (void) interrupt:(FIRDatabaseConfig *)config;
++ (void) resumeAll;
++ (void) resume:(FIRDatabaseConfig *)config;
++ (void) disposeRepos:(FIRDatabaseConfig *)config;
+
+@end
diff --git a/Firebase/Database/Core/FRepoManager.m b/Firebase/Database/Core/FRepoManager.m
new file mode 100644
index 0000000..6dccf7e
--- /dev/null
+++ b/Firebase/Database/Core/FRepoManager.m
@@ -0,0 +1,131 @@
+/*
+ * 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 "FRepoManager.h"
+#import "FRepo.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FAtomicNumber.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FIRDatabase_Private.h"
+
+@implementation FRepoManager
+
++ (NSMutableDictionary *)configs {
+ static dispatch_once_t pred = 0;
+ static NSMutableDictionary *configs;
+ dispatch_once(&pred, ^{
+ configs = [NSMutableDictionary dictionary];
+ });
+ return configs;
+}
+
+/**
+ * Used for legacy unit tests. The public API should go through FirebaseDatabase which
+ * calls createRepo.
+ */
++ (FRepo *) getRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config {
+ [config freeze];
+ NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", repoInfo.host, repoInfo.namespace];
+ NSMutableDictionary *configs = [FRepoManager configs];
+ @synchronized(configs) {
+ NSMutableDictionary *repos = configs[config.sessionIdentifier];
+ if (!repos || repos[repoHashString] == nil) {
+ // Calling this should create the repo.
+ [FIRDatabase createDatabaseForTests:repoInfo config:config];
+ }
+
+ return configs[config.sessionIdentifier][repoHashString];
+ }
+}
+
++ (FRepo *) createRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database {
+ [config freeze];
+ NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", repoInfo.host, repoInfo.namespace];
+ NSMutableDictionary *configs = [FRepoManager configs];
+ @synchronized(configs) {
+ NSMutableDictionary *repos = configs[config.sessionIdentifier];
+ if (!repos) {
+ repos = [NSMutableDictionary dictionary];
+ configs[config.sessionIdentifier] = repos;
+ }
+ FRepo *repo = repos[repoHashString];
+ if (repo == nil) {
+ repo = [[FRepo alloc] initWithRepoInfo:repoInfo config:config database:database];
+ repos[repoHashString] = repo;
+ return repo;
+ } else {
+ [NSException raise:@"RepoExists" format:@"createRepo called for Repo that already exists."];
+ return nil;
+ }
+ }
+}
+
++ (void) interrupt:(FIRDatabaseConfig *)config {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ NSMutableDictionary *configs = [FRepoManager configs];
+ NSMutableDictionary *repos = configs[config.sessionIdentifier];
+ for (FRepo* repo in [repos allValues]) {
+ [repo interrupt];
+ }
+ });
+}
+
++ (void) interruptAll {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ NSMutableDictionary *configs = [FRepoManager configs];
+ for (NSMutableDictionary *repos in [configs allValues]) {
+ for (FRepo* repo in [repos allValues]) {
+ [repo interrupt];
+ }
+ }
+ });
+}
+
++ (void) resume:(FIRDatabaseConfig *)config {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ NSMutableDictionary *configs = [FRepoManager configs];
+ NSMutableDictionary *repos = configs[config.sessionIdentifier];
+ for (FRepo* repo in [repos allValues]) {
+ [repo resume];
+ }
+ });
+}
+
++ (void) resumeAll {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ NSMutableDictionary *configs = [FRepoManager configs];
+ for (NSMutableDictionary *repos in [configs allValues]) {
+ for (FRepo* repo in [repos allValues]) {
+ [repo resume];
+ }
+ }
+ });
+}
+
++ (void)disposeRepos:(FIRDatabaseConfig *)config {
+ // Do this synchronously to make sure we release our references to LevelDB before returning, allowing LevelDB
+ // to close and release its exclusive locks.
+ dispatch_sync([FIRDatabaseQuery sharedQueue], ^{
+ FFLog(@"I-RDB040001", @"Disposing all repos for Config with name %@", config.sessionIdentifier);
+ NSMutableDictionary *configs = [FRepoManager configs];
+ for (FRepo* repo in [configs[config.sessionIdentifier] allValues]) {
+ [repo dispose];
+ }
+ [configs removeObjectForKey:config.sessionIdentifier];
+ });
+}
+
+@end
diff --git a/Firebase/Database/Core/FRepo_Private.h b/Firebase/Database/Core/FRepo_Private.h
new file mode 100644
index 0000000..109edac
--- /dev/null
+++ b/Firebase/Database/Core/FRepo_Private.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 "FRepo.h"
+#import "FSparseSnapshotTree.h"
+
+@class FSyncTree;
+@class FAtomicNumber;
+@class FEventRaiser;
+@class FSnapshotHolder;
+
+@interface FRepo ()
+
+- (void) runOnDisconnectEvents;
+
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+@property (nonatomic, strong) FPersistentConnection* connection;
+@property (nonatomic, strong) FSnapshotHolder* infoData;
+@property (nonatomic, strong) FSparseSnapshotTree* onDisconnect;
+@property (nonatomic, strong) FEventRaiser *eventRaiser;
+@property (nonatomic, strong) FSyncTree *serverSyncTree;
+
+// For testing.
+@property (nonatomic) long dataUpdateCount;
+@property (nonatomic) long rangeMergeUpdateCount;
+
+- (NSInteger)nextWriteId;
+
+@end
diff --git a/Firebase/Database/Core/FServerValues.h b/Firebase/Database/Core/FServerValues.h
new file mode 100644
index 0000000..2540c12
--- /dev/null
+++ b/Firebase/Database/Core/FServerValues.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 <Foundation/Foundation.h>
+#import "FSparseSnapshotTree.h"
+#import "FNode.h"
+#import "FCompoundWrite.h"
+#import "FClock.h"
+
+@interface FServerValues : NSObject
+
++ (NSDictionary*) generateServerValues:(id<FClock>)clock;
++ (id) resolveDeferredValueCompoundWrite:(FCompoundWrite*)write withServerValues:(NSDictionary*)serverValues;
++ (id<FNode>) resolveDeferredValueSnapshot:(id<FNode>)node withServerValues:(NSDictionary*)serverValues;
++ (id) resolveDeferredValueTree:(FSparseSnapshotTree*)tree withServerValues:(NSDictionary*)serverValues;
+
+@end
diff --git a/Firebase/Database/Core/FServerValues.m b/Firebase/Database/Core/FServerValues.m
new file mode 100644
index 0000000..89ee5d0
--- /dev/null
+++ b/Firebase/Database/Core/FServerValues.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 "FServerValues.h"
+#import "FConstants.h"
+#import "FLeafNode.h"
+#import "FChildrenNode.h"
+#import "FSnapshotUtilities.h"
+
+@implementation FServerValues
+
++ (NSDictionary*) generateServerValues:(id<FClock>)clock {
+ long long millis = (long long)([clock currentTime] * 1000);
+ return @{ @"timestamp": [NSNumber numberWithLongLong:millis] };
+}
+
++ (id) resolveDeferredValue:(id)val withServerValues:(NSDictionary*)serverValues {
+ if ([val isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dict = val;
+ if (dict[kServerValueSubKey] != nil) {
+ NSString* serverValueType = [dict objectForKey:kServerValueSubKey];
+ if (serverValues[serverValueType] != nil) {
+ return [serverValues objectForKey:serverValueType];
+ } else {
+ // TODO: Throw unrecognizedServerValue error here
+ }
+ }
+ }
+ return val;
+}
+
++ (FCompoundWrite *) resolveDeferredValueCompoundWrite:(FCompoundWrite *)write withServerValues:(NSDictionary *)serverValues {
+ __block FCompoundWrite *resolved = write;
+ [write enumerateWrites:^(FPath *path, id<FNode> node, BOOL *stop) {
+ id<FNode> resolvedNode = [FServerValues resolveDeferredValueSnapshot:node withServerValues:serverValues];
+ // Node actually changed, use pointer inequality here
+ if (resolvedNode != node) {
+ resolved = [resolved addWrite:resolvedNode atPath:path];
+ }
+ }];
+ return resolved;
+}
+
++ (id) resolveDeferredValueTree:(FSparseSnapshotTree*)tree withServerValues:(NSDictionary*)serverValues {
+ FSparseSnapshotTree* resolvedTree = [[FSparseSnapshotTree alloc] init];
+ [tree forEachTreeAtPath:[FPath empty] do:^(FPath* path, id<FNode> node) {
+ [resolvedTree rememberData:[FServerValues resolveDeferredValueSnapshot:node withServerValues:serverValues] onPath:path];
+ }];
+ return resolvedTree;
+}
+
++ (id<FNode>) resolveDeferredValueSnapshot:(id<FNode>)node withServerValues:(NSDictionary*)serverValues {
+ id priorityVal = [FServerValues resolveDeferredValue:[[node getPriority] val] withServerValues:serverValues];
+ id<FNode> priority = [FSnapshotUtilities nodeFrom:priorityVal];
+
+ if ([node isLeafNode]) {
+ id value = [self resolveDeferredValue:[node val] withServerValues:serverValues];
+ if (![value isEqual:[node val]] || ![priority isEqual:[node getPriority]]) {
+ return [[FLeafNode alloc] initWithValue:value withPriority:priority];
+ } else {
+ return node;
+ }
+ } else {
+ __block FChildrenNode* newNode = node;
+ if (![priority isEqual:[node getPriority]]) {
+ newNode = [newNode updatePriority:priority];
+ }
+
+ [node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ id newChildNode = [FServerValues resolveDeferredValueSnapshot:childNode withServerValues:serverValues];
+ if (![newChildNode isEqual:childNode]) {
+ newNode = [newNode updateImmediateChild:childKey withNewChild:newChildNode];
+ }
+ }];
+ return newNode;
+ }
+}
+
+@end
+
diff --git a/Firebase/Database/Core/FSnapshotHolder.h b/Firebase/Database/Core/FSnapshotHolder.h
new file mode 100644
index 0000000..9a1d871
--- /dev/null
+++ b/Firebase/Database/Core/FSnapshotHolder.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FNode.h"
+
+@interface FSnapshotHolder : NSObject
+
+- (id<FNode>) getNode:(FPath *)path;
+- (void) updateSnapshot:(FPath *)path withNewSnapshot:(id<FNode>)newSnapshotNode;
+
+@property (nonatomic, strong) id<FNode> rootNode;
+
+@end
diff --git a/Firebase/Database/Core/FSnapshotHolder.m b/Firebase/Database/Core/FSnapshotHolder.m
new file mode 100644
index 0000000..25c4625
--- /dev/null
+++ b/Firebase/Database/Core/FSnapshotHolder.m
@@ -0,0 +1,46 @@
+/*
+ * 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 "FSnapshotHolder.h"
+#import "FEmptyNode.h"
+
+@interface FSnapshotHolder()
+
+
+@end
+
+@implementation FSnapshotHolder
+
+@synthesize rootNode;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ self.rootNode = [FEmptyNode emptyNode];
+ }
+ return self;
+}
+
+- (id<FNode>) getNode:(FPath *)path {
+ return [self.rootNode getChild:path];
+}
+
+- (void) updateSnapshot:(FPath *)path withNewSnapshot:(id<FNode>)newSnapshotNode {
+ self.rootNode = [self.rootNode updateChild:path withNewChild:newSnapshotNode];
+}
+
+@end
diff --git a/Firebase/Database/Core/FSparseSnapshotTree.h b/Firebase/Database/Core/FSparseSnapshotTree.h
new file mode 100644
index 0000000..b860c9d
--- /dev/null
+++ b/Firebase/Database/Core/FSparseSnapshotTree.h
@@ -0,0 +1,34 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FNode.h"
+#import "FPath.h"
+#import "FTypedefs_Private.h"
+
+@class FSparseSnapshotTree;
+
+typedef void (^fbt_void_nsstring_sstree) (NSString*, FSparseSnapshotTree*);
+
+@interface FSparseSnapshotTree : NSObject
+
+- (id<FNode>) findPath:(FPath *)path;
+- (void) rememberData:(id<FNode>)data onPath:(FPath *)path;
+- (BOOL) forgetPath:(FPath *)path;
+- (void) forEachTreeAtPath:(FPath *)prefixPath do:(fbt_void_path_node)func;
+- (void) forEachChild:(fbt_void_nsstring_sstree)func;
+
+@end
diff --git a/Firebase/Database/Core/FSparseSnapshotTree.m b/Firebase/Database/Core/FSparseSnapshotTree.m
new file mode 100644
index 0000000..1f16888
--- /dev/null
+++ b/Firebase/Database/Core/FSparseSnapshotTree.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 "FSparseSnapshotTree.h"
+#import "FChildrenNode.h"
+
+@interface FSparseSnapshotTree () {
+ id<FNode> value;
+ NSMutableDictionary* children;
+}
+
+@end
+
+@implementation FSparseSnapshotTree
+
+- (id) init {
+ self = [super init];
+ if (self) {
+ value = nil;
+ children = nil;
+ }
+ return self;
+}
+
+- (id<FNode>) findPath:(FPath *)path {
+ if (value != nil) {
+ return [value getChild:path];
+ } else if (![path isEmpty] && children != nil) {
+ NSString* childKey = [path getFront];
+ path = [path popFront];
+ FSparseSnapshotTree* childTree = children[childKey];
+ if (childTree != nil) {
+ return [childTree findPath:path];
+ } else {
+ return nil;
+ }
+ } else {
+ return nil;
+ }
+}
+
+- (void) rememberData:(id<FNode>)data onPath:(FPath *)path {
+ if ([path isEmpty]) {
+ value = data;
+ children = nil;
+ } else if (value != nil) {
+ value = [value updateChild:path withNewChild:data];
+ } else {
+ if (children == nil) {
+ children = [[NSMutableDictionary alloc] init];
+ }
+
+ NSString* childKey = [path getFront];
+ if (children[childKey] == nil) {
+ children[childKey] = [[FSparseSnapshotTree alloc] init];
+ }
+
+ FSparseSnapshotTree* child = children[childKey];
+ path = [path popFront];
+ [child rememberData:data onPath:path];
+ }
+}
+
+- (BOOL) forgetPath:(FPath *)path {
+ if ([path isEmpty]) {
+ value = nil;
+ children = nil;
+ return YES;
+ } else {
+ if (value != nil) {
+ if ([value isLeafNode]) {
+ // non-empty path at leaf. the path leads to nowhere
+ return NO;
+ } else {
+ id<FNode> tmp = value;
+ value = nil;
+
+ [tmp enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [self rememberData:node onPath:[[FPath alloc] initWith:key]];
+ }];
+
+ // we've cleared out the value and set children. Call ourself again to hit the next case
+ return [self forgetPath:path];
+ }
+ } else if (children != nil) {
+ NSString* childKey = [path getFront];
+ path = [path popFront];
+
+ if (children[childKey] != nil) {
+ FSparseSnapshotTree* child = children[childKey];
+ BOOL safeToRemove = [child forgetPath:path];
+ if (safeToRemove) {
+ [children removeObjectForKey:childKey];
+ }
+ }
+
+ if ([children count] == 0) {
+ children = nil;
+ return YES;
+ } else {
+ return NO;
+ }
+ } else {
+ return YES;
+ }
+ }
+}
+
+- (void) forEachTreeAtPath:(FPath *)prefixPath do:(fbt_void_path_node)func {
+ if (value != nil) {
+ func(prefixPath, value);
+ } else {
+ [self forEachChild:^(NSString* key, FSparseSnapshotTree* tree) {
+ FPath* path = [prefixPath childFromString:key];
+ [tree forEachTreeAtPath:path do:func];
+ }];
+ }
+}
+
+
+- (void) forEachChild:(fbt_void_nsstring_sstree)func {
+ if (children != nil) {
+ for (NSString* key in children) {
+ FSparseSnapshotTree* tree = [children objectForKey:key];
+ func(key, tree);
+ }
+ }
+}
+
+
+@end
diff --git a/Firebase/Database/Core/FSyncPoint.h b/Firebase/Database/Core/FSyncPoint.h
new file mode 100644
index 0000000..4e5a4e2
--- /dev/null
+++ b/Firebase/Database/Core/FSyncPoint.h
@@ -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 <Foundation/Foundation.h>
+
+@protocol FOperation;
+@class FWriteTreeRef;
+@protocol FNode;
+@protocol FEventRegistration;
+@class FQuerySpec;
+@class FChildrenNode;
+@class FTupleRemovedQueriesEvents;
+@class FView;
+@class FPath;
+@class FCacheNode;
+@class FPersistenceManager;
+
+@interface FSyncPoint : NSObject
+
+- (id)initWithPersistenceManager:(FPersistenceManager *)persistence;
+
+- (BOOL) isEmpty;
+
+/**
+* Returns array of FEvent
+*/
+- (NSArray *) applyOperation:(id<FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id<FNode>)optCompleteServerCache;
+
+/**
+* Returns array of FEvent
+*/
+- (NSArray *) addEventRegistration:(id <FEventRegistration>)eventRegistration
+ forNonExistingViewForQuery:(FQuerySpec *)query
+ writesCache:(FWriteTreeRef *)writesCache
+ serverCache:(FCacheNode *)serverCache;
+
+- (NSArray *) addEventRegistration:(id <FEventRegistration>)eventRegistration
+ forExistingViewForQuery:(FQuerySpec *)query;
+
+- (FTupleRemovedQueriesEvents *) removeEventRegistration:(id <FEventRegistration>)eventRegistration
+ forQuery:(FQuerySpec *)query
+ cancelError:(NSError *)cancelError;
+/**
+* Returns array of FViews
+*/
+- (NSArray *) queryViews;
+- (id<FNode>) completeServerCacheAtPath:(FPath *)path;
+- (FView *) viewForQuery:(FQuerySpec *)query;
+- (BOOL) viewExistsForQuery:(FQuerySpec *)query;
+- (BOOL) hasCompleteView;
+- (FView *) completeView;
+
+@end
diff --git a/Firebase/Database/Core/FSyncPoint.m b/Firebase/Database/Core/FSyncPoint.m
new file mode 100644
index 0000000..cd429f1
--- /dev/null
+++ b/Firebase/Database/Core/FSyncPoint.m
@@ -0,0 +1,257 @@
+/*
+ * 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 "FSyncPoint.h"
+#import "FOperation.h"
+#import "FWriteTreeRef.h"
+#import "FNode.h"
+#import "FEventRegistration.h"
+#import "FIRDatabaseQuery.h"
+#import "FChildrenNode.h"
+#import "FTupleRemovedQueriesEvents.h"
+#import "FView.h"
+#import "FOperationSource.h"
+#import "FQuerySpec.h"
+#import "FQueryParams.h"
+#import "FPath.h"
+#import "FEmptyNode.h"
+#import "FViewCache.h"
+#import "FCacheNode.h"
+#import "FPersistenceManager.h"
+#import "FDataEvent.h"
+
+/**
+* SyncPoint represents a single location in a SyncTree with 1 or more event registrations, meaning we need to
+* maintain 1 or more Views at this location to cache server data and raise appropriate events for server changes
+* and user writes (set, transaction, update).
+*
+* It's responsible for:
+* - Maintaining the set of 1 or more views necessary at this location (a SyncPoint with 0 views should be removed).
+* - Proxying user / server operations to the views as appropriate (i.e. applyServerOverwrite,
+* applyUserOverwrite, etc.)
+*/
+@interface FSyncPoint ()
+/**
+* The Views being tracked at this location in the tree, stored as a map where the key is a
+* queryParams and the value is the View for that query.
+*
+* NOTE: This list will be quite small (usually 1, but perhaps 2 or 3; any more is an odd use case).
+*
+* Maps NSString -> FView
+*/
+@property (nonatomic, strong) NSMutableDictionary *views;
+
+@property (nonatomic, strong) FPersistenceManager *persistenceManager;
+@end
+
+@implementation FSyncPoint
+
+- (id) initWithPersistenceManager:(FPersistenceManager *)persistence {
+ self = [super init];
+ if (self) {
+ self.persistenceManager = persistence;
+ self.views = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (BOOL) isEmpty {
+ return [self.views count] == 0;
+}
+
+- (NSArray *) applyOperation:(id<FOperation>)operation
+ toView:(FView *)view
+ writesCache:(FWriteTreeRef *)writesCache
+ serverCache:(id<FNode>)optCompleteServerCache {
+ FViewOperationResult *result = [view applyOperation:operation writesCache:writesCache serverCache:optCompleteServerCache];
+ if (!view.query.loadsAllData) {
+ NSMutableSet *removed = [NSMutableSet set];
+ NSMutableSet *added = [NSMutableSet set];
+ [result.changes enumerateObjectsUsingBlock:^(FChange *change, NSUInteger idx, BOOL *stop) {
+ if (change.type == FIRDataEventTypeChildAdded) {
+ [added addObject:change.childKey];
+ } else if (change.type == FIRDataEventTypeChildRemoved) {
+ [removed addObject:change.childKey];
+ }
+ }];
+ if ([removed count] > 0 || [added count] > 0) {
+ [self.persistenceManager updateTrackedQueryKeysWithAddedKeys:added removedKeys:removed forQuery:view.query];
+ }
+ }
+ return result.events;
+}
+
+- (NSArray *) applyOperation:(id <FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id <FNode>)optCompleteServerCache {
+ FQueryParams *queryParams = operation.source.queryParams;
+ if (queryParams != nil) {
+ FView *view = [self.views objectForKey:queryParams];
+ NSAssert(view != nil, @"SyncTree gave us an op for an invalid query.");
+ return [self applyOperation:operation toView:view writesCache:writesCache serverCache:optCompleteServerCache];
+ } else {
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) {
+ NSArray *eventsForView = [self applyOperation:operation toView:view writesCache:writesCache serverCache:optCompleteServerCache];
+ [events addObjectsFromArray:eventsForView];
+ }];
+ return events;
+ }
+}
+
+/**
+* Add an event callback for the specified query
+* Returns Array of FEvent events to raise.
+*/
+- (NSArray *) addEventRegistration:(id <FEventRegistration>)eventRegistration
+ forNonExistingViewForQuery:(FQuerySpec *)query
+ writesCache:(FWriteTreeRef *)writesCache
+ serverCache:(FCacheNode *)serverCache {
+ NSAssert(self.views[query.params] == nil, @"Found view for query: %@", query.params);
+ // TODO: make writesCache take flag for complete server node
+ id<FNode> eventCache = [writesCache calculateCompleteEventCacheWithCompleteServerCache:serverCache.isFullyInitialized ? serverCache.node : nil];
+ BOOL eventCacheComplete;
+ if (eventCache != nil) {
+ eventCacheComplete = YES;
+ } else {
+ eventCache = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:serverCache.node];
+ eventCacheComplete = NO;
+ }
+
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:eventCache index:query.index];
+ FCacheNode *eventCacheNode = [[FCacheNode alloc] initWithIndexedNode:indexed
+ isFullyInitialized:eventCacheComplete
+ isFiltered:NO];
+ FViewCache *viewCache = [[FViewCache alloc] initWithEventCache:eventCacheNode serverCache:serverCache];
+ FView *view = [[FView alloc] initWithQuery:query initialViewCache:viewCache];
+ // If this is a non-default query we need to tell persistence our current view of the data
+ if (!query.loadsAllData) {
+ NSMutableSet *allKeys = [NSMutableSet set];
+ [view.eventCache enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [allKeys addObject:key];
+ }];
+ [self.persistenceManager setTrackedQueryKeys:allKeys forQuery:query];
+ }
+ self.views[query.params] = view;
+ return [self addEventRegistration:eventRegistration forExistingViewForQuery:query];
+}
+
+- (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration
+ forExistingViewForQuery:(FQuerySpec *)query {
+ FView *view = self.views[query.params];
+ NSAssert(view != nil, @"No view for query: %@", query);
+ [view addEventRegistration:eventRegistration];
+ return [view initialEvents:eventRegistration];
+}
+
+/**
+* Remove event callback(s). Return cancelEvents if a cancelError is specified.
+*
+* If query is the default query, we'll check all views for the specified eventRegistration.
+* If eventRegistration is nil, we'll remove all callbacks for the specified view(s).
+*
+* @return FTupleRemovedQueriesEvents removed queries and any cancel events
+*/
+- (FTupleRemovedQueriesEvents *) removeEventRegistration:(id <FEventRegistration>)eventRegistration
+ forQuery:(FQuerySpec *)query
+ cancelError:(NSError *)cancelError {
+ NSMutableArray *removedQueries = [[NSMutableArray alloc] init];
+ __block NSMutableArray *cancelEvents = [[NSMutableArray alloc] init];
+ BOOL hadCompleteView = [self hasCompleteView];
+ if ([query isDefault]) {
+ // When you do [ref removeObserverWithHandle:], we search all views for the registration to remove.
+ [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *viewQueryParams, FView *view, BOOL *stop) {
+ [cancelEvents addObjectsFromArray:[view removeEventRegistration:eventRegistration cancelError:cancelError]];
+ if ([view isEmpty]) {
+ [self.views removeObjectForKey:viewQueryParams];
+
+ // We'll deal with complete views later
+ if (![view.query loadsAllData]) {
+ [removedQueries addObject:view.query];
+ }
+ }
+ }];
+ } else {
+ // remove the callback from the specific view
+ FView *view = [self.views objectForKey:query.params];
+ if (view != nil) {
+ [cancelEvents addObjectsFromArray:[view removeEventRegistration:eventRegistration cancelError:cancelError]];
+
+ if ([view isEmpty]) {
+ [self.views removeObjectForKey:query.params];
+
+ // We'll deal with complete views later
+ if (![view.query loadsAllData]) {
+ [removedQueries addObject:view.query];
+ }
+ }
+ }
+ }
+
+ if (hadCompleteView && ![self hasCompleteView]) {
+ // We removed our last complete view
+ [removedQueries addObject:[FQuerySpec defaultQueryAtPath:query.path]];
+ }
+
+ return [[FTupleRemovedQueriesEvents alloc] initWithRemovedQueries:removedQueries cancelEvents:cancelEvents];
+}
+
+- (NSArray *) queryViews {
+ __block NSMutableArray *filteredViews = [[NSMutableArray alloc] init];
+
+ [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) {
+ if (![view.query loadsAllData]) {
+ [filteredViews addObject:view];
+ }
+ }];
+
+ return filteredViews;
+}
+
+- (id <FNode>) completeServerCacheAtPath:(FPath *)path {
+ __block id<FNode> serverCache = nil;
+ [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) {
+ serverCache = [view completeServerCacheFor:path];
+ *stop = (serverCache != nil);
+ }];
+ return serverCache;
+}
+
+- (FView *) viewForQuery:(FQuerySpec *)query {
+ return [self.views objectForKey:query.params];
+}
+
+- (BOOL) viewExistsForQuery:(FQuerySpec *)query {
+ return [self viewForQuery:query] != nil;
+}
+
+- (BOOL) hasCompleteView {
+ return [self completeView] != nil;
+}
+
+- (FView *) completeView {
+ __block FView *completeView = nil;
+
+ [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) {
+ if ([view.query loadsAllData]) {
+ completeView = view;
+ *stop = YES;
+ }
+ }];
+
+ return completeView;
+}
+
+
+@end
diff --git a/Firebase/Database/Core/FSyncTree.h b/Firebase/Database/Core/FSyncTree.h
new file mode 100644
index 0000000..887f721
--- /dev/null
+++ b/Firebase/Database/Core/FSyncTree.h
@@ -0,0 +1,61 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FListenProvider;
+@protocol FNode;
+@class FPath;
+@protocol FEventRegistration;
+@protocol FPersistedServerCache;
+@class FQuerySpec;
+@class FCompoundWrite;
+@class FPersistenceManager;
+@class FCompoundHash;
+@protocol FClock;
+
+@protocol FSyncTreeHash <NSObject>
+
+- (NSString *)simpleHash;
+- (FCompoundHash *)compoundHash;
+- (BOOL)includeCompoundHash;
+
+@end
+
+@interface FSyncTree : NSObject
+
+- (id) initWithListenProvider:(FListenProvider *)provider;
+- (id) initWithPersistenceManager:(FPersistenceManager *)persistenceManager
+ listenProvider:(FListenProvider *)provider;
+
+// These methods all return NSArray of FEvent
+- (NSArray *) applyUserOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible;
+- (NSArray *) applyUserMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId;
+- (NSArray *) ackUserWriteWithWriteId:(NSInteger)writeId revert:(BOOL)revert persist:(BOOL)persist clock:(id<FClock>)clock;
+- (NSArray *) applyServerOverwriteAtPath:(FPath *)path newData:(id<FNode>)newData;
+- (NSArray *) applyServerMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren;
+- (NSArray *) applyServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges;
+- (NSArray *) applyTaggedQueryOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData tagId:(NSNumber *)tagId;
+- (NSArray *) applyTaggedQueryMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren tagId:(NSNumber *)tagId;
+- (NSArray *) applyTaggedServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges tagId:(NSNumber *)tagId;
+- (NSArray *) addEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query;
+- (NSArray *) removeEventRegistration:(id <FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query cancelError:(NSError *)cancelError;
+- (void)keepQuery:(FQuerySpec *)query synced:(BOOL)keepSynced;
+- (NSArray *) removeAllWrites;
+
+- (id<FNode>) calcCompleteEventCacheAtPath:(FPath *)path excludeWriteIds:(NSArray *)writeIdsToExclude;
+
+@end
diff --git a/Firebase/Database/Core/FSyncTree.m b/Firebase/Database/Core/FSyncTree.m
new file mode 100644
index 0000000..37100c1
--- /dev/null
+++ b/Firebase/Database/Core/FSyncTree.m
@@ -0,0 +1,817 @@
+/*
+ * 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 "FSyncTree.h"
+#import "FListenProvider.h"
+#import "FWriteTree.h"
+#import "FNode.h"
+#import "FPath.h"
+#import "FEventRegistration.h"
+#import "FImmutableTree.h"
+#import "FOperation.h"
+#import "FWriteTreeRef.h"
+#import "FOverwrite.h"
+#import "FOperationSource.h"
+#import "FMerge.h"
+#import "FAckUserWrite.h"
+#import "FView.h"
+#import "FSyncPoint.h"
+#import "FEmptyNode.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FSnapshotHolder.h"
+#import "FChildrenNode.h"
+#import "FTupleRemovedQueriesEvents.h"
+#import "FAtomicNumber.h"
+#import "FEventRaiser.h"
+#import "FListenComplete.h"
+#import "FSnapshotUtilities.h"
+#import "FCacheNode.h"
+#import "FUtilities.h"
+#import "FCompoundWrite.h"
+#import "FWriteRecord.h"
+#import "FPersistenceManager.h"
+#import "FKeepSyncedEventRegistration.h"
+#import "FServerValues.h"
+#import "FCompoundHash.h"
+#import "FRangeMerge.h"
+
+// Size after which we start including the compound hash
+static const NSUInteger kFSizeThresholdForCompoundHash = 1024;
+
+@interface FListenContainer : NSObject<FSyncTreeHash>
+
+@property (nonatomic, strong) FView *view;
+@property (nonatomic, copy) fbt_nsarray_nsstring onComplete;
+
+@end
+
+@implementation FListenContainer
+
+- (instancetype)initWithView:(FView *)view onComplete:(fbt_nsarray_nsstring)onComplete {
+ self = [super init];
+ if (self != nil) {
+ self->_view = view;
+ self->_onComplete = onComplete;
+ }
+ return self;
+}
+
+- (id<FNode>)serverCache {
+ return self.view.serverCache;
+}
+
+- (FCompoundHash *)compoundHash {
+ return [FCompoundHash fromNode:[self serverCache]];
+}
+
+- (NSString *)simpleHash {
+ return [[self serverCache] dataHash];
+}
+
+- (BOOL)includeCompoundHash {
+ return [FSnapshotUtilities estimateSerializedNodeSize:[self serverCache]] > kFSizeThresholdForCompoundHash;
+}
+
+@end
+
+@interface FSyncTree ()
+
+/**
+* Tree of SyncPoints. There's a SyncPoint at any location that has 1 or more views.
+*/
+@property (nonatomic, strong) FImmutableTree *syncPointTree;
+
+/**
+* A tree of all pending user writes (user-initiated set, transactions, updates, etc)
+*/
+@property (nonatomic, strong) FWriteTree *pendingWriteTree;
+
+/**
+* Maps tagId -> FTuplePathQueryParams
+*/
+@property (nonatomic, strong) NSMutableDictionary *tagToQueryMap;
+@property (nonatomic, strong) NSMutableDictionary *queryToTagMap;
+@property (nonatomic, strong) FListenProvider *listenProvider;
+@property (nonatomic, strong) FPersistenceManager *persistenceManager;
+@property (nonatomic, strong) FAtomicNumber *queryTagCounter;
+@property (nonatomic, strong) NSMutableSet *keepSyncedQueries;
+
+@end
+
+/**
+* SyncTree is the central class for managing event callback registration, data caching, views
+* (query processing), and event generation. There are typically two SyncTree instances for
+* each Repo, one for the normal Firebase data, and one for the .info data.
+*
+* It has a number of responsibilities, including:
+* - Tracking all user event callbacks (registered via addEventRegistration: and removeEventRegistration:).
+* - Applying and caching data changes for user setValue:, runTransactionBlock:, and updateChildValues: calls
+* (applyUserOverwriteAtPath:, applyUserMergeAtPath:).
+* - Applying and caching data changes for server data changes (applyServerOverwriteAtPath:,
+* applyServerMergeAtPath:).
+* - Generating user-facing events for server and user changes (all of the apply* methods
+* return the set of events that need to be raised as a result).
+* - Maintaining the appropriate set of server listens to ensure we are always subscribed
+* to the correct set of paths and queries to satisfy the current set of user event
+* callbacks (listens are started/stopped using the provided listenProvider).
+*
+* NOTE: Although SyncTree tracks event callbacks and calculates events to raise, the actual
+* events are returned to the caller rather than raised synchronously.
+*/
+@implementation FSyncTree
+
+- (id) initWithListenProvider:(FListenProvider *)provider {
+ return [self initWithPersistenceManager:nil listenProvider:provider];
+}
+
+- (id) initWithPersistenceManager:(FPersistenceManager *)persistenceManager listenProvider:(FListenProvider *)provider {
+ self = [super init];
+ if (self) {
+ self.syncPointTree = [FImmutableTree empty];
+ self.pendingWriteTree = [[FWriteTree alloc] init];
+ self.tagToQueryMap = [[NSMutableDictionary alloc] init];
+ self.queryToTagMap = [[NSMutableDictionary alloc] init];
+ self.listenProvider = provider;
+ self.persistenceManager = persistenceManager;
+ self.queryTagCounter = [[FAtomicNumber alloc] init];
+ self.keepSyncedQueries = [NSMutableSet set];
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark Apply Operations
+
+/**
+* Apply data changes for a user-generated setValue: runTransactionBlock: updateChildValues:, etc.
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyUserOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible {
+ // Record pending write
+ [self.pendingWriteTree addOverwriteAtPath:path newData:newData writeId:writeId isVisible:visible];
+ if (!visible) {
+ return @[];
+ } else {
+ FOverwrite *operation = [[FOverwrite alloc] initWithSource:[FOperationSource userInstance] path:path snap:newData];
+ return [self applyOperationToSyncPoints:operation];
+ }
+}
+
+/**
+* Apply the data from a user-generated updateChildValues: call
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyUserMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId {
+ // Record pending merge
+ [self.pendingWriteTree addMergeAtPath:path changedChildren:changedChildren writeId:writeId];
+
+ FMerge *operation = [[FMerge alloc] initWithSource:[FOperationSource userInstance] path:path children:changedChildren];
+ return [self applyOperationToSyncPoints:operation];
+}
+
+/**
+ * Acknowledge a pending user write that was previously registered with applyUserOverwriteAtPath: or applyUserMergeAtPath:
+ * TODO[offline]: Taking a serverClock here is awkward, but server values are awkward. :-(
+ * @return NSArray of FEvent to raise.
+ */
+- (NSArray *) ackUserWriteWithWriteId:(NSInteger)writeId revert:(BOOL)revert persist:(BOOL)persist clock:(id<FClock>)clock {
+ FWriteRecord *write = [self.pendingWriteTree writeForId:writeId];
+ BOOL needToReevaluate = [self.pendingWriteTree removeWriteId:writeId];
+ if (write.visible) {
+ if (persist) {
+ [self.persistenceManager removeUserWrite:writeId];
+ }
+ if (!revert) {
+ NSDictionary *serverValues = [FServerValues generateServerValues:clock];
+ if ([write isOverwrite]) {
+ id<FNode> resolvedNode = [FServerValues resolveDeferredValueSnapshot:write.overwrite withServerValues:serverValues];
+ [self.persistenceManager applyUserWrite:resolvedNode toServerCacheAtPath:write.path];
+ } else {
+ FCompoundWrite *resolvedMerge = [FServerValues resolveDeferredValueCompoundWrite:write.merge withServerValues:serverValues];
+ [self.persistenceManager applyUserMerge:resolvedMerge toServerCacheAtPath:write.path];
+ }
+ }
+ }
+ if (!needToReevaluate) {
+ return @[];
+ } else {
+ __block FImmutableTree *affectedTree = [FImmutableTree empty];
+ if (write.isOverwrite) {
+ affectedTree = [affectedTree setValue:@YES atPath:[FPath empty]];
+ } else {
+ [write.merge enumerateWrites:^(FPath *path, id <FNode> node, BOOL *stop) {
+ affectedTree = [affectedTree setValue:@YES atPath:path];
+ }];
+ }
+ FAckUserWrite *operation = [[FAckUserWrite alloc] initWithPath:write.path affectedTree:affectedTree revert:revert];
+ return [self applyOperationToSyncPoints:operation];
+ }
+}
+
+/**
+* Apply new server data for the specified path
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyServerOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData {
+ [self.persistenceManager updateServerCacheWithNode:newData forQuery:[FQuerySpec defaultQueryAtPath:path]];
+ FOverwrite *operation = [[FOverwrite alloc] initWithSource:[FOperationSource serverInstance] path:path snap:newData];
+ return [self applyOperationToSyncPoints:operation];
+}
+
+/**
+* Applied new server data to be merged in at the specified path
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyServerMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren {
+ [self.persistenceManager updateServerCacheWithMerge:changedChildren atPath:path];
+ FMerge *operation = [[FMerge alloc] initWithSource:[FOperationSource serverInstance] path:path children:changedChildren];
+ return [self applyOperationToSyncPoints:operation];
+}
+
+- (NSArray *) applyServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges {
+ FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path];
+ if (syncPoint == nil) {
+ // Removed view, so it's safe to just ignore this update
+ return @[];
+ } else {
+ // This could be for any "complete" (unfiltered) view, and if there is more than one complete view, they should
+ // each have the same cache so it doesn't matter which one we use.
+ FView *view = [syncPoint completeView];
+ if (view != nil) {
+ id<FNode> serverNode = [view serverCache];
+ for (FRangeMerge *merge in ranges) {
+ serverNode = [merge applyToNode:serverNode];
+ }
+ return [self applyServerOverwriteAtPath:path newData:serverNode];
+ } else {
+ // There doesn't exist a view for this update, so it was removed and it's safe to just ignore this range
+ // merge
+ return @[];
+ }
+ }
+}
+
+/**
+* Apply a listen complete to a path
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyListenCompleteAtPath:(FPath *)path {
+ [self.persistenceManager setQueryComplete:[FQuerySpec defaultQueryAtPath:path]];
+ id<FOperation> operation = [[FListenComplete alloc] initWithSource:[FOperationSource serverInstance] path:path];
+ return [self applyOperationToSyncPoints:operation];
+}
+
+/**
+* Apply a listen complete to a path
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyTaggedListenCompleteAtPath:(FPath *)path tagId:(NSNumber *)tagId {
+ FQuerySpec *query = [self queryForTag:tagId];
+ if (query != nil) {
+ [self.persistenceManager setQueryComplete:query];
+ FPath *relativePath = [FPath relativePathFrom:query.path to:path];
+ id<FOperation> op = [[FListenComplete alloc] initWithSource:[FOperationSource forServerTaggedQuery:query.params]
+ path:relativePath];
+ return [self applyTaggedOperation:op atPath:query.path];
+ } else {
+ // We've already removed the query. No big deal, ignore the update.
+ return @[];
+ }
+}
+
+/**
+* Internal helper method to apply tagged operation
+*/
+- (NSArray *) applyTaggedOperation:(id<FOperation>)operation atPath:(FPath *)path {
+ FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path];
+ NSAssert(syncPoint != nil, @"Missing sync point for query tag that we're tracking.");
+ FWriteTreeRef *writesCache = [self.pendingWriteTree childWritesForPath:path];
+ return [syncPoint applyOperation:operation writesCache:writesCache serverCache:nil];
+}
+
+/**
+* Apply new server data for the specified tagged query
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyTaggedQueryOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData tagId:(NSNumber *)tagId {
+ FQuerySpec *query = [self queryForTag:tagId];
+ if (query != nil) {
+ FPath *relativePath = [FPath relativePathFrom:query.path to:path];
+ FQuerySpec *queryToOverwrite = relativePath.isEmpty ? query : [FQuerySpec defaultQueryAtPath:path];
+ [self.persistenceManager updateServerCacheWithNode:newData forQuery:queryToOverwrite];
+ FOverwrite *operation = [[FOverwrite alloc] initWithSource:[FOperationSource forServerTaggedQuery:query.params]
+ path:relativePath snap:newData];
+ return [self applyTaggedOperation:operation atPath:query.path];
+ } else {
+ // Query must have been removed already
+ return @[];
+ }
+}
+
+/**
+* Apply server data to be merged in for the specified tagged query
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) applyTaggedQueryMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren tagId:(NSNumber *)tagId {
+ FQuerySpec *query = [self queryForTag:tagId];
+ if (query != nil) {
+ FPath *relativePath = [FPath relativePathFrom:query.path to:path];
+ [self.persistenceManager updateServerCacheWithMerge:changedChildren atPath:path];
+ FMerge *operation = [[FMerge alloc] initWithSource:[FOperationSource forServerTaggedQuery:query.params]
+ path:relativePath
+ children:changedChildren];
+ return [self applyTaggedOperation:operation atPath:query.path];
+ } else {
+ // We've already removed the query. No big deal, ignore the update.
+ return @[];
+ }
+}
+
+- (NSArray *) applyTaggedServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges tagId:(NSNumber *)tagId {
+ FQuerySpec *query = [self queryForTag:tagId];
+ if (query != nil) {
+ NSAssert([path isEqual:query.path], @"Tagged update path and query path must match");
+ FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path];
+ NSAssert(syncPoint != nil, @"Missing sync point for query tag that we're tracking.");
+ FView *view = [syncPoint viewForQuery:query];
+ NSAssert(view != nil, @"Missing view for query tag that we're tracking");
+ id<FNode> serverNode = [view serverCache];
+ for (FRangeMerge *merge in ranges) {
+ serverNode = [merge applyToNode:serverNode];
+ }
+ return [self applyTaggedQueryOverwriteAtPath:path newData:serverNode tagId:tagId];
+ } else {
+ // We've already removed the query. No big deal, ignore the update.
+ return @[];
+ }
+}
+
+/**
+* Add an event callback for the specified query
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) addEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query {
+ FPath *path = query.path;
+
+ __block BOOL foundAncestorDefaultView = NO;
+ [self.syncPointTree forEachOnPath:query.path whileBlock:^BOOL(FPath *pathToSyncPoint, FSyncPoint *syncPoint) {
+ foundAncestorDefaultView = foundAncestorDefaultView || [syncPoint hasCompleteView];
+ return !foundAncestorDefaultView;
+ }];
+
+ [self.persistenceManager setQueryActive:query];
+
+ FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path];
+ if (syncPoint == nil) {
+ syncPoint = [[FSyncPoint alloc] initWithPersistenceManager:self.persistenceManager];
+ self.syncPointTree = [self.syncPointTree setValue:syncPoint atPath:path];
+ }
+
+ BOOL viewAlreadyExists = [syncPoint viewExistsForQuery:query];
+ NSArray *events;
+ if (viewAlreadyExists) {
+ events = [syncPoint addEventRegistration:eventRegistration forExistingViewForQuery:query];
+ } else {
+ if (![query loadsAllData]) {
+ // We need to track a tag for this query
+ NSAssert(self.queryToTagMap[query] == nil, @"View does not exist, but we have a tag");
+ NSNumber *tagId = [self.queryTagCounter getAndIncrement];
+ self.queryToTagMap[query] = tagId;
+ self.tagToQueryMap[tagId] = query;
+ }
+
+ FWriteTreeRef *writesCache = [self.pendingWriteTree childWritesForPath:path];
+ FCacheNode *serverCache = [self serverCacheForQuery:query];
+ events = [syncPoint addEventRegistration:eventRegistration
+ forNonExistingViewForQuery:query
+ writesCache:writesCache
+ serverCache:serverCache];
+
+ // There was no view and no default listen
+ if (!foundAncestorDefaultView) {
+ FView *view = [syncPoint viewForQuery:query];
+ NSMutableArray *mutableEvents = [events mutableCopy];
+ [mutableEvents addObjectsFromArray:[self setupListenerOnQuery:query view:view]];
+ events = mutableEvents;
+ }
+ }
+
+ return events;
+}
+
+- (FCacheNode *)serverCacheForQuery:(FQuerySpec *)query {
+ __block id<FNode> serverCacheNode = nil;
+
+ [self.syncPointTree forEachOnPath:query.path whileBlock:^BOOL(FPath *pathToSyncPoint, FSyncPoint *syncPoint) {
+ FPath *relativePath = [FPath relativePathFrom:pathToSyncPoint to:query.path];
+ serverCacheNode = [syncPoint completeServerCacheAtPath:relativePath];
+ return serverCacheNode == nil;
+ }];
+
+ FCacheNode *serverCache;
+ if (serverCacheNode != nil) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:serverCacheNode index:query.index];
+ serverCache = [[FCacheNode alloc] initWithIndexedNode:indexed isFullyInitialized:YES isFiltered:NO];
+ } else {
+ FCacheNode *persistenceServerCache = [self.persistenceManager serverCacheForQuery:query];
+ if (persistenceServerCache.isFullyInitialized) {
+ serverCache = persistenceServerCache;
+ } else {
+ serverCacheNode = [FEmptyNode emptyNode];
+
+ FImmutableTree *subtree = [self.syncPointTree subtreeAtPath:query.path];
+ [subtree forEachChild:^(NSString *childKey, FSyncPoint *childSyncPoint) {
+ id<FNode> completeCache = [childSyncPoint completeServerCacheAtPath:[FPath empty]];
+ if (completeCache) {
+ serverCacheNode = [serverCacheNode updateImmediateChild:childKey withNewChild:completeCache];
+ }
+ }];
+ // Fill the node with any available children we have
+ [persistenceServerCache.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ if (![serverCacheNode hasChild:key]) {
+ serverCacheNode = [serverCacheNode updateImmediateChild:key withNewChild:node];
+ }
+ }];
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:serverCacheNode index:query.index];
+ serverCache = [[FCacheNode alloc] initWithIndexedNode:indexed isFullyInitialized:NO isFiltered:NO];
+ }
+ }
+
+ return serverCache;
+}
+
+/**
+* Remove event callback(s).
+*
+* If query is the default query, we'll check all queries for the specified eventRegistration.
+* If eventRegistration is null, we'll remove all callbacks for the specified query/queries.
+*
+* @param eventRegistration if nil, all callbacks are removed
+* @param cancelError If provided, appropriate cancel events will be returned
+* @return NSArray of FEvent to raise.
+*/
+- (NSArray *) removeEventRegistration:(id <FEventRegistration>)eventRegistration
+ forQuery:(FQuerySpec *)query
+ cancelError:(NSError *)cancelError {
+ // Find the syncPoint first. Then deal with whether or not it has matching listeners
+ FPath *path = query.path;
+ FSyncPoint *maybeSyncPoint = [self.syncPointTree valueAtPath:path];
+ NSArray *cancelEvents = @[];
+
+ // A removal on a default query affects all queries at that location. A removal on an indexed query, even one without
+ // other query constraints, does *not* affect all queries at that location. So this check must be for 'default', and
+ // not loadsAllData:
+ if (maybeSyncPoint && ([query isDefault] || [maybeSyncPoint viewExistsForQuery:query])) {
+ FTupleRemovedQueriesEvents *removedAndEvents = [maybeSyncPoint removeEventRegistration:eventRegistration forQuery:query cancelError:cancelError];
+ if ([maybeSyncPoint isEmpty]) {
+ self.syncPointTree = [self.syncPointTree removeValueAtPath:path];
+ }
+ NSArray *removed = removedAndEvents.removedQueries;
+ cancelEvents = removedAndEvents.cancelEvents;
+
+ // We may have just removed one of many listeners and can short-circuit this whole process
+ // We may also not have removed a default listener, in which case all of the descendant listeners should already
+ // be properly set up.
+ //
+ // Since indexed queries can shadow if they don't have other query constraints, check for loadsAllData: instead
+ // of isDefault:
+ NSUInteger defaultQueryIndex = [removed indexOfObjectPassingTest:^BOOL(FQuerySpec *q, NSUInteger idx, BOOL *stop) {
+ return [q loadsAllData];
+ }];
+ BOOL removingDefault = defaultQueryIndex != NSNotFound;
+ [removed enumerateObjectsUsingBlock:^(FQuerySpec *query, NSUInteger idx, BOOL *stop) {
+ [self.persistenceManager setQueryInactive:query];
+ }];
+ NSNumber *covered = [self.syncPointTree findOnPath:path andApplyBlock:^id(FPath *relativePath, FSyncPoint *parentSyncPoint) {
+ return [NSNumber numberWithBool:[parentSyncPoint hasCompleteView]];
+ }];
+
+ if (removingDefault && ![covered boolValue]) {
+ FImmutableTree *subtree = [self.syncPointTree subtreeAtPath:path];
+ // There are potentially child listeners. Determine what if any listens we need to send before executing
+ // the removal
+ if (![subtree isEmpty]) {
+ // We need to fold over our subtree and collect the listeners to send
+ NSArray *newViews = [self collectDistinctViewsForSubTree:subtree];
+
+ // Ok, we've collected all the listens we need. Set them up.
+ [newViews enumerateObjectsUsingBlock:^(FView *view, NSUInteger idx, BOOL *stop) {
+ FQuerySpec *newQuery = view.query;
+ FListenContainer *listenContainer = [self createListenerForView:view];
+ self.listenProvider.startListening([self queryForListening:newQuery], [self tagForQuery:newQuery],
+ listenContainer, listenContainer.onComplete);
+ }];
+ } else {
+ // There's nothing below us, so nothing we need to start listening on
+ }
+ }
+
+ // If we removed anything and we're not covered by a higher up listen, we need to stop listening on this query.
+ // The above block has us covered in terms of making sure we're set up on listens lower in the tree.
+ // Also, note that if we have a cancelError, it's already been removed at the provider level.
+ if (![covered boolValue] && [removed count] > 0 && cancelError == nil) {
+ // If we removed a default, then we weren't listening on any of the other queries here. Just cancel the one
+ // default. Otherwise, we need to iterate through and cancel each individual query
+ if (removingDefault) {
+ // We don't tag default listeners
+ self.listenProvider.stopListening([self queryForListening:query], nil);
+ } else {
+ [removed enumerateObjectsUsingBlock:^(FQuerySpec *queryToRemove, NSUInteger idx, BOOL *stop) {
+ NSNumber *tagToRemove = [self.queryToTagMap objectForKey:queryToRemove];
+ self.listenProvider.stopListening([self queryForListening:queryToRemove], tagToRemove);
+ }];
+ }
+ }
+ // Now, clear all the tags we're tracking for the removed listens.
+ [self removeTags:removed];
+ } else {
+ // No-op, this listener must've been already removed
+ }
+ return cancelEvents;
+}
+
+- (void)keepQuery:(FQuerySpec *)query synced:(BOOL)keepSynced {
+ // Only do something if we actually need to add/remove an event registration
+ if (keepSynced && ![self.keepSyncedQueries containsObject:query]) {
+ [self addEventRegistration:[FKeepSyncedEventRegistration instance] forQuery:query];
+ [self.keepSyncedQueries addObject:query];
+ } else if (!keepSynced && [self.keepSyncedQueries containsObject:query]) {
+ [self removeEventRegistration:[FKeepSyncedEventRegistration instance] forQuery:query cancelError:nil];
+ [self.keepSyncedQueries removeObject:query];
+ }
+}
+
+- (NSArray *) removeAllWrites {
+ [self.persistenceManager removeAllUserWrites];
+ NSArray *removedWrites = [self.pendingWriteTree removeAllWrites];
+ if (removedWrites.count > 0) {
+ FImmutableTree *affectedTree = [[FImmutableTree empty] setValue:@YES atPath:[FPath empty]];
+ return [self applyOperationToSyncPoints:[[FAckUserWrite alloc] initWithPath:[FPath empty]
+ affectedTree:affectedTree revert:YES]];
+ } else {
+ return @[];
+ }
+}
+
+/**
+* Returns a complete cache, if we have one, of the data at a particular path. The location must have a listener above
+* it, but as this is only used by transaction code, that should always be the case anyways.
+*
+* Note: this method will *include* hidden writes from transaction with applyLocally set to false.
+* @param path The path to the data we want
+* @param writeIdsToExclude A specific set to be excluded
+*/
+- (id <FNode>) calcCompleteEventCacheAtPath:(FPath *)path excludeWriteIds:(NSArray *)writeIdsToExclude {
+ BOOL includeHiddenSets = YES;
+ FWriteTree *writeTree = self.pendingWriteTree;
+ id<FNode> serverCache = [self.syncPointTree findOnPath:path andApplyBlock:^id<FNode>(FPath *pathSoFar, FSyncPoint *syncPoint) {
+ FPath *relativePath = [FPath relativePathFrom:pathSoFar to:path];
+ id<FNode> serverCache = [syncPoint completeServerCacheAtPath:relativePath];
+ if (serverCache) {
+ return serverCache;
+ } else {
+ return nil;
+ }
+ }];
+ return [writeTree calculateCompleteEventCacheAtPath:path completeServerCache:serverCache excludeWriteIds:writeIdsToExclude includeHiddenWrites:includeHiddenSets];
+}
+
+#pragma mark -
+#pragma mark Private Methods
+/**
+* This collapses multiple unfiltered views into a single view, since we only need a single
+* listener for them.
+* @return NSArray of FView
+*/
+- (NSArray *) collectDistinctViewsForSubTree:(FImmutableTree *)subtree {
+ return [subtree foldWithBlock:^NSArray *(FPath *relativePath, FSyncPoint *maybeChildSyncPoint, NSDictionary *childMap) {
+ if (maybeChildSyncPoint && [maybeChildSyncPoint hasCompleteView]) {
+ FView *completeView = [maybeChildSyncPoint completeView];
+ return @[completeView];
+ } else {
+ // No complete view here, flatten any deeper listens into an array
+ NSMutableArray *views = [[NSMutableArray alloc] init];
+ if (maybeChildSyncPoint) {
+ views = [[maybeChildSyncPoint queryViews] mutableCopy];
+ }
+ [childMap enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, NSArray *childViews, BOOL *stop) {
+ [views addObjectsFromArray:childViews];
+ }];
+ return views;
+ }
+ }];
+}
+
+/**
+* @param queries NSArray of FQuerySpec
+*/
+- (void) removeTags:(NSArray *)queries {
+ [queries enumerateObjectsUsingBlock:^(FQuerySpec *removedQuery, NSUInteger idx, BOOL *stop) {
+ if (![removedQuery loadsAllData]) {
+ // We should have a tag for this
+ NSNumber *removedQueryTag = self.queryToTagMap[removedQuery];
+ [self.queryToTagMap removeObjectForKey:removedQuery];
+ [self.tagToQueryMap removeObjectForKey:removedQueryTag];
+ }
+ }];
+}
+
+- (FQuerySpec *) queryForListening:(FQuerySpec *)query {
+ if (query.loadsAllData && !query.isDefault) {
+ // We treat queries that load all data as default queries
+ return [FQuerySpec defaultQueryAtPath:query.path];
+ } else {
+ return query;
+ }
+}
+
+/**
+* For a given new listen, manage the de-duplication of outstanding subscriptions.
+* @return NSArray of FEvent events to support synchronous data sources
+*/
+- (NSArray *) setupListenerOnQuery:(FQuerySpec *)query view:(FView *)view {
+ FPath *path = query.path;
+ NSNumber *tagId = [self tagForQuery:query];
+ FListenContainer *listenContainer = [self createListenerForView:view];
+
+ NSArray *events = self.listenProvider.startListening([self queryForListening:query], tagId, listenContainer,
+ listenContainer.onComplete);
+
+ FImmutableTree *subtree = [self.syncPointTree subtreeAtPath:path];
+ // The root of this subtree has our query. We're here because we definitely need to send a listen for that, but we
+ // may need to shadow other listens as well.
+ if (tagId != nil) {
+ NSAssert(![subtree.value hasCompleteView], @"If we're adding a query, it shouldn't be shadowed");
+ } else {
+ // Shadow everything at or below this location, this is a default listener.
+ NSArray *queriesToStop = [subtree foldWithBlock:^id(FPath *relativePath, FSyncPoint *maybeChildSyncPoint, NSDictionary *childMap) {
+ if (![relativePath isEmpty] && maybeChildSyncPoint != nil && [maybeChildSyncPoint hasCompleteView]) {
+ return @[[maybeChildSyncPoint completeView].query];
+ } else {
+ // No default listener here, flatten any deeper queries into an array
+ NSMutableArray *queries = [[NSMutableArray alloc] init];
+ if (maybeChildSyncPoint != nil) {
+ for (FView *view in [maybeChildSyncPoint queryViews]) {
+ [queries addObject:view.query];
+ }
+ }
+ [childMap enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *childQueries, BOOL *stop) {
+ [queries addObjectsFromArray:childQueries];
+ }];
+ return queries;
+ }
+ }];
+ for (FQuerySpec *queryToStop in queriesToStop) {
+ self.listenProvider.stopListening([self queryForListening:queryToStop], [self tagForQuery:queryToStop]);
+ }
+ }
+ return events;
+}
+
+- (FListenContainer *) createListenerForView:(FView *)view {
+ FQuerySpec *query = view.query;
+ NSNumber *tagId = [self tagForQuery:query];
+
+ FListenContainer *listenContainer = [[FListenContainer alloc] initWithView:view
+ onComplete:^(NSString *status) {
+ if ([status isEqualToString:@"ok"]) {
+ if (tagId != nil) {
+ return [self applyTaggedListenCompleteAtPath:query.path tagId:tagId];
+ } else {
+ return [self applyListenCompleteAtPath:query.path];
+ }
+ } else {
+ // If a listen failed, kill all of the listeners here, not just the one that triggered the error.
+ // Note that this may need to be scoped to just this listener if we change permissions on filtered children
+ NSError *error = [FUtilities errorForStatus:status andReason:nil];
+ FFWarn(@"I-RDB038012", @"Listener at %@ failed: %@", query.path, status);
+ return [self removeEventRegistration:nil forQuery:query cancelError:error];
+ }
+ }];
+
+ return listenContainer;
+}
+
+/**
+* @return The query associated with the given tag, if we have one
+*/
+- (FQuerySpec *) queryForTag:(NSNumber *)tagId {
+ return self.tagToQueryMap[tagId];
+}
+
+/**
+* @return The tag associated with the given query
+*/
+- (NSNumber *) tagForQuery:(FQuerySpec *)query {
+ return self.queryToTagMap[query];
+}
+
+#pragma mark -
+#pragma mark applyOperation Helpers
+
+/**
+* A helper method that visits all descendant and ancestor SyncPoints, applying the operation.
+*
+* NOTES:
+* - Descendant SyncPoints will be visited first (since we raise events depth-first).
+
+* - We call applyOperation: on each SyncPoint passing three things:
+* 1. A version of the Operation that has been made relative to the SyncPoint location.
+* 2. A WriteTreeRef of any writes we have cached at the SyncPoint location.
+* 3. A snapshot Node with cached server data, if we have it.
+
+* - We concatenate all of the events returned by each SyncPoint and return the result.
+*
+* @return Array of FEvent
+*/
+- (NSArray *) applyOperationToSyncPoints:(id<FOperation>)operation {
+ return [self applyOperationHelper:operation syncPointTree:self.syncPointTree serverCache:nil
+ writesCache:[self.pendingWriteTree childWritesForPath:[FPath empty]]];
+}
+
+/**
+* Recursive helper for applyOperationToSyncPoints_
+*/
+- (NSArray *) applyOperationHelper:(id<FOperation>)operation syncPointTree:(FImmutableTree *)syncPointTree
+ serverCache:(id<FNode>)serverCache writesCache:(FWriteTreeRef *)writesCache {
+ if ([operation.path isEmpty]) {
+ return [self applyOperationDescendantsHelper:operation syncPointTree:syncPointTree serverCache:serverCache writesCache:writesCache];
+ } else {
+ FSyncPoint *syncPoint = syncPointTree.value;
+
+ // If we don't have cached server data, see if we can get it from this SyncPoint
+ if (serverCache == nil && syncPoint != nil) {
+ serverCache = [syncPoint completeServerCacheAtPath:[FPath empty]];
+ }
+
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ NSString *childKey = [operation.path getFront];
+ id<FOperation> childOperation = [operation operationForChild:childKey];
+ FImmutableTree *childTree = [syncPointTree.children get:childKey];
+ if (childTree != nil && childOperation != nil) {
+ id<FNode> childServerCache = serverCache ? [serverCache getImmediateChild:childKey] : nil;
+ FWriteTreeRef *childWritesCache = [writesCache childWriteTreeRef:childKey];
+ [events addObjectsFromArray:[self applyOperationHelper:childOperation syncPointTree:childTree serverCache:childServerCache writesCache:childWritesCache]];
+ }
+
+ if (syncPoint) {
+ [events addObjectsFromArray:[syncPoint applyOperation:operation writesCache:writesCache serverCache:serverCache]];
+ }
+
+ return events;
+ }
+}
+
+/**
+* Recursive helper for applyOperationToSyncPoints:
+*/
+- (NSArray *) applyOperationDescendantsHelper:(id<FOperation>)operation syncPointTree:(FImmutableTree *)syncPointTree
+ serverCache:(id<FNode>)serverCache writesCache:(FWriteTreeRef *)writesCache {
+ FSyncPoint *syncPoint = syncPointTree.value;
+
+ // If we don't have cached server data, see if we can get it from this SyncPoint
+ id<FNode> resolvedServerCache;
+ if (serverCache == nil & syncPoint != nil) {
+ resolvedServerCache = [syncPoint completeServerCacheAtPath:[FPath empty]];
+ } else {
+ resolvedServerCache = serverCache;
+ }
+
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+ [syncPointTree.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ id<FNode> childServerCache = nil;
+ if (resolvedServerCache != nil) {
+ childServerCache = [resolvedServerCache getImmediateChild:childKey];
+ }
+ FWriteTreeRef *childWritesCache = [writesCache childWriteTreeRef:childKey];
+ id<FOperation> childOperation = [operation operationForChild:childKey];
+ if (childOperation != nil) {
+ [events addObjectsFromArray:[self applyOperationDescendantsHelper:childOperation
+ syncPointTree:childTree
+ serverCache:childServerCache
+ writesCache:childWritesCache]];
+ }
+ }];
+
+ if (syncPoint) {
+ [events addObjectsFromArray:[syncPoint applyOperation:operation writesCache:writesCache serverCache:resolvedServerCache]];
+ }
+
+ return events;
+}
+
+@end
diff --git a/Firebase/Database/Core/FWriteRecord.h b/Firebase/Database/Core/FWriteRecord.h
new file mode 100644
index 0000000..a9b53fe
--- /dev/null
+++ b/Firebase/Database/Core/FWriteRecord.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FPath;
+@class FCompoundWrite;
+@protocol FNode;
+
+@interface FWriteRecord : NSObject
+
+- initWithPath:(FPath *)path overwrite:(id<FNode>)overwrite writeId:(NSInteger)writeId visible:(BOOL)isVisible;
+- initWithPath:(FPath *)path merge:(FCompoundWrite *)merge writeId:(NSInteger)writeId;
+
+@property (nonatomic, readonly) NSInteger writeId;
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, strong, readonly) id<FNode> overwrite;
+/**
+* Maps NSString -> id<FNode>
+*/
+@property (nonatomic, strong, readonly) FCompoundWrite *merge;
+@property (nonatomic, readonly) BOOL visible;
+
+- (BOOL)isMerge;
+- (BOOL)isOverwrite;
+
+@end
diff --git a/Firebase/Database/Core/FWriteRecord.m b/Firebase/Database/Core/FWriteRecord.m
new file mode 100644
index 0000000..47c952c
--- /dev/null
+++ b/Firebase/Database/Core/FWriteRecord.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 "FWriteRecord.h"
+#import "FPath.h"
+#import "FNode.h"
+#import "FCompoundWrite.h"
+
+@interface FWriteRecord ()
+@property (nonatomic, readwrite) NSInteger writeId;
+@property (nonatomic, strong, readwrite) FPath *path;
+@property (nonatomic, strong, readwrite) id<FNode> overwrite;
+@property (nonatomic, strong, readwrite) FCompoundWrite *merge;
+@property (nonatomic, readwrite) BOOL visible;
+@end
+
+@implementation FWriteRecord
+
+- (id)initWithPath:(FPath *)path overwrite:(id<FNode>)overwrite writeId:(NSInteger)writeId visible:(BOOL)isVisible {
+ self = [super init];
+ if (self) {
+ self.path = path;
+ if (overwrite == nil) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't pass nil as overwrite parameter to an overwrite write record"];
+ }
+ self.overwrite = overwrite;
+ self.merge = nil;
+ self.writeId = writeId;
+ self.visible = isVisible;
+ }
+ return self;
+}
+
+- (id)initWithPath:(FPath *)path merge:(FCompoundWrite *)merge writeId:(NSInteger)writeId {
+ self = [super init];
+ if (self) {
+ self.path = path;
+ if (merge == nil) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't pass nil as merge parameter to an merge write record"];
+ }
+ self.overwrite = nil;
+ self.merge = merge;
+ self.writeId = writeId;
+ self.visible = YES;
+ }
+ return self;
+}
+
+- (id<FNode>)overwrite {
+ if (self->_overwrite == nil) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't get overwrite for merge write record!"];
+ }
+ return self->_overwrite;
+}
+
+- (FCompoundWrite *)compoundWrite {
+ if (self->_merge == nil) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't get merge for overwrite write record!"];
+ }
+ return self->_merge;
+}
+
+- (BOOL)isMerge {
+ return self->_merge != nil;
+}
+
+- (BOOL)isOverwrite {
+ return self->_overwrite != nil;
+}
+
+- (NSString *)description {
+ if (self.isOverwrite) {
+ return [NSString stringWithFormat:@"FWriteRecord { writeId = %lu, path = %@, overwrite = %@, visible = %d }",
+ (unsigned long)self.writeId, self.path, self.overwrite, self.visible];
+ } else {
+ return [NSString stringWithFormat:@"FWriteRecord { writeId = %lu, path = %@, merge = %@ }",
+ (unsigned long)self.writeId, self.path, self.merge];
+ }
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[self class]]) {
+ return NO;
+ }
+ FWriteRecord *other = (FWriteRecord *)object;
+ if (self->_writeId != other->_writeId) return NO;
+ if (self->_path != other->_path && ![self->_path isEqual:other->_path]) return NO;
+ if (self->_overwrite != other->_overwrite && ![self->_overwrite isEqual:other->_overwrite]) return NO;
+ if (self->_merge != other->_merge && ![self->_merge isEqual:other->_merge]) return NO;
+ if (self->_visible != other->_visible) return NO;
+
+ return YES;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = self->_writeId * 17;
+ hash = hash * 31 + self->_path.hash;
+ hash = hash * 31 + self->_overwrite.hash;
+ hash = hash * 31 + self->_merge.hash;
+ hash = hash * 31 + ((self->_visible) ? 1 : 0);
+ return hash;
+}
+
+@end
diff --git a/Firebase/Database/Core/FWriteTree.h b/Firebase/Database/Core/FWriteTree.h
new file mode 100644
index 0000000..243bc9f
--- /dev/null
+++ b/Firebase/Database/Core/FWriteTree.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 <Foundation/Foundation.h>
+
+@class FPath;
+@protocol FNode;
+@class FCompoundWrite;
+@class FWriteTreeRef;
+@class FChildrenNode;
+@class FNamedNode;
+@class FWriteRecord;
+@protocol FIndex;
+@class FCacheNode;
+
+@interface FWriteTree : NSObject
+
+- (FWriteTreeRef *) childWritesForPath:(FPath *)path;
+- (void) addOverwriteAtPath:(FPath *)path newData:(id<FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible;
+- (void) addMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId;
+- (BOOL) removeWriteId:(NSInteger)writeId;
+- (NSArray *) removeAllWrites;
+- (FWriteRecord *)writeForId:(NSInteger)writeId;
+
+- (id<FNode>) calculateCompleteEventCacheAtPath:(FPath *)treePath
+ completeServerCache:(id<FNode>)completeServerCache
+ excludeWriteIds:(NSArray *)writeIdsToExclude
+ includeHiddenWrites:(BOOL)includeHiddenWrites;
+
+- (id<FNode>) calculateCompleteEventChildrenAtPath:(FPath *)treePath
+ completeServerChildren:(id<FNode>)completeServerChildren;
+
+- (id<FNode>) calculateEventCacheAfterServerOverwriteAtPath:(FPath *)treePath
+ childPath:(FPath *)childPath
+ existingEventSnap:(id<FNode>)existingEventSnap
+ existingServerSnap:(id<FNode>)existingServerSnap;
+
+- (id<FNode>) calculateCompleteChildAtPath:(FPath *)treePath
+ childKey:(NSString *)childKey
+ cache:(FCacheNode *)existingServerCache;
+
+- (id<FNode>) shadowingWriteAtPath:(FPath *)path;
+
+- (FNamedNode *) calculateNextNodeAfterPost:(FNamedNode *)post
+ atPath:(FPath *)path
+ completeServerData:(id<FNode>)completeServerData
+ reverse:(BOOL)reverse
+ index:(id<FIndex>)index;
+
+@end
diff --git a/Firebase/Database/Core/FWriteTree.m b/Firebase/Database/Core/FWriteTree.m
new file mode 100644
index 0000000..c5b08ea
--- /dev/null
+++ b/Firebase/Database/Core/FWriteTree.m
@@ -0,0 +1,458 @@
+/*
+ * 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 "FWriteTree.h"
+#import "FImmutableTree.h"
+#import "FPath.h"
+#import "FNode.h"
+#import "FWriteTreeRef.h"
+#import "FChildrenNode.h"
+#import "FNamedNode.h"
+#import "FWriteRecord.h"
+#import "FEmptyNode.h"
+#import "FIndex.h"
+#import "FCompoundWrite.h"
+#import "FCacheNode.h"
+
+@interface FWriteTree ()
+/**
+* A tree tracking the results of applying all visible writes. This does not include transactions with
+* applyLocally=false or writes that are completely shadowed by other writes.
+* Contains id<FNode> as values.
+*/
+@property (nonatomic, strong) FCompoundWrite *visibleWrites;
+/**
+* A list of pending writes, regardless of visibility and shadowed-ness. Used to calcuate arbitrary
+* sets of the changed data, such as hidden writes (from transactions) or changes with certain writes excluded (also
+* used by transactions).
+* Contains FWriteRecords.
+*/
+@property (nonatomic, strong) NSMutableArray *allWrites;
+@property (nonatomic) NSInteger lastWriteId;
+@end
+
+/**
+* FWriteTree tracks all pending user-initiated writes and has methods to calcuate the result of merging them with
+* underlying server data (to create "event cache" data). Pending writes are added with addOverwriteAtPath: and
+* addMergeAtPath: and removed with removeWriteId:.
+*/
+@implementation FWriteTree
+
+@synthesize allWrites;
+@synthesize lastWriteId;
+
+- (id) init {
+ self = [super init];
+ if (self) {
+ self.visibleWrites = [FCompoundWrite emptyWrite];
+ self.allWrites = [[NSMutableArray alloc] init];
+ self.lastWriteId = -1;
+ }
+ return self;
+}
+
+/**
+* Create a new WriteTreeRef for the given path. For use with a new sync point at the given path.
+*/
+- (FWriteTreeRef *) childWritesForPath:(FPath *)path {
+ return [[FWriteTreeRef alloc] initWithPath:path writeTree:self];
+}
+
+/**
+* Record a new overwrite from user code.
+* @param visible Is set to false by some transactions. It should be excluded from event caches.
+*/
+- (void) addOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible {
+ NSAssert(writeId > self.lastWriteId, @"Stacking an older write on top of a newer one");
+ FWriteRecord *record = [[FWriteRecord alloc] initWithPath:path overwrite:newData writeId:writeId visible:visible];
+ [self.allWrites addObject:record];
+
+ if (visible) {
+ self.visibleWrites = [self.visibleWrites addWrite:newData atPath:path];
+ }
+
+ self.lastWriteId = writeId;
+}
+
+/**
+* Record a new merge from user code.
+* @param changedChildren maps NSString -> id<FNode>
+*/
+- (void) addMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId {
+ NSAssert(writeId > self.lastWriteId, @"Stacking an older merge on top of newer one");
+ FWriteRecord *record = [[FWriteRecord alloc] initWithPath:path merge:changedChildren writeId:writeId];
+ [self.allWrites addObject:record];
+
+ self.visibleWrites = [self.visibleWrites addCompoundWrite:changedChildren atPath:path];
+ self.lastWriteId = writeId;
+}
+
+- (FWriteRecord *)writeForId:(NSInteger)writeId {
+ NSUInteger index = [self.allWrites indexOfObjectPassingTest:^BOOL(FWriteRecord *write, NSUInteger idx, BOOL *stop) {
+ return write.writeId == writeId;
+ }];
+ return (index == NSNotFound) ? nil : self.allWrites[index];
+}
+
+/**
+* Remove a write (either an overwrite or merge) that has been successfully acknowledged by the server. Recalculates the
+* tree if necessary. We return the path of the write and whether it may have been visible, meaning views need to
+* reevaluate.
+*
+* @return YES if the write may have been visible (meaning we'll need to reevaluate / raise events as a result).
+*/
+- (BOOL) removeWriteId:(NSInteger)writeId {
+ NSUInteger index = [self.allWrites indexOfObjectPassingTest:^BOOL(FWriteRecord *record, NSUInteger idx, BOOL *stop) {
+ if (record.writeId == writeId) {
+ return YES;
+ } else {
+ return NO;
+ }
+ }];
+ NSAssert(index != NSNotFound, @"[FWriteTree removeWriteId:] called with nonexistent writeId.");
+ FWriteRecord *writeToRemove = self.allWrites[index];
+ [self.allWrites removeObjectAtIndex:index];
+
+ BOOL removedWriteWasVisible = writeToRemove.visible;
+ BOOL removedWriteOverlapsWithOtherWrites = NO;
+ NSInteger i = [self.allWrites count] - 1;
+
+ while (removedWriteWasVisible && i >= 0) {
+ FWriteRecord *currentWrite = [self.allWrites objectAtIndex:i];
+ if (currentWrite.visible) {
+ if (i >= index && [self record:currentWrite containsPath:writeToRemove.path]) {
+ // The removed write was completely shadowed by a subsequent write.
+ removedWriteWasVisible = NO;
+ } else if ([writeToRemove.path contains:currentWrite.path]) {
+ // Either we're covering some writes or they're covering part of us (depending on which came first).
+ removedWriteOverlapsWithOtherWrites = YES;
+ }
+ }
+ i--;
+ }
+
+ if (!removedWriteWasVisible) {
+ return NO;
+ } else if (removedWriteOverlapsWithOtherWrites) {
+ // There's some shadowing going on. Just rebuild the visible writes from scratch.
+ [self resetTree];
+ return YES;
+ } else {
+ // There's no shadowing. We can safely just remove the write(s) from visibleWrites.
+ if ([writeToRemove isOverwrite]) {
+ self.visibleWrites = [self.visibleWrites removeWriteAtPath:writeToRemove.path];
+ } else {
+ FCompoundWrite *merge = writeToRemove.merge;
+ [merge enumerateWrites:^(FPath *path, id<FNode> node, BOOL *stop) {
+ self.visibleWrites = [self.visibleWrites removeWriteAtPath:[writeToRemove.path child:path]];
+ }];
+ }
+ return YES;
+ }
+}
+
+- (NSArray *) removeAllWrites {
+ NSArray *writes = self.allWrites;
+ self.visibleWrites = [FCompoundWrite emptyWrite];
+ self.allWrites = [NSMutableArray array];
+ return writes;
+}
+
+/**
+* @return A complete snapshot for the given path if there's visible write data at that path, else nil.
+* No server data is considered.
+*/
+- (id <FNode>) completeWriteDataAtPath:(FPath *)path {
+ return [self.visibleWrites completeNodeAtPath:path];
+}
+
+/**
+* Given optional, underlying server data, and an optional set of constraints (exclude some sets, include hidden
+* writes), attempt to calculate a complete snapshot for the given path
+* @param includeHiddenWrites Defaults to false, whether or not to layer on writes with visible set to false
+*/
+- (id <FNode>) calculateCompleteEventCacheAtPath:(FPath *)treePath completeServerCache:(id <FNode>)completeServerCache
+ excludeWriteIds:(NSArray *)writeIdsToExclude includeHiddenWrites:(BOOL)includeHiddenWrites {
+ if (writeIdsToExclude == nil && !includeHiddenWrites) {
+ id<FNode> shadowingNode = [self.visibleWrites completeNodeAtPath:treePath];
+ if (shadowingNode != nil) {
+ return shadowingNode;
+ } else {
+ // No cache here. Can't claim complete knowledge.
+ FCompoundWrite *subMerge = [self.visibleWrites childCompoundWriteAtPath:treePath];
+ if (subMerge.isEmpty) {
+ return completeServerCache;
+ } else if (completeServerCache == nil && ![subMerge hasCompleteWriteAtPath:[FPath empty]]) {
+ // We wouldn't have a complete snapshot since there's no underlying data and no complete shadow
+ return nil;
+ } else {
+ id<FNode> layeredCache = completeServerCache != nil ? completeServerCache : [FEmptyNode emptyNode];
+ return [subMerge applyToNode:layeredCache];
+ }
+ }
+ } else {
+ FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
+ if (!includeHiddenWrites && merge.isEmpty) {
+ return completeServerCache;
+ } else {
+ // If the server cache is null and we don't have a complete cache, we need to return nil
+ if (!includeHiddenWrites && completeServerCache == nil && ![merge hasCompleteWriteAtPath:[FPath empty]]) {
+ return nil;
+ } else {
+ BOOL (^filter) (FWriteRecord *) = ^(FWriteRecord *record) {
+ return (BOOL) ((record.visible || includeHiddenWrites) &&
+ (writeIdsToExclude == nil || ![writeIdsToExclude containsObject:[NSNumber numberWithInteger:record.writeId]]) &&
+ ([record.path contains:treePath] || [treePath contains:record.path]));
+ };
+ FCompoundWrite *mergeAtPath = [FWriteTree layerTreeFromWrites:self.allWrites filter:filter treeRoot:treePath];
+ id<FNode> layeredCache = completeServerCache ? completeServerCache : [FEmptyNode emptyNode];
+ return [mergeAtPath applyToNode:layeredCache];
+ }
+ }
+ }
+}
+
+/**
+* With optional, underlying server data, attempt to return a children node of children that we have complete data for.
+* Used when creating new views, to pre-fill their complete event children snapshot.
+*/
+- (FChildrenNode *) calculateCompleteEventChildrenAtPath:(FPath *)treePath
+ completeServerChildren:(id<FNode>)completeServerChildren {
+ __block id<FNode> completeChildren = [FEmptyNode emptyNode];
+ id<FNode> topLevelSet = [self.visibleWrites completeNodeAtPath:treePath];
+ if (topLevelSet != nil) {
+ if (![topLevelSet isLeafNode]) {
+ // We're shadowing everything. Return the children.
+ FChildrenNode *topChildrenNode = topLevelSet;
+ [topChildrenNode enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ completeChildren = [completeChildren updateImmediateChild:key withNewChild:node];
+ }];
+ }
+ return completeChildren;
+ } else {
+ // Layer any children we have on top of this
+ // We know we don't have a top-level set, so just enumerate existing children, and apply any updates
+ FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
+ [completeServerChildren enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ FCompoundWrite *childMerge = [merge childCompoundWriteAtPath:[[FPath alloc] initWith:key]];
+ id<FNode> newChildNode = [childMerge applyToNode:node];
+ completeChildren = [completeChildren updateImmediateChild:key withNewChild:newChildNode];
+ }];
+ // Add any complete children we have from the set.
+ for (FNamedNode *node in merge.completeChildren) {
+ completeChildren = [completeChildren updateImmediateChild:node.name withNewChild:node.node];
+ }
+ return completeChildren;
+ }
+}
+
+/**
+* Given that the underlying server data has updated, determine what, if anything, needs to be applied to the event cache.
+*
+* Possibilities
+*
+* 1. No write are shadowing. Events should be raised, the snap to be applied comes from the server data.
+*
+* 2. Some write is completely shadowing. No events to be raised.
+*
+* 3. Is partially shadowed. Events ..
+*
+* Either existingEventSnap or existingServerSnap must exist.
+*/
+- (id <FNode>) calculateEventCacheAfterServerOverwriteAtPath:(FPath *)treePath childPath:(FPath *)childPath existingEventSnap:(id <FNode>)existingEventSnap existingServerSnap:(id <FNode>)existingServerSnap {
+ NSAssert(existingEventSnap != nil || existingServerSnap != nil,
+ @"Either existingEventSnap or existingServerSanp must exist.");
+
+ FPath *path = [treePath child:childPath];
+ if ([self.visibleWrites hasCompleteWriteAtPath:path]) {
+ // At this point we can probably guarantee that we're in case 2, meaning no events
+ // May need to check visibility while doing the findRootMostValueAndPath call
+ return nil;
+ } else {
+ // This could be more efficient if the serverNode + updates doesn't change the eventSnap
+ // However this is tricky to find out, since user updates don't necessary change the server
+ // snap, e.g. priority updates on empty nodes, or deep deletes. Another special case is if the server
+ // adds nodes, but doesn't change any existing writes. It is therefore not enough to
+ // only check if the updates change the serverNode.
+ // Maybe check if the merge tree contains these special cases and only do a full overwrite in that case?
+ FCompoundWrite *childMerge = [self.visibleWrites childCompoundWriteAtPath:path];
+ if (childMerge.isEmpty) {
+ // We're not shadowing at all. Case 1
+ return [existingServerSnap getChild:childPath];
+ } else {
+ return [childMerge applyToNode:[existingServerSnap getChild:childPath]];
+ }
+ }
+}
+
+/**
+* Returns a complete child for a given server snap after applying all user writes or nil if there is no complete child
+* for this child key.
+*/
+- (id<FNode>) calculateCompleteChildAtPath:(FPath *)treePath childKey:(NSString *)childKey cache:(FCacheNode *)existingServerCache {
+ FPath *path = [treePath childFromString:childKey];
+ id<FNode> shadowingNode = [self.visibleWrites completeNodeAtPath:path];
+ if (shadowingNode != nil) {
+ return shadowingNode;
+ } else {
+ if ([existingServerCache isCompleteForChild:childKey]) {
+ FCompoundWrite *childMerge = [self.visibleWrites childCompoundWriteAtPath:path];
+ return [childMerge applyToNode:[existingServerCache.node getImmediateChild:childKey]];
+ } else {
+ return nil;
+ }
+ }
+}
+
+/**
+* Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at
+* a higher path, this will return the child of that write relative to the write and this path.
+* Returns null if there is no write at this path.
+*/
+- (id<FNode>) shadowingWriteAtPath:(FPath *)path {
+ return [self.visibleWrites completeNodeAtPath:path];
+}
+
+/**
+* This method is used when processing child remove events on a query. If we can, we pull in children that were outside
+* the window, but may now be in the window.
+*/
+- (FNamedNode *)calculateNextNodeAfterPost:(FNamedNode *)post
+ atPath:(FPath *)treePath
+ completeServerData:(id<FNode>)completeServerData
+ reverse:(BOOL)reverse
+ index:(id<FIndex>)index
+{
+ __block id<FNode> toIterate;
+ FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
+ id<FNode> shadowingNode = [merge completeNodeAtPath:[FPath empty]];
+ if (shadowingNode != nil) {
+ toIterate = shadowingNode;
+ } else if (completeServerData != nil) {
+ toIterate = [merge applyToNode:completeServerData];
+ } else {
+ return nil;
+ }
+
+ __block NSString *currentNextKey = nil;
+ __block id<FNode> currentNextNode = nil;
+ [toIterate enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ if ([index compareKey:key andNode:node toOtherKey:post.name andNode:post.node reverse:reverse] > NSOrderedSame &&
+ (!currentNextKey || [index compareKey:key andNode:node toOtherKey:currentNextKey andNode:currentNextNode reverse:reverse] < NSOrderedSame)) {
+ currentNextKey = key;
+ currentNextNode = node;
+ }
+ }];
+
+ if (currentNextKey != nil) {
+ return [FNamedNode nodeWithName:currentNextKey node:currentNextNode];
+ } else {
+ return nil;
+ }
+}
+
+#pragma mark -
+#pragma mark Private Methods
+
+- (BOOL) record:(FWriteRecord *)record containsPath:(FPath *)path {
+ if ([record isOverwrite]) {
+ return [record.path contains:path];
+ } else {
+ __block BOOL contains = NO;
+ [record.merge enumerateWrites:^(FPath *childPath, id<FNode> node, BOOL *stop) {
+ contains = [[record.path child:childPath] contains:path];
+ *stop = contains;
+ }];
+ return contains;
+ }
+}
+
+/**
+* Re-layer the writes and merges into a tree so we can efficiently calculate event snapshots
+*/
+- (void) resetTree {
+ self.visibleWrites = [FWriteTree layerTreeFromWrites:self.allWrites filter:[FWriteTree defaultFilter] treeRoot:[FPath empty]];
+ if ([self.allWrites count] > 0) {
+ FWriteRecord *lastRecord = self.allWrites[[self.allWrites count] - 1];
+ self.lastWriteId = lastRecord.writeId;
+ } else {
+ self.lastWriteId = -1;
+ }
+}
+
+/**
+* The default filter used when constructing the tree. Keep everything that's visible.
+*/
++ (BOOL (^)(FWriteRecord *record)) defaultFilter {
+ static BOOL (^filter)(FWriteRecord *);
+ static dispatch_once_t filterToken;
+ dispatch_once(&filterToken, ^{
+ filter = ^(FWriteRecord *record) {
+ return YES;
+ };
+ });
+ return filter;
+}
+
+/**
+* Static method. Given an array of WriteRecords, a filter for which ones to include, and a path, construct a merge
+* at that path
+* @return An FImmutableTree of id<FNode>s.
+*/
++ (FCompoundWrite *) layerTreeFromWrites:(NSArray *)writes filter:(BOOL (^)(FWriteRecord *record))filter treeRoot:(FPath *)treeRoot {
+ __block FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ [writes enumerateObjectsUsingBlock:^(FWriteRecord *record, NSUInteger idx, BOOL *stop) {
+ // Theory, a later set will either:
+ // a) abort a relevant transaction, so no need to worry about excluding it from calculating that transaction
+ // b) not be relevant to a transaction (separate branch), so again will not affect the data for that transaction
+ if (filter(record)) {
+ FPath *writePath = record.path;
+ if ([record isOverwrite]) {
+ if ([treeRoot contains:writePath]) {
+ FPath *relativePath = [FPath relativePathFrom:treeRoot to:writePath];
+ compoundWrite = [compoundWrite addWrite:record.overwrite atPath:relativePath];
+ } else if ([writePath contains:treeRoot]) {
+ id<FNode> child = [record.overwrite getChild:[FPath relativePathFrom:writePath to:treeRoot]];
+ compoundWrite = [compoundWrite addWrite:child atPath:[FPath empty]];
+ } else {
+ // There is no overlap between root path and write path, ignore write
+ }
+ } else {
+ if ([treeRoot contains:writePath]) {
+ FPath *relativePath = [FPath relativePathFrom:treeRoot to:writePath];
+ compoundWrite = [compoundWrite addCompoundWrite:record.merge atPath:relativePath];
+ } else if ([writePath contains:treeRoot]) {
+ FPath *relativePath = [FPath relativePathFrom:writePath to:treeRoot];
+ if (relativePath.isEmpty) {
+ compoundWrite = [compoundWrite addCompoundWrite:record.merge atPath:[FPath empty]];
+ } else {
+ id<FNode> child = [record.merge completeNodeAtPath:relativePath];
+ if (child != nil) {
+ // There exists a child in this node that matches the root path
+ id<FNode> deepNode = [child getChild:[relativePath popFront]];
+ compoundWrite = [compoundWrite addWrite:deepNode atPath:[FPath empty]];
+ }
+ }
+ } else {
+ // There is no overlap between root path and write path, ignore write
+ }
+ }
+ }
+ }];
+ return compoundWrite;
+}
+
+@end
diff --git a/Firebase/Database/Core/FWriteTreeRef.h b/Firebase/Database/Core/FWriteTreeRef.h
new file mode 100644
index 0000000..791dd26
--- /dev/null
+++ b/Firebase/Database/Core/FWriteTreeRef.h
@@ -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 <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FChildrenNode;
+@class FPath;
+@class FNamedNode;
+@class FWriteRecord;
+@class FWriteTree;
+@protocol FIndex;
+@class FCacheNode;
+
+@interface FWriteTreeRef : NSObject
+
+- (id) initWithPath:(FPath *)aPath writeTree:(FWriteTree *)tree;
+
+- (id <FNode>) calculateCompleteEventCacheWithCompleteServerCache:(id <FNode>)completeServerCache;
+
+- (FChildrenNode *) calculateCompleteEventChildrenWithCompleteServerChildren:(FChildrenNode *)completeServerChildren;
+
+- (id<FNode>) calculateEventCacheAfterServerOverwriteWithChildPath:(FPath *)childPath
+ existingEventSnap:(id<FNode>)existingEventSnap
+ existingServerSnap:(id<FNode>)existingServerSnap;
+
+- (id<FNode>) shadowingWriteAtPath:(FPath *)path;
+
+- (FNamedNode *) calculateNextNodeAfterPost:(FNamedNode *)post
+ completeServerData:(id<FNode>)completeServerData
+ reverse:(BOOL)reverse
+ index:(id<FIndex>)index;
+
+- (id<FNode>) calculateCompleteChild:(NSString *)childKey cache:(FCacheNode *)existingServerCache;
+
+- (FWriteTreeRef *) childWriteTreeRef:(NSString *)childKey;
+
+@end
diff --git a/Firebase/Database/Core/FWriteTreeRef.m b/Firebase/Database/Core/FWriteTreeRef.m
new file mode 100644
index 0000000..392369b
--- /dev/null
+++ b/Firebase/Database/Core/FWriteTreeRef.m
@@ -0,0 +1,133 @@
+/*
+ * 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 "FWriteTreeRef.h"
+#import "FPath.h"
+#import "FNode.h"
+#import "FWriteTree.h"
+#import "FChildrenNode.h"
+#import "FNamedNode.h"
+#import "FWriteRecord.h"
+#import "FIndex.h"
+#import "FCacheNode.h"
+
+@interface FWriteTreeRef ()
+/**
+* The path to this particular FWriteTreeRef. Used for calling methods on writeTree while exposing a simpler interface
+* to callers.
+*/
+@property (nonatomic, strong) FPath *path;
+/**
+* A reference to the actual tree of the write data. All methods are pass-through to the tree, but with the appropriate
+* path prefixed.
+*
+* This lets us make cheap references to points in the tree for sync points without having to copy and maintain all of
+* the data.
+*/
+@property (nonatomic, strong) FWriteTree *writeTree;
+@end
+
+/**
+* A FWriteTreeRef wraps a FWriteTree and a FPath, for convenient access to a particular subtree. All the methods just
+* proxy to the underlying FWriteTree.
+*/
+@implementation FWriteTreeRef
+- (id) initWithPath:(FPath *)aPath writeTree:(FWriteTree *)tree {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.writeTree = tree;
+ }
+ return self;
+}
+
+/**
+* @return If possible, returns a complete event cache, using the underlying server data if possible. In addition, can
+* be used to get a cache that includes hidden writes, and excludes arbitrary writes. Note that customizing the returned
+* node can lead to a more expensive calculation.
+*/
+- (id <FNode>) calculateCompleteEventCacheWithCompleteServerCache:(id<FNode>)completeServerCache {
+ return [self.writeTree calculateCompleteEventCacheAtPath:self.path completeServerCache:completeServerCache excludeWriteIds:nil includeHiddenWrites:NO];
+}
+
+/**
+* @return If possible, returns a children node containing all of the complete children we have data for. The returned
+* data is a mix of the given server data and write data.
+*/
+- (FChildrenNode *) calculateCompleteEventChildrenWithCompleteServerChildren:(id<FNode>)completeServerChildren {
+ return [self.writeTree calculateCompleteEventChildrenAtPath:self.path completeServerChildren:completeServerChildren];
+}
+
+/**
+* Given that either the underlying server data has updated or the outstanding writes have been updating, determine what,
+* if anything, needs to be applied to the event cache.
+*
+* Possibilities:
+*
+* 1. No writes are shadowing. Events should be raised, the snap to be applied comes from the server data.
+*
+* 2. Some writes are completly shadowing. No events to be raised.
+*
+* 3. Is partially shadowed. Events should be raised.
+*
+* Either existingEventSnap or existingServerSnap must exist, this is validated via an assert.
+*/
+- (id<FNode>) calculateEventCacheAfterServerOverwriteWithChildPath:(FPath *)childPath existingEventSnap:(id <FNode>)existingEventSnap existingServerSnap:(id <FNode>)existingServerSnap {
+ return [self.writeTree calculateEventCacheAfterServerOverwriteAtPath:self.path childPath:childPath existingEventSnap:existingEventSnap existingServerSnap:existingServerSnap];
+}
+
+/**
+* Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at a higher
+* path, this will return the child of that write relative to the write and this path.
+* Returns nil if there is no write at this path.
+*/
+- (id<FNode>) shadowingWriteAtPath:(FPath *)path {
+ return [self.writeTree shadowingWriteAtPath:[self.path child:path]];
+}
+
+/**
+* This method is used when processing child remove events on a query. If we can, we pull in children that are outside
+* the window, but may now be in the window.
+*/
+- (FNamedNode *)calculateNextNodeAfterPost:(FNamedNode *)post
+ completeServerData:(id<FNode>)completeServerData
+ reverse:(BOOL)reverse
+ index:(id<FIndex>)index
+{
+ return [self.writeTree calculateNextNodeAfterPost:post
+ atPath:self.path
+ completeServerData:completeServerData
+ reverse:reverse
+ index:index];
+}
+
+/**
+* Returns a complete child for a given server snap after applying all user writes or nil if there is no complete child
+* for this child key.
+*/
+- (id<FNode>) calculateCompleteChild:(NSString *)childKey cache:(FCacheNode *)existingServerCache {
+ return [self.writeTree calculateCompleteChildAtPath:self.path childKey:childKey cache:existingServerCache];
+}
+
+/**
+* @return a WriteTreeref for a child.
+*/
+- (FWriteTreeRef *) childWriteTreeRef:(NSString *)childKey {
+ return [[FWriteTreeRef alloc] initWithPath:[self.path childFromString:childKey] writeTree:self.writeTree];
+}
+
+
+@end
diff --git a/Firebase/Database/Core/Operation/FAckUserWrite.h b/Firebase/Database/Core/Operation/FAckUserWrite.h
new file mode 100644
index 0000000..a337996
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FAckUserWrite.h
@@ -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 "FOperation.h"
+
+@class FPath;
+@class FOperationSource;
+@class FImmutableTree;
+
+
+@interface FAckUserWrite : NSObject <FOperation>
+
+- initWithPath:(FPath *)operationPath affectedTree:(FImmutableTree *)affectedTree revert:(BOOL)shouldRevert;
+
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, readonly) FOperationType type;
+@property (nonatomic, strong, readonly) FPath *path;
+// A FImmutableTree, containing @YES for each affected path. Affected paths can't overlap.
+@property (nonatomic, strong, readonly) FImmutableTree *affectedTree;
+@property (nonatomic, readonly) BOOL revert;
+
+@end
diff --git a/Firebase/Database/Core/Operation/FAckUserWrite.m b/Firebase/Database/Core/Operation/FAckUserWrite.m
new file mode 100644
index 0000000..f81e7f5
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FAckUserWrite.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 "FAckUserWrite.h"
+#import "FPath.h"
+#import "FOperationSource.h"
+#import "FImmutableTree.h"
+
+
+@implementation FAckUserWrite
+
+- (id) initWithPath:(FPath *)operationPath affectedTree:(FImmutableTree *)tree revert:(BOOL)shouldRevert {
+ self = [super init];
+ if (self) {
+ self->_source = [FOperationSource userInstance];
+ self->_type = FOperationTypeAckUserWrite;
+ self->_path = operationPath;
+ self->_affectedTree = tree;
+ self->_revert = shouldRevert;
+ }
+ return self;
+}
+
+- (FAckUserWrite *) operationForChild:(NSString *)childKey {
+ if (![self.path isEmpty]) {
+ NSAssert([self.path.getFront isEqualToString:childKey], @"operationForChild called for unrelated child.");
+ return [[FAckUserWrite alloc] initWithPath:[self.path popFront] affectedTree:self.affectedTree revert:self.revert];
+ } else if (self.affectedTree.value != nil) {
+ NSAssert(self.affectedTree.children.isEmpty, @"affectedTree should not have overlapping affected paths.");
+ // All child locations are affected as well; just return same operation.
+ return self;
+ } else {
+ FImmutableTree *childTree = [self.affectedTree subtreeAtPath:[[FPath alloc] initWith:childKey]];
+ return [[FAckUserWrite alloc] initWithPath:[FPath empty] affectedTree:childTree revert:self.revert];
+ }
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FAckUserWrite { path=%@, revert=%d, affectedTree=%@ }", self.path, self.revert, self.affectedTree];
+}
+
+@end
diff --git a/Firebase/Database/Core/Operation/FMerge.h b/Firebase/Database/Core/Operation/FMerge.h
new file mode 100644
index 0000000..4cab613
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FMerge.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 "FOperation.h"
+
+@class FCompoundWrite;
+
+@interface FMerge : NSObject <FOperation>
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath children:(FCompoundWrite *)children;
+
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, readonly) FOperationType type;
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, strong, readonly) FCompoundWrite *children;
+
+@end
diff --git a/Firebase/Database/Core/Operation/FMerge.m b/Firebase/Database/Core/Operation/FMerge.m
new file mode 100644
index 0000000..8e6d924
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FMerge.m
@@ -0,0 +1,71 @@
+/*
+ * 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 "FMerge.h"
+#import "FOperationSource.h"
+#import "FPath.h"
+#import "FNode.h"
+#import "FOverwrite.h"
+#import "FCompoundWrite.h"
+
+@interface FMerge ()
+@property (nonatomic, strong, readwrite) FOperationSource *source;
+@property (nonatomic, readwrite) FOperationType type;
+@property (nonatomic, strong, readwrite) FPath *path;
+@property (nonatomic, strong) FCompoundWrite *children;
+@end
+
+@implementation FMerge
+
+@synthesize source;
+@synthesize type;
+@synthesize path;
+@synthesize children;
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath children:(FCompoundWrite *)someChildren {
+ self = [super init];
+ if (self) {
+ self.source = aSource;
+ self.type = FOperationTypeMerge;
+ self.path = aPath;
+ self.children = someChildren;
+ }
+ return self;
+}
+
+- (id<FOperation>) operationForChild:(NSString *)childKey {
+ if ([self.path isEmpty]) {
+ FCompoundWrite *childTree = [self.children childCompoundWriteAtPath:[[FPath alloc] initWith:childKey]];
+ if (childTree.isEmpty) {
+ return nil;
+ } else if (childTree.rootWrite != nil) {
+ // We have a snapshot for the child in question. This becomes an overwrite of the child.
+ return [[FOverwrite alloc] initWithSource:self.source path:[FPath empty] snap:childTree.rootWrite];
+ } else {
+ // This is a merge at a deeper level
+ return [[FMerge alloc] initWithSource:self.source path:[FPath empty] children:childTree];
+ }
+ } else {
+ NSAssert([self.path.getFront isEqualToString:childKey], @"Can't get a merge for a child not on the path of the operation");
+ return [[FMerge alloc] initWithSource:self.source path:[self.path popFront] children:self.children];
+ }
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FMerge { path=%@, soruce=%@ children=%@}", self.path, self.source, self.children];
+}
+
+@end
diff --git a/Firebase/Database/Core/Operation/FOperation.h b/Firebase/Database/Core/Operation/FOperation.h
new file mode 100644
index 0000000..2bbbbd2
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FOperation.h
@@ -0,0 +1,34 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FOperationSource;
+@class FPath;
+
+typedef NS_ENUM(NSInteger, FOperationType) {
+ FOperationTypeOverwrite = 0,
+ FOperationTypeMerge = 1,
+ FOperationTypeAckUserWrite = 2,
+ FOperationTypeListenComplete = 3
+};
+
+@protocol FOperation <NSObject>
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, readonly) FOperationType type;
+@property (nonatomic, strong, readonly) FPath *path;
+- (id<FOperation>) operationForChild:(NSString *)childKey;
+@end
diff --git a/Firebase/Database/Core/Operation/FOperationSource.h b/Firebase/Database/Core/Operation/FOperationSource.h
new file mode 100644
index 0000000..a069c2f
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FOperationSource.h
@@ -0,0 +1,34 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FQueryParams;
+
+@interface FOperationSource : NSObject
+
+@property (nonatomic, readonly) BOOL fromUser;
+@property (nonatomic, readonly) BOOL fromServer;
+@property (nonatomic, readonly) BOOL isTagged;
+@property (nonatomic, strong, readonly) FQueryParams *queryParams;
+
+- initWithFromUser:(BOOL)isFromUser fromServer:(BOOL)isFromServer queryParams:(FQueryParams *)params tagged:(BOOL)isTagged;
+
++ (FOperationSource *) userInstance;
++ (FOperationSource *) serverInstance;
++ (FOperationSource *) forServerTaggedQuery:(FQueryParams *)params;
+
+@end
diff --git a/Firebase/Database/Core/Operation/FOperationSource.m b/Firebase/Database/Core/Operation/FOperationSource.m
new file mode 100644
index 0000000..9a34a2e
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FOperationSource.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 "FOperationSource.h"
+#import "FPath.h"
+#import "FQueryParams.h"
+
+@interface FOperationSource ()
+@property (nonatomic, readwrite) BOOL fromUser;
+@property (nonatomic, readwrite) BOOL fromServer;
+@property (nonatomic, readwrite) BOOL isTagged;
+@property (nonatomic, strong, readwrite) FQueryParams *queryParams;
+@end
+
+@implementation FOperationSource
+
+@synthesize fromUser;
+@synthesize fromServer;
+@synthesize queryParams;
+
+- (id) initWithFromUser:(BOOL)isFromUser fromServer:(BOOL)isFromServer queryParams:(FQueryParams *)params tagged:(BOOL)tagged {
+ self = [super init];
+ if (self) {
+ self.fromUser = isFromUser;
+ self.fromServer = isFromServer;
+ self.queryParams = params;
+ self.isTagged = tagged;
+ }
+ return self;
+}
+
++ (FOperationSource *) userInstance {
+ static FOperationSource *user = nil;
+ static dispatch_once_t userToken;
+ dispatch_once(&userToken, ^{
+ user = [[FOperationSource alloc] initWithFromUser:YES fromServer:NO queryParams:nil tagged:NO];
+ });
+ return user;
+}
+
++ (FOperationSource *) serverInstance {
+ static FOperationSource *server = nil;
+ static dispatch_once_t serverToken;
+ dispatch_once(&serverToken, ^{
+ server = [[FOperationSource alloc] initWithFromUser:NO fromServer:YES queryParams:nil tagged:NO];
+ });
+ return server;
+}
+
++ (FOperationSource *) forServerTaggedQuery:(FQueryParams *)params {
+ return [[FOperationSource alloc] initWithFromUser:NO fromServer:YES queryParams:params tagged:YES];
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FOperationSource { fromUser=%d, fromServer=%d, queryId=%@, tagged=%d }",
+ self.fromUser, self.fromServer, self.queryParams, self.isTagged];
+}
+
+
+@end
diff --git a/Firebase/Database/Core/Operation/FOverwrite.h b/Firebase/Database/Core/Operation/FOverwrite.h
new file mode 100644
index 0000000..e950bed
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FOverwrite.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 "FOperation.h"
+
+@protocol FNode;
+
+@interface FOverwrite : NSObject <FOperation>
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath snap:(id<FNode>)aSnap;
+
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, readonly) FOperationType type;
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, strong, readonly) id<FNode> snap;
+
+@end
diff --git a/Firebase/Database/Core/Operation/FOverwrite.m b/Firebase/Database/Core/Operation/FOverwrite.m
new file mode 100644
index 0000000..b72d31a
--- /dev/null
+++ b/Firebase/Database/Core/Operation/FOverwrite.m
@@ -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 "FOverwrite.h"
+#import "FNode.h"
+#import "FOperationSource.h"
+
+@interface FOverwrite ()
+@property (nonatomic, strong, readwrite) FOperationSource *source;
+@property (nonatomic, readwrite) FOperationType type;
+@property (nonatomic, strong, readwrite) FPath *path;
+@property (nonatomic, strong) id<FNode> snap;
+@end
+
+@implementation FOverwrite
+
+@synthesize source;
+@synthesize type;
+@synthesize path;
+@synthesize snap;
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath snap:(id <FNode>)aSnap {
+ self = [super init];
+ if (self) {
+ self.source = aSource;
+ self.type = FOperationTypeOverwrite;
+ self.path = aPath;
+ self.snap = aSnap;
+ }
+ return self;
+}
+
+- (FOverwrite *) operationForChild:(NSString *)childKey {
+ if ([self.path isEmpty]) {
+ return [[FOverwrite alloc] initWithSource:self.source
+ path:[FPath empty]
+ snap:[self.snap getImmediateChild:childKey]];
+ } else {
+ return [[FOverwrite alloc] initWithSource:self.source
+ path:[self.path popFront]
+ snap:self.snap];
+ }
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FOverwrite { path=%@, source=%@, snapshot=%@ }", self.path, self.source, self.snap];
+}
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FIRRetryHelper.h b/Firebase/Database/Core/Utilities/FIRRetryHelper.h
new file mode 100644
index 0000000..ffe2726
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FIRRetryHelper.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 <Foundation/Foundation.h>
+
+@interface FIRRetryHelper : NSObject
+
+- (instancetype) initWithDispatchQueue:(dispatch_queue_t)dispatchQueue
+ minRetryDelayAfterFailure:(NSTimeInterval)minRetryDelayAfterFailure
+ maxRetryDelay:(NSTimeInterval)maxRetryDelay
+ retryExponent:(double)retryExponent
+ jitterFactor:(double)jitterFactor;
+
+- (void) retry:(void (^)())block;
+
+- (void) cancel;
+
+- (void) signalSuccess;
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FIRRetryHelper.m b/Firebase/Database/Core/Utilities/FIRRetryHelper.m
new file mode 100644
index 0000000..199e17d
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FIRRetryHelper.m
@@ -0,0 +1,139 @@
+/*
+ * 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 "FIRRetryHelper.h"
+#import "FUtilities.h"
+
+@interface FIRRetryHelperTask : NSObject
+
+@property (nonatomic, strong) void (^block)();
+
+@end
+
+@implementation FIRRetryHelperTask
+
+- (instancetype) initWithBlock:(void (^)())block {
+ self = [super init];
+ if (self != nil) {
+ self->_block = [block copy];
+ }
+ return self;
+}
+
+- (BOOL) isCanceled {
+ return self.block == nil;
+}
+
+- (void) cancel {
+ self.block = nil;
+}
+
+- (void) execute {
+ if (self.block) {
+ self.block();
+ }
+}
+
+@end
+
+
+
+@interface FIRRetryHelper ()
+
+@property (nonatomic, strong) dispatch_queue_t dispatchQueue;
+@property (nonatomic) NSTimeInterval minRetryDelayAfterFailure;
+@property (nonatomic) NSTimeInterval maxRetryDelay;
+@property (nonatomic) double retryExponent;
+@property (nonatomic) double jitterFactor;
+
+@property (nonatomic) BOOL lastWasSuccess;
+@property (nonatomic) NSTimeInterval currentRetryDelay;
+
+@property (nonatomic, strong) FIRRetryHelperTask *scheduledRetry;
+
+@end
+
+@implementation FIRRetryHelper
+
+- (instancetype) initWithDispatchQueue:(dispatch_queue_t)dispatchQueue
+ minRetryDelayAfterFailure:(NSTimeInterval)minRetryDelayAfterFailure
+ maxRetryDelay:(NSTimeInterval)maxRetryDelay
+ retryExponent:(double)retryExponent
+ jitterFactor:(double)jitterFactor {
+ self = [super init];
+ if (self != nil) {
+ self->_dispatchQueue = dispatchQueue;
+ self->_minRetryDelayAfterFailure = minRetryDelayAfterFailure;
+ self->_maxRetryDelay = maxRetryDelay;
+ self->_retryExponent = retryExponent;
+ self->_jitterFactor = jitterFactor;
+ self->_lastWasSuccess = YES;
+ }
+ return self;
+}
+
+- (void) retry:(void (^)())block {
+ if (self.scheduledRetry != nil) {
+ FFLog(@"I-RDB054001", @"Canceling existing retry attempt");
+ [self.scheduledRetry cancel];
+ self.scheduledRetry = nil;
+ }
+
+ NSTimeInterval delay;
+ if (self.lastWasSuccess) {
+ delay = 0;
+ } else {
+ if (self.currentRetryDelay == 0) {
+ self.currentRetryDelay = self.minRetryDelayAfterFailure;
+ } else {
+ NSTimeInterval newDelay = (self.currentRetryDelay * self.retryExponent);
+ self.currentRetryDelay = MIN(newDelay, self.maxRetryDelay);
+ }
+
+ delay = ((1 - self.jitterFactor) * self.currentRetryDelay) +
+ (self.jitterFactor * self.currentRetryDelay * [FUtilities randomDouble]);
+ FFLog(@"I-RDB054002", @"Scheduling retry in %fs", delay);
+
+ }
+ self.lastWasSuccess = NO;
+ FIRRetryHelperTask *task = [[FIRRetryHelperTask alloc] initWithBlock:block];
+ self.scheduledRetry = task;
+ dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (long long)(delay * NSEC_PER_SEC));
+ dispatch_after(popTime, self.dispatchQueue, ^{
+ if (![task isCanceled]) {
+ self.scheduledRetry = nil;
+ [task execute];
+ }
+ });
+}
+
+- (void) signalSuccess {
+ self.lastWasSuccess = YES;
+ self.currentRetryDelay = 0;
+}
+
+- (void) cancel {
+ if (self.scheduledRetry != nil) {
+ FFLog(@"I-RDB054003", @"Canceling existing retry attempt");
+ [self.scheduledRetry cancel];
+ self.scheduledRetry = nil;
+ } else {
+ FFLog(@"I-RDB054004", @"No existing retry attempt to cancel");
+ }
+ self.currentRetryDelay = 0;
+}
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FImmutableTree.h b/Firebase/Database/Core/Utilities/FImmutableTree.h
new file mode 100644
index 0000000..005a9f2
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FImmutableTree.h
@@ -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 "FImmutableSortedDictionary.h"
+#import "FPath.h"
+#import "FTuplePathValue.h"
+
+@interface FImmutableTree : NSObject
+
+- (id) initWithValue:(id)aValue;
+- (id) initWithValue:(id)aValue children:(FImmutableSortedDictionary *)childrenMap;
+
++ (FImmutableTree *) empty;
+- (BOOL) isEmpty;
+
+- (FTuplePathValue *) findRootMostMatchingPath:(FPath *)relativePath predicate:(BOOL (^)(id))predicate;
+- (FTuplePathValue *) findRootMostValueAndPath:(FPath *)relativePath;
+- (FImmutableTree *) subtreeAtPath:(FPath *)relativePath;
+- (FImmutableTree *) setValue:(id)newValue atPath:(FPath *)relativePath;
+- (FImmutableTree *) removeValueAtPath:(FPath *)relativePath;
+- (id) valueAtPath:(FPath *)relativePath;
+- (id) rootMostValueOnPath:(FPath *)path;
+- (id) rootMostValueOnPath:(FPath *)path matching:(BOOL (^)(id))predicate;
+- (id) leafMostValueOnPath:(FPath *)path;
+- (id) leafMostValueOnPath:(FPath *)relativePath matching:(BOOL (^)(id))predicate;
+- (BOOL) containsValueMatching:(BOOL (^)(id))predicate;
+- (FImmutableTree *) setTree:(FImmutableTree *)newTree atPath:(FPath *)relativePath;
+- (id) foldWithBlock:(id (^)(FPath *path, id value, NSDictionary *foldedChildren))block;
+- (id) findOnPath:(FPath *)path andApplyBlock:(id (^)(FPath *path, id value))block;
+- (FPath *) forEachOnPath:(FPath *)path whileBlock:(BOOL (^)(FPath *path, id value))block;
+- (FImmutableTree *) forEachOnPath:(FPath *)path performBlock:(void (^)(FPath *path, id value))block;
+- (void) forEach:(void (^)(FPath *path, id value))block;
+- (void) forEachChild:(void (^)(NSString *childKey, id childValue))block;
+
+@property (nonatomic, strong, readonly) id value;
+@property (nonatomic, strong, readonly) FImmutableSortedDictionary *children;
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FImmutableTree.m b/Firebase/Database/Core/Utilities/FImmutableTree.m
new file mode 100644
index 0000000..57bf74d
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FImmutableTree.m
@@ -0,0 +1,421 @@
+/*
+ * 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 "FImmutableTree.h"
+#import "FImmutableSortedDictionary.h"
+#import "FPath.h"
+#import "FUtilities.h"
+
+@interface FImmutableTree ()
+@property (nonatomic, strong, readwrite) id value;
+/**
+* Maps NSString -> FImmutableTree<T>, where <T> is type of value.
+*/
+@property (nonatomic, strong, readwrite) FImmutableSortedDictionary *children;
+@end
+
+@implementation FImmutableTree
+@synthesize value;
+@synthesize children;
+
+- (id) initWithValue:(id)aValue {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ self.children = [FImmutableTree emptyChildren];
+ }
+ return self;
+}
+
+- (id) initWithValue:(id)aValue children:(FImmutableSortedDictionary *)childrenMap {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ self.children = childrenMap;
+ }
+ return self;
+}
+
++ (FImmutableSortedDictionary *) emptyChildren {
+ static dispatch_once_t emptyChildrenToken;
+ static FImmutableSortedDictionary *emptyChildren;
+ dispatch_once(&emptyChildrenToken, ^{
+ emptyChildren = [FImmutableSortedDictionary dictionaryWithComparator:[FUtilities stringComparator]];
+ });
+ return emptyChildren;
+}
+
++ (FImmutableTree *) empty {
+ static dispatch_once_t emptyImmutableTreeToken;
+ static FImmutableTree *emptyTree = nil;
+ dispatch_once(&emptyImmutableTreeToken, ^{
+ emptyTree = [[FImmutableTree alloc] initWithValue:nil];
+ });
+ return emptyTree;
+}
+
+- (BOOL) isEmpty {
+ return self.value == nil && [self.children isEmpty];
+}
+
+/**
+* Given a path and a predicate, return the first node and the path to that node where the predicate returns true
+* // TODO Do a perf test. If we're creating a bunch of FTuplePathValue objects on the way back out, it may be better to pass down a pathSoFar FPath
+*/
+- (FTuplePathValue *) findRootMostMatchingPath:(FPath *)relativePath predicate:(BOOL (^)(id value))predicate {
+ if (self.value != nil && predicate(self.value)) {
+ return [[FTuplePathValue alloc] initWithPath:[FPath empty] value:self.value];
+ } else {
+ if ([relativePath isEmpty]) {
+ return nil;
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *child = [self.children get:front];
+ if (child != nil) {
+ FTuplePathValue *childExistingPathAndValue = [child findRootMostMatchingPath:[relativePath popFront] predicate:predicate];
+ if (childExistingPathAndValue != nil) {
+ FPath *fullPath = [[[FPath alloc] initWith:front] child:childExistingPathAndValue.path];
+ return [[FTuplePathValue alloc] initWithPath:fullPath value:childExistingPathAndValue.value];
+ } else {
+ return nil;
+ }
+ } else {
+ // No child matching path
+ return nil;
+ }
+ }
+ }
+}
+
+/**
+* Find, if it exists, the shortest subpath of the given path that points a defined value in the tree
+*/
+- (FTuplePathValue *) findRootMostValueAndPath:(FPath *)relativePath {
+ return [self findRootMostMatchingPath:relativePath predicate:^BOOL(__unsafe_unretained id value){
+ return YES;
+ }];
+}
+
+- (id) rootMostValueOnPath:(FPath *)path {
+ return [self rootMostValueOnPath:path matching:^BOOL(id value) {
+ return YES;
+ }];
+}
+
+- (id) rootMostValueOnPath:(FPath *)path matching:(BOOL (^)(id))predicate {
+ if (self.value != nil && predicate(self.value)) {
+ return self.value;
+ } else if (path.isEmpty) {
+ return nil;
+ } else {
+ return [[self.children get:path.getFront] rootMostValueOnPath:[path popFront] matching:predicate];
+ }
+}
+
+- (id) leafMostValueOnPath:(FPath *)path {
+ return [self leafMostValueOnPath:path matching:^BOOL(id value) {
+ return YES;
+ }];
+}
+
+- (id) leafMostValueOnPath:(FPath *)relativePath matching:(BOOL (^)(id))predicate {
+ __block id currentValue = self.value;
+ __block FImmutableTree *currentTree = self;
+ [relativePath enumerateComponentsUsingBlock:^(NSString *key, BOOL *stop) {
+ currentTree = [currentTree.children get:key];
+ if (currentTree == nil) {
+ *stop = YES;
+ } else {
+ id treeValue = currentTree.value;
+ if (treeValue != nil && predicate(treeValue)) {
+ currentValue = treeValue;
+ }
+ }
+ }];
+ return currentValue;
+}
+
+- (BOOL) containsValueMatching:(BOOL (^)(id))predicate {
+ if (self.value != nil && predicate(self.value)) {
+ return YES;
+ } else {
+ __block BOOL found = NO;
+ [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *key, FImmutableTree *subtree, BOOL *stop) {
+ found = [subtree containsValueMatching:predicate];
+ if (found) *stop = YES;
+ }];
+ return found;
+ }
+}
+
+- (FImmutableTree *) subtreeAtPath:(FPath *)relativePath {
+ if ([relativePath isEmpty]) {
+ return self;
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *childTree = [self.children get:front];
+ if (childTree != nil) {
+ return [childTree subtreeAtPath:[relativePath popFront]];
+ } else {
+ return [FImmutableTree empty];
+ }
+ }
+}
+
+/**
+* Sets a value at the specified path
+*/
+- (FImmutableTree *) setValue:(id)newValue atPath:(FPath *)relativePath {
+ if ([relativePath isEmpty]) {
+ return [[FImmutableTree alloc] initWithValue:newValue children:self.children];
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *child = [self.children get:front];
+ if (child == nil) {
+ child = [FImmutableTree empty];
+ }
+ FImmutableTree *newChild = [child setValue:newValue atPath:[relativePath popFront]];
+ FImmutableSortedDictionary *newChildren = [self.children insertKey:front withValue:newChild];
+ return [[FImmutableTree alloc] initWithValue:self.value children:newChildren];
+ }
+}
+
+/**
+* Remove the value at the specified path
+*/
+- (FImmutableTree *) removeValueAtPath:(FPath *)relativePath {
+ if ([relativePath isEmpty]) {
+ if ([self.children isEmpty]) {
+ return [FImmutableTree empty];
+ } else {
+ return [[FImmutableTree alloc] initWithValue:nil children:self.children];
+ }
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *child = [self.children get:front];
+ if (child) {
+ FImmutableTree *newChild = [child removeValueAtPath:[relativePath popFront]];
+ FImmutableSortedDictionary *newChildren;
+ if ([newChild isEmpty]) {
+ newChildren = [self.children removeKey:front];
+ } else {
+ newChildren = [self.children insertKey:front withValue:newChild];
+ }
+ if (self.value == nil && [newChildren isEmpty]) {
+ return [FImmutableTree empty];
+ } else {
+ return [[FImmutableTree alloc] initWithValue:self.value children:newChildren];
+ }
+ } else {
+ return self;
+ }
+ }
+}
+
+/**
+* Gets a value from the tree
+*/
+- (id) valueAtPath:(FPath *)relativePath {
+ if ([relativePath isEmpty]) {
+ return self.value;
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *child = [self.children get:front];
+ if (child) {
+ return [child valueAtPath:[relativePath popFront]];
+ } else {
+ return nil;
+ }
+ }
+}
+
+/**
+* Replaces the subtree at the specified path with the given new tree
+*/
+- (FImmutableTree *) setTree:(FImmutableTree *)newTree atPath:(FPath *)relativePath {
+ if ([relativePath isEmpty]) {
+ return newTree;
+ } else {
+ NSString *front = [relativePath getFront];
+ FImmutableTree *child = [self.children get:front];
+ if (child == nil) {
+ child = [FImmutableTree empty];
+ }
+ FImmutableTree *newChild = [child setTree:newTree atPath:[relativePath popFront]];
+ FImmutableSortedDictionary *newChildren;
+ if ([newChild isEmpty]) {
+ newChildren = [self.children removeKey:front];
+ } else {
+ newChildren = [self.children insertKey:front withValue:newChild];
+ }
+ return [[FImmutableTree alloc] initWithValue:self.value children:newChildren];
+ }
+}
+
+/**
+* Performs a depth first fold on this tree. Transforms a tree into a single value, given a function that operates on
+* the path to a node, an optional current value, and a map of the child names to folded subtrees
+*/
+- (id) foldWithBlock:(id (^)(FPath *path, id value, NSDictionary *foldedChildren))block {
+ return [self foldWithPathSoFar:[FPath empty] withBlock:block];
+}
+
+/**
+* Recursive helper for public facing foldWithBlock: method
+*/
+- (id) foldWithPathSoFar:(FPath *)pathSoFar withBlock:(id (^)(FPath *path, id value, NSDictionary *foldedChildren))block {
+ __block NSMutableDictionary *accum = [[NSMutableDictionary alloc] init];
+ [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ accum[childKey] = [childTree foldWithPathSoFar:[pathSoFar childFromString:childKey] withBlock:block];
+ }];
+ return block(pathSoFar, self.value, accum);
+}
+
+/**
+* Find the first matching value on the given path. Return the result of applying block to it.
+*/
+- (id) findOnPath:(FPath *)path andApplyBlock:(id (^)(FPath *path, id value))block {
+ return [self findOnPath:path pathSoFar:[FPath empty] andApplyBlock:block];
+}
+
+- (id) findOnPath:(FPath *)pathToFollow pathSoFar:(FPath *)pathSoFar andApplyBlock:(id (^)(FPath *path, id value))block {
+ id result = self.value ? block(pathSoFar, self.value) : nil;
+ if (result != nil) {
+ return result;
+ } else {
+ if ([pathToFollow isEmpty]) {
+ return nil;
+ } else {
+ NSString *front = [pathToFollow getFront];
+ FImmutableTree *nextChild = [self.children get:front];
+ if (nextChild != nil) {
+ return [nextChild findOnPath:[pathToFollow popFront] pathSoFar:[pathSoFar childFromString:front] andApplyBlock:block];
+ } else {
+ return nil;
+ }
+ }
+ }
+}
+/**
+* Call the block on each value along the path for as long as that function returns true
+* @return The path to the deepest location inspected
+*/
+- (FPath *) forEachOnPath:(FPath *)path whileBlock:(BOOL (^)(FPath *, id))block {
+ return [self forEachOnPath:path pathSoFar:[FPath empty] whileBlock:block];
+}
+
+- (FPath *) forEachOnPath:(FPath *)pathToFollow pathSoFar:(FPath *)pathSoFar whileBlock:(BOOL (^)(FPath *, id))block {
+ if ([pathToFollow isEmpty]) {
+ if (self.value) {
+ block(pathSoFar, self.value);
+ }
+ return pathSoFar;
+ } else {
+ BOOL shouldContinue = YES;
+ if (self.value) {
+ shouldContinue = block(pathSoFar, self.value);
+ }
+ if (shouldContinue) {
+ NSString *front = [pathToFollow getFront];
+ FImmutableTree *nextChild = [self.children get:front];
+ if (nextChild) {
+ return [nextChild forEachOnPath:[pathToFollow popFront] pathSoFar:[pathSoFar childFromString:front] whileBlock:block];
+ } else {
+ return pathSoFar;
+ }
+ } else {
+ return pathSoFar;
+ }
+ }
+}
+
+- (FImmutableTree *) forEachOnPath:(FPath *)path performBlock:(void (^)(FPath *path, id value))block {
+ return [self forEachOnPath:path pathSoFar:[FPath empty] performBlock:block];
+}
+
+- (FImmutableTree *) forEachOnPath:(FPath *)pathToFollow pathSoFar:(FPath *)pathSoFar performBlock:(void (^)(FPath *path, id value))block {
+ if ([pathToFollow isEmpty]) {
+ return self;
+ } else {
+ if (self.value) {
+ block(pathSoFar, self.value);
+ }
+ NSString *front = [pathToFollow getFront];
+ FImmutableTree *nextChild = [self.children get:front];
+ if (nextChild) {
+ return [nextChild forEachOnPath:[pathToFollow popFront] pathSoFar:[pathSoFar childFromString:front] performBlock:block];
+ } else {
+ return [FImmutableTree empty];
+ }
+ }
+}
+/**
+* Calls the given block for each node in the tree that has a value. Called in depth-first order
+*/
+- (void) forEach:(void (^)(FPath *path, id value))block {
+ [self forEachPathSoFar:[FPath empty] withBlock:block];
+}
+
+- (void) forEachPathSoFar:(FPath *)pathSoFar withBlock:(void (^)(FPath *path, id value))block {
+ [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ [childTree forEachPathSoFar:[pathSoFar childFromString:childKey] withBlock:block];
+ }];
+ if (self.value) {
+ block(pathSoFar, self.value);
+ }
+}
+
+- (void) forEachChild:(void (^)(NSString *childKey, id childValue))block {
+ [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if (childTree.value) {
+ block(childKey, childTree.value);
+ }
+ }];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FImmutableTree class]]) {
+ return NO;
+ }
+ FImmutableTree *other = (FImmutableTree *)object;
+ return (self.value == other.value || [self.value isEqual:other.value]) && [self.children isEqual:other.children];
+}
+
+- (NSUInteger)hash {
+ return self.children.hash * 31 + [self.value hash];
+}
+
+- (NSString *) description {
+ NSMutableString *string = [[NSMutableString alloc] init];
+ [string appendString:@"FImmutableTree { value="];
+ [string appendString:(self.value ? [self.value description] : @"<nil>")];
+ [string appendString:@", children={"];
+ [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ [string appendString:@" "];
+ [string appendString:childKey];
+ [string appendString:@"="];
+ [string appendString:[childTree.value description]];
+ }];
+ [string appendString:@" } }"];
+ return [NSString stringWithString:string];
+}
+
+- (NSString *) debugDescription {
+ return [self description];
+}
+
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FPath.h b/Firebase/Database/Core/Utilities/FPath.h
new file mode 100644
index 0000000..71a7167
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FPath.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@interface FPath : NSObject<NSCopying>
+
++ (FPath *) relativePathFrom:(FPath *)outer to:(FPath *)inner;
++ (FPath *) empty;
++ (FPath *) pathWithString:(NSString *)string;
+
+- (id)initWith:(NSString *)path;
+- (id)initWithPieces:(NSArray *)somePieces andPieceNum:(NSInteger)aPieceNum;
+
+- (id)copyWithZone:(NSZone *)zone;
+
+- (void)enumerateComponentsUsingBlock:(void (^)(NSString *key, BOOL *stop))block;
+- (NSString *) getFront;
+- (NSUInteger) length;
+- (FPath *) popFront;
+- (NSString *) getBack;
+- (NSString *) toString;
+- (NSString *) toStringWithTrailingSlash;
+- (NSString *) wireFormat;
+- (FPath *) parent;
+- (FPath *) child:(FPath *)childPathObj;
+- (FPath *) childFromString:(NSString *)childPath;
+- (BOOL) isEmpty;
+- (BOOL) contains:(FPath *)other;
+- (NSComparisonResult) compare:(FPath *)other;
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FPath.m b/Firebase/Database/Core/Utilities/FPath.m
new file mode 100644
index 0000000..485b903
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FPath.m
@@ -0,0 +1,298 @@
+/*
+ * 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 "FPath.h"
+
+#import "FUtilities.h"
+
+@interface FPath()
+
+@property (nonatomic, readwrite, assign) NSInteger pieceNum;
+@property (nonatomic, strong) NSArray * pieces;
+
+@end
+
+@implementation FPath
+
+#pragma mark -
+#pragma mark Initializers
+
++ (FPath *) relativePathFrom:(FPath *)outer to:(FPath *)inner {
+ NSString* outerFront = [outer getFront];
+ NSString* innerFront = [inner getFront];
+ if (outerFront == nil) {
+ return inner;
+ } else if ([outerFront isEqualToString:innerFront]) {
+ return [self relativePathFrom:[outer popFront] to:[inner popFront]];
+ } else {
+ @throw [[NSException alloc] initWithName:@"FirebaseDatabaseInternalError" reason:[NSString stringWithFormat:@"innerPath (%@) is not within outerPath (%@)", inner, outer] userInfo:nil];
+ }
+}
+
++ (FPath *)pathWithString:(NSString *)string
+{
+ return [[FPath alloc] initWith:string];
+}
+
+- (id)initWith:(NSString *)path
+{
+ self = [super init];
+ if (self) {
+ NSArray *pathPieces = [path componentsSeparatedByString:@"/"];
+ NSMutableArray *newPieces = [[NSMutableArray alloc] init];
+ for (NSInteger i = 0; i < pathPieces.count; i++) {
+ NSString *piece = [pathPieces objectAtIndex:i];
+ if (piece.length > 0) {
+ [newPieces addObject:piece];
+ }
+ }
+
+ self.pieces = newPieces;
+ self.pieceNum = 0;
+ }
+ return self;
+}
+
+- (id)initWithPieces:(NSArray *)somePieces andPieceNum:(NSInteger)aPieceNum {
+ self = [super init];
+ if (self) {
+ self.pieceNum = aPieceNum;
+ self.pieces = somePieces;
+ }
+ return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone
+{
+ // Immutable, so it's safe to return self
+ return self;
+}
+
+- (NSString *)description {
+ return [self toString];
+}
+
+#pragma mark -
+#pragma mark Public methods
+
+- (NSString *) getFront {
+ if(self.pieceNum >= self.pieces.count) {
+ return nil;
+ }
+ return [self.pieces objectAtIndex:self.pieceNum];
+}
+
+/**
+* @return The number of segments in this path
+*/
+- (NSUInteger) length {
+ return self.pieces.count - self.pieceNum;
+}
+
+- (FPath *) popFront {
+ NSInteger newPieceNum = self.pieceNum;
+ if (newPieceNum < self.pieces.count) {
+ newPieceNum++;
+ }
+ return [[FPath alloc] initWithPieces:self.pieces andPieceNum:newPieceNum];
+}
+
+- (NSString *) getBack {
+ if(self.pieceNum < self.pieces.count) {
+ return [self.pieces lastObject];
+ }
+ else {
+ return nil;
+ }
+}
+
+- (NSString *) toString {
+ return [self toStringWithTrailingSlash:NO];
+}
+
+- (NSString *) toStringWithTrailingSlash {
+ return [self toStringWithTrailingSlash:YES];
+}
+
+- (NSString *) toStringWithTrailingSlash:(BOOL)trailingSlash {
+ NSMutableString* pathString = [[NSMutableString alloc] init];
+ for(NSInteger i = self.pieceNum; i < self.pieces.count; i++) {
+ [pathString appendString:@"/"];
+ [pathString appendString:[self.pieces objectAtIndex:i]];
+ }
+ if ([pathString length] == 0) {
+ return @"/";
+ } else {
+ if (trailingSlash) {
+ [pathString appendString:@"/"];
+ }
+ return pathString;
+ }
+}
+
+- (NSString *)wireFormat {
+ if ([self isEmpty]) {
+ return @"/";
+ } else {
+ NSMutableString* pathString = [[NSMutableString alloc] init];
+ for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) {
+ if (i > self.pieceNum) {
+ [pathString appendString:@"/"];
+ }
+ [pathString appendString:[self.pieces objectAtIndex:i]];
+ }
+ return pathString;
+ }
+}
+
+- (FPath *) parent {
+ if(self.pieceNum >= self.pieces.count) {
+ return nil;
+ } else {
+ NSMutableArray* newPieces = [[NSMutableArray alloc] init];
+ for (NSInteger i = self.pieceNum; i < self.pieces.count - 1; i++) {
+ [newPieces addObject:[self.pieces objectAtIndex:i]];
+ }
+ return [[FPath alloc] initWithPieces:newPieces andPieceNum:0];
+ }
+}
+
+- (FPath *) child:(FPath *)childPathObj {
+ NSMutableArray* newPieces = [[NSMutableArray alloc] init];
+ for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) {
+ [newPieces addObject:[self.pieces objectAtIndex:i]];
+ }
+
+ for (NSInteger i = childPathObj.pieceNum; i < childPathObj.pieces.count; i++) {
+ [newPieces addObject:[childPathObj.pieces objectAtIndex:i]];
+ }
+
+ return [[FPath alloc] initWithPieces:newPieces andPieceNum:0];
+}
+
+- (FPath *)childFromString:(NSString *)childPath {
+ NSMutableArray* newPieces = [[NSMutableArray alloc] init];
+ for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) {
+ [newPieces addObject:[self.pieces objectAtIndex:i]];
+ }
+
+ NSArray *pathPieces = [childPath componentsSeparatedByString:@"/"];
+ for (unsigned int i = 0; i < pathPieces.count; i++) {
+ NSString *piece = [pathPieces objectAtIndex:i];
+ if (piece.length > 0) {
+ [newPieces addObject:piece];
+ }
+ }
+
+ return [[FPath alloc] initWithPieces:newPieces andPieceNum:0];
+}
+
+/**
+* @return True if there are no segments in this path
+*/
+- (BOOL) isEmpty {
+ return self.pieceNum >= self.pieces.count;
+}
+
+/**
+* @return Singleton to represent an empty path
+*/
++ (FPath *) empty {
+ static dispatch_once_t oneEmptyPath;
+ static FPath *emptyPath;
+ dispatch_once(&oneEmptyPath, ^{
+ emptyPath = [[FPath alloc] initWith:@""];
+ });
+ return emptyPath;
+}
+
+- (BOOL) contains:(FPath *)other {
+ if (self.length > other.length) {
+ return NO;
+ }
+
+ NSInteger i = self.pieceNum;
+ NSInteger j = other.pieceNum;
+ while (i < self.pieces.count) {
+ NSString* thisSeg = [self.pieces objectAtIndex:i];
+ NSString* otherSeg = [other.pieces objectAtIndex:j];
+ if (![thisSeg isEqualToString:otherSeg]) {
+ return NO;
+ }
+ ++i;
+ ++j;
+ }
+ return YES;
+}
+
+- (void) enumerateComponentsUsingBlock:(void (^)(NSString *, BOOL *))block {
+ BOOL stop = NO;
+ for (NSInteger i = self.pieceNum; !stop && i < self.pieces.count; i++) {
+ block(self.pieces[i], &stop);
+ }
+}
+
+- (NSComparisonResult) compare:(FPath *)other {
+ NSInteger myCount = self.pieces.count;
+ NSInteger otherCount = other.pieces.count;
+ for (NSInteger i = self.pieceNum, j = other.pieceNum; i < myCount && j < otherCount; i++, j++) {
+ NSComparisonResult comparison = [FUtilities compareKey:self.pieces[i] toKey:other.pieces[j]];
+ if (comparison != NSOrderedSame) {
+ return comparison;
+ }
+ }
+ if (self.length < other.length) {
+ return NSOrderedAscending;
+ } else if (other.length < self.length) {
+ return NSOrderedDescending;
+ } else {
+ NSAssert(self.length == other.length, @"Paths must be the same lengths");
+ return NSOrderedSame;
+ }
+}
+
+/**
+* @return YES if paths are the same
+*/
+- (BOOL)isEqual:(id)other
+{
+ if (other == self) {
+ return YES;
+ }
+ if (!other || ![other isKindOfClass:[self class]]) {
+ return NO;
+ }
+ FPath *otherPath = (FPath *)other;
+ if (self.length != otherPath.length) {
+ return NO;
+ }
+ for (NSUInteger i = self.pieceNum, j = otherPath.pieceNum; i < self.pieces.count; i++, j++) {
+ if (![self.pieces[i] isEqualToString:otherPath.pieces[j]]) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+- (NSUInteger) hash {
+ NSUInteger hashCode = 0;
+ for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) {
+ hashCode = hashCode * 37 + [self.pieces[i] hash];
+ }
+ return hashCode;
+}
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FTree.h b/Firebase/Database/Core/Utilities/FTree.h
new file mode 100644
index 0000000..8528526
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FTree.h
@@ -0,0 +1,48 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FTreeNode.h"
+#import "FPath.h"
+
+@interface FTree : NSObject
+
+- (id)init;
+- (id)initWithName:(NSString*)aName withParent:(FTree *)aParent withNode:(FTreeNode *)aNode;
+
+- (FTree *) subTree:(FPath*)path;
+- (id)getValue;
+- (void)setValue:(id)value;
+- (void) clear;
+- (BOOL) hasChildren;
+- (BOOL) isEmpty;
+- (void) forEachChildMutationSafe:(void (^)(FTree *))action;
+- (void) forEachChild:(void (^)(FTree *))action;
+- (void) forEachDescendant:(void (^)(FTree *))action;
+- (void) forEachDescendant:(void (^)(FTree *))action includeSelf:(BOOL)incSelf childrenFirst:(BOOL)childFirst;
+- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action;
+- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action includeSelf:(BOOL)incSelf;
+- (void) forEachImmediateDescendantWithValue:(void (^)(FTree *))action;
+- (BOOL) valueExistsAtOrAbove:(FPath *)path;
+- (FPath *)path;
+- (void) updateParents;
+- (void) updateChild:(NSString*)childName withNode:(FTree *)child;
+
+@property (nonatomic, strong) NSString* name;
+@property (nonatomic, strong) FTree* parent;
+@property (nonatomic, strong) FTreeNode* node;
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FTree.m b/Firebase/Database/Core/Utilities/FTree.m
new file mode 100644
index 0000000..8576ffb
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FTree.m
@@ -0,0 +1,183 @@
+/*
+ * 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 "FTree.h"
+#import "FTreeNode.h"
+#import "FPath.h"
+#import "FUtilities.h"
+
+@implementation FTree
+
+@synthesize name;
+@synthesize parent;
+@synthesize node;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ self.name = @"";
+ self.parent = nil;
+ self.node = [[FTreeNode alloc] init];
+ }
+ return self;
+}
+
+
+- (id)initWithName:(NSString*)aName withParent:(FTree *)aParent withNode:(FTreeNode *)aNode
+{
+ self = [super init];
+ if (self) {
+ self.name = aName != nil ? aName : @"";
+ self.parent = aParent != nil ? aParent : nil;
+ self.node = aNode != nil ? aNode : [[FTreeNode alloc] init];
+ }
+ return self;
+}
+
+- (FTree *) subTree:(FPath*)path {
+ FTree* child = self;
+ NSString* next = [path getFront];
+ while(next != nil) {
+ FTreeNode* childNode = child.node.children[next];
+ if (childNode == nil) {
+ childNode = [[FTreeNode alloc] init];
+ }
+ child = [[FTree alloc] initWithName:next withParent:child withNode:childNode];
+ path = [path popFront];
+ next = [path getFront];
+ }
+ return child;
+}
+
+- (id)getValue {
+ return self.node.value;
+}
+
+- (void)setValue:(id)value {
+ self.node.value = value;
+ [self updateParents];
+}
+
+- (void) clear {
+ self.node.value = nil;
+ [self.node.children removeAllObjects];
+ self.node.childCount = 0;
+ [self updateParents];
+}
+
+- (BOOL) hasChildren {
+ return self.node.childCount > 0;
+}
+
+- (BOOL) isEmpty {
+ return [self getValue] == nil && ![self hasChildren];
+}
+
+- (void) forEachChild:(void (^)(FTree *))action {
+ for(NSString* key in self.node.children) {
+ action([[FTree alloc] initWithName:key withParent:self withNode:[self.node.children objectForKey:key]]);
+ }
+}
+
+- (void) forEachChildMutationSafe:(void (^)(FTree *))action {
+ for(NSString* key in [self.node.children copy]) {
+ action([[FTree alloc] initWithName:key withParent:self withNode:[self.node.children objectForKey:key]]);
+ }
+}
+
+- (void) forEachDescendant:(void (^)(FTree *))action {
+ [self forEachDescendant:action includeSelf:NO childrenFirst:NO];
+}
+
+- (void) forEachDescendant:(void (^)(FTree *))action includeSelf:(BOOL)incSelf childrenFirst:(BOOL)childFirst {
+ if(incSelf && !childFirst) {
+ action(self);
+ }
+
+ [self forEachChild:^(FTree* child) {
+ [child forEachDescendant:action includeSelf:YES childrenFirst:childFirst];
+ }];
+
+ if(incSelf && childFirst) {
+ action(self);
+ }
+}
+
+- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action {
+ return [self forEachAncestor:action includeSelf:NO];
+}
+
+- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action includeSelf:(BOOL)incSelf {
+ FTree* aNode = (incSelf) ? self : self.parent;
+ while(aNode != nil) {
+ if(action(aNode)) {
+ return YES;
+ }
+ aNode = aNode.parent;
+ }
+ return NO;
+}
+
+- (void) forEachImmediateDescendantWithValue:(void (^)(FTree *))action {
+ [self forEachChild:^(FTree * child) {
+ if([child getValue] != nil) {
+ action(child);
+ }
+ else {
+ [child forEachImmediateDescendantWithValue:action];
+ }
+ }];
+}
+
+- (BOOL) valueExistsAtOrAbove:(FPath *)path {
+ FTreeNode* aNode = self.node;
+ while(aNode != nil) {
+ if(aNode.value != nil) {
+ return YES;
+ }
+ aNode = [aNode.children objectForKey:path.getFront];
+ path = [path popFront];
+ }
+ // XXX Check with Michael if this is correct; deviates from JS.
+ return NO;
+}
+
+- (FPath *)path {
+ return [[FPath alloc] initWith:(self.parent == nil) ? self.name :
+ [NSString stringWithFormat:@"%@/%@", [self.parent path], self.name ]];
+}
+
+- (void) updateParents {
+ [self.parent updateChild:self.name withNode:self];
+}
+
+- (void) updateChild:(NSString*)childName withNode:(FTree *)child {
+ BOOL childEmpty = [child isEmpty];
+ BOOL childExists = self.node.children[childName] != nil;
+ if(childEmpty && childExists) {
+ [self.node.children removeObjectForKey:childName];
+ self.node.childCount = self.node.childCount - 1;
+ [self updateParents];
+ }
+ else if(!childEmpty && !childExists) {
+ [self.node.children setObject:child.node forKey:childName];
+ self.node.childCount = self.node.childCount + 1;
+ [self updateParents];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FTreeNode.h b/Firebase/Database/Core/Utilities/FTreeNode.h
new file mode 100644
index 0000000..7e3497e
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FTreeNode.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 <Foundation/Foundation.h>
+
+@interface FTreeNode : NSObject
+
+@property (nonatomic, strong) NSMutableDictionary* children;
+@property (nonatomic, readwrite, assign) int childCount;
+@property (nonatomic, strong) id value;
+
+@end
diff --git a/Firebase/Database/Core/Utilities/FTreeNode.m b/Firebase/Database/Core/Utilities/FTreeNode.m
new file mode 100644
index 0000000..9cba9c5
--- /dev/null
+++ b/Firebase/Database/Core/Utilities/FTreeNode.m
@@ -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 "FTreeNode.h"
+
+@implementation FTreeNode
+
+@synthesize children;
+@synthesize childCount;
+@synthesize value;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ self.children = [[NSMutableDictionary alloc] init];
+ self.childCount = 0;
+ self.value = nil;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FCacheNode.h b/Firebase/Database/Core/View/FCacheNode.h
new file mode 100644
index 0000000..b23869c
--- /dev/null
+++ b/Firebase/Database/Core/View/FCacheNode.h
@@ -0,0 +1,44 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FIndexedNode;
+@class FPath;
+
+/**
+* A cache node only stores complete children. Additionally it holds a flag whether the node can be considered fully
+* initialized in the sense that we know at one point in time, this represented a valid state of the world, e.g.
+* initialized with data from the server, or a complete overwrite by the client. It is not necessarily complete because
+* it may have been from a tagged query. The filtered flag also tracks whether a node potentially had children removed
+* due to a filter.
+*/
+@interface FCacheNode : NSObject
+
+- (id) initWithIndexedNode:(FIndexedNode *)indexedNode
+ isFullyInitialized:(BOOL)fullyInitialized
+ isFiltered:(BOOL)filtered;
+
+- (BOOL) isCompleteForPath:(FPath *)path;
+- (BOOL) isCompleteForChild:(NSString *)childKey;
+
+@property (nonatomic, readonly) BOOL isFullyInitialized;
+@property (nonatomic, readonly) BOOL isFiltered;
+@property (nonatomic, strong, readonly) FIndexedNode *indexedNode;
+@property (nonatomic, strong, readonly) id<FNode> node;
+
+@end
diff --git a/Firebase/Database/Core/View/FCacheNode.m b/Firebase/Database/Core/View/FCacheNode.m
new file mode 100644
index 0000000..4767a25
--- /dev/null
+++ b/Firebase/Database/Core/View/FCacheNode.m
@@ -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 "FCacheNode.h"
+#import "FNode.h"
+#import "FPath.h"
+#import "FEmptyNode.h"
+#import "FIndexedNode.h"
+
+@interface FCacheNode ()
+@property (nonatomic, readwrite) BOOL isFullyInitialized;
+@property (nonatomic, readwrite) BOOL isFiltered;
+@property (nonatomic, strong, readwrite) FIndexedNode *indexedNode;
+@end
+
+@implementation FCacheNode
+- (id) initWithIndexedNode:(FIndexedNode *)indexedNode
+ isFullyInitialized:(BOOL)fullyInitialized
+ isFiltered:(BOOL)filtered
+{
+ self = [super init];
+ if (self) {
+ self.indexedNode = indexedNode;
+ self.isFullyInitialized = fullyInitialized;
+ self.isFiltered = filtered;
+ }
+ return self;
+}
+
+- (BOOL)isCompleteForPath:(FPath *)path {
+ if (path.isEmpty) {
+ return self.isFullyInitialized && !self.isFiltered;
+ } else {
+ NSString *childKey = [path getFront];
+ return [self isCompleteForChild:childKey];
+ }
+}
+
+- (BOOL)isCompleteForChild:(NSString *)childKey {
+ return (self.isFullyInitialized && !self.isFiltered) || [self.node hasChild:childKey];
+}
+
+- (id<FNode>)node {
+ return self.indexedNode.node;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FCancelEvent.h b/Firebase/Database/Core/View/FCancelEvent.h
new file mode 100644
index 0000000..38277f7
--- /dev/null
+++ b/Firebase/Database/Core/View/FCancelEvent.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 <Foundation/Foundation.h>
+#import "FEvent.h"
+
+@protocol FEventRegistration;
+
+
+@interface FCancelEvent : NSObject<FEvent>
+
+- initWithEventRegistration:(id<FEventRegistration>)eventRegistration error:(NSError *)error path:(FPath *)path;
+
+@property (nonatomic, strong, readonly) NSError *error;
+@property (nonatomic, strong, readonly) FPath *path;
+
+@end
diff --git a/Firebase/Database/Core/View/FCancelEvent.m b/Firebase/Database/Core/View/FCancelEvent.m
new file mode 100644
index 0000000..fb73f17
--- /dev/null
+++ b/Firebase/Database/Core/View/FCancelEvent.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 "FCancelEvent.h"
+#import "FPath.h"
+#import "FEventRegistration.h"
+
+@interface FCancelEvent ()
+@property (nonatomic, strong) id<FEventRegistration> eventRegistration;
+@property (nonatomic, strong, readwrite) NSError *error;
+@property (nonatomic, strong, readwrite) FPath *path;
+@end
+
+@implementation FCancelEvent
+
+@synthesize eventRegistration;
+@synthesize error;
+@synthesize path;
+
+- (id)initWithEventRegistration:(id <FEventRegistration>)registration error:(NSError *)anError path:(FPath *)aPath {
+ self = [super init];
+ if (self) {
+ self.eventRegistration = registration;
+ self.error = anError;
+ self.path = aPath;
+ }
+ return self;
+}
+
+- (void) fireEventOnQueue:(dispatch_queue_t)queue {
+ [self.eventRegistration fireEvent:self queue:queue];
+}
+
+- (BOOL) isCancelEvent {
+ return YES;
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"%@: cancel", self.path];
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FChange.h b/Firebase/Database/Core/View/FChange.h
new file mode 100644
index 0000000..d728fe0
--- /dev/null
+++ b/Firebase/Database/Core/View/FChange.h
@@ -0,0 +1,38 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FIRDatabaseReference.h"
+#import "FNode.h"
+#import "FIndexedNode.h"
+
+@interface FChange : NSObject
+
+@property (nonatomic, readonly) FIRDataEventType type;
+@property (nonatomic, strong, readonly) FIndexedNode *indexedNode;
+@property (nonatomic, strong, readonly) NSString *childKey;
+@property (nonatomic, strong, readonly) NSString *prevKey;
+@property (nonatomic, strong, readonly) FIndexedNode *oldIndexedNode;
+
+- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode;
+- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode childKey:(NSString *)childKey;
+- (id)initWithType:(FIRDataEventType)type
+ indexedNode:(FIndexedNode *)indexedNode
+ childKey:(NSString *)childKey
+ oldIndexedNode:(FIndexedNode *)oldIndexedNode;
+
+- (FChange *) changeWithPrevKey:(NSString *)prevKey;
+@end
diff --git a/Firebase/Database/Core/View/FChange.m b/Firebase/Database/Core/View/FChange.m
new file mode 100644
index 0000000..893fce4
--- /dev/null
+++ b/Firebase/Database/Core/View/FChange.m
@@ -0,0 +1,65 @@
+/*
+ * 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 "FChange.h"
+
+@interface FChange ()
+
+@property (nonatomic, strong, readwrite) NSString *prevKey;
+
+@end
+
+@implementation FChange
+
+- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode
+{
+ return [self initWithType:type indexedNode:indexedNode childKey:nil oldIndexedNode:nil];
+}
+
+- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode childKey:(NSString *)childKey
+{
+ return [self initWithType:type indexedNode:indexedNode childKey:childKey oldIndexedNode:nil];
+}
+
+- (id)initWithType:(FIRDataEventType)type
+ indexedNode:(FIndexedNode *)indexedNode
+ childKey:(NSString *)childKey
+ oldIndexedNode:(FIndexedNode *)oldIndexedNode
+{
+ self = [super init];
+ if (self != nil) {
+ self->_type = type;
+ self->_indexedNode = indexedNode;
+ self->_childKey = childKey;
+ self->_oldIndexedNode = oldIndexedNode;
+ }
+ return self;
+}
+
+- (FChange *) changeWithPrevKey:(NSString *)prevKey {
+ FChange *newChange = [[FChange alloc] initWithType:self.type
+ indexedNode:self.indexedNode
+ childKey:self.childKey
+ oldIndexedNode:self.oldIndexedNode];
+ newChange.prevKey = prevKey;
+ return newChange;
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"event: %d, data: %@", (int)self.type, [self.indexedNode.node val]];
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FChildEventRegistration.h b/Firebase/Database/Core/View/FChildEventRegistration.h
new file mode 100644
index 0000000..8da0b8f
--- /dev/null
+++ b/Firebase/Database/Core/View/FChildEventRegistration.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 <Foundation/Foundation.h>
+#import "FEventRegistration.h"
+#import "FTypedefs.h"
+
+@class FRepo;
+
+@interface FChildEventRegistration : NSObject <FEventRegistration>
+
+- (id) initWithRepo:(FRepo *)repo
+ handle:(FIRDatabaseHandle)fHandle
+ callbacks:(NSDictionary *)callbackBlocks
+ cancelCallback:(fbt_void_nserror)cancelCallbackBlock;
+
+/**
+* Maps FIRDataEventType (as NSNumber) to fbt_void_datasnapshot_nsstring
+*/
+@property (nonatomic, copy, readonly) NSDictionary *callbacks;
+@property (nonatomic, copy, readonly) fbt_void_nserror cancelCallback;
+@property (nonatomic, readonly) FIRDatabaseHandle handle;
+
+@end
diff --git a/Firebase/Database/Core/View/FChildEventRegistration.m b/Firebase/Database/Core/View/FChildEventRegistration.m
new file mode 100644
index 0000000..6308a90
--- /dev/null
+++ b/Firebase/Database/Core/View/FChildEventRegistration.m
@@ -0,0 +1,92 @@
+/*
+ * 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 "FChildEventRegistration.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FDataEvent.h"
+#import "FCancelEvent.h"
+
+@interface FChildEventRegistration ()
+@property (nonatomic, strong) FRepo *repo;
+@property (nonatomic, copy, readwrite) NSDictionary *callbacks;
+@property (nonatomic, copy, readwrite) fbt_void_nserror cancelCallback;
+@property (nonatomic, readwrite) FIRDatabaseHandle handle;
+@end
+
+@implementation FChildEventRegistration
+
+- (id)initWithRepo:(id)repo handle:(FIRDatabaseHandle)fHandle callbacks:(NSDictionary *)callbackBlocks cancelCallback:(fbt_void_nserror)cancelCallbackBlock {
+ self = [super init];
+ if (self) {
+ self.repo = repo;
+ self.handle = fHandle;
+ self.callbacks = callbackBlocks;
+ self.cancelCallback = cancelCallbackBlock;
+ }
+ return self;
+}
+
+- (BOOL) responseTo:(FIRDataEventType)eventType {
+ return self.callbacks != nil && [self.callbacks objectForKey:[NSNumber numberWithInteger:eventType]] != nil;
+}
+
+- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query {
+ FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:[query.path childFromString:change.childKey]];
+ FIRDataSnapshot *snapshot = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:change.indexedNode];
+
+ FDataEvent *eventData = [[FDataEvent alloc] initWithEventType:change.type eventRegistration:self
+ dataSnapshot:snapshot prevName:change.prevKey];
+ return eventData;
+}
+
+- (void) fireEvent:(id <FEvent>)event queue:(dispatch_queue_t)queue {
+ if ([event isCancelEvent]) {
+ FCancelEvent *cancelEvent = event;
+ FFLog(@"I-RDB061001", @"Raising cancel value event on %@", event.path);
+ NSAssert(self.cancelCallback != nil, @"Raising a cancel event on a listener with no cancel callback");
+ dispatch_async(queue, ^{
+ self.cancelCallback(cancelEvent.error);
+ });
+ } else if (self.callbacks != nil) {
+ FDataEvent *dataEvent = event;
+ FFLog(@"I-RDB061002", @"Raising event callback (%ld) on %@", (long)dataEvent.eventType, dataEvent.path);
+ fbt_void_datasnapshot_nsstring callback = [self.callbacks objectForKey:[NSNumber numberWithInteger:dataEvent.eventType]];
+
+ if (callback != nil) {
+ dispatch_async(queue, ^{
+ callback(dataEvent.snapshot, dataEvent.prevName);
+ });
+ }
+ }
+}
+
+- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path {
+ if (self.cancelCallback != nil) {
+ return [[FCancelEvent alloc] initWithEventRegistration:self error:error path:path];
+ } else {
+ return nil;
+ }
+}
+
+- (BOOL) matches:(id<FEventRegistration>)other {
+ return self.handle == NSNotFound || other.handle == NSNotFound || self.handle == other.handle;
+}
+
+
+@end
diff --git a/Firebase/Database/Core/View/FDataEvent.h b/Firebase/Database/Core/View/FDataEvent.h
new file mode 100644
index 0000000..da90b03
--- /dev/null
+++ b/Firebase/Database/Core/View/FDataEvent.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 <Foundation/Foundation.h>
+#import "FIRDataSnapshot.h"
+#import "FIRDatabaseReference.h"
+#import "FTupleUserCallback.h"
+#import "FEvent.h"
+
+@protocol FEventRegistration;
+@protocol FIndex;
+
+@interface FDataEvent : NSObject<FEvent>
+
+- initWithEventType:(FIRDataEventType)type eventRegistration:(id<FEventRegistration>)eventRegistration
+ dataSnapshot:(FIRDataSnapshot *)dataSnapshot;
+- initWithEventType:(FIRDataEventType)type eventRegistration:(id<FEventRegistration>)eventRegistration
+ dataSnapshot:(FIRDataSnapshot *)snapshot prevName:(NSString *)prevName;
+
+
+@property (nonatomic, strong, readonly) id<FEventRegistration> eventRegistration;
+@property (nonatomic, strong, readonly) FIRDataSnapshot * snapshot;
+@property (nonatomic, strong, readonly) NSString* prevName;
+@property (nonatomic, readonly) FIRDataEventType eventType;
+
+@end
diff --git a/Firebase/Database/Core/View/FDataEvent.m b/Firebase/Database/Core/View/FDataEvent.m
new file mode 100644
index 0000000..6c97faf
--- /dev/null
+++ b/Firebase/Database/Core/View/FDataEvent.m
@@ -0,0 +1,74 @@
+/*
+ * 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 "FDataEvent.h"
+#import "FEventRegistration.h"
+#import "FIndex.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@interface FDataEvent ()
+@property (nonatomic, strong, readwrite) id<FEventRegistration> eventRegistration;
+@property (nonatomic, strong, readwrite) FIRDataSnapshot *snapshot;
+@property (nonatomic, strong, readwrite) NSString *prevName;
+@property (nonatomic, readwrite) FIRDataEventType eventType;
+@end
+
+@implementation FDataEvent
+
+@synthesize eventRegistration;
+@synthesize snapshot;
+@synthesize prevName;
+@synthesize eventType;
+
+- (id)initWithEventType:(FIRDataEventType)type eventRegistration:(id <FEventRegistration>)registration dataSnapshot:(FIRDataSnapshot *)dataSnapshot {
+ return [self initWithEventType:type eventRegistration:registration dataSnapshot:dataSnapshot prevName:nil];
+}
+
+- (id)initWithEventType:(FIRDataEventType)type eventRegistration:(id <FEventRegistration>)registration dataSnapshot:(FIRDataSnapshot *)dataSnapshot prevName:(NSString *)previousName {
+ self = [super init];
+ if (self) {
+ self.eventRegistration = registration;
+ self.snapshot = dataSnapshot;
+ self.prevName = previousName;
+ self.eventType = type;
+ }
+ return self;
+}
+
+- (FPath *) path {
+ // Used for logging, so delay calculation
+ FIRDatabaseReference *ref = self.snapshot.ref;
+ if (self.eventType == FIRDataEventTypeValue) {
+ return ref.path;
+ } else {
+ return ref.parent.path;
+ }
+}
+
+- (void) fireEventOnQueue:(dispatch_queue_t)queue {
+ [self.eventRegistration fireEvent:self queue:queue];
+}
+
+- (BOOL) isCancelEvent {
+ return NO;
+}
+
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"event %d, data: %@", (int) eventType, [snapshot value]];
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FEvent.h b/Firebase/Database/Core/View/FEvent.h
new file mode 100644
index 0000000..6b9e31a
--- /dev/null
+++ b/Firebase/Database/Core/View/FEvent.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FIRDataEventType.h"
+
+@class FPath;
+
+@protocol FEvent <NSObject>
+- (FPath *) path;
+- (void) fireEventOnQueue:(dispatch_queue_t)queue;
+- (BOOL) isCancelEvent;
+- (NSString *) description;
+@end
diff --git a/Firebase/Database/Core/View/FEventRaiser.h b/Firebase/Database/Core/View/FEventRaiser.h
new file mode 100644
index 0000000..01a0130
--- /dev/null
+++ b/Firebase/Database/Core/View/FEventRaiser.h
@@ -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 "FTypedefs.h"
+
+@class FPath;
+@class FRepo;
+@class FIRDatabaseConfig;
+
+/**
+* Left as instance methods rather than class methods so that we could potentially callback on different queues for different repos.
+* This is semi-parallel to JS's FEventQueue
+*/
+@interface FEventRaiser : NSObject
+
+- (id)initWithQueue:(dispatch_queue_t)queue;
+
+- (void) raiseEvents:(NSArray *)eventDataList;
+- (void) raiseCallback:(fbt_void_void)callback;
+- (void) raiseCallbacks:(NSArray *)callbackList;
+
+@end
diff --git a/Firebase/Database/Core/View/FEventRaiser.m b/Firebase/Database/Core/View/FEventRaiser.m
new file mode 100644
index 0000000..94a0907
--- /dev/null
+++ b/Firebase/Database/Core/View/FEventRaiser.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 "FEventRaiser.h"
+#import "FDataEvent.h"
+#import "FTypedefs.h"
+#import "FUtilities.h"
+#import "FTupleUserCallback.h"
+#import "FRepo.h"
+#import "FRepoManager.h"
+
+@interface FEventRaiser ()
+
+@property (nonatomic, strong) dispatch_queue_t queue;
+
+@end
+
+/**
+* This class exists for symmetry with other clients, but since events are async, we don't need to do the complicated
+* stuff the JS client does to preserve event order.
+*/
+@implementation FEventRaiser
+
+- (id)init {
+ [NSException raise:NSInternalInconsistencyException format:@"Can't use default constructor"];
+ return nil;
+}
+
+- (id)initWithQueue:(dispatch_queue_t)queue {
+ self = [super init];
+ if (self != nil) {
+ self->_queue = queue;
+ }
+ return self;
+}
+
+- (void) raiseEvents:(NSArray *)eventDataList {
+ for (id<FEvent> event in eventDataList) {
+ [event fireEventOnQueue:self.queue];
+ }
+}
+
+- (void) raiseCallback:(fbt_void_void)callback {
+ dispatch_async(self.queue, callback);
+}
+
+- (void) raiseCallbacks:(NSArray *)callbackList {
+ for (fbt_void_void callback in callbackList) {
+ dispatch_async(self.queue, callback);
+ }
+}
+
++ (void) raiseCallbacks:(NSArray *)callbackList queue:(dispatch_queue_t)queue {
+ for (fbt_void_void callback in callbackList) {
+ dispatch_async(queue, callback);
+ }
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FEventRegistration.h b/Firebase/Database/Core/View/FEventRegistration.h
new file mode 100644
index 0000000..5b845ac
--- /dev/null
+++ b/Firebase/Database/Core/View/FEventRegistration.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 <Foundation/Foundation.h>
+#import "FChange.h"
+#import "FIRDataEventType.h"
+
+@protocol FEvent;
+@class FDataEvent;
+@class FCancelEvent;
+@class FQuerySpec;
+
+@protocol FEventRegistration <NSObject>
+- (BOOL) responseTo:(FIRDataEventType)eventType;
+- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query;
+- (void) fireEvent:(id<FEvent>)event queue:(dispatch_queue_t)queue;
+- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path;
+/**
+* Used to figure out what event registration match the event registration that needs to be removed.
+*/
+- (BOOL) matches:(id<FEventRegistration>)other;
+@property (nonatomic, readonly) FIRDatabaseHandle handle;
+@end
diff --git a/Firebase/Database/Core/View/FKeepSyncedEventRegistration.h b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.h
new file mode 100644
index 0000000..669e012
--- /dev/null
+++ b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.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 <Foundation/Foundation.h>
+
+#import "FEventRegistration.h"
+
+/**
+ * A singleton event registration to mark a query as keep synced
+ */
+@interface FKeepSyncedEventRegistration : NSObject<FEventRegistration>
+
++ (FKeepSyncedEventRegistration *)instance;
+
+@end
diff --git a/Firebase/Database/Core/View/FKeepSyncedEventRegistration.m b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.m
new file mode 100644
index 0000000..806d54f
--- /dev/null
+++ b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.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 "FKeepSyncedEventRegistration.h"
+
+@interface FKeepSyncedEventRegistration ()
+
+@end
+
+@implementation FKeepSyncedEventRegistration
+
++ (FKeepSyncedEventRegistration *)instance {
+ static dispatch_once_t onceToken;
+ static FKeepSyncedEventRegistration *keepSynced;
+ dispatch_once(&onceToken, ^{
+ keepSynced = [[FKeepSyncedEventRegistration alloc] init];
+ });
+ return keepSynced;
+}
+
+- (BOOL) responseTo:(FIRDataEventType)eventType {
+ return NO;
+}
+
+- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query {
+ [NSException raise:NSInternalInconsistencyException format:@"Should never create event for FKeepSyncedEventRegistration"];
+ return nil;
+}
+
+- (void) fireEvent:(id<FEvent>)event queue:(dispatch_queue_t)queue {
+ [NSException raise:NSInternalInconsistencyException format:@"Should never raise event for FKeepSyncedEventRegistration"];
+}
+
+- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path {
+ // Don't create cancel events....
+ return nil;
+}
+
+- (FIRDatabaseHandle) handle {
+ // TODO[offline]: returning arbitray, can't return NSNotFound since that is used to match other event registrations
+ // We should really redo this to match on different kind of events (single observer, all observers, cancelled)
+ // rather than on a NSNotFound handle...
+ return NSNotFound - 1;
+}
+
+- (BOOL) matches:(id<FEventRegistration>)other {
+ // Only matches singleton instance
+ return self == other;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FValueEventRegistration.h b/Firebase/Database/Core/View/FValueEventRegistration.h
new file mode 100644
index 0000000..1220c60
--- /dev/null
+++ b/Firebase/Database/Core/View/FValueEventRegistration.h
@@ -0,0 +1,34 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FEventRegistration.h"
+#import "FTypedefs.h"
+
+@class FRepo;
+
+@interface FValueEventRegistration : NSObject<FEventRegistration>
+
+- (id) initWithRepo:(FRepo *)repo
+ handle:(FIRDatabaseHandle)fHandle
+ callback:(fbt_void_datasnapshot)callbackBlock
+ cancelCallback:(fbt_void_nserror)cancelCallbackBlock;
+
+@property (nonatomic, copy, readonly) fbt_void_datasnapshot callback;
+@property (nonatomic, copy, readonly) fbt_void_nserror cancelCallback;
+@property (nonatomic, readonly) FIRDatabaseHandle handle;
+
+@end
diff --git a/Firebase/Database/Core/View/FValueEventRegistration.m b/Firebase/Database/Core/View/FValueEventRegistration.m
new file mode 100644
index 0000000..d351a4b
--- /dev/null
+++ b/Firebase/Database/Core/View/FValueEventRegistration.m
@@ -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 "FValueEventRegistration.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FCancelEvent.h"
+#import "FDataEvent.h"
+
+@interface FValueEventRegistration ()
+@property (nonatomic, strong) FRepo* repo;
+@property (nonatomic, copy, readwrite) fbt_void_datasnapshot callback;
+@property (nonatomic, copy, readwrite) fbt_void_nserror cancelCallback;
+@property (nonatomic, readwrite) FIRDatabaseHandle handle;
+@end
+
+@implementation FValueEventRegistration
+
+- (id) initWithRepo:(FRepo *)repo
+ handle:(FIRDatabaseHandle)fHandle
+ callback:(fbt_void_datasnapshot)callbackBlock
+ cancelCallback:(fbt_void_nserror)cancelCallbackBlock {
+ self = [super init];
+ if (self) {
+ self.repo = repo;
+ self.handle = fHandle;
+ self.callback = callbackBlock;
+ self.cancelCallback = cancelCallbackBlock;
+ }
+ return self;
+}
+
+- (BOOL) responseTo:(FIRDataEventType)eventType {
+ return eventType == FIRDataEventTypeValue;
+}
+
+- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query {
+ FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:query.path];
+ FIRDataSnapshot *snapshot = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:change.indexedNode];
+ FDataEvent *eventData = [[FDataEvent alloc] initWithEventType:FIRDataEventTypeValue eventRegistration:self
+ dataSnapshot:snapshot];
+ return eventData;
+}
+
+- (void) fireEvent:(id <FEvent>)event queue:(dispatch_queue_t)queue {
+ if ([event isCancelEvent]) {
+ FCancelEvent *cancelEvent = event;
+ FFLog(@"I-RDB065001", @"Raising cancel value event on %@", event.path);
+ NSAssert(self.cancelCallback != nil, @"Raising a cancel event on a listener with no cancel callback");
+ dispatch_async(queue, ^{
+ self.cancelCallback(cancelEvent.error);
+ });
+ } else if (self.callback != nil) {
+ FDataEvent *dataEvent = event;
+ FFLog(@"I-RDB065002", @"Raising value event on %@", dataEvent.snapshot.key);
+ dispatch_async(queue, ^{
+ self.callback(dataEvent.snapshot);
+ });
+ }
+}
+
+- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path {
+ if (self.cancelCallback != nil) {
+ return [[FCancelEvent alloc] initWithEventRegistration:self error:error path:path];
+ } else {
+ return nil;
+ }
+}
+
+- (BOOL) matches:(id<FEventRegistration>)other {
+ return self.handle == NSNotFound || other.handle == NSNotFound || self.handle == other.handle;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/FView.h b/Firebase/Database/Core/View/FView.h
new file mode 100644
index 0000000..2d0761a
--- /dev/null
+++ b/Firebase/Database/Core/View/FView.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 <Foundation/Foundation.h>
+
+@protocol FNode;
+@protocol FOperation;
+@protocol FEventRegistration;
+@class FWriteTreeRef;
+@class FQuerySpec;
+@class FChange;
+@class FPath;
+@class FViewCache;
+
+@interface FViewOperationResult : NSObject
+
+@property (nonatomic, strong, readonly) NSArray* changes;
+@property (nonatomic, strong, readonly) NSArray* events;
+
+@end
+
+
+@interface FView : NSObject
+
+@property (nonatomic, strong, readonly) FQuerySpec *query;
+
+- (id) initWithQuery:(FQuerySpec *)query initialViewCache:(FViewCache *)initialViewCache;
+
+- (id<FNode>) eventCache;
+- (id<FNode>) serverCache;
+- (id<FNode>) completeServerCacheFor:(FPath*)path;
+- (BOOL) isEmpty;
+
+- (void) addEventRegistration:(id<FEventRegistration>)eventRegistration;
+- (NSArray *) removeEventRegistration:(id<FEventRegistration>)eventRegistration cancelError:(NSError *)cancelError;
+
+- (FViewOperationResult *) applyOperation:(id <FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id <FNode>)optCompleteServerCache;
+- (NSArray *) initialEvents:(id<FEventRegistration>)registration;
+
+@end
diff --git a/Firebase/Database/Core/View/FView.m b/Firebase/Database/Core/View/FView.m
new file mode 100644
index 0000000..1aea4d7
--- /dev/null
+++ b/Firebase/Database/Core/View/FView.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 "FView.h"
+#import "FNode.h"
+#import "FWriteTreeRef.h"
+#import "FOperation.h"
+#import "FIRDatabaseQuery.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FEventRegistration.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FViewCache.h"
+#import "FPath.h"
+#import "FEventGenerator.h"
+#import "FOperationSource.h"
+#import "FCancelEvent.h"
+#import "FIndexedFilter.h"
+#import "FCacheNode.h"
+#import "FEmptyNode.h"
+#import "FViewProcessor.h"
+#import "FViewProcessorResult.h"
+#import "FIndexedNode.h"
+
+@interface FViewOperationResult ()
+
+@property (nonatomic, strong, readwrite) NSArray *changes;
+@property (nonatomic, strong, readwrite) NSArray *events;
+
+@end
+
+@implementation FViewOperationResult
+
+- (id)initWithChanges:(NSArray *)changes events:(NSArray *)events {
+ self = [super init];
+ if (self != nil) {
+ self->_changes = changes;
+ self->_events = events;
+ }
+ return self;
+}
+
+@end
+
+/**
+* A view represents a specific location and query that has 1 or more event registrations.
+*
+* It does several things:
+* - Maintains the list of event registration for this location/query.
+* - Maintains a cache of the data visible for this location/query.
+* - Applies new operations (via applyOperation), updates the cache, and based on the event
+* registrations returns the set of events to be raised.
+*/
+@interface FView ()
+
+@property (nonatomic, strong, readwrite) FQuerySpec *query;
+@property (nonatomic, strong) FViewProcessor *processor;
+@property (nonatomic, strong) FViewCache *viewCache;
+@property (nonatomic, strong) NSMutableArray *eventRegistrations;
+@property (nonatomic, strong) FEventGenerator *eventGenerator;
+
+@end
+
+@implementation FView
+- (id) initWithQuery:(FQuerySpec *)query initialViewCache:(FViewCache *)initialViewCache {
+ self = [super init];
+ if (self) {
+ self.query = query;
+
+ FIndexedFilter *indexFilter = [[FIndexedFilter alloc] initWithIndex:query.index];
+ id<FNodeFilter> filter = query.params.nodeFilter;
+ self.processor = [[FViewProcessor alloc] initWithFilter:filter];
+ FCacheNode *initialServerCache = initialViewCache.cachedServerSnap;
+ FCacheNode *initialEventCache = initialViewCache.cachedEventSnap;
+
+ // Don't filter server node with other filter than index, wait for tagged listen
+ FIndexedNode *emptyIndexedNode = [FIndexedNode indexedNodeWithNode:[FEmptyNode emptyNode] index:query.index];
+ FIndexedNode *serverSnap = [indexFilter updateFullNode:emptyIndexedNode
+ withNewNode:initialServerCache.indexedNode
+ accumulator:nil];
+ FIndexedNode *eventSnap = [filter updateFullNode:emptyIndexedNode
+ withNewNode:initialEventCache.indexedNode
+ accumulator:nil];
+ FCacheNode *newServerCache = [[FCacheNode alloc] initWithIndexedNode:serverSnap
+ isFullyInitialized:initialServerCache.isFullyInitialized
+ isFiltered:indexFilter.filtersNodes];
+ FCacheNode *newEventCache = [[FCacheNode alloc] initWithIndexedNode:eventSnap
+ isFullyInitialized:initialEventCache.isFullyInitialized
+ isFiltered:filter.filtersNodes];
+
+ self.viewCache = [[FViewCache alloc] initWithEventCache:newEventCache serverCache:newServerCache];
+
+ self.eventRegistrations = [[NSMutableArray alloc] init];
+
+ self.eventGenerator = [[FEventGenerator alloc] initWithQuery:query];
+ }
+
+ return self;
+}
+
+- (id <FNode>) serverCache {
+ return self.viewCache.cachedServerSnap.node;
+}
+
+- (id <FNode>) eventCache {
+ return self.viewCache.cachedEventSnap.node;
+}
+
+- (id <FNode>) completeServerCacheFor:(FPath*)path {
+ id<FNode> cache = self.viewCache.completeServerSnap;
+ if (cache) {
+ // If this isn't a "loadsAllData" view, then cache isn't actually a complete cache and
+ // we need to see if it contains the child we're interested in.
+ if ([self.query loadsAllData] ||
+ (!path.isEmpty && ![cache getImmediateChild:path.getFront].isEmpty)) {
+ return [cache getChild:path];
+ }
+ }
+ return nil;
+}
+
+- (BOOL) isEmpty {
+ return self.eventRegistrations.count == 0;
+}
+
+- (void) addEventRegistration:(id <FEventRegistration>)eventRegistration {
+ [self.eventRegistrations addObject:eventRegistration];
+}
+
+/**
+* @param eventRegistration If null, remove all callbacks.
+* @param cancelError If a cancelError is provided, appropriate cancel events will be returned.
+* @return Cancel events, if cancelError was provided.
+*/
+- (NSArray *) removeEventRegistration:(id <FEventRegistration>)eventRegistration cancelError:(NSError *)cancelError {
+ NSMutableArray *cancelEvents = [[NSMutableArray alloc] init];
+ if (cancelError != nil) {
+ NSAssert(eventRegistration == nil, @"A cancel should cancel all event registrations.");
+ FPath *path = self.query.path;
+ for (id <FEventRegistration> registration in self.eventRegistrations) {
+ FCancelEvent *maybeEvent = [registration createCancelEventFromError:cancelError path:path];
+ if (maybeEvent) {
+ [cancelEvents addObject:maybeEvent];
+ }
+ }
+ }
+
+ if (eventRegistration) {
+ NSUInteger i = 0;
+ while (i < self.eventRegistrations.count) {
+ id<FEventRegistration> existing = self.eventRegistrations[i];
+ if ([existing matches:eventRegistration]) {
+ [self.eventRegistrations removeObjectAtIndex:i];
+ } else {
+ i++;
+ }
+ }
+ } else {
+ [self.eventRegistrations removeAllObjects];
+ }
+ return cancelEvents;
+}
+
+/**
+ * Applies the given Operation, updates our cache, and returns the appropriate events and changes
+ */
+- (FViewOperationResult *) applyOperation:(id <FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id <FNode>)optCompleteServerCache {
+ if (operation.type == FOperationTypeMerge && operation.source.queryParams != nil) {
+ NSAssert(self.viewCache.completeServerSnap != nil, @"We should always have a full cache before handling merges");
+ NSAssert(self.viewCache.completeEventSnap != nil, @"Missing event cache, even though we have a server cache");
+ }
+ FViewCache *oldViewCache = self.viewCache;
+ FViewProcessorResult *result = [self.processor applyOperationOn:oldViewCache operation:operation writesCache:writesCache completeCache:optCompleteServerCache];
+
+ NSAssert(result.viewCache.cachedServerSnap.isFullyInitialized || !oldViewCache.cachedServerSnap.isFullyInitialized, @"Once a server snap is complete, it should never go back.");
+
+ self.viewCache = result.viewCache;
+ NSArray *events = [self generateEventsForChanges:result.changes eventCache:result.viewCache.cachedEventSnap.indexedNode registration:nil];
+ return [[FViewOperationResult alloc] initWithChanges:result.changes events:events];
+}
+
+- (NSArray *) initialEvents:(id<FEventRegistration>)registration {
+ FCacheNode *eventSnap = self.viewCache.cachedEventSnap;
+ NSMutableArray *initialChanges = [[NSMutableArray alloc] init];
+ [eventSnap.indexedNode.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:node];
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded indexedNode:indexed childKey:key];
+ [initialChanges addObject:change];
+ }];
+ if (eventSnap.isFullyInitialized) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeValue indexedNode:eventSnap.indexedNode];
+ [initialChanges addObject:change];
+ }
+ return [self generateEventsForChanges:initialChanges eventCache:eventSnap.indexedNode registration:registration];
+}
+
+- (NSArray *) generateEventsForChanges:(NSArray *)changes eventCache:(FIndexedNode *)eventCache registration:(id<FEventRegistration>)registration {
+ NSArray *registrations;
+ if (registration == nil) {
+ registrations = [[NSArray alloc] initWithArray:self.eventRegistrations];
+ } else {
+ registrations = [[NSArray alloc] initWithObjects:registration, nil];
+ }
+ return [self.eventGenerator generateEventsForChanges:changes eventCache:eventCache eventRegistrations:registrations];
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FView (%@)", self.query];
+}
+@end
diff --git a/Firebase/Database/Core/View/FViewCache.h b/Firebase/Database/Core/View/FViewCache.h
new file mode 100644
index 0000000..4d01877
--- /dev/null
+++ b/Firebase/Database/Core/View/FViewCache.h
@@ -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 <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FCacheNode;
+@class FIndexedNode;
+
+@interface FViewCache : NSObject
+
+- (id) initWithEventCache:(FCacheNode *)eventCache serverCache:(FCacheNode *)serverCache;
+
+- (FViewCache *) updateEventSnap:(FIndexedNode *)eventSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered;
+- (FViewCache *) updateServerSnap:(FIndexedNode *)serverSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered;
+
+@property (nonatomic, strong, readonly) FCacheNode *cachedEventSnap;
+@property (nonatomic, strong, readonly) id<FNode> completeEventSnap;
+@property (nonatomic, strong, readonly) FCacheNode *cachedServerSnap;
+@property (nonatomic, strong, readonly) id<FNode> completeServerSnap;
+
+@end
diff --git a/Firebase/Database/Core/View/FViewCache.m b/Firebase/Database/Core/View/FViewCache.m
new file mode 100644
index 0000000..c6ec8b1
--- /dev/null
+++ b/Firebase/Database/Core/View/FViewCache.m
@@ -0,0 +1,61 @@
+/*
+ * 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 "FViewCache.h"
+#import "FCacheNode.h"
+#import "FNode.h"
+#import "FEmptyNode.h"
+
+@interface FViewCache ()
+@property (nonatomic, strong, readwrite) FCacheNode *cachedEventSnap;
+@property (nonatomic, strong, readwrite) FCacheNode *cachedServerSnap;
+@end
+
+@implementation FViewCache
+
+- (id) initWithEventCache:(FCacheNode *)eventCache serverCache:(FCacheNode *)serverCache {
+ self = [super init];
+ if (self) {
+ self.cachedEventSnap = eventCache;
+ self.cachedServerSnap = serverCache;
+ }
+ return self;
+}
+
+- (FViewCache *) updateEventSnap:(FIndexedNode *)eventSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered {
+ FCacheNode *updatedEventCache = [[FCacheNode alloc] initWithIndexedNode:eventSnap
+ isFullyInitialized:complete
+ isFiltered:filtered];
+ return [[FViewCache alloc] initWithEventCache:updatedEventCache serverCache:self.cachedServerSnap];
+}
+
+- (FViewCache *) updateServerSnap:(FIndexedNode *)serverSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered {
+ FCacheNode *updatedServerCache = [[FCacheNode alloc] initWithIndexedNode:serverSnap
+ isFullyInitialized:complete
+ isFiltered:filtered];
+ return [[FViewCache alloc] initWithEventCache:self.cachedEventSnap serverCache:updatedServerCache];
+}
+
+- (id<FNode>) completeEventSnap {
+ return (self.cachedEventSnap.isFullyInitialized) ? self.cachedEventSnap.node : nil;
+}
+
+- (id<FNode>) completeServerSnap {
+ return (self.cachedServerSnap.isFullyInitialized) ? self.cachedServerSnap.node : nil;
+}
+
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h
new file mode 100644
index 0000000..59b0a85
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.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 <Foundation/Foundation.h>
+
+@class FChange;
+
+
+@interface FChildChangeAccumulator : NSObject
+
+- (id) init;
+- (void) trackChildChange:(FChange *)change;
+- (NSArray *) changes;
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m
new file mode 100644
index 0000000..e43fd7c
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m
@@ -0,0 +1,80 @@
+/*
+ * 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 "FChildChangeAccumulator.h"
+#import "FChange.h"
+#import "FIndex.h"
+
+@interface FChildChangeAccumulator ()
+@property (nonatomic, strong) NSMutableDictionary *changeMap;
+@end
+
+@implementation FChildChangeAccumulator
+
+- (id) init {
+ self = [super init];
+ if (self) {
+ self.changeMap = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (void) trackChildChange:(FChange *)change {
+ FIRDataEventType type = change.type;
+ NSString *childKey = change.childKey;
+ NSAssert(type == FIRDataEventTypeChildAdded || type == FIRDataEventTypeChildChanged || type == FIRDataEventTypeChildRemoved, @"Only child changes supported for tracking.");
+ NSAssert(![change.childKey isEqualToString:@".priority"], @"Changes not tracked on priority");
+ if (self.changeMap[childKey] != nil) {
+ FChange *oldChange = [self.changeMap objectForKey:childKey];
+ FIRDataEventType oldType = oldChange.type;
+ if (type == FIRDataEventTypeChildAdded && oldType == FIRDataEventTypeChildRemoved) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:change.indexedNode
+ childKey:childKey
+ oldIndexedNode:oldChange.indexedNode];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else if (type == FIRDataEventTypeChildRemoved && oldType == FIRDataEventTypeChildAdded) {
+ [self.changeMap removeObjectForKey:childKey];
+ } else if (type == FIRDataEventTypeChildRemoved && oldType == FIRDataEventTypeChildChanged) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:oldChange.oldIndexedNode
+ childKey:childKey];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else if (type == FIRDataEventTypeChildChanged && oldType == FIRDataEventTypeChildAdded) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:change.indexedNode
+ childKey:childKey];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else if (type == FIRDataEventTypeChildChanged && oldType == FIRDataEventTypeChildChanged) {
+ FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:change.indexedNode
+ childKey:childKey
+ oldIndexedNode:oldChange.oldIndexedNode];
+ [self.changeMap setObject:newChange forKey:childKey];
+ } else {
+ NSString *reason = [NSString stringWithFormat:@"Illegal combination of changes: %@ occurred after %@", change, oldChange];
+ @throw [[NSException alloc] initWithName:@"FirebaseDatabaseInternalError" reason:reason userInfo:nil];
+ }
+ } else {
+ [self.changeMap setObject:change forKey:childKey];
+ }
+}
+
+- (NSArray *) changes {
+ return [self.changeMap allValues];
+}
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FCompleteChildSource.h b/Firebase/Database/Core/View/Filter/FCompleteChildSource.h
new file mode 100644
index 0000000..4e99045
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FCompleteChildSource.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 <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FNamedNode;
+@protocol FIndex;
+
+@protocol FCompleteChildSource<NSObject>
+
+- (id<FNode>) completeChild:(NSString *)childKey;
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse;
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FIndexedFilter.h b/Firebase/Database/Core/View/Filter/FIndexedFilter.h
new file mode 100644
index 0000000..5081a77
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FIndexedFilter.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FNodeFilter.h"
+
+@protocol FIndex;
+
+
+@interface FIndexedFilter : NSObject<FNodeFilter>
+
+- (id) initWithIndex:(id<FIndex>)theIndex;
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FIndexedFilter.m b/Firebase/Database/Core/View/Filter/FIndexedFilter.m
new file mode 100644
index 0000000..44c411c
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FIndexedFilter.m
@@ -0,0 +1,147 @@
+/*
+ * 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 "FNode.h"
+#import "FIndexedFilter.h"
+#import "FChildChangeAccumulator.h"
+#import "FIndex.h"
+#import "FChange.h"
+#import "FChildrenNode.h"
+#import "FKeyIndex.h"
+#import "FEmptyNode.h"
+#import "FIndexedNode.h"
+
+@interface FIndexedFilter ()
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+@end
+
+@implementation FIndexedFilter
+- (id) initWithIndex:(id<FIndex>)theIndex {
+ self = [super init];
+ if (self) {
+ self.index = theIndex;
+ }
+ return self;
+}
+
+- (FIndexedNode *)updateChildIn:(FIndexedNode *)indexedNode
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ NSAssert([indexedNode hasIndex:self.index], @"The index in FIndexedNode must match the index of the filter");
+ id<FNode> node = indexedNode.node;
+ id<FNode> oldChildSnap = [node getImmediateChild:childKey];
+
+ // Check if anything actually changed.
+ if ([[oldChildSnap getChild:affectedPath] isEqual:[newChildSnap getChild:affectedPath]]) {
+ // There's an edge case where a child can enter or leave the view because affectedPath was set to null.
+ // In this case, affectedPath will appear null in both the old and new snapshots. So we need
+ // to avoid treating these cases as "nothing changed."
+ if (oldChildSnap.isEmpty == newChildSnap.isEmpty) {
+ // Nothing changed.
+ #ifdef DEBUG
+ NSAssert([oldChildSnap isEqual:newChildSnap], @"Old and new snapshots should be equal.");
+ #endif
+
+ return indexedNode;
+ }
+ }
+ if (optChangeAccumulator) {
+ if (newChildSnap.isEmpty) {
+ if ([node hasChild:childKey]) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ } else {
+ NSAssert(node.isLeafNode, @"A child remove without an old child only makes sense on a leaf node.");
+ }
+ } else if (oldChildSnap.isEmpty) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ } else {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey
+ oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ }
+ if (node.isLeafNode && newChildSnap.isEmpty) {
+ return indexedNode;
+ } else {
+ return [indexedNode updateChild:childKey withNewChild:newChildSnap];
+ }
+}
+
+- (FIndexedNode *)updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ if (optChangeAccumulator) {
+ [oldSnap.node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ if (![newSnap.node hasChild:childKey]) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:childNode]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ }];
+
+ [newSnap.node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ if ([oldSnap.node hasChild:childKey]) {
+ id<FNode> oldChildSnap = [oldSnap.node getImmediateChild:childKey];
+ if (![oldChildSnap isEqual:childNode]) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:[FIndexedNode indexedNodeWithNode:childNode]
+ childKey:childKey
+ oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ } else {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:childNode]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ }];
+ }
+ return newSnap;
+}
+
+- (FIndexedNode *)updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap
+{
+ if ([oldSnap.node isEmpty]) {
+ return oldSnap;
+ } else {
+ return [oldSnap updatePriority:priority];
+ }
+}
+
+- (BOOL) filtersNodes {
+ return NO;
+}
+
+- (id<FNodeFilter>) indexedFilter {
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FLimitedFilter.h b/Firebase/Database/Core/View/Filter/FLimitedFilter.h
new file mode 100644
index 0000000..1690980
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FLimitedFilter.h
@@ -0,0 +1,26 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FNodeFilter.h"
+
+@class FQueryParams;
+
+
+@interface FLimitedFilter : NSObject<FNodeFilter>
+
+- (id) initWithQueryParams:(FQueryParams *)params;
+@end
diff --git a/Firebase/Database/Core/View/Filter/FLimitedFilter.m b/Firebase/Database/Core/View/Filter/FLimitedFilter.m
new file mode 100644
index 0000000..8bc6e87
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FLimitedFilter.m
@@ -0,0 +1,243 @@
+/*
+ * 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 "FLimitedFilter.h"
+#import "FChildChangeAccumulator.h"
+#import "FIndex.h"
+#import "FRangedFilter.h"
+#import "FQueryParams.h"
+#import "FQueryParams.h"
+#import "FNamedNode.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FCompleteChildSource.h"
+#import "FChange.h"
+#import "FTreeSortedDictionary.h"
+
+@interface FLimitedFilter ()
+@property (nonatomic, strong) FRangedFilter *rangedFilter;
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+@property (nonatomic) NSInteger limit;
+@property (nonatomic) BOOL reverse;
+
+@end
+
+@implementation FLimitedFilter
+- (id) initWithQueryParams:(FQueryParams *)params {
+ self = [super init];
+ if (self) {
+ self.rangedFilter = [[FRangedFilter alloc] initWithQueryParams:params];
+ self.index = params.index;
+ self.limit = params.limit;
+ self.reverse = !params.isViewFromLeft;
+ }
+ return self;
+}
+
+
+- (FIndexedNode *)updateChildIn:(FIndexedNode *)oldSnap
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ if (![self.rangedFilter matchesKey:childKey andNode:newChildSnap]) {
+ newChildSnap = [FEmptyNode emptyNode];
+ }
+ if ([[oldSnap.node getImmediateChild:childKey] isEqual:newChildSnap]) {
+ // No change
+ return oldSnap;
+ } else if (oldSnap.node.numChildren < self.limit) {
+ return [[self.rangedFilter indexedFilter] updateChildIn:oldSnap
+ forChildKey:childKey
+ newChild:newChildSnap
+ affectedPath:affectedPath
+ fromSource:source
+ accumulator:optChangeAccumulator];
+ } else {
+ return [self fullLimitUpdateNode:oldSnap
+ forChildKey:childKey
+ newChild:newChildSnap
+ fromSource:source
+ accumulator:optChangeAccumulator];
+ }
+}
+
+- (FIndexedNode *)fullLimitUpdateNode:(FIndexedNode *)oldIndexed
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ NSAssert(oldIndexed.node.numChildren == self.limit, @"Should have number of children equal to limit.");
+
+ FNamedNode *windowBoundary = self.reverse ? oldIndexed.firstChild : oldIndexed.lastChild;
+
+ BOOL inRange = [self.rangedFilter matchesKey:childKey andNode:newChildSnap];
+ if ([oldIndexed.node hasChild:childKey]) {
+ // `childKey` was already in `oldSnap`. Figure out if it remains in the window or needs to be replaced.
+ id<FNode> oldChildSnap = [oldIndexed.node getImmediateChild:childKey];
+
+ // In case the `newChildSnap` falls outside the window, get the `nextChild` that might replace it.
+ FNamedNode *nextChild = [source childByIndex:self.index afterChild:windowBoundary isReverse:(BOOL)self.reverse];
+ if (nextChild != nil && ([nextChild.name isEqualToString:childKey] ||
+ [oldIndexed.node hasChild:nextChild.name])) {
+ // There is a weird edge case where a node is updated as part of a merge in the write tree, but hasn't
+ // been applied to the limited filter yet. Ignore this next child which will be updated later in
+ // the limited filter...
+ nextChild = [source childByIndex:self.index afterChild:nextChild isReverse:self.reverse];
+ }
+
+
+
+ // Figure out if `newChildSnap` is in range and ordered before `nextChild`
+ BOOL remainsInWindow = inRange && !newChildSnap.isEmpty;
+ remainsInWindow = remainsInWindow && (!nextChild || [self.index compareKey:nextChild.name
+ andNode:nextChild.node
+ toOtherKey:childKey
+ andNode:newChildSnap
+ reverse:self.reverse] >= NSOrderedSame);
+ if (remainsInWindow) {
+ // `newChildSnap` is ordered before `nextChild`, so it's a child changed event
+ if (optChangeAccumulator != nil) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey
+ oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ return [oldIndexed updateChild:childKey withNewChild:newChildSnap];
+ } else {
+ // `newChildSnap` is ordered after `nextChild`, so it's a child removed event
+ if (optChangeAccumulator != nil) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ FIndexedNode *newIndexed = [oldIndexed updateChild:childKey withNewChild:[FEmptyNode emptyNode]];
+
+ // We need to check if the `nextChild` is actually in range before adding it
+ BOOL nextChildInRange = (nextChild != nil) && [self.rangedFilter matchesKey:nextChild.name
+ andNode:nextChild.node];
+ if (nextChildInRange) {
+ if (optChangeAccumulator != nil) {
+ FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:nextChild.node]
+ childKey:nextChild.name];
+ [optChangeAccumulator trackChildChange:change];
+ }
+ return [newIndexed updateChild:nextChild.name withNewChild:nextChild.node];
+ } else {
+ return newIndexed;
+ }
+ }
+ } else if (newChildSnap.isEmpty) {
+ // We're deleting a node, but it was not in the window, so ignore it.
+ return oldIndexed;
+ } else if (inRange) {
+ // `newChildSnap` is in range, but was ordered after `windowBoundary`. If this has changed, we bump out the
+ // `windowBoundary` and add the `newChildSnap`
+ if ([self.index compareKey:windowBoundary.name
+ andNode:windowBoundary.node
+ toOtherKey:childKey
+ andNode:newChildSnap
+ reverse:self.reverse] >= NSOrderedSame) {
+ if (optChangeAccumulator != nil) {
+ FChange *removedChange = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved
+ indexedNode:[FIndexedNode indexedNodeWithNode:windowBoundary.node]
+ childKey:windowBoundary.name];
+ FChange *addedChange = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded
+ indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap]
+ childKey:childKey];
+ [optChangeAccumulator trackChildChange:removedChange];
+ [optChangeAccumulator trackChildChange:addedChange];
+ }
+ return [[oldIndexed updateChild:childKey withNewChild:newChildSnap] updateChild:windowBoundary.name
+ withNewChild:[FEmptyNode emptyNode]];
+ } else {
+ return oldIndexed;
+ }
+ } else {
+ // `newChildSnap` was not in range and remains not in range, so ignore it.
+ return oldIndexed;
+ }
+}
+
+- (FIndexedNode *)updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ __block FIndexedNode *filtered;
+ if (newSnap.node.isLeafNode || newSnap.node.isEmpty) {
+ // Make sure we have a children node with the correct index, not a leaf node
+ filtered = [FIndexedNode indexedNodeWithNode:[FEmptyNode emptyNode] index:self.index];
+ } else {
+ filtered = newSnap;
+ // Don't support priorities on queries.
+ filtered = [filtered updatePriority:[FEmptyNode emptyNode]];
+ FNamedNode *startPost = nil;
+ FNamedNode *endPost = nil;
+ if (self.reverse) {
+ startPost = self.rangedFilter.endPost;
+ endPost = self.rangedFilter.startPost;
+ } else {
+ startPost = self.rangedFilter.startPost;
+ endPost = self.rangedFilter.endPost;
+ }
+ __block BOOL foundStartPost = NO;
+ __block NSUInteger count = 0;
+ [newSnap enumerateChildrenReverse:self.reverse usingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ if (!foundStartPost && [self.index compareKey:startPost.name
+ andNode:startPost.node
+ toOtherKey:childKey
+ andNode:childNode
+ reverse:self.reverse] <= NSOrderedSame) {
+ // Start adding
+ foundStartPost = YES;
+ }
+ BOOL inRange = foundStartPost && count < self.limit;
+ inRange = inRange && [self.index compareKey:childKey
+ andNode:childNode
+ toOtherKey:endPost.name
+ andNode:endPost.node
+ reverse:self.reverse] <= NSOrderedSame;
+ if (inRange) {
+ count++;
+ } else {
+ filtered = [filtered updateChild:childKey withNewChild:[FEmptyNode emptyNode]];
+ }
+ }];
+ }
+ return [self.indexedFilter updateFullNode:oldSnap withNewNode:filtered accumulator:optChangeAccumulator];
+}
+
+- (FIndexedNode *)updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap
+{
+ // Don't support priorities on queries.
+ return oldSnap;
+}
+
+- (BOOL) filtersNodes {
+ return YES;
+}
+
+- (id<FNodeFilter>) indexedFilter {
+ return self.rangedFilter.indexedFilter;
+}
+
+@end
diff --git a/Firebase/Database/Core/View/Filter/FNodeFilter.h b/Firebase/Database/Core/View/Filter/FNodeFilter.h
new file mode 100644
index 0000000..f29a85a
--- /dev/null
+++ b/Firebase/Database/Core/View/Filter/FNodeFilter.h
@@ -0,0 +1,71 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FIndexedNode;
+@protocol FCompleteChildSource;
+@class FChildChangeAccumulator;
+@protocol FIndex;
+@class FPath;
+
+/**
+* FNodeFilter is used to update nodes and complete children of nodes while applying queries on the fly and keeping
+* track of any child changes. This class does not track value changes as value changes depend on more than just the
+* node itself. Different kind of queries require different kind of implementations of this interface.
+*/
+@protocol FNodeFilter<NSObject>
+
+/**
+* Update a single complete child in the snap. If the child equals the old child in the snap, this is a no-op.
+* The method expects an indexed snap.
+*/
+- (FIndexedNode *) updateChildIn:(FIndexedNode *)oldSnap
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator;
+
+/**
+* Update a node in full and output any resulting change from this complete update.
+*/
+- (FIndexedNode *) updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator;
+
+/**
+* Update the priority of the root node
+*/
+- (FIndexedNode *) updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap;
+
+/**
+* Returns true if children might be filtered due to query critiera
+*/
+- (BOOL) filtersNodes;
+
+/**
+* Returns the index filter that this filter uses to get a NodeFilter that doesn't filter any children.
+*/
+@property (nonatomic, strong, readonly) id<FNodeFilter> indexedFilter;
+
+/**
+* Returns the index that this filter uses
+*/
+@property (nonatomic, strong, readonly) id<FIndex> index;
+
+@end
diff --git a/Firebase/Database/FClock.h b/Firebase/Database/FClock.h
new file mode 100644
index 0000000..1924ad4
--- /dev/null
+++ b/Firebase/Database/FClock.h
@@ -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 <Foundation/Foundation.h>
+
+@protocol FClock <NSObject>
+
+- (NSTimeInterval)currentTime;
+
+@end
+
+@interface FSystemClock : NSObject<FClock>
+
++ (FSystemClock *)clock;
+
+@end
+
+@interface FOffsetClock : NSObject<FClock>
+
+- (id)initWithClock:(id<FClock>)clock offset:(NSTimeInterval)offset;
+
+@end
diff --git a/Firebase/Database/FClock.m b/Firebase/Database/FClock.m
new file mode 100644
index 0000000..2464056
--- /dev/null
+++ b/Firebase/Database/FClock.m
@@ -0,0 +1,58 @@
+/*
+ * 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 "FClock.h"
+
+@implementation FSystemClock
+
+- (NSTimeInterval)currentTime {
+ return [[NSDate date] timeIntervalSince1970];
+}
+
++ (FSystemClock *)clock {
+ static dispatch_once_t onceToken;
+ static FSystemClock *clock;
+ dispatch_once(&onceToken, ^{
+ clock = [[FSystemClock alloc] init];
+ });
+ return clock;
+}
+
+@end
+
+@interface FOffsetClock ()
+
+@property (nonatomic, strong) id<FClock> clock;
+@property (nonatomic) NSTimeInterval offset;
+
+@end
+
+@implementation FOffsetClock
+
+- (NSTimeInterval)currentTime {
+ return [self.clock currentTime] + self.offset;
+}
+
+- (id)initWithClock:(id<FClock>)clock offset:(NSTimeInterval)offset {
+ self = [super init];
+ if (self != nil) {
+ self->_clock = clock;
+ self->_offset = offset;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/FEventGenerator.h b/Firebase/Database/FEventGenerator.h
new file mode 100644
index 0000000..1bc011b
--- /dev/null
+++ b/Firebase/Database/FEventGenerator.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FQuerySpec;
+@class FIndexedNode;
+@protocol FNode;
+
+@interface FEventGenerator : NSObject
+- (id) initWithQuery:(FQuerySpec *)query;
+- (NSArray*) generateEventsForChanges:(NSArray*)changes eventCache:(FIndexedNode *)eventCache
+ eventRegistrations:(NSArray*)registrations;
+@end
diff --git a/Firebase/Database/FEventGenerator.m b/Firebase/Database/FEventGenerator.m
new file mode 100644
index 0000000..f6e8f47
--- /dev/null
+++ b/Firebase/Database/FEventGenerator.m
@@ -0,0 +1,141 @@
+/*
+ * 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 "FEventGenerator.h"
+#import "FNode.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FChange.h"
+#import "FNamedNode.h"
+#import "FEventRegistration.h"
+#import "FEvent.h"
+#import "FDataEvent.h"
+
+@interface FEventGenerator ()
+@property (nonatomic, strong) FQuerySpec *query;
+@end
+
+/**
+* An EventGenerator is used to convert "raw" changes (fb.core.view.Change) as computed by the
+* CacheDiffer into actual events (fb.core.view.Event) that can be raised. See generateEventsForChanges()
+* for details.
+*/
+@implementation FEventGenerator
+
+- (id)initWithQuery:(FQuerySpec *)query {
+ self = [super init];
+ if (self) {
+ self.query = query;
+ }
+ return self;
+}
+
+/**
+* Given a set of raw changes (no moved events, and prevName not specified yet), and a set of EventRegistrations that
+* should be notified of these changes, generate the actual events to be raised.
+*
+* Notes:
+* - child_moved events will be synthesized at this time for any child_changed events that affect our index
+* - prevName will be calculated based on the index ordering
+*
+* @param changes NSArray of FChange, not necessarily in order.
+* @param registrations is NSArray of FEventRegistration.
+* @return NSArray of FEvent.
+*/
+- (NSArray *) generateEventsForChanges:(NSArray *)changes
+ eventCache:(FIndexedNode *)eventCache
+ eventRegistrations:(NSArray *)registrations
+{
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+
+ // child_moved is index-specific, so check all our child_changed events to see if we need to materialize
+ // child_moved events with this view's index
+ NSMutableArray *moves = [[NSMutableArray alloc] init];
+ for (FChange *change in changes) {
+ if (change.type == FIRDataEventTypeChildChanged && [self.query.index indexedValueChangedBetween:change.oldIndexedNode.node
+ and:change.indexedNode.node]) {
+ FChange *moveChange = [[FChange alloc] initWithType:FIRDataEventTypeChildMoved
+ indexedNode:change.indexedNode
+ childKey:change.childKey
+ oldIndexedNode:nil];
+ [moves addObject:moveChange];
+ }
+ }
+
+ [self generateEvents:events forType:FIRDataEventTypeChildRemoved changes:changes eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeChildAdded changes:changes eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeChildMoved changes:moves eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeChildChanged changes:changes eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeValue changes:changes eventCache:eventCache eventRegistrations:registrations];
+
+ return events;
+}
+
+- (void) generateEvents:(NSMutableArray *)events
+ forType:(FIRDataEventType)eventType
+ changes:(NSArray *)changes
+ eventCache:(FIndexedNode *)eventCache
+ eventRegistrations:(NSArray *)registrations
+{
+ NSMutableArray *filteredChanges = [[NSMutableArray alloc] init];
+ for (FChange *change in changes) {
+ if (change.type == eventType) {
+ [filteredChanges addObject:change];
+ }
+ }
+
+ id<FIndex> index = self.query.index;
+
+ [filteredChanges sortUsingComparator:^NSComparisonResult(FChange *one, FChange *two) {
+ if (one.childKey == nil || two.childKey == nil) {
+ @throw [[NSException alloc] initWithName:@"InternalInconsistencyError"
+ reason:@"Should only compare child_ events"
+ userInfo:nil];
+ }
+ return [index compareKey:one.childKey
+ andNode:one.indexedNode.node
+ toOtherKey:two.childKey
+ andNode:two.indexedNode.node];
+ }];
+
+ for (FChange *change in filteredChanges) {
+ for (id<FEventRegistration> registration in registrations) {
+ if ([registration responseTo:eventType]) {
+ id<FEvent> event = [self generateEventForChange:change registration:registration eventCache:eventCache];
+ [events addObject:event];
+ }
+ }
+ }
+}
+
+- (id<FEvent>) generateEventForChange:(FChange *)change
+ registration:(id<FEventRegistration>)registration
+ eventCache:(FIndexedNode *)eventCache
+{
+ FChange *materializedChange;
+ if (change.type == FIRDataEventTypeValue || change.type == FIRDataEventTypeChildRemoved) {
+ materializedChange = change;
+ } else {
+ NSString *prevChildKey = [eventCache predecessorForChildKey:change.childKey
+ childNode:change.indexedNode.node
+ index:self.query.index];
+ materializedChange = [change changeWithPrevKey:prevChildKey];
+ }
+ return [registration createEventFrom:materializedChange query:self.query];
+}
+
+@end
diff --git a/Firebase/Database/FIRDatabaseConfig_Private.h b/Firebase/Database/FIRDatabaseConfig_Private.h
new file mode 100644
index 0000000..b0a9dc4
--- /dev/null
+++ b/Firebase/Database/FIRDatabaseConfig_Private.h
@@ -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 "FIRDatabaseConfig.h"
+#import "FAuthTokenProvider.h"
+
+@protocol FStorageEngine;
+
+@interface FIRDatabaseConfig ()
+
+@property (nonatomic, readonly) BOOL isFrozen;
+@property (nonatomic, strong, readonly) NSString *sessionIdentifier;
+@property (nonatomic, strong) id<FAuthTokenProvider> authTokenProvider;
+@property (nonatomic, strong) id<FStorageEngine> forceStorageEngine;
+
+- (void)freeze;
+
++ (FIRDatabaseConfig *)configForName:(NSString *)name;
+
++ (FIRDatabaseConfig *)defaultConfig;
+
+@end
diff --git a/Firebase/Database/FIRDatabaseReference.h b/Firebase/Database/FIRDatabaseReference.h
new file mode 100644
index 0000000..eb3fecd
--- /dev/null
+++ b/Firebase/Database/FIRDatabaseReference.h
@@ -0,0 +1,719 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FIRDatabaseQuery.h"
+#import "FIRDatabase.h"
+#import "FIRDatabaseSwiftNameSupport.h"
+#import "FIRDataSnapshot.h"
+#import "FIRMutableData.h"
+#import "FIRTransactionResult.h"
+#import "FIRServerValue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDatabase;
+
+/**
+ * A FIRDatabaseReference represents a particular location in your Firebase Database
+ * and can be used for reading or writing data to that Firebase Database location.
+ *
+ * This class is the starting point for all Firebase Database operations. After you've
+ * obtained your first FIRDatabaseReference via [FIRDatabase reference], you can use it
+ * to read data (ie. observeEventType:withBlock:), write data (ie. setValue:), and to
+ * create new FIRDatabaseReferences (ie. child:).
+ */
+FIR_SWIFT_NAME(DatabaseReference)
+@interface FIRDatabaseReference : FIRDatabaseQuery
+
+
+#pragma mark - Getting references to children locations
+
+/**
+ * Gets a FIRDatabaseReference for the location at the specified relative path.
+ * The relative path can either be a simple child key (e.g. 'fred') or a
+ * deeper slash-separated path (e.g. 'fred/name/first').
+ *
+ * @param pathString A relative path from this location to the desired child location.
+ * @return A FIRDatabaseReference for the specified relative path.
+ */
+- (FIRDatabaseReference *)child:(NSString *)pathString;
+
+/**
+ * childByAppendingPath: is deprecated, use child: instead.
+ */
+- (FIRDatabaseReference *)childByAppendingPath:(NSString *)pathString __deprecated_msg("use child: instead");
+
+/**
+ * childByAutoId generates a new child location using a unique key and returns a
+ * FIRDatabaseReference to it. This is useful when the children of a Firebase Database
+ * location represent a list of items.
+ *
+ * The unique key generated by childByAutoId: is prefixed with a client-generated
+ * timestamp so that the resulting list will be chronologically-sorted.
+ *
+ * @return A FIRDatabaseReference for the generated location.
+ */
+- (FIRDatabaseReference *) childByAutoId;
+
+
+#pragma mark - Writing data
+
+/** Write data to this Firebase Database location.
+
+This will overwrite any data at this location and all child locations.
+
+Data types that can be set are:
+
+- NSString -- @"Hello World"
+- NSNumber (also includes boolean) -- @YES, @43, @4.333
+- NSDictionary -- @{@"key": @"value", @"nested": @{@"another": @"value"} }
+- NSArray
+
+The effect of the write will be visible immediately and the corresponding
+events will be triggered. Synchronization of the data to the Firebase Database
+servers will also be started.
+
+Passing null for the new value is equivalent to calling remove:;
+all data at this location or any child location will be deleted.
+
+Note that setValue: will remove any priority stored at this location, so if priority
+is meant to be preserved, you should use setValue:andPriority: instead.
+
+@param value The value to be written.
+ */
+- (void) setValue:(nullable id)value;
+
+
+/**
+ * The same as setValue: with a block that gets triggered after the write operation has
+ * been committed to the Firebase Database servers.
+ *
+ * @param value The value to be written.
+ * @param block The block to be called after the write has been committed to the Firebase Database servers.
+ */
+- (void) setValue:(nullable id)value withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * The same as setValue: with an additional priority to be attached to the data being written.
+ * Priorities are used to order items.
+ *
+ * @param value The value to be written.
+ * @param priority The priority to be attached to that data.
+ */
+- (void) setValue:(nullable id)value andPriority:(nullable id)priority;
+
+
+/**
+ * The same as setValue:andPriority: with a block that gets triggered after the write operation has
+ * been committed to the Firebase Database servers.
+ *
+ * @param value The value to be written.
+ * @param priority The priority to be attached to that data.
+ * @param block The block to be called after the write has been committed to the Firebase Database servers.
+ */
+- (void) setValue:(nullable id)value andPriority:(nullable id)priority withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Remove the data at this Firebase Database location. Any data at child locations will also be deleted.
+ *
+ * The effect of the delete will be visible immediately and the corresponding events
+ * will be triggered. Synchronization of the delete to the Firebase Database servers will
+ * also be started.
+ *
+ * remove: is equivalent to calling setValue:nil
+ */
+- (void) removeValue;
+
+
+/**
+ * The same as remove: with a block that gets triggered after the remove operation has
+ * been committed to the Firebase Database servers.
+ *
+ * @param block The block to be called after the remove has been committed to the Firebase Database servers.
+ */
+- (void) removeValueWithCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+/**
+ * Sets a priority for the data at this Firebase Database location.
+ * Priorities can be used to provide a custom ordering for the children at a location
+ * (if no priorities are specified, the children are ordered by key).
+ *
+ * You cannot set a priority on an empty location. For this reason
+ * setValue:andPriority: should be used when setting initial data with a specific priority
+ * and setPriority: should be used when updating the priority of existing data.
+ *
+ * Children are sorted based on this priority using the following rules:
+ *
+ * Children with no priority come first.
+ * Children with a number as their priority come next. They are sorted numerically by priority (small to large).
+ * Children with a string as their priority come last. They are sorted lexicographically by priority.
+ * Whenever two children have the same priority (including no priority), they are sorted by key. Numeric
+ * keys come first (sorted numerically), followed by the remaining keys (sorted lexicographically).
+ *
+ * Note that priorities are parsed and ordered as IEEE 754 double-precision floating-point numbers.
+ * Keys are always stored as strings and are treated as numbers only when they can be parsed as a
+ * 32-bit integer
+ *
+ * @param priority The priority to set at the specified location.
+ */
+- (void) setPriority:(nullable id)priority;
+
+
+/**
+ * The same as setPriority: with a block that is called once the priority has
+ * been committed to the Firebase Database servers.
+ *
+ * @param priority The priority to set at the specified location.
+ * @param block The block that is triggered after the priority has been written on the servers.
+ */
+- (void) setPriority:(nullable id)priority withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+/**
+ * Updates the values at the specified paths in the dictionary without overwriting other
+ * keys at this location.
+ *
+ * @param values A dictionary of the keys to change and their new values
+ */
+- (void) updateChildValues:(NSDictionary *)values;
+
+/**
+ * The same as update: with a block that is called once the update has been committed to the
+ * Firebase Database servers
+ *
+ * @param values A dictionary of the keys to change and their new values
+ * @param block The block that is triggered after the update has been written on the Firebase Database servers
+ */
+- (void) updateChildValues:(NSDictionary *)values withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+#pragma mark - Attaching observers to read data
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+#pragma mark - Detaching observers
+
+/**
+ * Detach a block previously attached with observeEventType:withBlock:.
+ *
+ * @param handle The handle returned by the call to observeEventType:withBlock: which we are trying to remove.
+ */
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle;
+
+/**
+ * By calling `keepSynced:YES` on a location, the data for that location will automatically be downloaded and
+ * kept in sync, even when no listeners are attached for that location. Additionally, while a location is kept
+ * synced, it will not be evicted from the persistent disk cache.
+ *
+ * @param keepSynced Pass YES to keep this location synchronized, pass NO to stop synchronization.
+ */
+- (void) keepSynced:(BOOL)keepSynced;
+
+
+/**
+ * Removes all observers at the current reference, but does not remove any observers at child references.
+ * removeAllObservers must be called again for each child reference where a listener was established to remove the observers.
+ */
+- (void) removeAllObservers;
+
+#pragma mark - Querying and limiting
+
+
+/**
+ * queryLimitedToFirst: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryLimitedToFirst: will respond to at most the first limit child nodes.
+ *
+ * @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+ * @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+ */
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit;
+
+
+/**
+ * queryLimitedToLast: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryLimitedToLast: will respond to at most the last limit child nodes.
+ *
+ * @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+ * @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+ */
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit;
+
+/**
+ * queryOrderBy: is used to generate a reference to a view of the data that's been sorted by the values of
+ * a particular child key. This method is intended to be used in combination with queryStartingAtValue:,
+ * queryEndingAtValue:, or queryEqualToValue:.
+ *
+ * @param key The child key to use in ordering data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, ordered by the values of the specified child key.
+ */
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)key;
+
+/**
+ * queryOrderedByKey: is used to generate a reference to a view of the data that's been sorted by child key.
+ * This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child keys.
+ */
+- (FIRDatabaseQuery *) queryOrderedByKey;
+
+/**
+ * queryOrderedByPriority: is used to generate a reference to a view of the data that's been sorted by child
+ * priority. This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child priorities.
+ */
+- (FIRDatabaseQuery *) queryOrderedByPriority;
+
+/**
+ * queryStartingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue: will respond to events at nodes with a value
+ * greater than or equal to startValue.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue;
+
+/**
+ * queryStartingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue:childKey will respond to events at nodes with a value
+ * greater than startValue, or equal to startValue and with a key greater than or equal to childKey.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The lower bound, inclusive, for the key of nodes with value equal to startValue
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEndingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue: will respond to events at nodes with a value
+ * less than or equal to endValue.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue;
+
+/**
+ * queryEndingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue:childKey will respond to events at nodes with a value
+ * less than endValue, or equal to endValue and with a key less than or equal to childKey.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The upper bound, inclusive, for the key of nodes with value equal to endValue
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEqualToValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue: will respond to events at nodes with a value equal
+ * to the supplied argument.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value;
+
+/**
+ * queryEqualToValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue:childKey will respond to events at nodes with a value
+ * equal to the supplied argument with a key equal to childKey. There will be at most one node that matches because
+ * child keys are unique.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @param childKey The key of nodes with the right value
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value and the key.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value childKey:(nullable NSString *)childKey;
+
+#pragma mark - Managing presence
+
+/**
+ * Ensure the data at this location is set to the specified value when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * onDisconnectSetValue: is especially useful for implementing "presence" systems,
+ * where a value should be changed or cleared when a user disconnects
+ * so that he appears "offline" to other users.
+ *
+ * @param value The value to be set after the connection is lost.
+ */
+- (void) onDisconnectSetValue:(nullable id)value;
+
+
+/**
+ * Ensure the data at this location is set to the specified value when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * The completion block will be triggered when the operation has been successfully queued up on the Firebase Database servers
+ *
+ * @param value The value to be set after the connection is lost.
+ * @param block Block to be triggered when the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectSetValue:(nullable id)value withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Ensure the data at this location is set to the specified value and priority when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * @param value The value to be set after the connection is lost.
+ * @param priority The priority to be set after the connection is lost.
+ */
+- (void) onDisconnectSetValue:(nullable id)value andPriority:(id)priority;
+
+
+/**
+ * Ensure the data at this location is set to the specified value and priority when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * The completion block will be triggered when the operation has been successfully queued up on the Firebase Database servers
+ *
+ * @param value The value to be set after the connection is lost.
+ * @param priority The priority to be set after the connection is lost.
+ * @param block Block to be triggered when the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectSetValue:(nullable id)value andPriority:(nullable id)priority withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Ensure the data at this location is removed when
+ * the client is disconnected (due to closing the app, navigating
+ * to a new page, or network issues).
+ *
+ * onDisconnectRemoveValue is especially useful for implementing "presence" systems.
+ */
+- (void) onDisconnectRemoveValue;
+
+
+/**
+ * Ensure the data at this location is removed when
+ * the client is disconnected (due to closing the app, navigating
+ * to a new page, or network issues).
+ *
+ * onDisconnectRemoveValueWithCompletionBlock: is especially useful for implementing "presence" systems.
+ *
+ * @param block Block to be triggered when the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectRemoveValueWithCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+
+/**
+ * Ensure the data has the specified child values updated when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ *
+ * @param values A dictionary of child node keys and the values to set them to after the connection is lost.
+ */
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values;
+
+
+/**
+ * Ensure the data has the specified child values updated when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ *
+ * @param values A dictionary of child node keys and the values to set them to after the connection is lost.
+ * @param block A block that will be called once the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Cancel any operations that are set to run on disconnect. If you previously called onDisconnectSetValue:,
+ * onDisconnectRemoveValue:, or onDisconnectUpdateChildValues:, and no longer want the values updated when the
+ * connection is lost, call cancelDisconnectOperations:
+ */
+- (void) cancelDisconnectOperations;
+
+
+/**
+ * Cancel any operations that are set to run on disconnect. If you previously called onDisconnectSetValue:,
+ * onDisconnectRemoveValue:, or onDisconnectUpdateChildValues:, and no longer want the values updated when the
+ * connection is lost, call cancelDisconnectOperations:
+ *
+ * @param block A block that will be triggered once the Firebase Database servers have acknowledged the cancel request.
+ */
+- (void) cancelDisconnectOperationsWithCompletionBlock:(nullable void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+#pragma mark - Manual Connection Management
+
+/**
+ * Manually disconnect the Firebase Database client from the server and disable automatic reconnection.
+ *
+ * The Firebase Database client automatically maintains a persistent connection to the Firebase Database server,
+ * which will remain active indefinitely and reconnect when disconnected. However, the goOffline( )
+ * and goOnline( ) methods may be used to manually control the client connection in cases where
+ * a persistent connection is undesirable.
+ *
+ * While offline, the Firebase Database client will no longer receive data updates from the server. However,
+ * all database operations performed locally will continue to immediately fire events, allowing
+ * your application to continue behaving normally. Additionally, each operation performed locally
+ * will automatically be queued and retried upon reconnection to the Firebase Database server.
+ *
+ * To reconnect to the Firebase Database server and begin receiving remote events, see goOnline( ).
+ * Once the connection is reestablished, the Firebase Database client will transmit the appropriate data
+ * and fire the appropriate events so that your client "catches up" automatically.
+ *
+ * Note: Invoking this method will impact all Firebase Database connections.
+ */
++ (void) goOffline;
+
+/**
+ * Manually reestablish a connection to the Firebase Database server and enable automatic reconnection.
+ *
+ * The Firebase Database client automatically maintains a persistent connection to the Firebase Database server,
+ * which will remain active indefinitely and reconnect when disconnected. However, the goOffline( )
+ * and goOnline( ) methods may be used to manually control the client connection in cases where
+ * a persistent connection is undesirable.
+ *
+ * This method should be used after invoking goOffline( ) to disable the active connection.
+ * Once reconnected, the Firebase Database client will automatically transmit the proper data and fire
+ * the appropriate events so that your client "catches up" automatically.
+ *
+ * To disconnect from the Firebase Database server, see goOffline( ).
+ *
+ * Note: Invoking this method will impact all Firebase Database connections.
+ */
++ (void) goOnline;
+
+
+#pragma mark - Transactions
+
+/**
+ * Performs an optimistic-concurrency transactional update to the data at this location. Your block will be called with a FIRMutableData
+ * instance that contains the current data at this location. Your block should update this data to the value you
+ * wish to write to this location, and then return an instance of FIRTransactionResult with the new data.
+ *
+ * If, when the operation reaches the server, it turns out that this client had stale data, your block will be run
+ * again with the latest data from the server.
+ *
+ * When your block is run, you may decide to abort the transaction by returning [FIRTransactionResult abort].
+ *
+ * @param block This block receives the current data at this location and must return an instance of FIRTransactionResult
+ */
+- (void) runTransactionBlock:(FIRTransactionResult * (^) (FIRMutableData* currentData))block;
+
+
+/**
+ * Performs an optimistic-concurrency transactional update to the data at this location. Your block will be called with a FIRMutableData
+ * instance that contains the current data at this location. Your block should update this data to the value you
+ * wish to write to this location, and then return an instance of FIRTransactionResult with the new data.
+ *
+ * If, when the operation reaches the server, it turns out that this client had stale data, your block will be run
+ * again with the latest data from the server.
+ *
+ * When your block is run, you may decide to abort the transaction by returning [FIRTransactionResult abort].
+ *
+ * @param block This block receives the current data at this location and must return an instance of FIRTransactionResult
+ * @param completionBlock This block will be triggered once the transaction is complete, whether it was successful or not. It will indicate if there was an error, whether or not the data was committed, and what the current value of the data at this location is.
+ */
+- (void)runTransactionBlock:(FIRTransactionResult * (^) (FIRMutableData* currentData))block andCompletionBlock:(void (^) (NSError *__nullable error, BOOL committed, FIRDataSnapshot *__nullable snapshot))completionBlock;
+
+
+
+/**
+ * Performs an optimistic-concurrency transactional update to the data at this location. Your block will be called with a FIRMutableData
+ * instance that contains the current data at this location. Your block should update this data to the value you
+ * wish to write to this location, and then return an instance of FIRTransactionResult with the new data.
+ *
+ * If, when the operation reaches the server, it turns out that this client had stale data, your block will be run
+ * again with the latest data from the server.
+ *
+ * When your block is run, you may decide to abort the transaction by return [FIRTransactionResult abort].
+ *
+ * Since your block may be run multiple times, this client could see several immediate states that don't exist on the server. You can suppress those immediate states until the server confirms the final state of the transaction.
+ *
+ * @param block This block receives the current data at this location and must return an instance of FIRTransactionResult
+ * @param completionBlock This block will be triggered once the transaction is complete, whether it was successful or not. It will indicate if there was an error, whether or not the data was committed, and what the current value of the data at this location is.
+ * @param localEvents Set this to NO to suppress events raised for intermediate states, and only get events based on the final state of the transaction.
+ */
+- (void)runTransactionBlock:(FIRTransactionResult * (^) (FIRMutableData* currentData))block andCompletionBlock:(nullable void (^) (NSError *__nullable error, BOOL committed, FIRDataSnapshot *__nullable snapshot))completionBlock withLocalEvents:(BOOL)localEvents;
+
+
+#pragma mark - Retrieving String Representation
+
+/**
+ * Gets the absolute URL of this Firebase Database location.
+ *
+ * @return The absolute URL of the referenced Firebase Database location.
+ */
+- (NSString *) description;
+
+#pragma mark - Properties
+
+/**
+ * Gets a FIRDatabaseReference for the parent location.
+ * If this instance refers to the root of your Firebase Database, it has no parent,
+ * and therefore parent( ) will return null.
+ *
+ * @return A FIRDatabaseReference for the parent location.
+ */
+@property (strong, readonly, nonatomic, nullable) FIRDatabaseReference * parent;
+
+
+/**
+ * Gets a FIRDatabaseReference for the root location
+ *
+ * @return A new FIRDatabaseReference to root location.
+ */
+@property (strong, readonly, nonatomic) FIRDatabaseReference * root;
+
+
+/**
+ * Gets the last token in a Firebase Database location (e.g. 'fred' in https&#58;//SampleChat.firebaseIO-demo.com/users/fred)
+ *
+ * @return The key of the location this reference points to.
+ */
+@property (strong, readonly, nonatomic) NSString* key;
+
+/**
+ * Gets the URL for the Firebase Database location referenced by this FIRDatabaseReference.
+ *
+ * @return The url of the location this reference points to.
+ */
+@property (strong, readonly, nonatomic) NSString* URL;
+
+/**
+ * Gets the FIRDatabase instance associated with this reference.
+ *
+ * @return The FIRDatabase object for this reference.
+ */
+@property (strong, readonly, nonatomic) FIRDatabase *database;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/FIRDatabaseReference.m b/Firebase/Database/FIRDatabaseReference.m
new file mode 100644
index 0000000..4f27493
--- /dev/null
+++ b/Firebase/Database/FIRDatabaseReference.m
@@ -0,0 +1,404 @@
+/*
+ * 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 "FIRApp.h"
+#import "FIRDatabaseReference.h"
+#import "FIROptions.h"
+#import "FUtilities.h"
+#import "FNextPushId.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FValidation.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FStringUtilities.h"
+#import "FSnapshotUtilities.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FQueryParams.h"
+#import "FIRDatabase.h"
+
+@implementation FIRDatabaseReference
+
++ (FIRDatabaseConfig *)defaultConfig {
+ return [FIRDatabaseConfig defaultConfig];
+}
+
+#pragma mark -
+#pragma mark Constructors
+
+- (id) initWithConfig:(FIRDatabaseConfig *)config {
+ FParsedUrl* parsedUrl = [FUtilities parseUrl:[[FIRApp defaultApp] options].databaseURL];
+ [FValidation validateFrom:@"initWithUrl:" validURL:parsedUrl];
+ return [self initWithRepo:[FRepoManager getRepo:parsedUrl.repoInfo config:config] path:parsedUrl.path];
+}
+
+- (id) initWithRepo:(FRepo *)repo path:(FPath *)path {
+ return [super initWithRepo:repo
+ path:path
+ params:[FQueryParams defaultInstance]
+ orderByCalled:NO
+ priorityMethodCalled:NO];
+}
+
+
+#pragma mark -
+#pragma mark Ancillary methods
+
+- (NSString *) key {
+ if([self.path isEmpty]) {
+ return nil;
+ }
+ else {
+ return [self.path getBack];
+ }
+}
+
+- (FIRDatabase *) database {
+ return self.repo.database;
+}
+
+- (FIRDatabaseReference *) parent {
+ FPath* parentPath = [self.path parent];
+ FIRDatabaseReference * parent = nil;
+ if (parentPath != nil ) {
+ parent = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:parentPath];
+ }
+ return parent;
+}
+
+- (NSString *) URL {
+ FIRDatabaseReference * parent = [self parent];
+ return parent == nil ? [self.repo description] : [NSString stringWithFormat:@"%@/%@", [parent description], [FStringUtilities urlEncoded:self.key]];
+}
+
+- (NSString *) description {
+ return [self URL];
+}
+
+- (FIRDatabaseReference *) root {
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:[[FPath alloc] initWith:@""]];
+}
+
+#pragma mark -
+#pragma mark Child methods
+
+- (FIRDatabaseReference *)childByAppendingPath:(NSString *)pathString {
+ return [self child:pathString];
+}
+
+- (FIRDatabaseReference *)child:(NSString *)pathString {
+ if ([self.path getFront] == nil) {
+ // we're at the root
+ [FValidation validateFrom:@"child:" validRootPathString:pathString];
+ } else {
+ [FValidation validateFrom:@"child:" validPathString:pathString];
+ }
+ FPath* path = [self.path childFromString:pathString];
+ FIRDatabaseReference * firebaseRef = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:path];
+ return firebaseRef;
+}
+
+- (FIRDatabaseReference *) childByAutoId {
+ [FValidation validateFrom:@"childByAutoId:" writablePath:self.path];
+
+ NSString* name = [FNextPushId get:self.repo.serverTime];
+ return [self child:name];
+}
+
+#pragma mark -
+#pragma mark Basic write methods
+
+- (void) setValue:(id)value {
+ [self setValueInternal:value andPriority:nil withCompletionBlock:nil from:@"setValue:"];
+}
+
+- (void) setValue:(id)value withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self setValueInternal:value andPriority:nil withCompletionBlock:block from:@"setValue:withCompletionBlock:"];
+}
+
+- (void) setValue:(id)value andPriority:(id)priority {
+ [self setValueInternal:value andPriority:priority withCompletionBlock:nil from:@"setValue:andPriority:"];
+}
+
+- (void) setValue:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self setValueInternal:value andPriority:priority withCompletionBlock:block from:@"setValue:andPriority:withCompletionBlock:"];
+}
+
+- (void) setValueInternal:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:value priority:priority withValidationFrom:fn];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo set:self.path withNode:newNode withCallback:userCallback];
+ });
+}
+
+
+- (void) removeValue {
+ [self setValueInternal:nil andPriority:nil withCompletionBlock:nil from:@"removeValue:"];
+}
+
+- (void) removeValueWithCompletionBlock:(fbt_void_nserror_ref)block {
+ [self setValueInternal:nil andPriority:nil withCompletionBlock:block from:@"removeValueWithCompletionBlock:"];
+}
+
+
+- (void) setPriority:(id)priority {
+ [self setPriorityInternal:priority withCompletionBlock:nil from:@"setPriority:"];
+}
+
+- (void) setPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block {
+
+ [self setPriorityInternal:priority withCompletionBlock:block from:@"setPriority:withCompletionBlock:"];
+}
+
+- (void) setPriorityInternal:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo set:[self.path childFromString:@".priority"] withNode:[FSnapshotUtilities nodeFrom:priority] withCallback:userCallback];
+ });
+}
+
+
+- (void) updateChildValues:(NSDictionary *)values {
+ [self updateChildValuesInternal:values withCompletionBlock:nil from:@"updateChildValues:"];
+}
+
+- (void) updateChildValues:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self updateChildValuesInternal:values withCompletionBlock:block from:@"updateChildValues:withCompletionBlock:"];
+}
+
+- (void) updateChildValuesInternal:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ FCompoundWrite *merge = [FSnapshotUtilities compoundWriteFromDictionary:values withValidationFrom:fn];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo update:self.path withNodes:merge withCallback:userCallback];
+ });
+}
+
+#pragma mark -
+#pragma mark Disconnect Operations
+
+- (void) onDisconnectSetValue:(id)value {
+ [self onDisconnectSetValueInternal:value andPriority:nil withCompletionBlock:nil from:@"onDisconnectSetValue:"];
+}
+
+- (void) onDisconnectSetValue:(id)value withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectSetValueInternal:value andPriority:nil withCompletionBlock:block from:@"onDisconnectSetValue:withCompletionBlock:"];
+}
+
+- (void) onDisconnectSetValue:(id)value andPriority:(id)priority {
+ [self onDisconnectSetValueInternal:value andPriority:priority withCompletionBlock:nil from:@"onDisconnectSetValue:andPriority:"];
+}
+
+- (void) onDisconnectSetValue:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectSetValueInternal:value andPriority:priority withCompletionBlock:block from:@"onDisconnectSetValue:andPriority:withCompletionBlock:"];
+}
+
+- (void) onDisconnectSetValueInternal:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ id<FNode> newNodeUnresolved = [FSnapshotUtilities nodeFrom:value priority:priority withValidationFrom:fn];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo onDisconnectSet:self.path withNode:newNodeUnresolved withCallback:userCallback];
+ });
+}
+
+
+- (void) onDisconnectRemoveValue {
+ [self onDisconnectSetValueInternal:nil andPriority:nil withCompletionBlock:nil from:@"onDisconnectRemoveValue:"];
+}
+
+- (void) onDisconnectRemoveValueWithCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectSetValueInternal:nil andPriority:nil withCompletionBlock:block from:@"onDisconnectRemoveValueWithCompletionBlock:"];
+}
+
+
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values {
+ [self onDisconnectUpdateChildValuesInternal:values withCompletionBlock:nil from:@"onDisconnectUpdateChildValues:"];
+}
+
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectUpdateChildValuesInternal:values withCompletionBlock:block from:@"onDisconnectUpdateChildValues:withCompletionBlock:"];
+}
+
+- (void) onDisconnectUpdateChildValuesInternal:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ FCompoundWrite *merge = [FSnapshotUtilities compoundWriteFromDictionary:values withValidationFrom:fn];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo onDisconnectUpdate:self.path withNodes:merge withCallback:userCallback];
+ });
+}
+
+
+- (void) cancelDisconnectOperations {
+ [self cancelDisconnectOperationsWithCompletionBlock:nil];
+}
+
+- (void) cancelDisconnectOperationsWithCompletionBlock:(fbt_void_nserror_ref)block {
+ fbt_void_nserror_ref callback = nil;
+ if (block != nil) {
+ callback = [block copy];
+ }
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo onDisconnectCancel:self.path withCallback:callback];
+ });
+}
+
+#pragma mark -
+#pragma mark Connection management methods
+
++ (void) goOffline {
+ [FRepoManager interruptAll];
+}
+
++ (void) goOnline {
+ [FRepoManager resumeAll];
+}
+
+
+#pragma mark -
+#pragma mark Data reading methods deferred to FQuery
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {
+ return [self observeEventType:eventType withBlock:block withCancelBlock:nil];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+ return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ return [super observeEventType:eventType withBlock:block withCancelBlock:cancelBlock];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ return [super observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:cancelBlock];
+}
+
+
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle {
+ [super removeObserverWithHandle:handle];
+}
+
+
+- (void) removeAllObservers {
+ [super removeAllObservers];
+}
+
+- (void) keepSynced:(BOOL)keepSynced {
+ [super keepSynced:keepSynced];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {
+ [self observeSingleEventOfType:eventType withBlock:block withCancelBlock:nil];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+ [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [super observeSingleEventOfType:eventType withBlock:block withCancelBlock:cancelBlock];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [super observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:cancelBlock];
+}
+
+#pragma mark -
+#pragma mark Query methods
+// These methods suppress warnings from having method definitions in FIRDatabaseReference.h for docs generation.
+
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit {
+ return [super queryLimitedToFirst:limit];
+}
+
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit {
+ return [super queryLimitedToLast:limit];
+}
+
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)key {
+ return [super queryOrderedByChild:key];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByKey {
+ return [super queryOrderedByKey];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByPriority {
+ return [super queryOrderedByPriority];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue {
+ return [super queryStartingAtValue:startValue];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue childKey:(NSString *)childKey {
+ return [super queryStartingAtValue:startValue childKey:childKey];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue {
+ return [super queryEndingAtValue:endValue];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue childKey:(NSString *)childKey {
+ return [super queryEndingAtValue:endValue childKey:childKey];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value {
+ return [super queryEqualToValue:value];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value childKey:(NSString *)childKey {
+ return [super queryEqualToValue:value childKey:childKey];
+}
+
+
+#pragma mark -
+#pragma mark Transaction methods
+
+- (void) runTransactionBlock:(fbt_transactionresult_mutabledata)block {
+ [FValidation validateFrom:@"runTransactionBlock:" writablePath:self.path];
+ [self runTransactionBlock:block andCompletionBlock:nil withLocalEvents:YES];
+}
+
+- (void) runTransactionBlock:(fbt_transactionresult_mutabledata)update andCompletionBlock:(fbt_void_nserror_bool_datasnapshot)completionBlock {
+ [FValidation validateFrom:@"runTransactionBlock:andCompletionBlock:" writablePath:self.path];
+ [self runTransactionBlock:update andCompletionBlock:completionBlock withLocalEvents:YES];
+}
+
+- (void) runTransactionBlock:(fbt_transactionresult_mutabledata)block andCompletionBlock:(fbt_void_nserror_bool_datasnapshot)completionBlock withLocalEvents:(BOOL)localEvents {
+ [FValidation validateFrom:@"runTransactionBlock:andCompletionBlock:withLocalEvents:" writablePath:self.path];
+ fbt_transactionresult_mutabledata updateCopy = [block copy];
+ fbt_void_nserror_bool_datasnapshot onCompleteCopy = [completionBlock copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo startTransactionOnPath:self.path update:updateCopy onComplete:onCompleteCopy withLocalEvents:localEvents];
+ });
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..ef72cf0
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj
@@ -0,0 +1,438 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ EDB1C0A11653283D0041897E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0A01653283D0041897E /* Foundation.framework */; };
+ EDB1C0B01653283D0041897E /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0AF1653283D0041897E /* SenTestingKit.framework */; };
+ EDB1C0B21653283D0041897E /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0B11653283D0041897E /* UIKit.framework */; };
+ EDB1C0B31653283D0041897E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0A01653283D0041897E /* Foundation.framework */; };
+ EDB1C0B61653283D0041897E /* libFImmutableSortedDictionary.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */; };
+ EDB1C0BC1653283D0041897E /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = EDB1C0BA1653283D0041897E /* InfoPlist.strings */; };
+ EDB1C0BF1653283D0041897E /* FImmutableSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0BE1653283D0041897E /* FImmutableSortedDictionaryTests.m */; };
+ EDB1C0D21653286B0041897E /* FImmutableSortedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */; };
+ EDB1C0D31653286B0041897E /* FImmutableSortedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */; };
+ EDB1C0D41653286B0041897E /* FLLRBEmptyNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */; };
+ EDB1C0D51653286B0041897E /* FLLRBEmptyNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */; };
+ EDB1C0D61653286B0041897E /* FLLRBValueNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0D11653286B0041897E /* FLLRBValueNode.m */; };
+ EDB1C0D71653286B0041897E /* FLLRBValueNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0D11653286B0041897E /* FLLRBValueNode.m */; };
+ EDB1C0ED165331140041897E /* FImmutableSortedDictionaryEnumerator.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0EC165331140041897E /* FImmutableSortedDictionaryEnumerator.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ EDB1C0B41653283D0041897E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = EDB1C0941653283D0041897E /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = EDB1C09C1653283D0041897E;
+ remoteInfo = FImmutableSortedDictionary;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ EDB1C09B1653283D0041897E /* CopyFiles */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "include/${PRODUCT_NAME}";
+ dstSubfolderSpec = 16;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libFImmutableSortedDictionary.a; sourceTree = BUILT_PRODUCTS_DIR; };
+ EDB1C0A01653283D0041897E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+ EDB1C0A41653283D0041897E /* FImmutableSortedDictionary-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FImmutableSortedDictionary-Prefix.pch"; sourceTree = "<group>"; };
+ EDB1C0AE1653283D0041897E /* FImmutableSortedDictionaryTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FImmutableSortedDictionaryTests.octest; sourceTree = BUILT_PRODUCTS_DIR; };
+ EDB1C0AF1653283D0041897E /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; };
+ EDB1C0B11653283D0041897E /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
+ EDB1C0B91653283D0041897E /* FImmutableSortedDictionaryTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "FImmutableSortedDictionaryTests-Info.plist"; sourceTree = "<group>"; };
+ EDB1C0BB1653283D0041897E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+ EDB1C0BD1653283D0041897E /* FImmutableSortedDictionaryTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FImmutableSortedDictionaryTests.h; sourceTree = "<group>"; };
+ EDB1C0BE1653283D0041897E /* FImmutableSortedDictionaryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FImmutableSortedDictionaryTests.m; sourceTree = "<group>"; };
+ EDB1C0CB1653286B0041897E /* FImmutableSortedDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FImmutableSortedDictionary.h; sourceTree = "<group>"; };
+ EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FImmutableSortedDictionary.m; sourceTree = "<group>"; };
+ EDB1C0CD1653286B0041897E /* FLLRBNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLLRBNode.h; sourceTree = "<group>"; };
+ EDB1C0CE1653286B0041897E /* FLLRBEmptyNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLLRBEmptyNode.h; sourceTree = "<group>"; };
+ EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLLRBEmptyNode.m; sourceTree = "<group>"; };
+ EDB1C0D01653286B0041897E /* FLLRBValueNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLLRBValueNode.h; sourceTree = "<group>"; };
+ EDB1C0D11653286B0041897E /* FLLRBValueNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLLRBValueNode.m; sourceTree = "<group>"; };
+ EDB1C0EB165331140041897E /* FImmutableSortedDictionaryEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FImmutableSortedDictionaryEnumerator.h; sourceTree = "<group>"; };
+ EDB1C0EC165331140041897E /* FImmutableSortedDictionaryEnumerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FImmutableSortedDictionaryEnumerator.m; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ EDB1C09A1653283D0041897E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0A11653283D0041897E /* Foundation.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ EDB1C0AA1653283D0041897E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0B01653283D0041897E /* SenTestingKit.framework in Frameworks */,
+ EDB1C0B21653283D0041897E /* UIKit.framework in Frameworks */,
+ EDB1C0B31653283D0041897E /* Foundation.framework in Frameworks */,
+ EDB1C0B61653283D0041897E /* libFImmutableSortedDictionary.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ EDB1C0921653283D0041897E = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0A21653283D0041897E /* FImmutableSortedDictionary */,
+ EDB1C0B71653283D0041897E /* FImmutableSortedDictionaryTests */,
+ EDB1C09F1653283D0041897E /* Frameworks */,
+ EDB1C09E1653283D0041897E /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ EDB1C09E1653283D0041897E /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */,
+ EDB1C0AE1653283D0041897E /* FImmutableSortedDictionaryTests.octest */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ EDB1C09F1653283D0041897E /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0A01653283D0041897E /* Foundation.framework */,
+ EDB1C0AF1653283D0041897E /* SenTestingKit.framework */,
+ EDB1C0B11653283D0041897E /* UIKit.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ EDB1C0A21653283D0041897E /* FImmutableSortedDictionary */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0CB1653286B0041897E /* FImmutableSortedDictionary.h */,
+ EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */,
+ EDB1C0CD1653286B0041897E /* FLLRBNode.h */,
+ EDB1C0CE1653286B0041897E /* FLLRBEmptyNode.h */,
+ EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */,
+ EDB1C0D01653286B0041897E /* FLLRBValueNode.h */,
+ EDB1C0D11653286B0041897E /* FLLRBValueNode.m */,
+ EDB1C0EB165331140041897E /* FImmutableSortedDictionaryEnumerator.h */,
+ EDB1C0EC165331140041897E /* FImmutableSortedDictionaryEnumerator.m */,
+ EDB1C0A31653283D0041897E /* Supporting Files */,
+ );
+ path = FImmutableSortedDictionary;
+ sourceTree = "<group>";
+ };
+ EDB1C0A31653283D0041897E /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0A41653283D0041897E /* FImmutableSortedDictionary-Prefix.pch */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+ EDB1C0B71653283D0041897E /* FImmutableSortedDictionaryTests */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0BD1653283D0041897E /* FImmutableSortedDictionaryTests.h */,
+ EDB1C0BE1653283D0041897E /* FImmutableSortedDictionaryTests.m */,
+ EDB1C0B81653283D0041897E /* Supporting Files */,
+ );
+ path = FImmutableSortedDictionaryTests;
+ sourceTree = "<group>";
+ };
+ EDB1C0B81653283D0041897E /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0B91653283D0041897E /* FImmutableSortedDictionaryTests-Info.plist */,
+ EDB1C0BA1653283D0041897E /* InfoPlist.strings */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ EDB1C09C1653283D0041897E /* FImmutableSortedDictionary */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = EDB1C0C21653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionary" */;
+ buildPhases = (
+ EDB1C0991653283D0041897E /* Sources */,
+ EDB1C09A1653283D0041897E /* Frameworks */,
+ EDB1C09B1653283D0041897E /* CopyFiles */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = FImmutableSortedDictionary;
+ productName = FImmutableSortedDictionary;
+ productReference = EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */;
+ productType = "com.apple.product-type.library.static";
+ };
+ EDB1C0AD1653283D0041897E /* FImmutableSortedDictionaryTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = EDB1C0C51653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionaryTests" */;
+ buildPhases = (
+ EDB1C0A91653283D0041897E /* Sources */,
+ EDB1C0AA1653283D0041897E /* Frameworks */,
+ EDB1C0AB1653283D0041897E /* Resources */,
+ EDB1C0AC1653283D0041897E /* ShellScript */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ EDB1C0B51653283D0041897E /* PBXTargetDependency */,
+ );
+ name = FImmutableSortedDictionaryTests;
+ productName = FImmutableSortedDictionaryTests;
+ productReference = EDB1C0AE1653283D0041897E /* FImmutableSortedDictionaryTests.octest */;
+ productType = "com.apple.product-type.bundle";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ EDB1C0941653283D0041897E /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0450;
+ ORGANIZATIONNAME = Firebase;
+ };
+ buildConfigurationList = EDB1C0971653283D0041897E /* Build configuration list for PBXProject "FImmutableSortedDictionary" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ );
+ mainGroup = EDB1C0921653283D0041897E;
+ productRefGroup = EDB1C09E1653283D0041897E /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ EDB1C09C1653283D0041897E /* FImmutableSortedDictionary */,
+ EDB1C0AD1653283D0041897E /* FImmutableSortedDictionaryTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ EDB1C0AB1653283D0041897E /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0BC1653283D0041897E /* InfoPlist.strings in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ EDB1C0AC1653283D0041897E /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ EDB1C0991653283D0041897E /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0D21653286B0041897E /* FImmutableSortedDictionary.m in Sources */,
+ EDB1C0D41653286B0041897E /* FLLRBEmptyNode.m in Sources */,
+ EDB1C0D61653286B0041897E /* FLLRBValueNode.m in Sources */,
+ EDB1C0ED165331140041897E /* FImmutableSortedDictionaryEnumerator.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ EDB1C0A91653283D0041897E /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0BF1653283D0041897E /* FImmutableSortedDictionaryTests.m in Sources */,
+ EDB1C0D31653286B0041897E /* FImmutableSortedDictionary.m in Sources */,
+ EDB1C0D51653286B0041897E /* FLLRBEmptyNode.m in Sources */,
+ EDB1C0D71653286B0041897E /* FLLRBValueNode.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ EDB1C0B51653283D0041897E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = EDB1C09C1653283D0041897E /* FImmutableSortedDictionary */;
+ targetProxy = EDB1C0B41653283D0041897E /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ EDB1C0BA1653283D0041897E /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ EDB1C0BB1653283D0041897E /* en */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ EDB1C0C01653283D0041897E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 6.0;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ EDB1C0C11653283D0041897E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 6.0;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ EDB1C0C31653283D0041897E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DSTROOT = /tmp/FImmutableSortedDictionary.dst;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ };
+ name = Debug;
+ };
+ EDB1C0C41653283D0041897E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DSTROOT = /tmp/FImmutableSortedDictionary.dst;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ };
+ name = Release;
+ };
+ EDB1C0C61653283D0041897E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ FRAMEWORK_SEARCH_PATHS = (
+ "\"$(SDKROOT)/Developer/Library/Frameworks\"",
+ "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ INFOPLIST_FILE = "FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = octest;
+ };
+ name = Debug;
+ };
+ EDB1C0C71653283D0041897E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ FRAMEWORK_SEARCH_PATHS = (
+ "\"$(SDKROOT)/Developer/Library/Frameworks\"",
+ "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ INFOPLIST_FILE = "FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = octest;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ EDB1C0971653283D0041897E /* Build configuration list for PBXProject "FImmutableSortedDictionary" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ EDB1C0C01653283D0041897E /* Debug */,
+ EDB1C0C11653283D0041897E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ EDB1C0C21653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionary" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ EDB1C0C31653283D0041897E /* Debug */,
+ EDB1C0C41653283D0041897E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ EDB1C0C51653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionaryTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ EDB1C0C61653283D0041897E /* Debug */,
+ EDB1C0C71653283D0041897E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = EDB1C0941653283D0041897E /* Project object */;
+}
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.h
new file mode 100644
index 0000000..0c6c989
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.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 <Foundation/Foundation.h>
+#import "FImmutableSortedDictionary.h"
+
+/**
+ * This is an array backed implementation of FImmutableSortedDictionary. It uses arrays and linear lookups to achieve
+ * good memory efficiency while maintaining good performance for small collections. It also uses less allocations than
+ * a comparable red black tree. To avoid degrading performance with increasing collection size it will automatically
+ * convert to a FTreeSortedDictionary after an insert call above a certain threshold.
+ */
+@interface FArraySortedDictionary : FImmutableSortedDictionary
+
++ (FArraySortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator;
+
+- (id)initWithComparator:(NSComparator)comparator;
+
+#pragma mark -
+#pragma mark Properties
+
+@property (nonatomic, copy, readonly) NSComparator comparator;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m
new file mode 100644
index 0000000..f572b6b
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m
@@ -0,0 +1,282 @@
+/*
+ * 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 "FArraySortedDictionary.h"
+#import "FTreeSortedDictionary.h"
+
+@interface FArraySortedDictionaryEnumerator : NSEnumerator
+
+- (id)initWithKeys:(NSArray *)keys startPos:(NSInteger)pos isReverse:(BOOL)reverse;
+- (id)nextObject;
+
+@property (nonatomic) NSInteger pos;
+@property (nonatomic) BOOL reverse;
+@property (nonatomic, strong) NSArray *keys;
+
+@end
+
+@implementation FArraySortedDictionaryEnumerator
+
+- (id)initWithKeys:(NSArray *)keys startPos:(NSInteger)pos isReverse:(BOOL)reverse
+{
+ self = [super init];
+ if (self != nil) {
+ self->_pos = pos;
+ self->_reverse = reverse;
+ self->_keys = keys;
+ }
+ return self;
+}
+
+- (id)nextObject
+{
+ NSInteger pos = self->_pos;
+ if (pos >= 0 && pos < self.keys.count) {
+ if (self.reverse) {
+ self->_pos--;
+ } else {
+ self->_pos++;
+ }
+ return self.keys[pos];
+ } else {
+ return nil;
+ }
+}
+
+@end
+
+@interface FArraySortedDictionary ()
+
+- (id)initWithComparator:(NSComparator)comparator;
+
+@property (nonatomic, copy, readwrite) NSComparator comparator;
+@property (nonatomic, strong) NSArray *keys;
+@property (nonatomic, strong) NSArray *values;
+
+@end
+
+@implementation FArraySortedDictionary
+
++ (FArraySortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator
+{
+ NSMutableArray *keys = [NSMutableArray arrayWithCapacity:dictionary.count];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ [keys addObject:key];
+ }];
+ [keys sortUsingComparator:comparator];
+
+ [keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+ if (idx > 0) {
+ if (comparator(keys[idx - 1], obj) != NSOrderedAscending) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create FImmutableSortedDictionary with keys with same ordering!"];
+ }
+ }
+ }];
+
+ NSMutableArray *values = [NSMutableArray arrayWithCapacity:keys.count];
+ NSInteger pos = 0;
+ for (id key in keys) {
+ values[pos++] = dictionary[key];
+ }
+ NSAssert(values.count == keys.count, @"We added as many keys as values");
+ return [[FArraySortedDictionary alloc] initWithComparator:comparator keys:keys values:values];
+}
+
+- (id)initWithComparator:(NSComparator)comparator
+{
+ self = [super init];
+ if (self != nil) {
+ self->_comparator = comparator;
+ self->_keys = [NSArray array];
+ self->_values = [NSArray array];
+ }
+ return self;
+}
+
+- (id)initWithComparator:(NSComparator)comparator keys:(NSArray *)keys values:(NSArray *)values
+{
+ self = [super init];
+ if (self != nil) {
+ self->_comparator = comparator;
+ self->_keys = keys;
+ self->_values = values;
+ }
+ return self;
+}
+
+- (NSInteger) findInsertPositionForKey:(id)key
+{
+ NSInteger newPos = 0;
+ while (newPos < self.keys.count && self.comparator(self.keys[newPos], key) < NSOrderedSame) {
+ newPos++;
+ }
+ return newPos;
+}
+
+- (NSInteger) findKey:(id)key
+{
+ if (key == nil) {
+ return NSNotFound;
+ }
+ for (NSInteger pos = 0; pos < self.keys.count; pos++) {
+ NSComparisonResult result = self.comparator(key, self.keys[pos]);
+ if (result == NSOrderedSame) {
+ return pos;
+ } else if (result == NSOrderedAscending) {
+ return NSNotFound;
+ }
+ }
+ return NSNotFound;
+}
+
+- (FImmutableSortedDictionary *) insertKey:(id)key withValue:(id)value
+{
+ NSInteger pos = [self findKey:key];
+
+ if (pos == NSNotFound) {
+ /*
+ * If we're above the threshold we want to convert it to a tree backed implementation to not have
+ * degrading performance
+ */
+ if (self.count >= SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD) {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:self.count];
+ for (NSInteger i = 0; i < self.keys.count; i++) {
+ dict[self.keys[i]] = self.values[i];
+ }
+ dict[key] = value;
+ return [FTreeSortedDictionary fromDictionary:dict withComparator:self.comparator];
+ } else {
+ NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys];
+ NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values];
+ NSInteger newPos = [self findInsertPositionForKey:key];
+ [newKeys insertObject:key atIndex:newPos];
+ [newValues insertObject:value atIndex:newPos];
+ return [[FArraySortedDictionary alloc] initWithComparator:self.comparator keys:newKeys values:newValues];
+ }
+ } else {
+ NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys];
+ NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values];
+ newKeys[pos] = key;
+ newValues[pos] = value;
+ return [[FArraySortedDictionary alloc] initWithComparator:self.comparator keys:newKeys values:newValues];
+ }
+}
+
+- (FImmutableSortedDictionary *) removeKey:(id)key
+{
+ NSInteger pos = [self findKey:key];
+ if (pos == NSNotFound) {
+ return self;
+ } else {
+ NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys];
+ NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values];
+ [newKeys removeObjectAtIndex:pos];
+ [newValues removeObjectAtIndex:pos];
+ return [[FArraySortedDictionary alloc] initWithComparator:self.comparator keys:newKeys values:newValues];
+ }
+}
+
+- (id) get:(id)key
+{
+ NSInteger pos = [self findKey:key];
+ if (pos == NSNotFound) {
+ return nil;
+ } else {
+ return self.values[pos];
+ }
+}
+
+- (id) getPredecessorKey:(id) key {
+ NSInteger pos = [self findKey:key];
+ if (pos == NSNotFound) {
+ [NSException raise:NSInternalInconsistencyException format:@"Can't get predecessor key for non-existent key"];
+ return nil;
+ } else if (pos == 0) {
+ return nil;
+ } else {
+ return self.keys[pos - 1];
+ }
+}
+
+- (BOOL) isEmpty {
+ return self.keys.count == 0;
+}
+
+- (int) count
+{
+ return (int)self.keys.count;
+}
+
+- (id) minKey
+{
+ return [self.keys firstObject];
+}
+
+- (id) maxKey
+{
+ return [self.keys lastObject];
+}
+
+- (void) enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block
+{
+ [self enumerateKeysAndObjectsReverse:NO usingBlock:block];
+}
+
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block
+{
+ if (reverse) {
+ BOOL stop = NO;
+ for (NSInteger i = self.keys.count - 1; i >= 0; i--) {
+ block(self.keys[i], self.values[i], &stop);
+ if (stop) return;
+ }
+ } else {
+ BOOL stop = NO;
+ for (NSInteger i = 0; i < self.keys.count; i++) {
+ block(self.keys[i], self.values[i], &stop);
+ if (stop) return;
+ }
+ }
+}
+
+- (BOOL) contains:(id)key {
+ return [self findKey:key] != NSNotFound;
+}
+
+- (NSEnumerator *) keyEnumerator {
+ return [self.keys objectEnumerator];
+}
+
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey {
+ NSInteger startPos = [self findInsertPositionForKey:startKey];
+ return [[FArraySortedDictionaryEnumerator alloc] initWithKeys:self.keys startPos:startPos isReverse:NO];
+}
+
+- (NSEnumerator *) reverseKeyEnumerator {
+ return [self.keys reverseObjectEnumerator];
+}
+
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey {
+ NSInteger startPos = [self findInsertPositionForKey:startKey];
+ // if there's no exact match, findKeyOrInsertPosition will return the index *after* the closest match, but
+ // since this is a reverse iterator, we want to start just *before* the closest match.
+ if (startPos >= self.keys.count || self.comparator(self.keys[startPos], startKey) != NSOrderedSame) {
+ startPos -= 1;
+ }
+ return [[FArraySortedDictionaryEnumerator alloc] initWithKeys:self.keys startPos:startPos isReverse:YES];
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch
new file mode 100644
index 0000000..88d2408
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch
@@ -0,0 +1,7 @@
+//
+// Prefix header for all source files of the 'FImmutableSortedDictionary' target in the 'FImmutableSortedDictionary' project
+//
+
+#ifdef __OBJC__
+ #import <Foundation/Foundation.h>
+#endif
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h
new file mode 100644
index 0000000..1e7e5a3
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+
+/**
+ * @fileoverview Implementation of an immutable SortedMap using a Left-leaning
+ * Red-Black Tree, adapted from the implementation in Mugs
+ * (http://mads379.github.com/mugs/) by Mads Hartmann Jensen
+ * (mads379@gmail.com).
+ *
+ * Original paper on Left-leaning Red-Black Trees:
+ * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
+ *
+ * Invariant 1: No red node has a red child
+ * Invariant 2: Every leaf path has the same number of black nodes
+ * Invariant 3: Only the left child can be red (left leaning)
+ */
+
+#import <Foundation/Foundation.h>
+
+/**
+ * The size threshold where we use a tree backed sorted map instead of an array backed sorted map.
+ * This is a more or less arbitrary chosen value, that was chosen to be large enough to fit most of object kind
+ * of Firebase data, but small enough to not notice degradation in performance for inserting and lookups.
+ * Feel free to empirically determine this constant, but don't expect much gain in real world performance.
+ */
+#define SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD 25
+
+@interface FImmutableSortedDictionary : NSObject
+
++ (FImmutableSortedDictionary *)dictionaryWithComparator:(NSComparator)comparator;
++ (FImmutableSortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator;
+
+- (FImmutableSortedDictionary *) insertKey:(id)aKey withValue:(id)aValue;
+- (FImmutableSortedDictionary *) removeKey:(id)aKey;
+- (id) get:(id) key;
+- (id) getPredecessorKey:(id) key;
+- (BOOL) isEmpty;
+- (int) count;
+- (id) minKey;
+- (id) maxKey;
+- (void) enumerateKeysAndObjectsUsingBlock:(void(^)(id key, id value, BOOL *stop))block;
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void(^)(id key, id value, BOOL *stop))block;
+- (BOOL) contains:(id)key;
+- (NSEnumerator *) keyEnumerator;
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey;
+- (NSEnumerator *) reverseKeyEnumerator;
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey;
+
+#pragma mark -
+#pragma mark Methods similar to NSMutableDictionary
+
+- (FImmutableSortedDictionary *) setObject:(id)anObject forKey:(id)aKey;
+- (id) objectForKey:(id)key;
+- (FImmutableSortedDictionary *) removeObjectForKey:(id)aKey;
+
+@end
+
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m
new file mode 100644
index 0000000..006c12d
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m
@@ -0,0 +1,158 @@
+/*
+ * 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 "FImmutableSortedDictionary.h"
+#import "FArraySortedDictionary.h"
+#import "FTreeSortedDictionary.h"
+
+#define THROW_ABSTRACT_METHOD_EXCEPTION(sel) do { \
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException \
+ reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(sel)] \
+ userInfo:nil]; \
+} while(0)
+
+@implementation FImmutableSortedDictionary
+
++ (FImmutableSortedDictionary *)dictionaryWithComparator:(NSComparator)comparator
+{
+ return [[FArraySortedDictionary alloc] initWithComparator:comparator];
+}
+
++ (FImmutableSortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator
+{
+ if (dictionary.count <= SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD) {
+ return [FArraySortedDictionary fromDictionary:dictionary withComparator:comparator];
+ } else {
+ return [FTreeSortedDictionary fromDictionary:dictionary withComparator:comparator];
+ }
+}
+
+- (FImmutableSortedDictionary *) insertKey:(id)aKey withValue:(id)aValue {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(insertKey:withValue:));
+}
+
+- (FImmutableSortedDictionary *) removeKey:(id)aKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(removeKey:));
+}
+
+- (id) get:(id) key {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(get:));
+}
+
+- (id) getPredecessorKey:(id) key {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(getPredecessorKey:));
+}
+
+- (BOOL) isEmpty {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(isEmpty));
+}
+
+- (int) count {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector((count)));
+}
+
+- (id) minKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(minKey));
+}
+
+- (id) maxKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(maxKey));
+}
+
+- (void) enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(enumerateKeysAndObjectsUsingBlock:));
+}
+
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(enumerateKeysAndObjectsReverse:usingBlock:));
+}
+
+- (BOOL) contains:(id)key {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(contains:));
+}
+
+- (NSEnumerator *) keyEnumerator {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(keyEnumerator));
+}
+
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(keyEnumeratorFrom:));
+}
+
+- (NSEnumerator *) reverseKeyEnumerator {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(reverseKeyEnumerator));
+}
+
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(reverseKeyEnumeratorFrom:));
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FImmutableSortedDictionary class]]) {
+ return NO;
+ }
+ FImmutableSortedDictionary *other = (FImmutableSortedDictionary *)object;
+ if (self.count != other.count) {
+ return NO;
+ }
+ __block BOOL isEqual = YES;
+ [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ id otherValue = [other objectForKey:key];
+ isEqual = isEqual && (value == otherValue || [value isEqual:otherValue]);
+ *stop = !isEqual;
+ }];
+ return isEqual;
+}
+
+- (NSUInteger)hash {
+ __block NSUInteger hash = 0;
+ [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ hash = (hash * 31 + [key hash]) * 17 + [value hash];
+ }];
+ return hash;
+}
+
+- (NSString *)description {
+ NSMutableString *str = [[NSMutableString alloc] init];
+ __block BOOL first = YES;
+ [str appendString:@"{ "];
+ [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ if (!first) {
+ [str appendString:@", "];
+ }
+ first = NO;
+ [str appendString:[NSString stringWithFormat:@"%@: %@", key, value]];
+ }];
+ [str appendString:@" }"];
+ return str;
+}
+
+#pragma mark -
+#pragma mark Methods similar to NSMutableDictionary
+
+- (FImmutableSortedDictionary *) setObject:(__unsafe_unretained id)anObject forKey:(__unsafe_unretained id)aKey {
+ return [self insertKey:aKey withValue:anObject];
+}
+
+- (FImmutableSortedDictionary *) removeObjectForKey:(__unsafe_unretained id)aKey {
+ return [self removeKey:aKey];
+}
+
+- (id) objectForKey:(__unsafe_unretained id)key {
+ return [self get:key];
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h
new file mode 100644
index 0000000..bb1a39c
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h
@@ -0,0 +1,38 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@interface FImmutableSortedSet : NSObject
+
++ (FImmutableSortedSet *)setWithKeysFromDictionary:(NSDictionary *)array comparator:(NSComparator)comparator;
+
+- (BOOL)containsObject:(id)object;
+- (FImmutableSortedSet *)addObject:(id)object;
+- (FImmutableSortedSet *)removeObject:(id)object;
+- (id)firstObject;
+- (id)lastObject;
+- (NSUInteger)count;
+- (BOOL)isEmpty;
+
+- (id)predecessorEntry:(id)entry;
+
+- (void)enumerateObjectsUsingBlock:(void (^)(id obj, BOOL *stop))block;
+- (void)enumerateObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id obj, BOOL *stop))block;
+
+- (NSEnumerator *)objectEnumerator;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m
new file mode 100644
index 0000000..09c4164
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m
@@ -0,0 +1,131 @@
+/*
+ * 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 "FImmutableSortedSet.h"
+#import "FImmutableSortedDictionary.h"
+
+@interface FImmutableSortedSet ()
+
+@property (nonatomic, strong) FImmutableSortedDictionary *dictionary;
+
+@end
+
+@implementation FImmutableSortedSet
+
++ (FImmutableSortedSet *)setWithKeysFromDictionary:(NSDictionary *)dictionary comparator:(NSComparator)comparator
+{
+ FImmutableSortedDictionary *setDict = [FImmutableSortedDictionary fromDictionary:dictionary withComparator:comparator];
+ return [[FImmutableSortedSet alloc] initWithDictionary:setDict];
+}
+
+- (id)initWithDictionary:(FImmutableSortedDictionary *)dictionary
+{
+ self = [super init];
+ if (self != nil) {
+ self->_dictionary = dictionary;
+ }
+ return self;
+}
+
+- (BOOL)contains:(id)object
+{
+ return [self.dictionary contains:object];
+}
+
+- (FImmutableSortedSet *)addObject:(id)object
+{
+ FImmutableSortedDictionary *newDictionary = [self.dictionary insertKey:object withValue:[NSNull null]];
+ if (newDictionary != self.dictionary) {
+ return [[FImmutableSortedSet alloc] initWithDictionary:newDictionary];
+ } else {
+ return self;
+ }
+}
+
+- (FImmutableSortedSet *)removeObject:(id)object
+{
+ FImmutableSortedDictionary *newDictionary = [self.dictionary removeObjectForKey:object];
+ if (newDictionary != self.dictionary) {
+ return [[FImmutableSortedSet alloc] initWithDictionary:newDictionary];
+ } else {
+ return self;
+ }
+}
+
+- (BOOL)containsObject:(id)object
+{
+ return [self.dictionary contains:object];
+}
+
+- (id)firstObject
+{
+ return [self.dictionary minKey];
+}
+
+- (id)lastObject
+{
+ return [self.dictionary maxKey];
+}
+
+- (id)predecessorEntry:(id)entry
+{
+ return [self.dictionary getPredecessorKey:entry];
+}
+
+- (NSUInteger)count
+{
+ return [self.dictionary count];
+}
+
+- (BOOL)isEmpty
+{
+ return [self.dictionary isEmpty];
+}
+
+- (void)enumerateObjectsUsingBlock:(void (^)(id, BOOL *))block
+{
+ [self enumerateObjectsReverse:NO usingBlock:block];
+}
+
+- (void)enumerateObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, BOOL *))block
+{
+ [self.dictionary enumerateKeysAndObjectsReverse:reverse usingBlock:^(id key, id value, BOOL *stop) {
+ block(key, stop);
+ }];
+}
+
+- (NSEnumerator *)objectEnumerator
+{
+ return [self.dictionary keyEnumerator];
+}
+
+- (NSString *)description
+{
+ NSMutableString *str = [[NSMutableString alloc] init];
+ __block BOOL first = YES;
+ [str appendString:@"FImmutableSortedSet ( "];
+ [self enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
+ if (!first) {
+ [str appendString:@", "];
+ }
+ first = NO;
+ [str appendString:[NSString stringWithFormat:@"%@", obj]];
+ }];
+ [str appendString:@" )"];
+ return str;
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h
new file mode 100644
index 0000000..3257447
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h
@@ -0,0 +1,43 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FLLRBNode.h"
+
+@interface FLLRBEmptyNode : NSObject <FLLRBNode>
+
++ (id)emptyNode;
+
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight;
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator;
+- (id<FLLRBNode>) remove:(id) aKey withComparator:(NSComparator)aComparator;
+- (int) count;
+- (BOOL) isEmpty;
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action;
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action;
+- (id<FLLRBNode>) min;
+- (id) minKey;
+- (id) maxKey;
+- (BOOL) isRed;
+- (int) check;
+
+@property (nonatomic, strong) id key;
+@property (nonatomic, strong) id value;
+@property (nonatomic, strong) FLLRBColor* color;
+@property (nonatomic, strong) id<FLLRBNode> left;
+@property (nonatomic, strong) id<FLLRBNode> right;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m
new file mode 100644
index 0000000..adbc6ca
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m
@@ -0,0 +1,87 @@
+/*
+ * 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 "FLLRBEmptyNode.h"
+#import "FLLRBValueNode.h"
+
+@implementation FLLRBEmptyNode
+
+@synthesize key, value, color, left, right;
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"[key=%@ val=%@ color=%@]", key, value, (color ? @"true" : @"false")];
+}
+
++ (id)emptyNode
+{
+ static dispatch_once_t pred = 0;
+ __strong static id _sharedObject = nil;
+ dispatch_once(&pred, ^{
+ _sharedObject = [[self alloc] init]; // or some other init method
+ });
+ return _sharedObject;
+}
+
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight {
+ return self;
+}
+
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator {
+ FLLRBValueNode* result = [[FLLRBValueNode alloc] initWithKey:aKey withValue:aValue withColor:nil withLeft:nil withRight:nil];
+ return result;
+}
+
+- (id<FLLRBNode>) remove:(id) key withComparator:(NSComparator)aComparator {
+ return self;
+}
+
+- (int) count {
+ return 0;
+}
+
+- (BOOL) isEmpty {
+ return YES;
+}
+
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action {
+ return NO;
+}
+
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action {
+ return NO;
+}
+
+- (id<FLLRBNode>) min {
+ return self;
+}
+
+- (id) minKey {
+ return nil;
+}
+
+- (id) maxKey {
+ return nil;
+}
+
+- (BOOL) isRed {
+ return NO;
+}
+
+- (int) check {
+ return 0;
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h
new file mode 100644
index 0000000..2634494
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#define RED @true
+#define BLACK @false
+
+typedef NSNumber FLLRBColor;
+
+@protocol FLLRBNode <NSObject>
+
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight;
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator;
+- (id<FLLRBNode>) remove:(id) key withComparator:(NSComparator)aComparator;
+- (int) count;
+- (BOOL) isEmpty;
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action;
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action;
+- (id<FLLRBNode>) min;
+- (id) minKey;
+- (id) maxKey;
+- (BOOL) isRed;
+- (int) check;
+
+@property (nonatomic, strong) id key;
+@property (nonatomic, strong) id value;
+@property (nonatomic, strong) FLLRBColor* color;
+@property (nonatomic, strong) id<FLLRBNode> left;
+@property (nonatomic, strong) id<FLLRBNode> right;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h
new file mode 100644
index 0000000..2c64b8a
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FLLRBNode.h"
+
+@interface FLLRBValueNode : NSObject <FLLRBNode>
+
+
+- (id)initWithKey:(id) key withValue:(id) value withColor:(FLLRBColor*) color withLeft:(id<FLLRBNode>)left withRight:(id<FLLRBNode>)right;
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight;
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator;
+- (id<FLLRBNode>) remove:(id) aKey withComparator:(NSComparator)aComparator;
+- (int) count;
+- (BOOL) isEmpty;
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action;
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action;
+- (id<FLLRBNode>) min;
+- (id) minKey;
+- (id) maxKey;
+- (BOOL) isRed;
+- (int) check;
+
+- (BOOL) checkMaxDepth;
+
+@property (nonatomic, strong) id key;
+@property (nonatomic, strong) id value;
+@property (nonatomic, strong) FLLRBColor* color;
+@property (nonatomic, strong) id<FLLRBNode> left;
+@property (nonatomic, strong) id<FLLRBNode> right;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m
new file mode 100644
index 0000000..f361278
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m
@@ -0,0 +1,245 @@
+/*
+ * 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 "FLLRBValueNode.h"
+#import "FLLRBEmptyNode.h"
+
+@implementation FLLRBValueNode
+
+@synthesize key, value, color, left, right;
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"[key=%@ val=%@ color=%@]", key, value, (color ? @"true" : @"false")];
+}
+
+- (id)initWithKey:(__unsafe_unretained id) aKey withValue:(__unsafe_unretained id) aValue withColor:(__unsafe_unretained FLLRBColor*) aColor withLeft:(__unsafe_unretained id<FLLRBNode>)aLeft withRight:(__unsafe_unretained id<FLLRBNode>)aRight
+{
+ self = [super init];
+ if (self) {
+ self.key = aKey;
+ self.value = aValue;
+ self.color = aColor != nil ? aColor : RED;
+ self.left = aLeft != nil ? aLeft : [FLLRBEmptyNode emptyNode];
+ self.right = aRight != nil ? aRight : [FLLRBEmptyNode emptyNode];
+ }
+ return self;
+}
+
+- (id)copyWith:(__unsafe_unretained id) aKey withValue:(__unsafe_unretained id) aValue withColor:(__unsafe_unretained FLLRBColor*) aColor withLeft:(__unsafe_unretained id<FLLRBNode>)aLeft withRight:(__unsafe_unretained id<FLLRBNode>)aRight {
+ return [[FLLRBValueNode alloc] initWithKey:(aKey != nil) ? aKey : self.key
+ withValue:(aValue != nil) ? aValue : self.value
+ withColor:(aColor != nil) ? aColor : self.color
+ withLeft:(aLeft != nil) ? aLeft : self.left
+ withRight:(aRight != nil) ? aRight : self.right];
+}
+
+- (int) count {
+ return [self.left count] + 1 + [self.right count];
+}
+
+- (BOOL) isEmpty {
+ return NO;
+}
+
+/**
+* Early terminates if aciton returns YES.
+* @return The first truthy value returned by action, or the last falsey value returned by action.
+*/
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action {
+ return [self.left inorderTraversal:action] ||
+ action(self.key, self.value) ||
+ [self.right inorderTraversal:action];
+}
+
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action {
+ return [self.right reverseTraversal:action] ||
+ action(self.key, self.value) ||
+ [self.left reverseTraversal:action];
+}
+
+- (id<FLLRBNode>) min {
+ if([self.left isEmpty]) {
+ return self;
+ }
+ else {
+ return [self.left min];
+ }
+}
+
+- (id) minKey {
+ return [[self min] key];
+}
+
+- (id) maxKey {
+ if([self.right isEmpty]) {
+ return self.key;
+ }
+ else {
+ return [self.right maxKey];
+ }
+}
+
+- (id<FLLRBNode>) insertKey:(__unsafe_unretained id) aKey forValue:(__unsafe_unretained id)aValue withComparator:(NSComparator)aComparator {
+ NSComparisonResult cmp = aComparator(aKey, self.key);
+ FLLRBValueNode* n = self;
+
+ if(cmp == NSOrderedAscending) {
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:[n.left insertKey:aKey forValue:aValue withComparator:aComparator] withRight:nil];
+ }
+ else if(cmp == NSOrderedSame) {
+ n = [n copyWith:nil withValue:aValue withColor:nil withLeft:nil withRight:nil];
+ }
+ else {
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:nil withRight:[n.right insertKey:aKey forValue:aValue withComparator:aComparator]];
+ }
+
+ return [n fixUp];
+}
+
+- (id<FLLRBNode>) removeMin {
+
+ if([self.left isEmpty]) {
+ return [FLLRBEmptyNode emptyNode];
+ }
+
+ FLLRBValueNode* n = self;
+ if(! [n.left isRed] && ! [n.left.left isRed]) {
+ n = [n moveRedLeft];
+ }
+
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:[(FLLRBValueNode*)n.left removeMin] withRight:nil];
+ return [n fixUp];
+}
+
+
+- (id<FLLRBNode>) fixUp {
+ FLLRBValueNode* n = self;
+ if([n.right isRed] && ! [n.left isRed]) n = [n rotateLeft];
+ if([n.left isRed] && [n.left.left isRed]) n = [n rotateRight];
+ if([n.left isRed] && [n.right isRed]) n = [n colorFlip];
+ return n;
+}
+
+- (FLLRBValueNode*) moveRedLeft {
+ FLLRBValueNode* n = [self colorFlip];
+ if([n.right.left isRed]) {
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:nil withRight:[(FLLRBValueNode*)n.right rotateRight]];
+ n = [n rotateLeft];
+ n = [n colorFlip];
+ }
+ return n;
+}
+
+- (FLLRBValueNode*) moveRedRight {
+ FLLRBValueNode* n = [self colorFlip];
+ if([n.left.left isRed]) {
+ n = [n rotateRight];
+ n = [n colorFlip];
+ }
+ return n;
+}
+
+- (id<FLLRBNode>) rotateLeft {
+ id<FLLRBNode> nl = [self copyWith:nil withValue:nil withColor:RED withLeft:nil withRight:self.right.left];
+ return [self.right copyWith:nil withValue:nil withColor:self.color withLeft:nl withRight:nil];;
+}
+
+- (id<FLLRBNode>) rotateRight {
+ id<FLLRBNode> nr = [self copyWith:nil withValue:nil withColor:RED withLeft:self.left.right withRight:nil];
+ return [self.left copyWith:nil withValue:nil withColor:self.color withLeft:nil withRight:nr];
+}
+
+- (id<FLLRBNode>) colorFlip {
+ id<FLLRBNode> nleft = [self.left copyWith:nil withValue:nil withColor:[NSNumber numberWithBool:![self.left.color boolValue]] withLeft:nil withRight:nil];
+ id<FLLRBNode> nright = [self.right copyWith:nil withValue:nil withColor:[NSNumber numberWithBool:![self.right.color boolValue]] withLeft:nil withRight:nil];
+
+ return [self copyWith:nil withValue:nil withColor:[NSNumber numberWithBool:![self.color boolValue]] withLeft:nleft withRight:nright];
+}
+
+- (id<FLLRBNode>) remove:(__unsafe_unretained id) aKey withComparator:(NSComparator)comparator {
+ id<FLLRBNode> smallest;
+ FLLRBValueNode* n = self;
+
+ if(comparator(aKey, n.key) == NSOrderedAscending) {
+ if(![n.left isEmpty] && ![n.left isRed] && ![n.left.left isRed]) {
+ n = [n moveRedLeft];
+ }
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:[n.left remove:aKey withComparator:comparator] withRight:nil];
+ }
+ else {
+ if([n.left isRed]) {
+ n = [n rotateRight];
+ }
+
+ if(![n.right isEmpty] && ![n.right isRed] && ![n.right.left isRed]) {
+ n = [n moveRedRight];
+ }
+
+ if(comparator(aKey, n.key) == NSOrderedSame) {
+ if([n.right isEmpty]) {
+ return [FLLRBEmptyNode emptyNode];
+ }
+ else {
+ smallest = [n.right min];
+ n = [n copyWith:smallest.key withValue:smallest.value withColor:nil withLeft:nil withRight:[(FLLRBValueNode*)n.right removeMin]];
+ }
+ }
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:nil withRight:[n.right remove:aKey withComparator:comparator]];
+ }
+ return [n fixUp];
+}
+
+- (BOOL) isRed {
+ return [self.color boolValue];
+}
+
+- (BOOL) checkMaxDepth {
+ int blackDepth = [self check];
+ if(pow(2.0, blackDepth) <= ([self count] + 1)) {
+ return YES;
+ }
+ else {
+ return NO;
+ }
+}
+
+- (int) check {
+ int blackDepth = 0;
+
+ if([self isRed] && [self.left isRed]) {
+ @throw [[NSException alloc] initWithName:@"check" reason:@"Red node has a red child" userInfo:nil];
+ }
+
+ if([self.right isRed]) {
+ @throw [[NSException alloc] initWithName:@"check" reason:@"Right child is red" userInfo:nil];
+ }
+
+ blackDepth = [self.left check];
+// NSLog(err);
+ if(blackDepth != [self.right check]) {
+ NSString* err = [NSString stringWithFormat:@"(%@ -> %@)blackDepth: %d ; self.right check: %d", self.value, [self.color boolValue] ? @"red" : @"black", blackDepth, [self.right check]];
+// return 10;
+ @throw [[NSException alloc] initWithName:@"check" reason:err userInfo:nil];
+ }
+ else {
+ int ret = blackDepth + ([self isRed] ? 0 : 1);
+// NSLog(@"black depth is: %d; other is: %d, ret is: %d", blackDepth, ([self isRed] ? 0 : 1), ret);
+ return ret;
+ }
+}
+
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.h
new file mode 100644
index 0000000..d7fe835
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.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 <Foundation/Foundation.h>
+#import "FTreeSortedDictionary.h"
+
+@interface FTreeSortedDictionaryEnumerator : NSEnumerator
+
+- (id)initWithImmutableSortedDictionary:(FTreeSortedDictionary *)aDict startKey:(id)startKey isReverse:(BOOL)reverse;
+- (id)nextObject;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m
new file mode 100644
index 0000000..6636d1e
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m
@@ -0,0 +1,99 @@
+/*
+ * 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 "FTreeSortedDictionaryEnumerator.h"
+
+@interface FTreeSortedDictionaryEnumerator()
+@property (nonatomic, strong) FTreeSortedDictionary* immutableSortedDictionary;
+@property (nonatomic, strong) NSMutableArray* stack;
+@property (nonatomic) BOOL isReverse;
+
+@end
+
+@implementation FTreeSortedDictionaryEnumerator
+
+- (id)initWithImmutableSortedDictionary:(FTreeSortedDictionary *)aDict
+ startKey:(id)startKey isReverse:(BOOL)reverse {
+ self = [super init];
+ if (self) {
+ self.immutableSortedDictionary = aDict;
+ self.stack = [[NSMutableArray alloc] init];
+ self.isReverse = reverse;
+
+ NSComparator comparator = aDict.comparator;
+ id<FLLRBNode> node = self.immutableSortedDictionary.root;
+
+ NSInteger cmp;
+ while(![node isEmpty]) {
+ cmp = startKey ? comparator(node.key, startKey) : 1;
+ // flip the comparison if we're going in reverse
+ if (self.isReverse) cmp *= -1;
+
+ if (cmp < 0) {
+ // This node is less than our start key. Ignore it.
+ if (self.isReverse) {
+ node = node.left;
+ } else {
+ node = node.right;
+ }
+ } else if (cmp == 0) {
+ // This node is exactly equal to our start key. Push it on the stack, but stop iterating:
+ [self.stack addObject:node];
+ break;
+ } else {
+ // This node is greater than our start key, add it to the stack and move on to the next one.
+ [self.stack addObject:node];
+ if (self.isReverse) {
+ node = node.right;
+ } else {
+ node = node.left;
+ }
+ }
+ }
+ }
+ return self;
+}
+
+- (id)nextObject {
+ if([self.stack count] == 0) {
+ return nil;
+ }
+
+ id<FLLRBNode> node = nil;
+ @synchronized(self.stack) {
+ node = [self.stack lastObject];
+ [self.stack removeLastObject];
+ }
+ id result = node.key;
+
+ if (self.isReverse) {
+ node = node.left;
+ while (![node isEmpty]) {
+ [self.stack addObject:node];
+ node = node.right;
+ }
+ } else {
+ node = node.right;
+ while (![node isEmpty]) {
+ [self.stack addObject:node];
+ node = node.left;
+ }
+ }
+
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist
new file mode 100644
index 0000000..42887ee
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.firebase.mobile.ios.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+
diff --git a/Firebase/Database/FIndex.h b/Firebase/Database/FIndex.h
new file mode 100644
index 0000000..8ab08c8
--- /dev/null
+++ b/Firebase/Database/FIndex.h
@@ -0,0 +1,50 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FImmutableSortedDictionary;
+@class FNamedNode;
+@protocol FNode;
+
+@protocol FIndex<NSObject, NSCopying>
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2;
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse;
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2;
+
+- (BOOL) isDefinedOn:(id<FNode>)node;
+- (BOOL) indexedValueChangedBetween:(id<FNode>)oldNode and:(id<FNode>)newNode;
+- (FNamedNode*) minPost;
+- (FNamedNode*) maxPost;
+- (FNamedNode*) makePost:(id<FNode>)indexValue name:(NSString*)name;
+- (NSString*) queryDefinition;
+
+@end
+
+@interface FIndex : NSObject
+
++ (id<FIndex>)indexFromQueryDefinition:(NSString *)string;
+
+@end
diff --git a/Firebase/Database/FIndex.m b/Firebase/Database/FIndex.m
new file mode 100644
index 0000000..61980c7
--- /dev/null
+++ b/Firebase/Database/FIndex.m
@@ -0,0 +1,38 @@
+/*
+ * 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 "FIndex.h"
+
+#import "FKeyIndex.h"
+#import "FValueIndex.h"
+#import "FPathIndex.h"
+#import "FPriorityIndex.h"
+
+@implementation FIndex
+
++ (id<FIndex>)indexFromQueryDefinition:(NSString *)string {
+ if ([string isEqualToString:@".key"]) {
+ return [FKeyIndex keyIndex];
+ } else if ([string isEqualToString:@".value"]) {
+ return [FValueIndex valueIndex];
+ } else if ([string isEqualToString:@".priority"]) {
+ return [FPriorityIndex priorityIndex];
+ } else {
+ return [[FPathIndex alloc] initWithPath:[[FPath alloc] initWith:string]];
+ }
+}
+
+@end
diff --git a/Firebase/Database/FKeyIndex.h b/Firebase/Database/FKeyIndex.h
new file mode 100644
index 0000000..a6bf787
--- /dev/null
+++ b/Firebase/Database/FKeyIndex.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 <Foundation/Foundation.h>
+#import "FIndex.h"
+
+
+@interface FKeyIndex : NSObject<FIndex>
++ (id<FIndex>) keyIndex;
+@end
diff --git a/Firebase/Database/FKeyIndex.m b/Firebase/Database/FKeyIndex.m
new file mode 100644
index 0000000..68ad461
--- /dev/null
+++ b/Firebase/Database/FKeyIndex.m
@@ -0,0 +1,115 @@
+/*
+ * 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 "FKeyIndex.h"
+#import "FNamedNode.h"
+#import "FSnapshotUtilities.h"
+#import "FUtilities.h"
+#import "FEmptyNode.h"
+
+@interface FKeyIndex ()
+
+@property (nonatomic, strong) FNamedNode *maxPost;
+
+@end
+
+@implementation FKeyIndex
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ self.maxPost = [[FNamedNode alloc] initWithName:[FUtilities maxName] andNode:[FEmptyNode emptyNode]];
+ }
+ return self;
+
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ return [FUtilities compareKey:key1 toKey:key2];
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id <FNode>)node {
+ return YES;
+}
+
+- (BOOL)indexedValueChangedBetween:(id <FNode>)oldNode and:(id <FNode>)newNode {
+ return NO; // The key for a node never changes.
+}
+
+- (FNamedNode *)minPost {
+ return [FNamedNode min];
+}
+
+- (FNamedNode *)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ NSString *key = indexValue.val;
+ NSAssert([key isKindOfClass:[NSString class]], @"KeyIndex indexValue must always be a string.");
+ // We just use empty node, but it'll never be compared, since our comparator only looks at name.
+ return [[FNamedNode alloc] initWithName:key andNode:[FEmptyNode emptyNode]];
+}
+
+- (NSString *) queryDefinition {
+ return @".key";
+}
+
+- (NSString *) description {
+ return @"FKeyIndex";
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ // since we're a singleton.
+ return (other == self);
+}
+
+- (NSUInteger) hash {
+ return [@".key" hash];
+}
+
+
++ (id<FIndex>) keyIndex {
+ static id<FIndex> keyIndex;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ keyIndex = [[FKeyIndex alloc] init];
+ });
+ return keyIndex;
+}
+@end
diff --git a/Firebase/Database/FListenComplete.h b/Firebase/Database/FListenComplete.h
new file mode 100644
index 0000000..914a3e4
--- /dev/null
+++ b/Firebase/Database/FListenComplete.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 <Foundation/Foundation.h>
+#import "FOperation.h"
+
+
+@interface FListenComplete : NSObject <FOperation>
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath;
+
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, readonly) FOperationType type;
+
+@end
diff --git a/Firebase/Database/FListenComplete.m b/Firebase/Database/FListenComplete.m
new file mode 100644
index 0000000..8573075
--- /dev/null
+++ b/Firebase/Database/FListenComplete.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 "FListenComplete.h"
+#import "FOperationSource.h"
+#import "FPath.h"
+
+@interface FListenComplete ()
+@property (nonatomic, strong, readwrite) FOperationSource *source;
+@property (nonatomic, strong, readwrite) FPath *path;
+@property (nonatomic, readwrite) FOperationType type;
+@end
+
+@implementation FListenComplete
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath {
+ NSAssert(!aSource.fromUser, @"Can't have a listen complete from a user source");
+ self = [super init];
+ if (self) {
+ self.source = aSource;
+ self.path = aPath;
+ self.type = FOperationTypeListenComplete;
+ }
+ return self;
+}
+
+- (id <FOperation>) operationForChild:(NSString *)childKey {
+ if ([self.path isEmpty]) {
+ return [[FListenComplete alloc] initWithSource:self.source path:[FPath empty]];
+ } else {
+ return [[FListenComplete alloc] initWithSource:self.source path:[self.path popFront]];
+ }
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FListenComplete { path=%@, source=%@ }", self.path, self.source];
+}
+
+@end
diff --git a/Firebase/Database/FMaxNode.h b/Firebase/Database/FMaxNode.h
new file mode 100644
index 0000000..6aff8c6
--- /dev/null
+++ b/Firebase/Database/FMaxNode.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 <Foundation/Foundation.h>
+#import "FChildrenNode.h"
+
+
+@interface FMaxNode : FChildrenNode
+ + (id<FNode>) maxNode;
+@end
diff --git a/Firebase/Database/FMaxNode.m b/Firebase/Database/FMaxNode.m
new file mode 100644
index 0000000..3c93684
--- /dev/null
+++ b/Firebase/Database/FMaxNode.m
@@ -0,0 +1,61 @@
+/*
+ * 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 "FMaxNode.h"
+#import "FUtilities.h"
+#import "FEmptyNode.h"
+
+
+@implementation FMaxNode {
+
+}
+- (id) init {
+ self = [super init];
+ if (self) {
+
+ }
+ return self;
+}
+
++ (id<FNode>) maxNode {
+ static FMaxNode *maxNode = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ maxNode = [[FMaxNode alloc] init];
+ });
+ return maxNode;
+}
+
+- (NSComparisonResult) compare:(id<FNode>)other {
+ if (other == self) {
+ return NSOrderedSame;
+ } else {
+ return NSOrderedDescending;
+ }
+}
+
+- (BOOL)isEqual:(id)other {
+ return other == self;
+}
+
+- (id<FNode>) getImmediateChild:(NSString *) childName {
+ return [FEmptyNode emptyNode];
+}
+
+- (BOOL) isEmpty {
+ return NO;
+}
+@end
diff --git a/Firebase/Database/FNamedNode.h b/Firebase/Database/FNamedNode.h
new file mode 100644
index 0000000..ac9baa6
--- /dev/null
+++ b/Firebase/Database/FNamedNode.h
@@ -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 <Foundation/Foundation.h>
+#import "FNode.h"
+
+@interface FNamedNode : NSObject<NSCopying>
+
+@property (nonatomic, strong, readonly) NSString* name;
+@property (nonatomic, strong, readonly) id<FNode> node;
+
+
+-(id)initWithName:(NSString*)name andNode:(id<FNode>)node;
+
++ (FNamedNode *)nodeWithName:(NSString *)name node:(id<FNode>)node;
+
++ (FNamedNode*) min;
++ (FNamedNode*) max;
+@end
diff --git a/Firebase/Database/FNamedNode.m b/Firebase/Database/FNamedNode.m
new file mode 100644
index 0000000..d11787b
--- /dev/null
+++ b/Firebase/Database/FNamedNode.m
@@ -0,0 +1,94 @@
+/*
+ * 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 "FNamedNode.h"
+#import "FUtilities.h"
+#import "FEmptyNode.h"
+#import "FMaxNode.h"
+#import "FIndex.h"
+
+@interface FNamedNode ()
+@property (nonatomic, strong, readwrite) NSString* name;
+@property (nonatomic, strong, readwrite) id<FNode> node;
+@end
+
+@implementation FNamedNode
+
++ (FNamedNode *)nodeWithName:(NSString *)name node:(id<FNode>)node
+{
+ return [[FNamedNode alloc] initWithName:name andNode:node];
+}
+
+- (id)initWithName:(NSString *)name andNode:(id <FNode>)node {
+ self = [super init];
+ if (self) {
+ self.name = name;
+ self.node = node;
+ }
+ return self;
+}
+
+- (id)copy
+{
+ return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone
+{
+ return self;
+}
+
++ (FNamedNode *)min {
+ static FNamedNode *min = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ min = [[FNamedNode alloc] initWithName:[FUtilities minName] andNode:[FEmptyNode emptyNode]];
+ });
+ return min;
+}
+
++ (FNamedNode *)max {
+ static FNamedNode *max = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ max = [[FNamedNode alloc] initWithName:[FUtilities maxName] andNode:[FMaxNode maxNode]];
+ });
+ return max;
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"NamedNode[%@] %@", self.name, self.node];
+}
+
+- (BOOL) isEqual:(id)object {
+ if (self == object) { return YES; }
+ if (object == nil || ![object isKindOfClass:[FNamedNode class]]) { return NO; }
+
+ FNamedNode *namedNode = object;
+ if (![self.name isEqualToString:namedNode.name]) { return NO; }
+ if (![self.node isEqual:namedNode.node]) { return NO; }
+
+ return YES;
+}
+
+- (NSUInteger) hash {
+ NSUInteger nameHash = [self.name hash];
+ NSUInteger nodeHash = [self.node hash];
+ NSUInteger result = 31 * nameHash + nodeHash;
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/FPathIndex.h b/Firebase/Database/FPathIndex.h
new file mode 100644
index 0000000..cf92ad1
--- /dev/null
+++ b/Firebase/Database/FPathIndex.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 <Foundation/Foundation.h>
+#import "FIndex.h"
+#import "FPath.h"
+
+@interface FPathIndex : NSObject<FIndex>
+- (id) initWithPath:(FPath *)path;
+@end
diff --git a/Firebase/Database/FPathIndex.m b/Firebase/Database/FPathIndex.m
new file mode 100644
index 0000000..39913aa
--- /dev/null
+++ b/Firebase/Database/FPathIndex.m
@@ -0,0 +1,125 @@
+/*
+ * 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 "FPathIndex.h"
+#import "FUtilities.h"
+#import "FMaxNode.h"
+#import "FEmptyNode.h"
+#import "FSnapshotUtilities.h"
+#import "FNamedNode.h"
+#import "FPath.h"
+
+@interface FPathIndex ()
+ @property (nonatomic, strong) FPath *path;
+@end
+
+@implementation FPathIndex
+
+- (id) initWithPath:(FPath *)path {
+ self = [super init];
+ if (self) {
+ if (path.isEmpty || [path.getFront isEqualToString:@".priority"]) {
+ [NSException raise:NSInvalidArgumentException format:@"Invalid path for PathIndex: %@", path];
+ }
+ _path = path;
+ }
+ return self;
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ id<FNode> child1 = [node1 getChild:self.path];
+ id<FNode> child2 = [node2 getChild:self.path];
+ NSComparisonResult indexCmp = [child1 compare:child2];
+ if (indexCmp == NSOrderedSame) {
+ return [FUtilities compareKey:key1 toKey:key2];
+ } else {
+ return indexCmp;
+ }
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id <FNode>)node {
+ return ![node getChild:self.path].isEmpty;
+}
+
+- (BOOL)indexedValueChangedBetween:(id <FNode>)oldNode and:(id <FNode>)newNode {
+ id<FNode> oldValue = [oldNode getChild:self.path];
+ id<FNode> newValue = [newNode getChild:self.path];
+ return [oldValue compare:newValue] != NSOrderedSame;
+}
+
+- (FNamedNode *)minPost {
+ return FNamedNode.min;
+}
+
+- (FNamedNode *)maxPost {
+ id<FNode> maxNode = [[FEmptyNode emptyNode] updateChild:self.path
+ withNewChild:[FMaxNode maxNode]];
+
+ return [[FNamedNode alloc] initWithName:[FUtilities maxName] andNode:maxNode];
+}
+
+- (FNamedNode*)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ id<FNode> node = [[FEmptyNode emptyNode] updateChild:self.path withNewChild:indexValue];
+ return [[FNamedNode alloc] initWithName:name andNode:node];
+}
+
+- (NSString *)queryDefinition {
+ return [self.path wireFormat];
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"FPathIndex(%@)", self.path];
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ // Safe since we're immutable.
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ if (![other isKindOfClass:[FPathIndex class]]) {
+ return NO;
+ }
+ return ([self.path isEqual:((FPathIndex*)other).path]);
+}
+
+- (NSUInteger) hash {
+ return [self.path hash];
+}
+
+@end
diff --git a/Firebase/Database/FPriorityIndex.h b/Firebase/Database/FPriorityIndex.h
new file mode 100644
index 0000000..8b5904d
--- /dev/null
+++ b/Firebase/Database/FPriorityIndex.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 <Foundation/Foundation.h>
+
+#import "FIndex.h"
+
+@interface FPriorityIndex : NSObject<FIndex>
++ (id<FIndex>) priorityIndex;
+@end
diff --git a/Firebase/Database/FPriorityIndex.m b/Firebase/Database/FPriorityIndex.m
new file mode 100644
index 0000000..2d06ffa
--- /dev/null
+++ b/Firebase/Database/FPriorityIndex.m
@@ -0,0 +1,118 @@
+/*
+ * 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 "FPriorityIndex.h"
+
+#import "FNode.h"
+#import "FUtilities.h"
+#import "FNamedNode.h"
+#import "FEmptyNode.h"
+#import "FLeafNode.h"
+#import "FMaxNode.h"
+
+// TODO: Abstract into some common base class?
+
+@implementation FPriorityIndex
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ id<FNode> child1 = [node1 getPriority];
+ id<FNode> child2 = [node2 getPriority];
+ NSComparisonResult indexCmp = [child1 compare:child2];
+ if (indexCmp == NSOrderedSame) {
+ return [FUtilities compareKey:key1 toKey:key2];
+ } else {
+ return indexCmp;
+ }
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id <FNode>)node {
+ return !node.getPriority.isEmpty;
+}
+
+- (BOOL)indexedValueChangedBetween:(id <FNode>)oldNode and:(id <FNode>)newNode {
+ id<FNode> oldValue = [oldNode getPriority];
+ id<FNode> newValue = [newNode getPriority];
+ return ![oldValue isEqual:newValue];
+}
+
+- (FNamedNode *)minPost {
+ return FNamedNode.min;
+}
+
+- (FNamedNode *)maxPost {
+ return [self makePost:[FMaxNode maxNode] name:[FUtilities maxName]];
+}
+
+- (FNamedNode*)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ id<FNode> node = [[FLeafNode alloc] initWithValue:@"[PRIORITY-POST]" withPriority:indexValue];
+ return [[FNamedNode alloc] initWithName:name andNode:node];
+}
+
+- (NSString *)queryDefinition {
+ return @".priority";
+}
+
+- (NSString *)description {
+ return @"FPriorityIndex";
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ // Safe since we're immutable.
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ return [other isKindOfClass:[FPriorityIndex class]];
+}
+
+- (NSUInteger) hash {
+ // chosen by a fair dice roll. Guaranteed to be random
+ return 3155577;
+}
+
++ (id<FIndex>) priorityIndex {
+ static id<FIndex> index;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ index = [[FPriorityIndex alloc] init];
+ });
+
+ return index;
+}
+
+@end
diff --git a/Firebase/Database/FRangedFilter.h b/Firebase/Database/FRangedFilter.h
new file mode 100644
index 0000000..1457778
--- /dev/null
+++ b/Firebase/Database/FRangedFilter.h
@@ -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 <Foundation/Foundation.h>
+#import "FNodeFilter.h"
+
+@class FQueryParams;
+@class FNamedNode;
+
+@interface FRangedFilter : NSObject<FNodeFilter>
+
+- (id) initWithQueryParams:(FQueryParams *)params;
+- (BOOL) matchesKey:(NSString *)key andNode:(id<FNode>)node;
+
+
+@property (nonatomic, strong, readonly) FNamedNode *startPost;
+@property (nonatomic, strong, readonly) FNamedNode *endPost;
+
+@end
diff --git a/Firebase/Database/FRangedFilter.m b/Firebase/Database/FRangedFilter.m
new file mode 100644
index 0000000..5c4bbeb
--- /dev/null
+++ b/Firebase/Database/FRangedFilter.m
@@ -0,0 +1,118 @@
+/*
+ * 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 "FRangedFilter.h"
+#import "FChildChangeAccumulator.h"
+#import "FNamedNode.h"
+#import "FQueryParams.h"
+#import "FIndexedFilter.h"
+#import "FQueryParams.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FIndexedNode.h"
+
+@interface FRangedFilter ()
+@property (nonatomic, strong, readwrite) id<FNodeFilter> indexedFilter;
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+@property (nonatomic, strong, readwrite) FNamedNode *startPost;
+@property (nonatomic, strong, readwrite) FNamedNode *endPost;
+@end
+
+@implementation FRangedFilter
+- (id) initWithQueryParams:(FQueryParams *)params {
+ self = [super init];
+ if (self) {
+ self.indexedFilter = [[FIndexedFilter alloc] initWithIndex:params.index];
+ self.index = params.index;
+ self.startPost = [FRangedFilter startPostFromQueryParams:params];
+ self.endPost = [FRangedFilter endPostFromQueryParams:params];
+ }
+ return self;
+}
+
+
++ (FNamedNode *) startPostFromQueryParams:(FQueryParams *)params {
+ if ([params hasStart]) {
+ NSString *startKey = params.indexStartKey;
+ return [params.index makePost:params.indexStartValue name:startKey];
+ } else {
+ return params.index.minPost;
+ }
+}
+
++ (FNamedNode *) endPostFromQueryParams:(FQueryParams *)params {
+ if ([params hasEnd]) {
+ NSString *endKey = params.indexEndKey;
+ return [params.index makePost:params.indexEndValue name:endKey];
+ } else {
+ return params.index.maxPost;
+ }
+}
+
+- (BOOL) matchesKey:(NSString *)key andNode:(id<FNode>)node {
+ return ([self.index compareKey:self.startPost.name andNode:self.startPost.node toOtherKey:key andNode:node] <= NSOrderedSame &&
+ [self.index compareKey:key andNode:node toOtherKey:self.endPost.name andNode:self.endPost.node] <= NSOrderedSame);
+}
+
+- (FIndexedNode *)updateChildIn:(FIndexedNode *)oldSnap
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ if (![self matchesKey:childKey andNode:newChildSnap]) {
+ newChildSnap = [FEmptyNode emptyNode];
+ }
+ return [self.indexedFilter updateChildIn:oldSnap
+ forChildKey:childKey
+ newChild:newChildSnap
+ affectedPath:affectedPath
+ fromSource:source
+ accumulator:optChangeAccumulator];
+}
+
+- (FIndexedNode *) updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ __block FIndexedNode *filtered;
+ if (newSnap.node.isLeafNode) {
+ // Make sure we have a children node with the correct index, not a leaf node
+ filtered = [FIndexedNode indexedNodeWithNode:[FEmptyNode emptyNode] index:self.index];
+ } else {
+ // Dont' support priorities on queries
+ filtered = [newSnap updatePriority:[FEmptyNode emptyNode]];
+ [newSnap.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ if (![self matchesKey:key andNode:node]) {
+ filtered = [filtered updateChild:key withNewChild:[FEmptyNode emptyNode]];
+ }
+ }];
+ }
+ return [self.indexedFilter updateFullNode:oldSnap withNewNode:filtered accumulator:optChangeAccumulator];
+}
+
+- (FIndexedNode *) updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap
+{
+ // Don't support priorities on queries
+ return oldSnap;
+}
+
+- (BOOL) filtersNodes {
+ return YES;
+}
+
+@end
diff --git a/Firebase/Database/FTransformedEnumerator.h b/Firebase/Database/FTransformedEnumerator.h
new file mode 100644
index 0000000..75391a8
--- /dev/null
+++ b/Firebase/Database/FTransformedEnumerator.h
@@ -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 <Foundation/Foundation.h>
+
+
+@interface FTransformedEnumerator : NSEnumerator
+- (id)initWithEnumerator:(NSEnumerator*) enumerator andTransform:(id (^)(id))transform;
+- (id)nextObject;
+
+@end
diff --git a/Firebase/Database/FTransformedEnumerator.m b/Firebase/Database/FTransformedEnumerator.m
new file mode 100644
index 0000000..bb36e94
--- /dev/null
+++ b/Firebase/Database/FTransformedEnumerator.m
@@ -0,0 +1,43 @@
+/*
+ * 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 "FTransformedEnumerator.h"
+
+@interface FTransformedEnumerator ()
+@property (nonatomic, strong) NSEnumerator *enumerator;
+@property (nonatomic, copy) id (^transform)(id);
+@end
+
+@implementation FTransformedEnumerator
+- (id)initWithEnumerator:(NSEnumerator *)enumerator andTransform:(id (^)(id))transform {
+ self = [super init];
+ if (self) {
+ self.enumerator = enumerator;
+ self.transform = transform;
+ }
+ return self;
+}
+
+- (id)nextObject {
+ id next = self.enumerator.nextObject;
+ if (next != nil) {
+ return self.transform(next);
+ } else {
+ return nil;
+ }
+}
+
+@end
diff --git a/Firebase/Database/FTreeSortedDictionary.h b/Firebase/Database/FTreeSortedDictionary.h
new file mode 100644
index 0000000..de75988
--- /dev/null
+++ b/Firebase/Database/FTreeSortedDictionary.h
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Implementation of an immutable SortedMap using a Left-leaning
+ * Red-Black Tree, adapted from the implementation in Mugs
+ * (http://mads379.github.com/mugs/) by Mads Hartmann Jensen
+ * (mads379@gmail.com).
+ *
+ * Original paper on Left-leaning Red-Black Trees:
+ * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
+ *
+ * Invariant 1: No red node has a red child
+ * Invariant 2: Every leaf path has the same number of black nodes
+ * Invariant 3: Only the left child can be red (left leaning)
+ */
+
+#import <Foundation/Foundation.h>
+#import "FImmutableSortedDictionary.h"
+#import "FLLRBNode.h"
+
+@interface FTreeSortedDictionary : FImmutableSortedDictionary
+
+@property (nonatomic, copy, readonly) NSComparator comparator;
+@property (nonatomic, strong, readonly) id<FLLRBNode> root;
+
+- (id)initWithComparator:(NSComparator)aComparator;
+
+// Override methods to return subtype
+- (FTreeSortedDictionary *) insertKey:(id)aKey withValue:(id)aValue;
+- (FTreeSortedDictionary *) removeKey:(id)aKey;
+
+@end
diff --git a/Firebase/Database/FTreeSortedDictionary.m b/Firebase/Database/FTreeSortedDictionary.m
new file mode 100644
index 0000000..d3b00f9
--- /dev/null
+++ b/Firebase/Database/FTreeSortedDictionary.m
@@ -0,0 +1,342 @@
+/*
+ * 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 "FTreeSortedDictionary.h"
+#import "FLLRBEmptyNode.h"
+#import "FLLRBValueNode.h"
+#import "FTreeSortedDictionaryEnumerator.h"
+
+typedef void (^fbt_void_nsnumber_int)(NSNumber* color, NSUInteger chunkSize);
+
+@interface FTreeSortedDictionary ()
+
+@property (nonatomic, strong) id<FLLRBNode> root;
+@property (nonatomic, copy, readwrite) NSComparator comparator;
+
+@end
+
+@implementation FTreeSortedDictionary
+
+- (id)initWithComparator:(NSComparator)aComparator {
+ self = [super init];
+ if (self) {
+ self.root = [FLLRBEmptyNode emptyNode];
+ self.comparator = aComparator;
+ }
+ return self;
+}
+
+- (id)initWithComparator:(NSComparator)aComparator withRoot:(__unsafe_unretained id<FLLRBNode>)aRoot {
+ self = [super init];
+ if (self) {
+ self.root = aRoot;
+ self.comparator = aComparator;
+ }
+ return self;
+}
+
+/**
+ * Returns a copy of the map, with the specified key/value added or replaced.
+ */
+- (FTreeSortedDictionary *) insertKey:(__unsafe_unretained id)aKey withValue:(__unsafe_unretained id)aValue {
+ return [[FTreeSortedDictionary alloc] initWithComparator:self.comparator
+ withRoot:[[self.root insertKey:aKey forValue:aValue withComparator:self.comparator]
+ copyWith:nil
+ withValue:nil
+ withColor:BLACK
+ withLeft:nil
+ withRight:nil]];
+}
+
+
+- (FTreeSortedDictionary *) removeKey:(__unsafe_unretained id)aKey {
+ // Remove is somewhat expensive even if the key doesn't exist (the tree does rebalancing and stuff). So avoid it.
+ if (![self contains:aKey]) {
+ return self;
+ } else {
+ return [[FTreeSortedDictionary alloc]
+ initWithComparator:self.comparator
+ withRoot:[[self.root remove:aKey withComparator:self.comparator]
+ copyWith:nil
+ withValue:nil
+ withColor:BLACK
+ withLeft:nil
+ withRight:nil]];
+ }
+}
+
+- (id) get:(__unsafe_unretained id) key {
+ if (key == nil) {
+ return nil;
+ }
+ NSComparisonResult cmp;
+ id<FLLRBNode> node = self.root;
+ while(![node isEmpty]) {
+ cmp = self.comparator(key, node.key);
+ if(cmp == NSOrderedSame) {
+ return node.value;
+ }
+ else if (cmp == NSOrderedAscending) {
+ node = node.left;
+ }
+ else {
+ node = node.right;
+ }
+ }
+ return nil;
+}
+
+- (id) getPredecessorKey:(__unsafe_unretained id) key {
+ NSComparisonResult cmp;
+ id<FLLRBNode> node = self.root;
+ id<FLLRBNode> rightParent = nil;
+ while(![node isEmpty]) {
+ cmp = self.comparator(key, node.key);
+ if(cmp == NSOrderedSame) {
+ if(![node.left isEmpty]) {
+ node = node.left;
+ while(! [node.right isEmpty]) {
+ node = node.right;
+ }
+ return node.key;
+ }
+ else if (rightParent != nil) {
+ return rightParent.key;
+ }
+ else {
+ return nil;
+ }
+ }
+ else if (cmp == NSOrderedAscending) {
+ node = node.left;
+ }
+ else if (cmp == NSOrderedDescending) {
+ rightParent = node;
+ node = node.right;
+ }
+ }
+ @throw [NSException exceptionWithName:@"NonexistentKey" reason:@"getPredecessorKey called with nonexistent key." userInfo:@{@"key": [key description] }];
+}
+
+- (BOOL) isEmpty {
+ return [self.root isEmpty];
+}
+
+- (int) count {
+ return [self.root count];
+}
+
+- (id) minKey {
+ return [self.root minKey];
+}
+
+- (id) maxKey {
+ return [self.root maxKey];
+}
+
+- (void) enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block
+{
+ [self enumerateKeysAndObjectsReverse:NO usingBlock:block];
+}
+
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block
+{
+ if (reverse) {
+ __block BOOL stop = NO;
+ [self.root reverseTraversal:^BOOL(id key, id value) {
+ block(key, value, &stop);
+ return stop;
+ }];
+ } else {
+ __block BOOL stop = NO;
+ [self.root inorderTraversal:^BOOL(id key, id value) {
+ block(key, value, &stop);
+ return stop;
+ }];
+ }
+}
+
+- (BOOL) contains:(__unsafe_unretained id)key {
+ return ([self objectForKey:key] != nil);
+}
+
+- (NSEnumerator *) keyEnumerator {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:nil isReverse:NO];
+}
+
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:startKey isReverse:NO];
+}
+
+- (NSEnumerator *) reverseKeyEnumerator {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:nil isReverse:YES];
+}
+
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:startKey isReverse:YES];
+}
+
+
+#pragma mark -
+#pragma mark Tree Builder
+
+// Code to efficiently build a RB Tree
+typedef struct _base1_2list {
+ unsigned int bits;
+ unsigned short count;
+ unsigned short current;
+} Base1_2List;
+
+Base1_2List *base1_2List_new(unsigned int length);
+void base1_2List_free(Base1_2List* list);
+unsigned int log_base2(unsigned int num);
+BOOL base1_2List_next(Base1_2List* list);
+
+unsigned int log_base2(unsigned int num) {
+ return (unsigned int)(log(num) / log(2));
+}
+
+/**
+ * Works like an iterator, so it moves to the next bit. Do not call more than list->count times.
+ * @return whether or not the next bit is a 1 in base {1,2}.
+ */
+BOOL base1_2List_next(Base1_2List* list) {
+ BOOL result = !(list->bits & (0x1 << list->current));
+ list->current--;
+ return result;
+}
+
+static inline unsigned bit_mask(int x) {
+ return (x >= sizeof(unsigned) * CHAR_BIT) ? (unsigned) -1 : (1U << x) - 1;
+}
+
+/**
+ * We represent the base{1,2} number as the combination of a binary number and a number of bits that we care about
+ * We iterate backwards, from most significant bit to least, to build up the llrb nodes. 0 base 2 => 1 base {1,2}, 1 base 2 => 2 base {1,2}
+ */
+Base1_2List *base1_2List_new(unsigned int length) {
+ size_t sz = sizeof(Base1_2List);
+ Base1_2List* list = calloc(1, sz);
+ // Calculate the number of bits that we care about
+ list->count = (unsigned short)log_base2(length + 1);
+ unsigned int mask = bit_mask(list->count);
+ list->bits = (length + 1) & mask;
+ list->current = list->count - 1;
+ return list;
+}
+
+
+void base1_2List_free(Base1_2List* list) {
+ free(list);
+}
+
++ (id<FLLRBNode>) buildBalancedTree:(NSArray *)keys dictionary:(NSDictionary *)dictionary subArrayStartIndex:(NSUInteger)startIndex length:(NSUInteger)length {
+ length = MIN(keys.count - startIndex, length); // Bound length by the actual length of the array
+ if (length == 0) {
+ return nil;
+ } else if (length == 1) {
+ id key = keys[startIndex];
+ return [[FLLRBValueNode alloc] initWithKey:key withValue:dictionary[key] withColor:BLACK withLeft:nil withRight:nil];
+ } else {
+ NSUInteger middle = length / 2;
+ id<FLLRBNode> left = [FTreeSortedDictionary buildBalancedTree:keys dictionary:dictionary subArrayStartIndex:startIndex length:middle];
+ id<FLLRBNode> right = [FTreeSortedDictionary buildBalancedTree:keys dictionary:dictionary subArrayStartIndex:(startIndex+middle+1) length:middle];
+ id key = keys[startIndex + middle];
+ return [[FLLRBValueNode alloc] initWithKey:key withValue:dictionary[key] withColor:BLACK withLeft:left withRight:right];
+ }
+}
+
++ (id<FLLRBNode>) rootFrom12List:(Base1_2List *)base1_2List keyList:(NSArray *)keyList dictionary:(NSDictionary *)dictionary {
+ __block id<FLLRBNode> root = nil;
+ __block id<FLLRBNode> node = nil;
+ __block NSUInteger index = keyList.count;
+
+ fbt_void_nsnumber_int buildPennant = ^(NSNumber* color, NSUInteger chunkSize) {
+ NSUInteger startIndex = index - chunkSize + 1;
+ index -= chunkSize;
+ id key = keyList[index];
+ id<FLLRBNode> childTree = [self buildBalancedTree:keyList dictionary:dictionary subArrayStartIndex:startIndex length:(chunkSize - 1)];
+ id<FLLRBNode> pennant = [[FLLRBValueNode alloc] initWithKey:key withValue:dictionary[key] withColor:color withLeft:nil withRight:childTree];
+ //attachPennant(pennant);
+ if (node) {
+ node.left = pennant;
+ node = pennant;
+ } else {
+ root = pennant;
+ node = pennant;
+ }
+ };
+
+ for (int i = 0; i < base1_2List->count; ++i) {
+ BOOL isOne = base1_2List_next(base1_2List);
+ NSUInteger chunkSize = (NSUInteger)pow(2.0, base1_2List->count - (i + 1));
+ if (isOne) {
+ buildPennant(BLACK, chunkSize);
+ } else {
+ buildPennant(BLACK, chunkSize);
+ buildPennant(RED, chunkSize);
+ }
+ }
+ return root;
+}
+
+/**
+ * Uses the algorithm linked here:
+ * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.46.1458
+ */
+
++ (FImmutableSortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator
+{
+ // Steps:
+ // 0. Sort the array
+ // 1. Calculate the 1-2 number
+ // 2. Build From 1-2 number
+ // 0. for each digit in 1-2 number
+ // 0. calculate chunk size
+ // 1. build 1 or 2 pennants of that size
+ // 2. attach pennants and update node pointer
+ // 1. return root
+ NSMutableArray *sortedKeyList = [NSMutableArray arrayWithCapacity:dictionary.count];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ [sortedKeyList addObject:key];
+ }];
+ [sortedKeyList sortUsingComparator:comparator];
+
+ [sortedKeyList enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+ if (idx > 0) {
+ if (comparator(sortedKeyList[idx - 1], obj) != NSOrderedAscending) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create FImmutableSortedDictionary with keys with same ordering!"];
+ }
+ }
+ }];
+
+ Base1_2List* list = base1_2List_new((unsigned int)sortedKeyList.count);
+ id<FLLRBNode> root = [self rootFrom12List:list keyList:sortedKeyList dictionary:dictionary];
+ base1_2List_free(list);
+
+ if (root != nil) {
+ return [[FTreeSortedDictionary alloc] initWithComparator:comparator withRoot:root];
+ } else {
+ return [[FTreeSortedDictionary alloc] initWithComparator:comparator];
+ }
+}
+
+@end
+
diff --git a/Firebase/Database/FValueIndex.h b/Firebase/Database/FValueIndex.h
new file mode 100644
index 0000000..0f1c7f7
--- /dev/null
+++ b/Firebase/Database/FValueIndex.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 <Foundation/Foundation.h>
+#import "FIndex.h"
+
+
+@interface FValueIndex : NSObject<FIndex>
++ (id<FIndex>) valueIndex;
+@end
diff --git a/Firebase/Database/FValueIndex.m b/Firebase/Database/FValueIndex.m
new file mode 100644
index 0000000..7ef9bff
--- /dev/null
+++ b/Firebase/Database/FValueIndex.m
@@ -0,0 +1,106 @@
+/*
+ * 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 "FValueIndex.h"
+#import "FNamedNode.h"
+#import "FSnapshotUtilities.h"
+#import "FUtilities.h"
+#import "FMaxNode.h"
+
+@implementation FValueIndex
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ NSComparisonResult indexCmp = [node1 compare:node2];
+ if (indexCmp == NSOrderedSame) {
+ return [FUtilities compareKey:key1 toKey:key2];
+ } else {
+ return indexCmp;
+ }
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id<FNode>)node {
+ return YES;
+}
+
+- (BOOL)indexedValueChangedBetween:(id<FNode>)oldNode and:(id<FNode>)newNode {
+ return ![oldNode isEqual:newNode];
+}
+
+- (FNamedNode *)minPost {
+ return FNamedNode.min;
+}
+
+- (FNamedNode *)maxPost {
+ return FNamedNode.max;
+}
+
+- (FNamedNode *)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ return [[FNamedNode alloc] initWithName:name andNode:indexValue];
+}
+
+- (NSString *)queryDefinition {
+ return @".value";
+}
+
+- (NSString *) description {
+ return @"FValueIndex";
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ // since we're a singleton.
+ return (other == self);
+}
+
+- (NSUInteger) hash {
+ return [@".value" hash];
+}
+
+
++ (id<FIndex>) valueIndex {
+ static id<FIndex> valueIndex;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ valueIndex = [[FValueIndex alloc] init];
+ });
+ return valueIndex;
+}
+@end
diff --git a/Firebase/Database/FViewProcessor.h b/Firebase/Database/FViewProcessor.h
new file mode 100644
index 0000000..59bfd2d
--- /dev/null
+++ b/Firebase/Database/FViewProcessor.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 <Foundation/Foundation.h>
+
+@class FViewCache;
+@class FViewProcessorResult;
+@class FChildChangeAccumulator;
+@protocol FNode;
+@class FWriteTreeRef;
+@class FPath;
+@protocol FOperation;
+@protocol FNodeFilter;
+
+
+@interface FViewProcessor : NSObject
+
+- (id)initWithFilter:(id<FNodeFilter>)nodeFilter;
+
+- (FViewProcessorResult *)applyOperationOn:(FViewCache *)oldViewCache operation:(id<FOperation>)operation writesCache:(FWriteTreeRef *)writesCache completeCache:(id <FNode>)optCompleteCache;
+- (FViewCache *) revertUserWriteOn:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator;
+
+
+@end
diff --git a/Firebase/Database/FViewProcessor.m b/Firebase/Database/FViewProcessor.m
new file mode 100644
index 0000000..41ff91d
--- /dev/null
+++ b/Firebase/Database/FViewProcessor.m
@@ -0,0 +1,654 @@
+/*
+ * 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 "FViewProcessor.h"
+#import "FCompleteChildSource.h"
+#import "FWriteTreeRef.h"
+#import "FViewCache.h"
+#import "FCacheNode.h"
+#import "FNode.h"
+#import "FOperation.h"
+#import "FOperationSource.h"
+#import "FChildChangeAccumulator.h"
+#import "FNodeFilter.h"
+#import "FOverwrite.h"
+#import "FMerge.h"
+#import "FAckUserWrite.h"
+#import "FViewProcessorResult.h"
+#import "FIRDataEventType.h"
+#import "FChange.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FPath.h"
+#import "FKeyIndex.h"
+#import "FCompoundWrite.h"
+#import "FImmutableTree.h"
+
+/**
+* An implementation of FCompleteChildSource that never returns any additional children
+*/
+@interface FNoCompleteChildSource: NSObject<FCompleteChildSource>
+@end
+
+@implementation FNoCompleteChildSource
++ (FNoCompleteChildSource *) instance {
+ static FNoCompleteChildSource *source = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ source = [[FNoCompleteChildSource alloc] init];
+ });
+ return source;
+}
+
+- (id<FNode>) completeChild:(NSString *)childKey {
+ return nil;
+}
+
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse {
+ return nil;
+}
+@end
+
+/**
+* An implementation of FCompleteChildSource that uses a FWriteTree in addition to any other server data or
+* old event caches available to calculate complete children.
+*/
+@interface FWriteTreeCompleteChildSource: NSObject<FCompleteChildSource>
+@property (nonatomic, strong) FWriteTreeRef *writes;
+@property (nonatomic, strong) FViewCache *viewCache;
+@property (nonatomic, strong) id<FNode> optCompleteServerCache;
+@end
+
+@implementation FWriteTreeCompleteChildSource
+- (id) initWithWrites:(FWriteTreeRef *)writes viewCache:(FViewCache *)viewCache serverCache:(id<FNode>)optCompleteServerCache {
+ self = [super init];
+ if (self) {
+ self.writes = writes;
+ self.viewCache = viewCache;
+ self.optCompleteServerCache = optCompleteServerCache;
+ }
+ return self;
+}
+
+- (id<FNode>) completeChild:(NSString *)childKey {
+ FCacheNode *node = self.viewCache.cachedEventSnap;
+ if ([node isCompleteForChild:childKey]) {
+ return [node.node getImmediateChild:childKey];
+ } else {
+ FCacheNode *serverNode;
+ if (self.optCompleteServerCache) {
+ // Since we're only ever getting child nodes, we can use the key index here
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:self.optCompleteServerCache index:[FKeyIndex keyIndex]];
+ serverNode = [[FCacheNode alloc] initWithIndexedNode:indexed isFullyInitialized:YES isFiltered:NO];
+ } else {
+ serverNode = self.viewCache.cachedServerSnap;
+ }
+ return [self.writes calculateCompleteChild:childKey cache:serverNode];
+ }
+}
+
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse {
+ id<FNode> completeServerData = self.optCompleteServerCache != nil
+ ? self.optCompleteServerCache
+ : self.viewCache.completeServerSnap;
+ return [self.writes calculateNextNodeAfterPost:child
+ completeServerData:completeServerData
+ reverse:reverse
+ index:index];
+}
+
+@end
+
+@interface FViewProcessor ()
+@property (nonatomic, strong) id<FNodeFilter> filter;
+@end
+
+@implementation FViewProcessor
+
+- (id)initWithFilter:(id<FNodeFilter>)nodeFilter {
+ self = [super init];
+ if (self) {
+ self.filter = nodeFilter;
+ }
+ return self;
+}
+
+- (FViewProcessorResult *)applyOperationOn:(FViewCache *)oldViewCache operation:(id<FOperation>)operation writesCache:(FWriteTreeRef *)writesCache completeCache:(id <FNode>)optCompleteCache {
+ FChildChangeAccumulator *accumulator = [[FChildChangeAccumulator alloc] init];
+ FViewCache *newViewCache;
+
+ if (operation.type == FOperationTypeOverwrite) {
+ FOverwrite *overwrite = (FOverwrite *) operation;
+ if (operation.source.fromUser) {
+ newViewCache = [self applyUserOverwriteTo:oldViewCache
+ changePath:overwrite.path
+ changedSnap:overwrite.snap
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ NSAssert(operation.source.fromServer, @"Unknown source for overwrite.");
+ // We filter the node if it's a tagged update or the node has been previously filtered and the update is
+ // not at the root in which case it is ok (and necessary) to mark the node unfiltered again
+ BOOL filterServerNode = overwrite.source.isTagged || (oldViewCache.cachedServerSnap.isFiltered &&
+ !overwrite.path.isEmpty);
+ newViewCache = [self applyServerOverwriteTo:oldViewCache
+ changePath:overwrite.path
+ snap:overwrite.snap
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeMerge) {
+ FMerge *merge = (FMerge*)operation;
+ if (operation.source.fromUser) {
+ newViewCache = [self applyUserMergeTo:oldViewCache
+ path:merge.path
+ changedChildren:merge.children
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ NSAssert(operation.source.fromServer, @"Unknown source for merge.");
+ // We filter the node if it's a tagged update or the node has been previously filtered
+ BOOL filterServerNode = merge.source.isTagged || oldViewCache.cachedServerSnap.isFiltered;
+ newViewCache = [self applyServerMergeTo:oldViewCache
+ path:merge.path
+ changedChildren:merge.children
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeAckUserWrite) {
+ FAckUserWrite *ackWrite = (FAckUserWrite *) operation;
+ if (!ackWrite.revert) {
+ newViewCache = [self ackUserWriteOn:oldViewCache
+ ackPath:ackWrite.path
+ affectedTree:ackWrite.affectedTree
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ newViewCache = [self revertUserWriteOn:oldViewCache
+ path:ackWrite.path
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeListenComplete) {
+ newViewCache = [self listenCompleteOldCache:oldViewCache
+ path:operation.path
+ writesCache:writesCache
+ serverCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Unknown operation encountered %zd.", operation.type];
+ return nil;
+ }
+
+ NSArray *changes = [self maybeAddValueFromOldViewCache:oldViewCache newViewCache:newViewCache changes:accumulator.changes];
+ FViewProcessorResult *results = [[FViewProcessorResult alloc] initWithViewCache:newViewCache changes:changes];
+ return results;
+}
+
+- (NSArray *) maybeAddValueFromOldViewCache:(FViewCache *)oldViewCache newViewCache:(FViewCache *)newViewCache changes:(NSArray *)changes {
+ NSArray *newChanges = changes;
+ FCacheNode *eventSnap = newViewCache.cachedEventSnap;
+ if (eventSnap.isFullyInitialized) {
+ BOOL isLeafOrEmpty = eventSnap.node.isLeafNode || eventSnap.node.isEmpty;
+ if ([changes count] > 0 ||
+ !oldViewCache.cachedEventSnap.isFullyInitialized ||
+ (isLeafOrEmpty && ![eventSnap.node isEqual:oldViewCache.completeEventSnap]) ||
+ ![eventSnap.node.getPriority isEqual:oldViewCache.completeEventSnap.getPriority]) {
+ FChange *valueChange = [[FChange alloc] initWithType:FIRDataEventTypeValue indexedNode:eventSnap.indexedNode];
+ NSMutableArray *mutableChanges = [changes mutableCopy];
+ [mutableChanges addObject:valueChange];
+ newChanges = mutableChanges;
+ }
+ }
+ return newChanges;
+}
+
+- (FViewCache *) generateEventCacheAfterServerEvent:(FViewCache *)viewCache
+ path:(FPath *)changePath
+ writesCache:(FWriteTreeRef *)writesCache
+ source:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldEventSnap = viewCache.cachedEventSnap;
+ if ([writesCache shadowingWriteAtPath:changePath] != nil) {
+ // we have a shadowing write, ignore changes.
+ return viewCache;
+ } else {
+ FIndexedNode *newEventCache;
+ if (changePath.isEmpty) {
+ // TODO: figure out how this plays with "sliding ack windows"
+ NSAssert(viewCache.cachedServerSnap.isFullyInitialized, @"If change path is empty, we must have complete server data");
+ id<FNode> nodeWithLocalWrites;
+ if (viewCache.cachedServerSnap.isFiltered) {
+ // We need to special case this, because we need to only apply writes to complete children, or
+ // we might end up raising events for incomplete children. If the server data is filtered deep
+ // writes cannot be guaranteed to be complete
+ id<FNode> serverCache = viewCache.completeServerSnap;
+ FChildrenNode *completeChildren = ([serverCache isKindOfClass:[FChildrenNode class]]) ? serverCache : [FEmptyNode emptyNode];
+ nodeWithLocalWrites = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:completeChildren];
+ } else {
+ nodeWithLocalWrites = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ }
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:nodeWithLocalWrites index:self.filter.index];
+ newEventCache = [self.filter updateFullNode:viewCache.cachedEventSnap.indexedNode
+ withNewNode:indexedNode
+ accumulator:accumulator];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if ([childKey isEqualToString:@".priority"]) {
+ NSAssert(changePath.length == 1, @"Can't have a priority with additional path components");
+ id<FNode> oldEventNode = oldEventSnap.node;
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+ // we might have overwrites for this priority
+ id<FNode> updatedPriority = [writesCache calculateEventCacheAfterServerOverwriteWithChildPath:changePath
+ existingEventSnap:oldEventNode
+ existingServerSnap:serverNode];
+ if (updatedPriority != nil) {
+ newEventCache = [self.filter updatePriority:updatedPriority forNode:oldEventSnap.indexedNode];
+ } else {
+ // priority didn't change, keep old node
+ newEventCache = oldEventSnap.indexedNode;
+ }
+ } else {
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> newEventChild;
+ if ([oldEventSnap isCompleteForChild:childKey]) {
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+ id<FNode> eventChildUpdate = [writesCache calculateEventCacheAfterServerOverwriteWithChildPath:changePath existingEventSnap:oldEventSnap.node existingServerSnap:serverNode];
+ if (eventChildUpdate != nil) {
+ newEventChild = [[oldEventSnap.node getImmediateChild:childKey] updateChild:childChangePath withNewChild:eventChildUpdate];
+ } else {
+ // Nothing changed, just keep the old child
+ newEventChild = [oldEventSnap.node getImmediateChild:childKey];
+ }
+ } else {
+ newEventChild = [writesCache calculateCompleteChild:childKey cache:viewCache.cachedServerSnap];
+ }
+ if (newEventChild != nil) {
+ newEventCache = [self.filter updateChildIn:oldEventSnap.indexedNode
+ forChildKey:childKey
+ newChild:newEventChild
+ affectedPath:childChangePath
+ fromSource:source
+ accumulator:accumulator];
+ } else {
+ // No complete children available or no change
+ newEventCache = oldEventSnap.indexedNode;
+ }
+ }
+ }
+ return [viewCache updateEventSnap:newEventCache
+ isComplete:(oldEventSnap.isFullyInitialized || changePath.isEmpty)
+ isFiltered:self.filter.filtersNodes];
+ }
+}
+
+- (FViewCache *) applyServerOverwriteTo:(FViewCache *)oldViewCache changePath:(FPath *)changePath snap:(id<FNode>)changedSnap
+ writesCache:(FWriteTreeRef *)writesCache completeCache:(id<FNode>)optCompleteCache
+ filterServerNode:(BOOL)filterServerNode accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldServerSnap = oldViewCache.cachedServerSnap;
+ FIndexedNode *newServerCache;
+ id<FNodeFilter> serverFilter = filterServerNode ? self.filter : self.filter.indexedFilter;
+
+ if (changePath.isEmpty) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:changedSnap index:serverFilter.index];
+ newServerCache = [serverFilter updateFullNode:oldServerSnap.indexedNode withNewNode:indexed accumulator:nil];
+ } else if (serverFilter.filtersNodes && !oldServerSnap.isFiltered) {
+ // We want to filter the server node, but we didn't filter the server node yet, so simulate a full update
+ NSAssert(![changePath isEmpty], @"An empty path should been caught in the other branch");
+ NSString *childKey = [changePath getFront];
+ FPath *updatePath = [changePath popFront];
+ id<FNode> newChild = [[oldServerSnap.node getImmediateChild:childKey] updateChild:updatePath
+ withNewChild:changedSnap];
+ FIndexedNode *indexed = [oldServerSnap.indexedNode updateChild:childKey withNewChild:newChild];
+ newServerCache = [serverFilter updateFullNode:oldServerSnap.indexedNode withNewNode:indexed accumulator:nil];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if (![oldServerSnap isCompleteForPath:changePath] && changePath.length > 1) {
+ // We don't update incomplete nodes with updates intended for other listeners.
+ return oldViewCache;
+ }
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> childNode = [oldServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChildNode = [childNode updateChild:childChangePath withNewChild:changedSnap];
+ if ([childKey isEqualToString:@".priority"]) {
+ newServerCache = [serverFilter updatePriority:newChildNode forNode:oldServerSnap.indexedNode];
+ } else {
+ newServerCache = [serverFilter updateChildIn:oldServerSnap.indexedNode
+ forChildKey:childKey
+ newChild:newChildNode
+ affectedPath:childChangePath
+ fromSource:[FNoCompleteChildSource instance]
+ accumulator:nil];
+ }
+ }
+ FViewCache *newViewCache = [oldViewCache updateServerSnap:newServerCache
+ isComplete:(oldServerSnap.isFullyInitialized || changePath.isEmpty)
+ isFiltered:serverFilter.filtersNodes];
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:newViewCache
+ serverCache:optCompleteCache];
+ return [self generateEventCacheAfterServerEvent:newViewCache
+ path:changePath
+ writesCache:writesCache
+ source:source
+ accumulator:accumulator];
+}
+
+- (FViewCache *) applyUserOverwriteTo:(FViewCache *)oldViewCache
+ changePath:(FPath *)changePath
+ changedSnap:(id<FNode>)changedSnap
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldEventSnap = oldViewCache.cachedEventSnap;
+ FViewCache *newViewCache;
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:oldViewCache
+ serverCache:optCompleteCache];
+ if (changePath.isEmpty) {
+ FIndexedNode *newIndexed = [FIndexedNode indexedNodeWithNode:changedSnap index:self.filter.index];
+ FIndexedNode *newEventCache = [self.filter updateFullNode:oldEventSnap.indexedNode
+ withNewNode:newIndexed
+ accumulator:accumulator];
+ newViewCache = [oldViewCache updateEventSnap:newEventCache isComplete:YES isFiltered:self.filter.filtersNodes];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if ([childKey isEqualToString:@".priority"]) {
+ FIndexedNode *newEventCache = [self.filter updatePriority:changedSnap
+ forNode:oldViewCache.cachedEventSnap.indexedNode];
+ newViewCache = [oldViewCache updateEventSnap:newEventCache
+ isComplete:oldEventSnap.isFullyInitialized
+ isFiltered:oldEventSnap.isFiltered];
+ } else {
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> oldChild = [oldEventSnap.node getImmediateChild:childKey];
+ id<FNode> newChild;
+ if (childChangePath.isEmpty) {
+ // Child overwrite, we can replace the child
+ newChild = changedSnap;
+ } else {
+ id<FNode> childNode = [source completeChild:childKey];
+ if (childNode != nil) {
+ if ([[childChangePath getBack] isEqualToString:@".priority"] && [childNode getChild:[childChangePath parent]].isEmpty) {
+ // This is a priority update on an empty node. If this node exists on the server, the server
+ // will send down the priority in the update, so ignore for now
+ newChild = childNode;
+ } else {
+ newChild = [childNode updateChild:childChangePath withNewChild:changedSnap];
+ }
+ } else {
+ newChild = [FEmptyNode emptyNode];
+ }
+ }
+ if (![oldChild isEqual:newChild]) {
+ FIndexedNode *newEventSnap = [self.filter updateChildIn:oldEventSnap.indexedNode
+ forChildKey:childKey
+ newChild:newChild
+ affectedPath:childChangePath
+ fromSource:source
+ accumulator:accumulator];
+ newViewCache = [oldViewCache updateEventSnap:newEventSnap isComplete:oldEventSnap.isFullyInitialized isFiltered:self.filter.filtersNodes];
+ } else {
+ newViewCache = oldViewCache;
+ }
+ }
+ }
+ return newViewCache;
+}
+
++ (BOOL) cache:(FViewCache *)viewCache hasChild:(NSString *)childKey {
+ return [viewCache.cachedEventSnap isCompleteForChild:childKey];
+}
+
+/**
+* @param changedChildren NSDictionary of child name (NSString*) to child value (id<FNode>)
+*/
+- (FViewCache *) applyUserMergeTo:(FViewCache *)viewCache
+ path:(FPath *)path
+ changedChildren:(FCompoundWrite *)changedChildren
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)serverCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ // HACK: In the case of a limit query, there may be some changes that bump things out of the
+ // window leaving room for new items. It's important we process these changes first, so we
+ // iterate the changes twice, first processing any that affect items currently in view.
+ // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
+ // and event snap. I'm not sure if this will result in edge cases when a child is in one but
+ // not the other.
+ __block FViewCache *curViewCache = viewCache;
+
+ [changedChildren enumerateWrites:^(FPath *relativePath, id<FNode> childNode, BOOL *stop) {
+ FPath *writePath = [path child:relativePath];
+ if ([FViewProcessor cache:viewCache hasChild:[writePath getFront]]) {
+ curViewCache = [self applyUserOverwriteTo:curViewCache
+ changePath:writePath
+ changedSnap:childNode
+ writesCache:writesCache
+ completeCache:serverCache
+ accumulator:accumulator];
+ }
+ }];
+
+ [changedChildren enumerateWrites:^(FPath *relativePath, id<FNode> childNode, BOOL *stop) {
+ FPath *writePath = [path child:relativePath];
+ if (![FViewProcessor cache:viewCache hasChild:[writePath getFront]]) {
+ curViewCache = [self applyUserOverwriteTo:curViewCache
+ changePath:writePath
+ changedSnap:childNode
+ writesCache:writesCache
+ completeCache:serverCache
+ accumulator:accumulator];
+ }
+ }];
+
+ return curViewCache;
+}
+
+- (FViewCache *) applyServerMergeTo:(FViewCache *)viewCache
+ path:(FPath *)path
+ changedChildren:(FCompoundWrite *)changedChildren
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)serverCache
+ filterServerNode:(BOOL)filterServerNode
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ // If we don't have a cache yet, this merge was intended for a previously listen in the same location. Ignore it and
+ // wait for the complete data update coming soon.
+ if (viewCache.cachedServerSnap.node.isEmpty && !viewCache.cachedServerSnap.isFullyInitialized) {
+ return viewCache;
+ }
+
+ // HACK: In the case of a limit query, there may be some changes that bump things out of the
+ // window leaving room for new items. It's important we process these changes first, so we
+ // iterate the changes twice, first processing any that affect items currently in view.
+ // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
+ // and event snap. I'm not sure if this will result in edge cases when a child is in one but
+ // not the other.
+ __block FViewCache *curViewCache = viewCache;
+ FCompoundWrite *actualMerge;
+ if (path.isEmpty) {
+ actualMerge = changedChildren;
+ } else {
+ actualMerge = [[FCompoundWrite emptyWrite] addCompoundWrite:changedChildren atPath:path];
+ }
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+
+ NSDictionary *childCompoundWrites = actualMerge.childCompoundWrites;
+ [childCompoundWrites enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FCompoundWrite *childMerge, BOOL *stop) {
+ if ([serverNode hasChild:childKey]) {
+ id<FNode> serverChild = [viewCache.cachedServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChild = [childMerge applyToNode:serverChild];
+ curViewCache = [self applyServerOverwriteTo:curViewCache
+ changePath:[[FPath alloc] initWith:childKey]
+ snap:newChild
+ writesCache:writesCache
+ completeCache:serverCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ }];
+
+ [childCompoundWrites enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FCompoundWrite *childMerge, BOOL *stop) {
+ bool isUnknownDeepMerge = ![viewCache.cachedServerSnap isCompleteForChild:childKey] && childMerge.rootWrite == nil;
+ if (![serverNode hasChild:childKey] && !isUnknownDeepMerge) {
+ id<FNode> serverChild = [viewCache.cachedServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChild = [childMerge applyToNode:serverChild];
+ curViewCache = [self applyServerOverwriteTo:curViewCache
+ changePath:[[FPath alloc] initWith:childKey]
+ snap:newChild
+ writesCache:writesCache
+ completeCache:serverCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ }];
+
+ return curViewCache;
+}
+
+- (FViewCache *) ackUserWriteOn:(FViewCache *)viewCache
+ ackPath:(FPath *)ackPath
+ affectedTree:(FImmutableTree *)affectedTree
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id <FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+
+ if ([writesCache shadowingWriteAtPath:ackPath] != nil) {
+ return viewCache;
+ }
+
+ // Only filter server node if it is currently filtered
+ BOOL filterServerNode = viewCache.cachedServerSnap.isFiltered;
+
+ // Essentially we'll just get our existing server cache for the affected paths and re-apply it as a server update
+ // now that it won't be shadowed.
+ FCacheNode *serverCache = viewCache.cachedServerSnap;
+ if (affectedTree.value != nil) {
+ // This is an overwrite.
+ if ((ackPath.isEmpty && serverCache.isFullyInitialized) || [serverCache isCompleteForPath:ackPath]) {
+ return [self applyServerOverwriteTo:viewCache changePath:ackPath snap:[serverCache.node getChild:ackPath]
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ } else if (ackPath.isEmpty) {
+ // This is a goofy edge case where we are acking data at this location but don't have full data. We
+ // should just re-apply whatever we have in our cache as a merge.
+ FCompoundWrite *changedChildren = [FCompoundWrite emptyWrite];
+ for(FNamedNode *child in serverCache.node.childEnumerator) {
+ changedChildren = [changedChildren addWrite:child.node atKey:child.name];
+ }
+ return [self applyServerMergeTo:viewCache path:ackPath changedChildren:changedChildren
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ } else {
+ return viewCache;
+ }
+ } else {
+ // This is a merge.
+ __block FCompoundWrite *changedChildren = [FCompoundWrite emptyWrite];
+ [affectedTree forEach:^(FPath *mergePath, id value) {
+ FPath *serverCachePath = [ackPath child:mergePath];
+ if ([serverCache isCompleteForPath:serverCachePath]) {
+ changedChildren = [changedChildren addWrite:[serverCache.node getChild:serverCachePath] atPath:mergePath];
+ }
+ }];
+ return [self applyServerMergeTo:viewCache path:ackPath changedChildren:changedChildren
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ }
+}
+
+- (FViewCache *) revertUserWriteOn:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ if ([writesCache shadowingWriteAtPath:path] != nil) {
+ return viewCache;
+ } else {
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:viewCache
+ serverCache:optCompleteCache];
+ FIndexedNode *oldEventCache = viewCache.cachedEventSnap.indexedNode;
+ FIndexedNode *newEventCache;
+ if (path.isEmpty || [[path getFront] isEqualToString:@".priority"]) {
+ id<FNode> newNode;
+ if (viewCache.cachedServerSnap.isFullyInitialized) {
+ newNode = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ } else {
+ newNode = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:viewCache.cachedServerSnap.node];
+ }
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:newNode index:self.filter.index];
+ newEventCache = [self.filter updateFullNode:oldEventCache withNewNode:indexedNode accumulator:accumulator];
+ } else {
+ NSString *childKey = [path getFront];
+ id<FNode> newChild = [writesCache calculateCompleteChild:childKey cache:viewCache.cachedServerSnap];
+ if (newChild == nil && [viewCache.cachedServerSnap isCompleteForChild:childKey]) {
+ newChild = [oldEventCache.node getImmediateChild:childKey];
+ }
+ if (newChild != nil) {
+ newEventCache = [self.filter updateChildIn:oldEventCache
+ forChildKey:childKey
+ newChild:newChild
+ affectedPath:[path popFront]
+ fromSource:source
+ accumulator:accumulator];
+ } else if (newChild == nil && [viewCache.cachedEventSnap.node hasChild:childKey]) {
+ // No complete child available, delete the existing one, if any
+ newEventCache = [self.filter updateChildIn:oldEventCache
+ forChildKey:childKey
+ newChild:[FEmptyNode emptyNode]
+ affectedPath:[path popFront]
+ fromSource:source
+ accumulator:accumulator];
+ } else {
+ newEventCache = oldEventCache;
+ }
+ if (newEventCache.node.isEmpty && viewCache.cachedServerSnap.isFullyInitialized) {
+ // We might have reverted all child writes. Maybe the old event was a leaf node.
+ id<FNode> complete = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ if (complete.isLeafNode) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:complete];
+ newEventCache = [self.filter updateFullNode:newEventCache
+ withNewNode:indexed
+ accumulator:accumulator];
+ }
+ }
+ }
+ BOOL complete = viewCache.cachedServerSnap.isFullyInitialized || [writesCache shadowingWriteAtPath:[FPath empty]] != nil;
+ return [viewCache updateEventSnap:newEventCache isComplete:complete isFiltered:self.filter.filtersNodes];
+ }
+}
+
+- (FViewCache *) listenCompleteOldCache:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ serverCache:(id<FNode>)servercache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldServerNode = viewCache.cachedServerSnap;
+ FViewCache *newViewCache = [viewCache updateServerSnap:oldServerNode.indexedNode
+ isComplete:(oldServerNode.isFullyInitialized || path.isEmpty)
+ isFiltered:oldServerNode.isFiltered];
+ return [self generateEventCacheAfterServerEvent:newViewCache path:path writesCache:writesCache source:[FNoCompleteChildSource instance] accumulator:accumulator];
+}
+
+@end
diff --git a/Firebase/Database/FViewProcessorResult.h b/Firebase/Database/FViewProcessorResult.h
new file mode 100644
index 0000000..e211d19
--- /dev/null
+++ b/Firebase/Database/FViewProcessorResult.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 <Foundation/Foundation.h>
+
+@class FViewCache;
+
+
+@interface FViewProcessorResult : NSObject
+@property (nonatomic, strong, readonly) FViewCache *viewCache;
+/**
+* List of FChanges.
+*/
+@property (nonatomic, strong, readonly) NSArray *changes;
+
+- (id) initWithViewCache:(FViewCache *)viewCache changes:(NSArray *)changes;
+@end
diff --git a/Firebase/Database/FViewProcessorResult.m b/Firebase/Database/FViewProcessorResult.m
new file mode 100644
index 0000000..3327888
--- /dev/null
+++ b/Firebase/Database/FViewProcessorResult.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 "FViewProcessorResult.h"
+#import "FViewCache.h"
+
+@interface FViewProcessorResult ()
+@property (nonatomic, strong, readwrite) FViewCache *viewCache;
+@property (nonatomic, strong, readwrite) NSArray *changes;
+@end
+
+@implementation FViewProcessorResult
+- (id) initWithViewCache:(FViewCache *)viewCache changes:(NSArray *)changes {
+ self = [super init];
+ if (self) {
+ self.viewCache = viewCache;
+ self.changes = changes;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Firebase-Prefix.pch b/Firebase/Database/Firebase-Prefix.pch
new file mode 100644
index 0000000..0158d95
--- /dev/null
+++ b/Firebase/Database/Firebase-Prefix.pch
@@ -0,0 +1,7 @@
+//
+// Prefix header for all source files of the 'Firebase' target in the 'Firebase' project
+//
+
+#ifdef __OBJC__
+ #import <Foundation/Foundation.h>
+#endif
diff --git a/Firebase/Database/FirebaseDatabase.podspec b/Firebase/Database/FirebaseDatabase.podspec
new file mode 100644
index 0000000..4db371e
--- /dev/null
+++ b/Firebase/Database/FirebaseDatabase.podspec
@@ -0,0 +1,48 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseDatabase'
+ s.version = '4.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]',
+ 'third_party/Wrap-leveldb/APLevelDB.mm',
+ 'third_party/SocketRocket/fbase64.c'
+ s.public_header_files =
+ 'Api/FirebaseDatabase.h',
+ 'Api/FIRDataEventType.h',
+ 'Api/FIRDataSnapshot.h',
+ 'Api/FIRDatabaseQuery.h',
+ 'Api/FIRDatabaseSwiftNameSupport.h',
+ 'Api/FIRMutableData.h',
+ 'Api/FIRServerValue.h',
+ 'Api/FIRTransactionResult.h',
+ 'Api/FIRDatabase.h',
+ 'FIRDatabaseReference.h'
+ s.library = 'c++'
+ s.library = 'icucore'
+ s.framework = 'CFNetwork'
+ s.framework = 'Security'
+ s.framework = 'SystemConfiguration'
+ s.dependency 'leveldb-library'
+# s.dependency 'FirebaseDev/Core'
+ s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' +
+ 'FIRDatabase_VERSION=' + s.version.to_s }
+end
diff --git a/Firebase/Database/Info.plist b/Firebase/Database/Info.plist
new file mode 100644
index 0000000..c707a67
--- /dev/null
+++ b/Firebase/Database/Info.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.firebase.$(PRODUCT_NAME:rfc1034identifier)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>XXX_TAG_VERSION_XXX</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>XXX_TAG_VERSION_XXX</string>
+ <key>NSPrincipalClass</key>
+ <string></string>
+</dict>
+</plist>
diff --git a/Firebase/Database/Login/FAuthTokenProvider.h b/Firebase/Database/Login/FAuthTokenProvider.h
new file mode 100644
index 0000000..dca0026
--- /dev/null
+++ b/Firebase/Database/Login/FAuthTokenProvider.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 <Foundation/Foundation.h>
+
+#import "FTypedefs.h"
+#import "FTypedefs_Private.h"
+
+@protocol FAuthTokenProvider <NSObject>
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback;
+
+- (void) listenForTokenChanges:(fbt_void_nsstring)listener;
+
+@end
+
+@interface FAuthTokenProvider : NSObject
+
++ (id<FAuthTokenProvider>) authTokenProviderForApp:(id)app;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/Firebase/Database/Login/FAuthTokenProvider.m b/Firebase/Database/Login/FAuthTokenProvider.m
new file mode 100644
index 0000000..e406ae7
--- /dev/null
+++ b/Firebase/Database/Login/FAuthTokenProvider.m
@@ -0,0 +1,162 @@
+/*
+ * 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 "FAuthTokenProvider.h"
+#import "FUtilities.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FIRNoopAuthTokenProvider.h"
+
+static NSString *const FIRAuthStateDidChangeInternalNotification = @"FIRAuthStateDidChangeInternalNotification";
+static NSString *const FIRAuthStateDidChangeInternalNotificationTokenKey = @"FIRAuthStateDidChangeInternalNotificationTokenKey";
+
+
+/**
+ * This is a hack that defines all the methods we need from FIRFirebaseApp. At runtime we use reflection to get an
+ * actual instance of FIRFirebaseApp. Since protocols don't carry any runtime information and selectors are invoked
+ * by name we can write code against this protocol as long as the method signatures of FIRFirebaseApp don't change.
+ */
+@protocol FIRFirebaseAppLike <NSObject>
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(void (^)(NSString *_Nullable token, NSError *_Nullable error))callback;
+
+@end
+
+
+/**
+ * This is a hack that defines all the methods we need from FIRAuth.
+ */
+@protocol FIRFirebaseAuthLike <NSObject>
+
+- (id<FIRFirebaseAppLike>) app;
+
+@end
+
+/**
+ * This is a hack that copies the definitions of Firebear error codes. If the error codes change in the original code, this
+ * will break at runtime due to undefined behavior!
+ */
+typedef NS_ENUM(NSUInteger, FIRErrorCode) {
+ /*! @var FIRErrorCodeNoAuth
+ @brief Represents the case where an auth-related message was sent to a @c FIRFirebaseApp
+ instance which has no associated @c FIRAuth instance.
+ */
+ FIRErrorCodeNoAuth,
+
+ /*! @var FIRErrorCodeNoSignedInUser
+ @brief Represents the case where an attempt was made to fetch a token when there is no signed
+ in user.
+ */
+ FIRErrorCodeNoSignedInUser,
+};
+
+
+@interface FAuthStateListenerWrapper : NSObject
+
+@property (nonatomic, copy) fbt_void_nsstring listener;
+
+@property (nonatomic, weak) id<FIRFirebaseAppLike> app;
+
+@end
+
+@implementation FAuthStateListenerWrapper
+
+- (instancetype) initWithListener:(fbt_void_nsstring)listener app:(id<FIRFirebaseAppLike>)app {
+ self = [super init];
+ if (self != nil) {
+ self->_listener = listener;
+ self->_app = app;
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(authStateDidChangeNotification:)
+ name:FIRAuthStateDidChangeInternalNotification
+ object:nil];
+ }
+ return self;
+}
+
+- (void) authStateDidChangeNotification:(NSNotification *)notification {
+ id<FIRFirebaseAuthLike> auth = notification.object;
+ if (auth.app == self->_app) {
+ NSDictionary *userInfo = notification.userInfo;
+ NSString *token = userInfo[FIRAuthStateDidChangeInternalNotificationTokenKey];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ self.listener(token);
+ });
+ }
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+@end
+
+
+@interface FIRFirebearAuthTokenProvider : NSObject <FAuthTokenProvider>
+
+@property (nonatomic, strong) id<FIRFirebaseAppLike> app;
+/** Strong references to the auth listeners as they are only weak in FIRFirebaseApp */
+@property (nonatomic, strong) NSMutableArray *authListeners;
+
+- (instancetype) initWithFirebaseApp:(id<FIRFirebaseAppLike>)app;
+
+@end
+
+@implementation FIRFirebearAuthTokenProvider
+
+- (instancetype) initWithFirebaseApp:(id<FIRFirebaseAppLike>)app {
+ self = [super init];
+ if (self != nil) {
+ self->_app = app;
+ self->_authListeners = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback {
+ // TODO: Don't fetch token if there is no current user
+ [self.app getTokenForcingRefresh:forceRefresh withCallback:^(NSString * _Nullable token, NSError * _Nullable error) {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ if (error != nil) {
+ if (error.code == FIRErrorCodeNoAuth) {
+ FFLog(@"I-RDB073001", @"Firebase Auth is not configured, not going to use authentication.");
+ callback(nil, nil);
+ } else if (error.code == FIRErrorCodeNoSignedInUser) {
+ // No signed in user is an expected case, callback as success with no token
+ callback(nil, nil);
+ } else {
+ callback(nil, error);
+ }
+ } else {
+ callback(token, nil);
+ }
+ });
+ }];
+}
+
+- (void) listenForTokenChanges:(_Nonnull fbt_void_nsstring)listener {
+ FAuthStateListenerWrapper *wrapper = [[FAuthStateListenerWrapper alloc] initWithListener:listener app:self.app];
+ [self.authListeners addObject:wrapper];
+}
+
+@end
+
+@implementation FAuthTokenProvider
+
++ (id<FAuthTokenProvider>) authTokenProviderForApp:(id)app {
+ return [[FIRFirebearAuthTokenProvider alloc] initWithFirebaseApp:app];
+}
+
+@end
diff --git a/Firebase/Database/Login/FIRNoopAuthTokenProvider.h b/Firebase/Database/Login/FIRNoopAuthTokenProvider.h
new file mode 100644
index 0000000..e27ddb4
--- /dev/null
+++ b/Firebase/Database/Login/FIRNoopAuthTokenProvider.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 <Foundation/Foundation.h>
+#import "FAuthTokenProvider.h"
+
+@interface FIRNoopAuthTokenProvider : NSObject <FAuthTokenProvider>
+
+@end
diff --git a/Firebase/Database/Login/FIRNoopAuthTokenProvider.m b/Firebase/Database/Login/FIRNoopAuthTokenProvider.m
new file mode 100644
index 0000000..8bf467b
--- /dev/null
+++ b/Firebase/Database/Login/FIRNoopAuthTokenProvider.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 "FIRNoopAuthTokenProvider.h"
+#import "FAuthTokenProvider.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@implementation FIRNoopAuthTokenProvider
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ callback(nil, nil);
+ });
+}
+
+- (void) listenForTokenChanges:(fbt_void_nsstring)listener {
+ // no-op, because token never changes
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FCachePolicy.h b/Firebase/Database/Persistence/FCachePolicy.h
new file mode 100644
index 0000000..16b49fb
--- /dev/null
+++ b/Firebase/Database/Persistence/FCachePolicy.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 <Foundation/Foundation.h>
+
+@protocol FCachePolicy <NSObject>
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries;
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck;
+- (float)percentOfQueriesToPruneAtOnce;
+- (NSUInteger)maxNumberOfQueriesToKeep;
+
+@end
+
+
+@interface FLRUCachePolicy : NSObject<FCachePolicy>
+
+@property (nonatomic, readonly) NSUInteger maxSize;
+
+- (id)initWithMaxSize:(NSUInteger)maxSize;
+
+@end
+
+@interface FNoCachePolicy : NSObject<FCachePolicy>
+
++ (FNoCachePolicy *)noCachePolicy;
+
+@end
diff --git a/Firebase/Database/Persistence/FCachePolicy.m b/Firebase/Database/Persistence/FCachePolicy.m
new file mode 100644
index 0000000..7da76ef
--- /dev/null
+++ b/Firebase/Database/Persistence/FCachePolicy.m
@@ -0,0 +1,79 @@
+/*
+ * 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 "FCachePolicy.h"
+
+@interface FLRUCachePolicy ()
+
+@property (nonatomic, readwrite) NSUInteger maxSize;
+
+@end
+
+static const NSUInteger kFServerUpdatesBetweenCacheSizeChecks = 1000;
+static const NSUInteger kFMaxNumberOfPrunableQueriesToKeep = 1000;
+static const float kFPercentOfQueriesToPruneAtOnce = 0.2f;
+
+@implementation FLRUCachePolicy
+
+- (id)initWithMaxSize:(NSUInteger)maxSize {
+ self = [super init];
+ if (self != nil) {
+ self->_maxSize = maxSize;
+ }
+ return self;
+}
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries {
+ return cacheSize > self.maxSize || numTrackedQueries > kFMaxNumberOfPrunableQueriesToKeep;
+}
+
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck {
+ return serverUpdatesSinceLastCheck > kFServerUpdatesBetweenCacheSizeChecks;
+}
+
+- (float)percentOfQueriesToPruneAtOnce {
+ return kFPercentOfQueriesToPruneAtOnce;
+}
+
+- (NSUInteger)maxNumberOfQueriesToKeep {
+ return kFMaxNumberOfPrunableQueriesToKeep;
+}
+
+@end
+
+@implementation FNoCachePolicy
+
++ (FNoCachePolicy *)noCachePolicy {
+ return [[FNoCachePolicy alloc] init];
+}
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries {
+ return NO;
+}
+
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck {
+ return NO;
+}
+
+- (float)percentOfQueriesToPruneAtOnce {
+ return 0;
+}
+
+- (NSUInteger)maxNumberOfQueriesToKeep {
+ return NSUIntegerMax;
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FLevelDBStorageEngine.h b/Firebase/Database/Persistence/FLevelDBStorageEngine.h
new file mode 100644
index 0000000..059a071
--- /dev/null
+++ b/Firebase/Database/Persistence/FLevelDBStorageEngine.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 <Foundation/Foundation.h>
+
+#import "FStorageEngine.h"
+#import "FNode.h"
+#import "FPath.h"
+#import "FCompoundWrite.h"
+#import "FQuerySpec.h"
+
+@class FCacheNode;
+@class FTrackedQuery;
+@class FPruneForest;
+@class FRepoInfo;
+
+@interface FLevelDBStorageEngine : NSObject<FStorageEngine>
+
+- (id)initWithPath:(NSString *)path;
+
+- (void)runLegacyMigration:(FRepoInfo *)info;
+- (void)purgeEverything;
+
+@end
diff --git a/Firebase/Database/Persistence/FLevelDBStorageEngine.m b/Firebase/Database/Persistence/FLevelDBStorageEngine.m
new file mode 100644
index 0000000..4b324b8
--- /dev/null
+++ b/Firebase/Database/Persistence/FLevelDBStorageEngine.m
@@ -0,0 +1,717 @@
+/*
+ * 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 "FLevelDBStorageEngine.h"
+
+#import "APLevelDB.h"
+#import "FSnapshotUtilities.h"
+#import "FWriteRecord.h"
+#import "FTrackedQuery.h"
+#import "FQueryParams.h"
+#import "FEmptyNode.h"
+#import "FPruneForest.h"
+#import "FUtilities.h"
+#import "FPendingPut.h" // For legacy migration
+
+@interface FLevelDBStorageEngine ()
+
+@property (nonatomic, strong) NSString *basePath;
+@property (nonatomic, strong) APLevelDB *writesDB;
+@property (nonatomic, strong) APLevelDB *serverCacheDB;
+
+@end
+
+// WARNING: If you change this, you need to write a migration script
+static NSString * const kFPersistenceVersion = @"1";
+
+static NSString * const kFServerDBPath = @"server_data";
+static NSString * const kFWritesDBPath = @"writes";
+
+static NSString * const kFUserWriteId = @"id";
+static NSString * const kFUserWritePath = @"path";
+static NSString * const kFUserWriteOverwrite = @"o";
+static NSString * const kFUserWriteMerge = @"m";
+
+static NSString * const kFTrackedQueryId = @"id";
+static NSString * const kFTrackedQueryPath = @"path";
+static NSString * const kFTrackedQueryParams = @"p";
+static NSString * const kFTrackedQueryLastUse = @"lu";
+static NSString * const kFTrackedQueryIsComplete = @"c";
+static NSString * const kFTrackedQueryIsActive = @"a";
+
+static NSString * const kFServerCachePrefix = @"/server_cache/";
+// '~' is the last non-control character in the ASCII table until 127
+// We wan't the entire range of thing stored in the DB
+static NSString * const kFServerCacheRangeEnd = @"/server_cache~";
+static NSString * const kFTrackedQueriesPrefix = @"/tracked_queries/";
+static NSString * const kFTrackedQueryKeysPrefix = @"/tracked_query_keys/";
+
+// Failed to load JSON because a valid JSON turns out to be NaN while deserializing
+static const NSInteger kFNanFailureCode = 3840;
+
+static NSString* writeRecordKey(NSUInteger writeId) {
+ return [NSString stringWithFormat:@"%lu", (unsigned long)(writeId)];
+}
+
+static NSString* serverCacheKey(FPath *path) {
+ return [NSString stringWithFormat:@"%@%@", kFServerCachePrefix, ([path toStringWithTrailingSlash])];
+}
+
+static NSString* trackedQueryKey(NSUInteger trackedQueryId) {
+ return [NSString stringWithFormat:@"%@%lu", kFTrackedQueriesPrefix, (unsigned long)trackedQueryId];
+}
+
+static NSString* trackedQueryKeysKeyPrefix(NSUInteger trackedQueryId) {
+ return [NSString stringWithFormat:@"%@%lu/", kFTrackedQueryKeysPrefix, (unsigned long)trackedQueryId];
+}
+
+static NSString* trackedQueryKeysKey(NSUInteger trackedQueryId, NSString *key) {
+ return [NSString stringWithFormat:@"%@%lu/%@", kFTrackedQueryKeysPrefix, (unsigned long)trackedQueryId, key];
+}
+
+@implementation FLevelDBStorageEngine
+#pragma mark - Constructors
+
+- (id)initWithPath:(NSString*)dbPath
+{
+ self = [super init];
+ if (self) {
+ self.basePath = [[FLevelDBStorageEngine firebaseDir] stringByAppendingPathComponent:dbPath];
+ /* For reference:
+ serverDataDB = [aPersistence createDbByName:@"server_data"];
+ FPangolinDB *completenessDb = [aPersistence createDbByName:@"server_complete"];
+ */
+ [FLevelDBStorageEngine ensureDir:self.basePath markAsDoNotBackup:YES];
+ [self runMigration];
+ [self openDatabases];
+ }
+ return self;
+}
+
+- (void)runMigration {
+ // Currently we're at version 1, so all we need to do is write that to a file
+ NSString *versionFile = [self.basePath stringByAppendingPathComponent:@"version"];
+ NSError *error;
+ NSString *oldVersion = [NSString stringWithContentsOfFile:versionFile encoding:NSUTF8StringEncoding error:&error];
+ if (!oldVersion) {
+ // This is probably fine, we don't have a version file yet
+ BOOL success = [kFPersistenceVersion writeToFile:versionFile atomically:NO encoding:NSUTF8StringEncoding error:&error];
+ if (!success) {
+ FFWarn(@"I-RDB076001", @"Failed to write version for database: %@", error);
+ }
+ } else if ([oldVersion isEqualToString:kFPersistenceVersion]) {
+ // Everythings fine no need for migration
+ } else {
+ // If we add more versions in the future, we need to run migration here
+ [NSException raise:NSInternalInconsistencyException format:@"Unrecognized database version: %@", oldVersion];
+ }
+}
+
+- (void)runLegacyMigration:(FRepoInfo *)info {
+ NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDir = [dirPaths objectAtIndex:0];
+ NSString *firebaseDir = [documentsDir stringByAppendingPathComponent:@"firebase"];
+ NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", info.host, info.namespace];
+ NSString *legacyBaseDir = [NSString stringWithFormat:@"%@/1/%@/v1", firebaseDir, repoHashString];
+ if ([[NSFileManager defaultManager] fileExistsAtPath:legacyBaseDir]) {
+ FFWarn(@"I-RDB076002", @"Legacy database found, migrating...");
+ // We only need to migrate writes
+ NSError *error = nil;
+ APLevelDB *writes = [APLevelDB levelDBWithPath:[legacyBaseDir stringByAppendingPathComponent:@"outstanding_puts"] error:&error];
+ if (writes != nil) {
+ __block NSUInteger numberOfWritesRestored = 0;
+ // Maybe we could use write batches, but what the heck, I'm sure it'll go fine :P
+ [writes enumerateKeysAndValuesAsData:^(NSString *key, NSData *data, BOOL *stop) {
+ id pendingPut = [NSKeyedUnarchiver unarchiveObjectWithData:data];
+ if ([pendingPut isKindOfClass:[FPendingPut class]]) {
+ FPendingPut *put = pendingPut;
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:put.data priority:put.priority];
+ [self saveUserOverwrite:newNode atPath:put.path writeId:[key integerValue]];
+ numberOfWritesRestored++;
+ } else if ([pendingPut isKindOfClass:[FPendingPutPriority class]]) {
+ // This is for backwards compatibility. Older clients will save FPendingPutPriority. New ones will need to read it and translate.
+ FPendingPutPriority *putPriority = pendingPut;
+ FPath *priorityPath = [putPriority.path childFromString:@".priority"];
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:putPriority.priority priority:nil];
+ [self saveUserOverwrite:newNode atPath:priorityPath writeId:[key integerValue]];
+ numberOfWritesRestored++;
+ } else if ([pendingPut isKindOfClass:[FPendingUpdate class]]) {
+ FPendingUpdate *update = pendingPut;
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:update.data];
+ [self saveUserMerge:merge atPath:update.path writeId:[key integerValue]];
+ numberOfWritesRestored++;
+ } else {
+ FFWarn(@"I-RDB076003", @"Failed to migrate legacy write, meh!");
+ }
+ }];
+ FFWarn(@"I-RDB076004", @"Migrated %lu writes", (unsigned long)numberOfWritesRestored);
+ [writes close];
+ FFWarn(@"I-RDB076005", @"Deleting legacy database...");
+ BOOL success = [[NSFileManager defaultManager] removeItemAtPath:legacyBaseDir error:&error];
+ if (!success) {
+ FFWarn(@"I-RDB076006", @"Failed to delete legacy database: %@", error);
+ } else {
+ FFWarn(@"I-RDB076007", @"Finished migrating legacy database.");
+ }
+ } else {
+ FFWarn(@"I-RDB076008", @"Failed to migrate old database: %@", error);
+ }
+ }
+}
+
+- (void)openDatabases {
+ self.serverCacheDB = [self createDB:kFServerDBPath];
+ self.writesDB = [self createDB:kFWritesDBPath];
+}
+
+- (void)purgeEverything {
+ [self close];
+ [@[kFServerDBPath, kFWritesDBPath]
+ enumerateObjectsUsingBlock:^(NSString *dbPath, NSUInteger idx, BOOL *stop) {
+ NSString *path = [self.basePath stringByAppendingPathComponent:dbPath];
+ NSError *error;
+ FFDebug(@"I-RDB076009", @"Deleting database at path %@", path);
+ BOOL success = [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
+ if (!success) {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to delete database files: %@", error];
+ }
+ }];
+
+ [self openDatabases];
+}
+
+- (void)close {
+ // autoreleasepool will cause deallocation which will close the DB
+ @autoreleasepool {
+ [self.serverCacheDB close];
+ self.serverCacheDB = nil;
+ [self.writesDB close];
+ self.writesDB = nil;
+ }
+}
+
++ (NSString *) firebaseDir {
+#if TARGET_OS_IPHONE
+ NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDir = [dirPaths objectAtIndex:0];
+ return [documentsDir stringByAppendingPathComponent:@"firebase"];
+#else // this must be OSX then
+ return [NSHomeDirectory() stringByAppendingPathComponent:@".firebase"];
+#endif
+}
+
+- (APLevelDB *)createDB:(NSString *)name {
+ NSError *err = nil;
+ NSString *path = [self.basePath stringByAppendingPathComponent:name];
+ APLevelDB *db = [APLevelDB levelDBWithPath:path error:&err];
+ if(err) {
+ NSString *reason = [NSString stringWithFormat:@"Error initializing persistence: %@", [err description]];
+ @throw [NSException exceptionWithName:@"FirebaseDatabasePersistenceFailure" reason:reason userInfo:nil];
+ }
+ return db;
+}
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ NSDictionary *write =
+ @{ kFUserWriteId: @(writeId),
+ kFUserWritePath: [path toStringWithTrailingSlash],
+ kFUserWriteOverwrite: [node valForExport:YES] };
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:write options:0 error:&error];
+ NSAssert(data, @"Failed to serialize user overwrite: %@, (Error: %@)", write, error);
+ [self.writesDB setData:data forKey:writeRecordKey(writeId)];
+}
+
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ NSDictionary *write =
+ @{ kFUserWriteId: @(writeId),
+ kFUserWritePath: [path toStringWithTrailingSlash],
+ kFUserWriteMerge: [merge valForExport:YES] };
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:write options:0 error:&error];
+ NSAssert(data, @"Failed to serialize user merge: %@ (Error: %@)", write, error);
+ [self.writesDB setData:data forKey:writeRecordKey(writeId)];
+}
+
+- (void)removeUserWrite:(NSUInteger)writeId {
+ [self.writesDB removeKey:writeRecordKey(writeId)];
+}
+
+- (void)removeAllUserWrites {
+ __block NSUInteger count = 0;
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.writesDB beginWriteBatch];
+ [self.writesDB enumerateKeys:^(NSString *key, BOOL *stop) {
+ [batch removeKey:key];
+ count++;
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076010", @"Failed to remove all users writes on disk!");
+ } else {
+ FFDebug(@"I-RDB076011", @"Removed %lu writes in %fms", (unsigned long)count, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (NSArray *)userWrites {
+ NSDate *date = [NSDate date];
+ NSMutableArray *writes = [NSMutableArray array];
+ [self.writesDB enumerateKeysAndValuesAsData:^(NSString *key, NSData *data, BOOL *stop) {
+ NSError *error = nil;
+ NSDictionary *writeJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
+ if (writeJSON == nil) {
+ if (error.code == kFNanFailureCode) {
+ FFWarn(@"I-RDB076012", @"Failed to deserialize write (%@), likely because of out of range doubles (Error: %@)",
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],
+ error);
+ FFWarn(@"I-RDB076013", @"Removing failed write with key %@", key);
+ [self.writesDB removeKey:key];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialize write: %@", error];
+ }
+ } else {
+ NSInteger writeId = ((NSNumber *)writeJSON[kFUserWriteId]).integerValue;
+ FPath *path = [FPath pathWithString:writeJSON[kFUserWritePath]];
+ FWriteRecord *writeRecord;
+ if (writeJSON[kFUserWriteMerge] != nil) {
+ // It's a merge
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:writeJSON[kFUserWriteMerge]];
+ writeRecord = [[FWriteRecord alloc] initWithPath:path merge:merge writeId:writeId];
+ } else {
+ // It's an overwrite
+ NSAssert(writeJSON[kFUserWriteOverwrite] != nil, @"Persisted write did not contain merge or overwrite!");
+ id<FNode> node = [FSnapshotUtilities nodeFrom:writeJSON[kFUserWriteOverwrite]];
+ writeRecord = [[FWriteRecord alloc] initWithPath:path overwrite:node writeId:writeId visible:YES];
+ }
+ [writes addObject:writeRecord];
+ }
+ }];
+ // Make sure writes are sorted
+ [writes sortUsingComparator:^NSComparisonResult(FWriteRecord *one, FWriteRecord *two) {
+ if (one.writeId < two.writeId) {
+ return NSOrderedAscending;
+ } else if (one.writeId > two.writeId) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ }];
+ FFDebug(@"I-RDB076014", @"Loaded %lu writes in %fms", (unsigned long)writes.count, [date timeIntervalSinceNow]*-1000);
+ return writes;
+}
+
+- (id<FNode>)serverCacheAtPath:(FPath *)path {
+ NSDate *start = [NSDate date];
+ id data = [self internalNestedDataForPath:path];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:data];
+ FFDebug(@"I-RDB076015", @"Loaded node with %d children at %@ in %fms", [node numChildren], path, [start timeIntervalSinceNow]*-1000);
+ return node;
+}
+
+- (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path {
+ NSDate *start = [NSDate date];
+ __block id<FNode> node = [FEmptyNode emptyNode];
+ [keys enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ id data = [self internalNestedDataForPath:[path childFromString:key]];
+ node = [node updateImmediateChild:key withNewChild:[FSnapshotUtilities nodeFrom:data]];
+ }];
+ FFDebug(@"I-RDB076016", @"Loaded node with %d children for %lu keys at %@ in %fms", [node numChildren], (unsigned long)keys.count, path, [start timeIntervalSinceNow]*-1000);
+ return node;
+}
+
+- (void)updateServerCache:(id<FNode>)node atPath:(FPath *)path merge:(BOOL)merge {
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ // Remove any leaf nodes that might be higher up
+ [self removeAllLeafNodesOnPath:path batch:batch];
+ __block NSUInteger counter = 0;
+ if (merge) {
+ // remove any children that exist
+ [node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ FPath *childPath = [path childFromString:childKey];
+ [self removeAllWithPrefix:serverCacheKey(childPath) batch:batch database:self.serverCacheDB];
+ [self saveNodeInternal:childNode atPath:childPath batch:batch counter:&counter];
+ }];
+ } else {
+ // remove everything
+ [self removeAllWithPrefix:serverCacheKey(path) batch:batch database:self.serverCacheDB];
+ [self saveNodeInternal:node atPath:path batch:batch counter:&counter];
+ }
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076017", @"Failed to update server cache on disk!");
+ } else {
+ FFDebug(@"I-RDB076018", @"Saved %lu leaf nodes for overwrite in %fms", (unsigned long)counter, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path {
+ NSDate *start = [NSDate date];
+ __block NSUInteger counter = 0;
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ // Remove any leaf nodes that might be higher up
+ [self removeAllLeafNodesOnPath:path batch:batch];
+ [merge enumerateWrites:^(FPath *relativePath, id<FNode> node, BOOL *stop) {
+ FPath *childPath = [path child:relativePath];
+ [self removeAllWithPrefix:serverCacheKey(childPath) batch:batch database:self.serverCacheDB];
+ [self saveNodeInternal:node atPath:childPath batch:batch counter:&counter];
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076019", @"Failed to update server cache on disk!");
+ } else {
+ FFDebug(@"I-RDB076020", @"Saved %lu leaf nodes for merge in %fms", (unsigned long)counter, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)saveNodeInternal:(id<FNode>)node atPath:(FPath *)path batch:(id<APLevelDBWriteBatch>)batch counter:(NSUInteger *)counter {
+ id data = [node valForExport:YES];
+ if(data != nil && ![data isKindOfClass:[NSNull class]]) {
+ [self internalSetNestedData:data forKey:serverCacheKey(path) withBatch:batch counter:counter];
+ }
+}
+
+- (NSUInteger)serverCacheEstimatedSizeInBytes {
+ // Use the exact size, because for pruning the approximate size can lead to weird situations where we prune everything
+ // because no compaction is ever run
+ return [self.serverCacheDB exactSizeFrom:kFServerCachePrefix to:kFServerCacheRangeEnd];
+}
+
+- (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)path {
+ // TODO: be more intelligent, don't scan entire database...
+
+ __block NSUInteger pruned = 0;
+ __block NSUInteger kept = 0;
+ NSDate *start = [NSDate date];
+
+ NSString *prefix = serverCacheKey(path);
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+
+ [self.serverCacheDB enumerateKeysWithPrefix:prefix usingBlock:^(NSString *dbKey, BOOL *stop) {
+ NSString *pathStr = [dbKey substringFromIndex:prefix.length];
+ FPath *relativePath = [[FPath alloc] initWith:pathStr];
+ if ([pruneForest shouldPruneUnkeptDescendantsAtPath:relativePath]) {
+ pruned++;
+ [batch removeKey:dbKey];
+ } else {
+ kept++;
+ }
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076021", @"Failed to prune cache on disk!");
+ } else {
+ FFDebug(@"I-RDB076022", @"Pruned %lu paths, kept %lu paths in %fms", (unsigned long)pruned, (unsigned long)kept, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+#pragma mark - Tracked Queries
+
+- (NSArray *)loadTrackedQueries {
+ NSDate *date = [NSDate date];
+ NSMutableArray *trackedQueries = [NSMutableArray array];
+ [self.serverCacheDB enumerateKeysWithPrefix:kFTrackedQueriesPrefix asData:^(NSString *key, NSData *data, BOOL *stop) {
+ NSError *error = nil;
+ NSDictionary *queryJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
+ if (queryJSON == nil) {
+ if (error.code == kFNanFailureCode) {
+ FFWarn(@"I-RDB076023", @"Failed to deserialize tracked query (%@), likely because of out of range doubles (Error: %@)",
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],
+ error);
+ FFWarn(@"I-RDB076024", @"Removing failed tracked query with key %@", key);
+ [self.serverCacheDB removeKey:key];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialize tracked query: %@", error];
+ }
+ } else {
+ NSUInteger queryId = ((NSNumber *)queryJSON[kFTrackedQueryId]).unsignedIntegerValue;
+ FPath *path = [FPath pathWithString:queryJSON[kFTrackedQueryPath]];
+ FQueryParams *params = [FQueryParams fromQueryObject:queryJSON[kFTrackedQueryParams]];
+ FQuerySpec *query = [[FQuerySpec alloc] initWithPath:path params:params];
+ BOOL isComplete = [queryJSON[kFTrackedQueryIsComplete] boolValue];
+ BOOL isActive = [queryJSON[kFTrackedQueryIsActive] boolValue];
+ NSTimeInterval lastUse = [queryJSON[kFTrackedQueryLastUse] doubleValue];
+
+ FTrackedQuery *trackedQuery = [[FTrackedQuery alloc] initWithId:queryId
+ query:query
+ lastUse:lastUse
+ isActive:isActive
+ isComplete:isComplete];
+
+ [trackedQueries addObject:trackedQuery];
+ }
+ }];
+ FFDebug(@"I-RDB076025", @"Loaded %lu tracked queries in %fms", (unsigned long)trackedQueries.count, [date timeIntervalSinceNow]*-1000);
+ return trackedQueries;
+}
+
+- (void)removeTrackedQuery:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ [batch removeKey:trackedQueryKey(queryId)];
+ __block NSUInteger keyCount = 0;
+ [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) usingBlock:^(NSString *key, BOOL *stop) {
+ [batch removeKey:key];
+ keyCount++;
+ }];
+
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076026", @"Failed to remove tracked query on disk!");
+ } else {
+ FFDebug(@"I-RDB076027", @"Removed query with id %lu (and removed %lu keys) in %fms",
+ (unsigned long)queryId,
+ (unsigned long)keyCount,
+ [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)saveTrackedQuery:(FTrackedQuery *)query {
+ NSDate *start = [NSDate date];
+ NSDictionary *trackedQuery =
+ @{
+ kFTrackedQueryId: @(query.queryId),
+ kFTrackedQueryPath: [query.query.path toStringWithTrailingSlash],
+ kFTrackedQueryParams: [query.query.params wireProtocolParams],
+ kFTrackedQueryLastUse: @(query.lastUse),
+ kFTrackedQueryIsComplete: @(query.isComplete),
+ kFTrackedQueryIsActive: @(query.isActive)
+ };
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:trackedQuery options:0 error:&error];
+ NSAssert(data, @"Failed to serialize tracked query (Error: %@)", error);
+ [self.serverCacheDB setData:data forKey:trackedQueryKey(query.queryId)];
+ FFDebug(@"I-RDB076028", @"Saved tracked query %lu in %fms", (unsigned long)query.queryId, [start timeIntervalSinceNow]*-1000);
+}
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ __block NSUInteger removed = 0;
+ __block NSUInteger added = 0;
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ NSMutableSet *seenKeys = [NSMutableSet set];
+ // First, delete any keys that might be stored and are not part of the current keys
+ [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) asStrings:^(NSString *dbKey, NSString *actualKey, BOOL *stop) {
+ if ([keys containsObject:actualKey]) {
+ // Already in DB
+ [seenKeys addObject:actualKey];
+ } else {
+ // Not part of set, delete key
+ [batch removeKey:dbKey];
+ removed++;
+ }
+ }];
+
+ // Next add any keys that are missing in the database
+ [keys enumerateObjectsUsingBlock:^(NSString *childKey, BOOL *stop) {
+ if (![seenKeys containsObject:childKey]) {
+ [batch setString:childKey forKey:trackedQueryKeysKey(queryId, childKey)];
+ added++;
+ }
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076029", @"Failed to set tracked queries on disk!");
+ } else {
+ FFDebug(@"I-RDB076030", @"Set %lu tracked keys (%lu added, %lu removed) for query %lu in %fms",
+ (unsigned long)keys.count,
+ (unsigned long)added,
+ (unsigned long)removed,
+ (unsigned long)queryId,
+ [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQueryId:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ [removed enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ [batch removeKey:trackedQueryKeysKey(queryId, key)];
+ }];
+ [added enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ [batch setString:key forKey:trackedQueryKeysKey(queryId, key)];
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076031", @"Failed to update tracked queries on disk!");
+ } else {
+ FFDebug(@"I-RDB076032", @"Added %lu tracked keys, removed %lu for query %lu in %fms", (unsigned long)added.count, (unsigned long)removed.count, (unsigned long)queryId, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ NSMutableSet *set = [NSMutableSet set];
+ [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) asStrings:^(NSString *dbKey, NSString *actualKey, BOOL *stop) {
+ [set addObject:actualKey];
+ }];
+ FFDebug(@"I-RDB076033", @"Loaded %lu tracked keys for query %lu in %fms", (unsigned long)set.count, (unsigned long)queryId, [start timeIntervalSinceNow]*-1000);
+ return set;
+}
+
+#pragma mark - Internal methods
+
+- (void)removeAllLeafNodesOnPath:(FPath *)path batch:(id<APLevelDBWriteBatch>)batch {
+ while (!path.isEmpty) {
+ [batch removeKey:serverCacheKey(path)];
+ path = [path parent];
+ }
+ // Make sure to delete any nodes at the root
+ [batch removeKey:serverCacheKey([FPath empty])];
+}
+
+- (void)removeAllWithPrefix:(NSString *)prefix batch:(id<APLevelDBWriteBatch>)batch database:(APLevelDB *)database {
+ assert(prefix != nil);
+
+ [database enumerateKeysWithPrefix:prefix usingBlock:^(NSString *key, BOOL *stop) {
+ [batch removeKey:key];
+ }];
+}
+
+#pragma mark - Internal helper methods
+
+- (void)internalSetNestedData:(id)value forKey:(NSString *)key withBatch:(id<APLevelDBWriteBatch>)batch counter:(NSUInteger *)counter {
+ if([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dictionary = value;
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id childKey, id obj, BOOL *stop) {
+ assert(obj != nil);
+ NSString* childPath = [NSString stringWithFormat:@"%@%@/", key, childKey];
+ [self internalSetNestedData:obj forKey:childPath withBatch:batch counter:counter];
+ }];
+ }
+ else {
+ NSData *data = [self serializePrimitive:value];
+ [batch setData:data forKey:key];
+ (*counter)++;
+ }
+}
+
+- (id)internalNestedDataForPath:(FPath *)path {
+ NSAssert(path != nil, @"Path was nil!");
+
+ NSString *baseKey = serverCacheKey(path);
+
+ // HACK to make sure iter is freed now to avoid race conditions (if self.db is deleted before iter, you get an access violation).
+ @autoreleasepool {
+ APLevelDBIterator* iter = [APLevelDBIterator iteratorWithLevelDB:self.serverCacheDB];
+
+ [iter seekToKey:baseKey];
+ if (iter.key == nil || ![iter.key hasPrefix:baseKey]) {
+ // No data.
+ return nil;
+ } else {
+ return [self internalNestedDataFromIterator:iter andKeyPrefix:baseKey];
+ }
+ }
+}
+
+- (id) internalNestedDataFromIterator:(APLevelDBIterator*)iterator andKeyPrefix:(NSString*)prefix {
+ NSString* key = iterator.key;
+
+ if ([key isEqualToString:prefix]) {
+ id result = [self deserializePrimitive:iterator.valueAsData];
+ [iterator nextKey];
+ return result;
+ } else {
+ NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
+ while (key != nil && [key hasPrefix:prefix]) {
+ NSString *relativePath = [key substringFromIndex:prefix.length];
+ NSArray* pathPieces = [relativePath componentsSeparatedByString:@"/"];
+ assert(pathPieces.count > 0);
+ NSString *childName = pathPieces[0];
+ NSString *childPath = [NSString stringWithFormat:@"%@%@/", prefix, childName];
+ id childValue = [self internalNestedDataFromIterator:iterator andKeyPrefix:childPath];
+ [dict setValue:childValue forKey:childName];
+
+ key = iterator.key;
+ }
+ return dict;
+ }
+}
+
+
+- (NSData*) serializePrimitive:(id)value {
+ // HACK: The built-in serialization only works on dicts and arrays. So we create an array and then strip off
+ // the leading / trailing byte (the [ and ]).
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:@[value] options:0 error:&error];
+ NSAssert(data, @"Failed to serialize primitive: %@", error);
+
+ return [data subdataWithRange:NSMakeRange(1, data.length - 2)];
+}
+
+- (id)fixDoubleParsing:(id)value {
+ // The parser for double values in JSONSerialization at the root takes some short-cuts and delivers wrong results
+ // (wrong rounding) for some double values, including 2.47. Because we use the exact bytes for hashing on the server
+ // this will lead to hash mismatches. The parser of NSNumber seems to be more in line with what the server expects,
+ // so we use that here
+ if ([value isKindOfClass:[NSNumber class]]) {
+ CFNumberType type = CFNumberGetType((CFNumberRef)value);
+ if (type == kCFNumberDoubleType || type == kCFNumberFloatType) {
+ // The NSJSON parser returns all numbers as double values, even those that contain no exponent. To
+ // make sure that the String conversion below doesn't unexpectedly reduce precision, we make sure that
+ // our number is indeed not an integer.
+ if ((double)(long long)[value doubleValue] != [value doubleValue]) {
+ NSString *doubleString = [value stringValue];
+ return [NSNumber numberWithDouble:[doubleString doubleValue]];
+ }
+ }
+ }
+ return value;
+}
+
+- (id) deserializePrimitive:(NSData*)data {
+ NSError *error = nil;
+ id result = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
+ if (result != nil) {
+ return [self fixDoubleParsing:result];
+ } else {
+ if (error.code == kFNanFailureCode) {
+ FFWarn(@"I-RDB076034", @"Failed to load primitive %@, likely because doubles where out of range (Error: %@)",
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding], error);
+ return [NSNull null];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialiaze primitive: %@", error];
+ return nil;
+ }
+ }
+
+}
+
++ (void)ensureDir:(NSString*)path markAsDoNotBackup:(BOOL)markAsDoNotBackup {
+ NSError* error;
+ BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&error];
+ if (!success) {
+ @throw [NSException exceptionWithName:@"FailedToCreatePersistenceDir" reason:@"Failed to create persistence directory." userInfo:@{ @"path": path }];
+ }
+
+ if (markAsDoNotBackup) {
+ NSURL *firebaseDirURL = [NSURL fileURLWithPath:path];
+ success = [firebaseDirURL setResourceValue:@YES
+ forKey:NSURLIsExcludedFromBackupKey
+ error:&error];
+ if (!success) {
+ FFWarn(@"I-RDB076035", @"Failed to mark firebase database folder as do not backup: %@", error);
+ [NSException raise:@"Error marking as do not backup" format:@"Failed to mark folder %@ as do not backup", firebaseDirURL];
+ }
+ }
+}
+
+
+@end
diff --git a/Firebase/Database/Persistence/FPendingPut.h b/Firebase/Database/Persistence/FPendingPut.h
new file mode 100644
index 0000000..0d8de55
--- /dev/null
+++ b/Firebase/Database/Persistence/FPendingPut.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 <Foundation/Foundation.h>
+#import "FPath.h"
+
+// These are all legacy classes and are used to migrate older persistence data base to newer ones
+// These classes should not be used in newer code
+
+@interface FPendingPut : NSObject<NSCoding>
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) id data;
+@property (nonatomic, strong) id priority;
+
+- (id) initWithPath:(FPath*)aPath andData:(id)aData andPriority:aPriority;
+- (void)encodeWithCoder:(NSCoder *)aCoder;
+- (id)initWithCoder:(NSCoder *)aDecoder;
+@end
+
+
+@interface FPendingPutPriority : NSObject<NSCoding>
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) id priority;
+
+- (id) initWithPath:(FPath*)aPath andPriority:(id)aPriority;
+- (void)encodeWithCoder:(NSCoder *)aCoder;
+- (id)initWithCoder:(NSCoder *)aDecoder;
+
+@end
+
+
+@interface FPendingUpdate : NSObject<NSCoding>
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) NSDictionary* data;
+
+- (id) initWithPath:(FPath*)aPath andData:(NSDictionary*)aData;
+- (void)encodeWithCoder:(NSCoder *)aCoder;
+- (id)initWithCoder:(NSCoder *)aDecoder;
+@end
diff --git a/Firebase/Database/Persistence/FPendingPut.m b/Firebase/Database/Persistence/FPendingPut.m
new file mode 100644
index 0000000..12be825
--- /dev/null
+++ b/Firebase/Database/Persistence/FPendingPut.m
@@ -0,0 +1,112 @@
+/*
+ * 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 "FPendingPut.h"
+
+@implementation FPendingPut
+
+@synthesize path;
+@synthesize data;
+
+- (id) initWithPath:(FPath *)aPath andData:(id)aData andPriority:(id)aPriority {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.data = aData;
+ self.priority = aPriority;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[self.path description] forKey:@"path"];
+ [aCoder encodeObject:self.data forKey:@"data"];
+ [aCoder encodeObject:self.priority forKey:@"priority"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super init];
+ if(self) {
+ self.path = [[FPath alloc] initWith:[aDecoder decodeObjectForKey:@"path"]];
+ self.data = [aDecoder decodeObjectForKey:@"data"];
+ self.priority = [aDecoder decodeObjectForKey:@"priority"];
+ }
+ return self;
+}
+
+@end
+
+
+@implementation FPendingPutPriority
+
+@synthesize path;
+@synthesize priority;
+
+- (id) initWithPath:(FPath *)aPath andPriority:(id)aPriority {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.priority = aPriority;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[self.path description] forKey:@"path"];
+ [aCoder encodeObject:self.priority forKey:@"priority"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super init];
+ if(self) {
+ self.path = [[FPath alloc] initWith:[aDecoder decodeObjectForKey:@"path"]];
+ self.priority = [aDecoder decodeObjectForKey:@"priority"];
+ }
+ return self;
+}
+
+@end
+
+
+@implementation FPendingUpdate
+
+@synthesize path;
+@synthesize data;
+
+- (id) initWithPath:(FPath *)aPath andData:(id)aData {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.data = aData;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[self.path description] forKey:@"path"];
+ [aCoder encodeObject:self.data forKey:@"data"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super init];
+ if(self) {
+ self.path = [[FPath alloc] initWith:[aDecoder decodeObjectForKey:@"path"]];
+ self.data = [aDecoder decodeObjectForKey:@"data"];
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FPersistenceManager.h b/Firebase/Database/Persistence/FPersistenceManager.h
new file mode 100644
index 0000000..a3688b3
--- /dev/null
+++ b/Firebase/Database/Persistence/FPersistenceManager.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 <Foundation/Foundation.h>
+
+#import "FNode.h"
+#import "FCompoundWrite.h"
+#import "FQuerySpec.h"
+#import "FRepoInfo.h"
+#import "FStorageEngine.h"
+#import "FCachePolicy.h"
+#import "FCacheNode.h"
+
+@interface FPersistenceManager : NSObject
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine cachePolicy:(id<FCachePolicy>)cachePolicy;
+- (void)close;
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)removeUserWrite:(NSUInteger)writeId;
+- (void)removeAllUserWrites;
+- (NSArray *)userWrites;
+
+- (FCacheNode *)serverCacheForQuery:(FQuerySpec *)spec;
+- (void)updateServerCacheWithNode:(id<FNode>)node forQuery:(FQuerySpec *)spec;
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path;
+
+- (void)applyUserWrite:(id<FNode>)write toServerCacheAtPath:(FPath *)path;
+- (void)applyUserMerge:(FCompoundWrite *)merge toServerCacheAtPath:(FPath *)path;
+
+- (void)setQueryComplete:(FQuerySpec *)spec;
+- (void)setQueryActive:(FQuerySpec *)spec;
+- (void)setQueryInactive:(FQuerySpec *)spec;
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQuery:(FQuerySpec *)query;
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQuery:(FQuerySpec *)query;
+
+@end
diff --git a/Firebase/Database/Persistence/FPersistenceManager.m b/Firebase/Database/Persistence/FPersistenceManager.m
new file mode 100644
index 0000000..fb38192
--- /dev/null
+++ b/Firebase/Database/Persistence/FPersistenceManager.m
@@ -0,0 +1,190 @@
+/*
+ * 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 "FPersistenceManager.h"
+#import "FLevelDBStorageEngine.h"
+#import "FCacheNode.h"
+#import "FIndexedNode.h"
+#import "FTrackedQueryManager.h"
+#import "FTrackedQuery.h"
+#import "FUtilities.h"
+#import "FPruneForest.h"
+#import "FClock.h"
+
+@interface FPersistenceManager ()
+
+@property (nonatomic, strong) id<FStorageEngine> storageEngine;
+@property (nonatomic, strong) id<FCachePolicy> cachePolicy;
+@property (nonatomic, strong) FTrackedQueryManager *trackedQueryManager;
+@property (nonatomic) NSUInteger serverCacheUpdatesSinceLastPruneCheck;
+
+@end
+
+@implementation FPersistenceManager
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine cachePolicy:(id<FCachePolicy>)cachePolicy {
+ self = [super init];
+ if (self != nil) {
+ self->_storageEngine = storageEngine;
+ self->_cachePolicy = cachePolicy;
+ self->_trackedQueryManager = [[FTrackedQueryManager alloc] initWithStorageEngine:self.storageEngine
+ clock:[FSystemClock clock]];
+ }
+ return self;
+}
+
+- (void)close {
+ [self.storageEngine close];
+ self.storageEngine = nil;
+ self.trackedQueryManager = nil;
+}
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ [self.storageEngine saveUserOverwrite:node atPath:path writeId:writeId];
+}
+
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ [self.storageEngine saveUserMerge:merge atPath:path writeId:writeId];
+}
+
+- (void)removeUserWrite:(NSUInteger)writeId {
+ [self.storageEngine removeUserWrite:writeId];
+}
+
+- (void)removeAllUserWrites {
+ [self.storageEngine removeAllUserWrites];
+}
+
+- (NSArray *)userWrites {
+ return [self.storageEngine userWrites];
+}
+
+- (FCacheNode *)serverCacheForQuery:(FQuerySpec *)query {
+ NSSet *trackedKeys;
+ BOOL complete;
+ // TODO[offline]: Should we use trackedKeys to find out if this location is a child of a complete query?
+ if ([self.trackedQueryManager isQueryComplete:query]) {
+ complete = YES;
+ FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query];
+ if (!query.loadsAllData && trackedQuery.isComplete) {
+ trackedKeys = [self.storageEngine trackedQueryKeysForQuery:trackedQuery.queryId];
+ } else {
+ trackedKeys = nil;
+ }
+ } else {
+ complete = NO;
+ trackedKeys = [self.trackedQueryManager knownCompleteChildrenAtPath:query.path];
+ }
+
+ id<FNode> node;
+ if (trackedKeys != nil) {
+ node = [self.storageEngine serverCacheForKeys:trackedKeys atPath:query.path];
+ } else {
+ node = [self.storageEngine serverCacheAtPath:query.path];
+ }
+
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:node index:query.index];
+ return [[FCacheNode alloc] initWithIndexedNode:indexedNode isFullyInitialized:complete isFiltered:(trackedKeys != nil)];
+}
+
+- (void)updateServerCacheWithNode:(id<FNode>)node forQuery:(FQuerySpec *)query {
+ BOOL merge = !query.loadsAllData;
+ [self.storageEngine updateServerCache:node atPath:query.path merge:merge];
+ [self setQueryComplete:query];
+ [self doPruneCheckAfterServerUpdate];
+}
+
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path {
+ [self.storageEngine updateServerCacheWithMerge:merge atPath:path];
+ [self doPruneCheckAfterServerUpdate];
+}
+
+- (void)applyUserMerge:(FCompoundWrite *)merge toServerCacheAtPath:(FPath *)path {
+ // TODO[offline]: rework this to be more efficient
+ [merge enumerateWrites:^(FPath *relativePath, id<FNode> node, BOOL *stop) {
+ [self applyUserWrite:node toServerCacheAtPath:[path child:relativePath]];
+ }];
+}
+
+- (void)applyUserWrite:(id<FNode>)write toServerCacheAtPath:(FPath *)path {
+ // This is a hack to guess whether we already cached this because we got a server data update for this
+ // write via an existing active default query. If we didn't, then we'll manually cache this and add a
+ // tracked query to mark it complete and keep it cached.
+ // Unfortunately this is just a guess and it's possible that we *did* get an update (e.g. via a filtered
+ // query) and by overwriting the cache here, we'll actually store an incorrect value (e.g. in the case
+ // that we wrote a ServerValue.TIMESTAMP and the server resolved it to a different value).
+ // TODO[offline]: Consider reworking.
+ if (![self.trackedQueryManager hasActiveDefaultQueryAtPath:path]) {
+ [self.storageEngine updateServerCache:write atPath:path merge:NO];
+ [self.trackedQueryManager ensureCompleteTrackedQueryAtPath:path];
+ }
+}
+
+- (void)setQueryComplete:(FQuerySpec *)query {
+ if (query.loadsAllData) {
+ [self.trackedQueryManager setQueriesCompleteAtPath:query.path];
+ } else {
+ [self.trackedQueryManager setQueryComplete:query];
+ }
+}
+
+- (void)setQueryActive:(FQuerySpec *)spec {
+ [self.trackedQueryManager setQueryActive:spec];
+}
+
+- (void)setQueryInactive:(FQuerySpec *)spec {
+ [self.trackedQueryManager setQueryInactive:spec];
+}
+
+- (void)doPruneCheckAfterServerUpdate {
+ self.serverCacheUpdatesSinceLastPruneCheck++;
+ if ([self.cachePolicy shouldCheckCacheSize:self.serverCacheUpdatesSinceLastPruneCheck]) {
+ FFDebug(@"I-RDB078001", @"Reached prune check threshold. Checking...");
+ NSDate *date = [NSDate date];
+ self.serverCacheUpdatesSinceLastPruneCheck = 0;
+ BOOL canPrune = YES;
+ NSUInteger cacheSize = [self.storageEngine serverCacheEstimatedSizeInBytes];
+ FFDebug(@"I-RDB078002", @"Server cache size: %lu", (unsigned long)cacheSize);
+ while (canPrune && [self.cachePolicy shouldPruneCacheWithSize:cacheSize
+ numberOfTrackedQueries:self.trackedQueryManager.numberOfPrunableQueries]) {
+ FPruneForest *pruneForest = [self.trackedQueryManager pruneOldQueries:self.cachePolicy];
+ if (pruneForest.prunesAnything) {
+ [self.storageEngine pruneCache:pruneForest atPath:[FPath empty]];
+ } else {
+ canPrune = NO;
+ }
+ cacheSize = [self.storageEngine serverCacheEstimatedSizeInBytes];
+ FFDebug(@"I-RDB078003", @"Cache size after pruning: %lu", (unsigned long)cacheSize);
+ }
+ FFDebug(@"I-RDB078004", @"Pruning round took %fms", [date timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQuery:(FQuerySpec *)query {
+ NSAssert(!query.loadsAllData, @"We should only track keys for filtered queries");
+ FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query];
+ NSAssert(trackedQuery.isActive, @"We only expect tracked keys for currently-active queries.");
+ [self.storageEngine setTrackedQueryKeys:keys forQueryId:trackedQuery.queryId];
+}
+
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQuery:(FQuerySpec *)query {
+ NSAssert(!query.loadsAllData, @"We should only track keys for filtered queries");
+ FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query];
+ NSAssert(trackedQuery.isActive, @"We only expect tracked keys for currently-active queries.");
+ [self.storageEngine updateTrackedQueryKeysWithAddedKeys:added removedKeys:removed forQueryId:trackedQuery.queryId];
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FPruneForest.h b/Firebase/Database/Persistence/FPruneForest.h
new file mode 100644
index 0000000..9e77217
--- /dev/null
+++ b/Firebase/Database/Persistence/FPruneForest.h
@@ -0,0 +1,38 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FPath;
+
+@interface FPruneForest : NSObject
+
++ (FPruneForest *)empty;
+
+- (BOOL)prunesAnything;
+- (BOOL)shouldPruneUnkeptDescendantsAtPath:(FPath *)path;
+- (BOOL)shouldKeepPath:(FPath *)path;
+- (BOOL)affectsPath:(FPath *)path;
+- (FPruneForest *)child:(NSString *)childKey;
+- (FPruneForest *)childAtPath:(FPath *)childKey;
+- (FPruneForest *)prunePath:(FPath *)path;
+- (FPruneForest *)keepPath:(FPath *)path;
+- (FPruneForest *)keepAll:(NSSet *)children atPath:(FPath *)path;
+- (FPruneForest *)pruneAll:(NSSet *)children atPath:(FPath *)path;
+
+- (void)enumarateKeptNodesUsingBlock:(void (^)(FPath *path))block;
+
+@end
diff --git a/Firebase/Database/Persistence/FPruneForest.m b/Firebase/Database/Persistence/FPruneForest.m
new file mode 100644
index 0000000..3dae6d8
--- /dev/null
+++ b/Firebase/Database/Persistence/FPruneForest.m
@@ -0,0 +1,177 @@
+/*
+ * 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 "FPruneForest.h"
+
+#import "FImmutableTree.h"
+
+@interface FPruneForest ()
+
+@property (nonatomic, strong) FImmutableTree *pruneForest;
+
+@end
+
+@implementation FPruneForest
+
+static BOOL (^kFPrunePredicate)(id) = ^BOOL(NSNumber *pruneValue) {
+ return [pruneValue boolValue];
+};
+
+static BOOL (^kFKeepPredicate)(id) = ^BOOL(NSNumber *pruneValue) {
+ return ![pruneValue boolValue];
+};
+
+
++ (FImmutableTree *)pruneTree {
+ static dispatch_once_t onceToken;
+ static FImmutableTree *pruneTree;
+ dispatch_once(&onceToken, ^{
+ pruneTree = [[FImmutableTree alloc] initWithValue:@YES];
+ });
+ return pruneTree;
+}
+
++ (FImmutableTree *)keepTree {
+ static dispatch_once_t onceToken;
+ static FImmutableTree *keepTree;
+ dispatch_once(&onceToken, ^{
+ keepTree = [[FImmutableTree alloc] initWithValue:@NO];
+ });
+ return keepTree;
+}
+
+- (id) initWithForest:(FImmutableTree *)tree {
+ self = [super init];
+ if (self != nil) {
+ self->_pruneForest = tree;
+ }
+ return self;
+}
+
++ (FPruneForest *)empty {
+ static dispatch_once_t onceToken;
+ static FPruneForest *forest;
+ dispatch_once(&onceToken, ^{
+ forest = [[FPruneForest alloc] initWithForest:[FImmutableTree empty]];
+ });
+ return forest;
+}
+
+- (BOOL)prunesAnything {
+ return [self.pruneForest containsValueMatching:kFPrunePredicate];
+}
+
+- (BOOL)shouldPruneUnkeptDescendantsAtPath:(FPath *)path {
+ NSNumber *shouldPrune = [self.pruneForest leafMostValueOnPath:path];
+ return shouldPrune != nil && [shouldPrune boolValue];
+}
+
+- (BOOL)shouldKeepPath:(FPath *)path {
+ NSNumber *shouldPrune = [self.pruneForest leafMostValueOnPath:path];
+ return shouldPrune != nil && ![shouldPrune boolValue];
+}
+
+- (BOOL)affectsPath:(FPath *)path {
+ return [self.pruneForest rootMostValueOnPath:path] != nil || ![[self.pruneForest subtreeAtPath:path] isEmpty];
+}
+
+- (FPruneForest *)child:(NSString *)childKey {
+ FImmutableTree *childPruneForest = [self.pruneForest.children get:childKey];
+ if (childPruneForest == nil) {
+ if (self.pruneForest.value != nil) {
+ childPruneForest = [self.pruneForest.value boolValue] ? [FPruneForest pruneTree] : [FPruneForest keepTree];
+ } else {
+ childPruneForest = [FImmutableTree empty];
+ }
+ } else {
+ if (childPruneForest.value == nil && self.pruneForest.value != nil) {
+ childPruneForest = [childPruneForest setValue:self.pruneForest.value atPath:[FPath empty]];
+ }
+ }
+ return [[FPruneForest alloc] initWithForest:childPruneForest];
+}
+
+- (FPruneForest *)childAtPath:(FPath *)path {
+ if (path.isEmpty) {
+ return self;
+ } else {
+ return [[self child:path.getFront] childAtPath:[path popFront]];
+ }
+}
+
+- (FPruneForest *)prunePath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't prune path that was kept previously!"];
+ }
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFPrunePredicate]) {
+ // This path will already be pruned
+ return self;
+ } else {
+ FImmutableTree *newPruneForest = [self.pruneForest setTree:[FPruneForest pruneTree] atPath:path];
+ return [[FPruneForest alloc] initWithForest:newPruneForest];
+ }
+}
+
+- (FPruneForest *)keepPath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ // This path will already be kept
+ return self;
+ } else {
+ FImmutableTree *newPruneForest = [self.pruneForest setTree:[FPruneForest keepTree] atPath:path];
+ return [[FPruneForest alloc] initWithForest:newPruneForest];
+ }
+}
+
+- (FPruneForest *)keepAll:(NSSet *)children atPath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ // This path will already be kept
+ return self;
+ } else {
+ return [self setPruneValue:[FPruneForest keepTree] forAll:children atPath:path];
+ }
+}
+
+- (FPruneForest *)pruneAll:(NSSet *)children atPath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't prune path that was kept previously!"];
+ }
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFPrunePredicate]) {
+ // This path will already be kept
+ return self;
+ } else {
+ return [self setPruneValue:[FPruneForest pruneTree] forAll:children atPath:path];
+ }
+}
+
+- (FPruneForest *)setPruneValue:(FImmutableTree *)pruneValue forAll:(NSSet *)children atPath:(FPath *)path {
+ FImmutableTree *subtree = [self.pruneForest subtreeAtPath:path];
+ __block FImmutableSortedDictionary *childrenDictionary = subtree.children;
+ [children enumerateObjectsUsingBlock:^(NSString *childKey, BOOL *stop) {
+ childrenDictionary = [childrenDictionary insertKey:childKey withValue:pruneValue];
+ }];
+ FImmutableTree *newSubtree = [[FImmutableTree alloc] initWithValue:subtree.value children:childrenDictionary];
+ return [[FPruneForest alloc] initWithForest:[self.pruneForest setTree:newSubtree atPath:path]];
+}
+
+- (void)enumarateKeptNodesUsingBlock:(void (^)(FPath *))block {
+ [self.pruneForest forEach:^(FPath *path, id value) {
+ if (value != nil && ![value boolValue]) {
+ block(path);
+ }
+ }];
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FStorageEngine.h b/Firebase/Database/Persistence/FStorageEngine.h
new file mode 100644
index 0000000..4f168e7
--- /dev/null
+++ b/Firebase/Database/Persistence/FStorageEngine.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 <Foundation/Foundation.h>
+
+@protocol FNode;
+@class FPruneForest;
+@class FPath;
+@class FCompoundWrite;
+@class FQuerySpec;
+@class FTrackedQuery;
+
+@protocol FStorageEngine <NSObject>
+
+- (void)close;
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)removeUserWrite:(NSUInteger)writeId;
+- (void)removeAllUserWrites;
+- (NSArray *)userWrites;
+
+- (id<FNode>)serverCacheAtPath:(FPath *)path;
+- (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path;
+- (void)updateServerCache:(id<FNode>)node atPath:(FPath *)path merge:(BOOL)merge;
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path;
+- (NSUInteger)serverCacheEstimatedSizeInBytes;
+
+- (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)path;
+
+- (NSArray *)loadTrackedQueries;
+- (void)removeTrackedQuery:(NSUInteger)queryId;
+- (void)saveTrackedQuery:(FTrackedQuery *)query;
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId;
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQueryId:(NSUInteger)queryId;
+- (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId;
+
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQuery.h b/Firebase/Database/Persistence/FTrackedQuery.h
new file mode 100644
index 0000000..7bc8ef1
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQuery.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FQuerySpec;
+
+@interface FTrackedQuery : NSObject
+
+@property (nonatomic, readonly) NSUInteger queryId;
+@property (nonatomic, strong, readonly) FQuerySpec *query;
+@property (nonatomic, readonly) NSTimeInterval lastUse;
+@property (nonatomic, readonly) BOOL isComplete;
+@property (nonatomic, readonly) BOOL isActive;
+
+- (id)initWithId:(NSUInteger)queryId query:(FQuerySpec *)query lastUse:(NSTimeInterval)lastUse isActive:(BOOL)isActive;
+- (id)initWithId:(NSUInteger)queryId
+ query:(FQuerySpec *)query
+ lastUse:(NSTimeInterval)lastUse
+ isActive:(BOOL)isActive
+ isComplete:(BOOL)isComplete;
+
+- (FTrackedQuery *)updateLastUse:(NSTimeInterval)lastUse;
+- (FTrackedQuery *)setComplete;
+- (FTrackedQuery *)setActiveState:(BOOL)isActive;
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQuery.m b/Firebase/Database/Persistence/FTrackedQuery.m
new file mode 100644
index 0000000..1720805
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQuery.m
@@ -0,0 +1,102 @@
+/*
+ * 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 "FTrackedQuery.h"
+
+#import "FQuerySpec.h"
+
+@interface FTrackedQuery ()
+
+@property (nonatomic, readwrite) NSUInteger queryId;
+@property (nonatomic, strong, readwrite) FQuerySpec *query;
+@property (nonatomic, readwrite) NSTimeInterval lastUse;
+@property (nonatomic, readwrite) BOOL isComplete;
+@property (nonatomic, readwrite) BOOL isActive;
+
+@end
+
+
+@implementation FTrackedQuery
+
+- (id)initWithId:(NSUInteger)queryId
+ query:(FQuerySpec *)query
+ lastUse:(NSTimeInterval)lastUse
+ isActive:(BOOL)isActive
+ isComplete:(BOOL)isComplete {
+ self = [super init];
+ if (self != nil) {
+ self->_queryId = queryId;
+ self->_query = query;
+ self->_lastUse = lastUse;
+ self->_isComplete = isComplete;
+ self->_isActive = isActive;
+ }
+ return self;
+}
+
+- (id)initWithId:(NSUInteger)queryId query:(FQuerySpec *)query lastUse:(NSTimeInterval)lastUse isActive:(BOOL)isActive {
+ return [self initWithId:queryId query:query lastUse:lastUse isActive:isActive isComplete:NO];
+}
+
+- (FTrackedQuery *)updateLastUse:(NSTimeInterval)lastUse {
+ return [[FTrackedQuery alloc] initWithId:self.queryId
+ query:self.query
+ lastUse:lastUse
+ isActive:self.isActive
+ isComplete:self.isComplete];
+}
+
+- (FTrackedQuery *)setComplete {
+ return [[FTrackedQuery alloc] initWithId:self.queryId
+ query:self.query
+ lastUse:self.lastUse
+ isActive:self.isActive
+ isComplete:YES];
+}
+
+- (FTrackedQuery *)setActiveState:(BOOL)isActive {
+ return [[FTrackedQuery alloc] initWithId:self.queryId
+ query:self.query
+ lastUse:self.lastUse
+ isActive:isActive
+ isComplete:self.isComplete];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FTrackedQuery class]]) {
+ return NO;
+ }
+ FTrackedQuery *other = (FTrackedQuery *)object;
+ if (self.queryId != other.queryId) return NO;
+ if (self.query != other.query && ![self.query isEqual:other.query]) return NO;
+ if (self.lastUse != other.lastUse) return NO;
+ if (self.isComplete != other.isComplete) return NO;
+ if (self.isActive != other.isActive) return NO;
+
+ return YES;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = self.queryId;
+ hash = hash * 31 + self.query.hash;
+ hash = hash * 31 + (self.isActive ? 1 : 0);
+ hash = hash * 31 + (NSUInteger)self.lastUse;
+ hash = hash * 31 + (self.isComplete ? 1 : 0);
+ hash = hash * 31 + (self.isActive ? 1 : 0);
+ return hash;
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQueryManager.h b/Firebase/Database/Persistence/FTrackedQueryManager.h
new file mode 100644
index 0000000..ba2631b
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQueryManager.h
@@ -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 <Foundation/Foundation.h>
+
+@protocol FStorageEngine;
+@protocol FClock;
+@protocol FCachePolicy;
+@class FQuerySpec;
+@class FPath;
+@class FTrackedQuery;
+@class FPruneForest;
+
+@interface FTrackedQueryManager : NSObject
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine clock:(id<FClock>)clock;
+
+- (FTrackedQuery *)findTrackedQuery:(FQuerySpec *)query;
+
+- (BOOL)isQueryComplete:(FQuerySpec *)query;
+
+- (void)removeTrackedQuery:(FQuerySpec *)query;
+- (void)setQueryComplete:(FQuerySpec *)query;
+- (void)setQueriesCompleteAtPath:(FPath *)path;
+- (void)setQueryActive:(FQuerySpec *)query;
+- (void)setQueryInactive:(FQuerySpec *)query;
+
+- (BOOL)hasActiveDefaultQueryAtPath:(FPath *)path;
+- (void)ensureCompleteTrackedQueryAtPath:(FPath *)path;
+
+- (FPruneForest *)pruneOldQueries:(id<FCachePolicy>)cachePolicy;
+- (NSUInteger)numberOfPrunableQueries;
+- (NSSet *)knownCompleteChildrenAtPath:(FPath *)path;
+
+// For testing
+- (void)verifyCache;
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQueryManager.m b/Firebase/Database/Persistence/FTrackedQueryManager.m
new file mode 100644
index 0000000..bf9753d
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQueryManager.m
@@ -0,0 +1,321 @@
+/*
+ * 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 "FTrackedQueryManager.h"
+#import "FImmutableTree.h"
+#import "FLevelDBStorageEngine.h"
+#import "FUtilities.h"
+#import "FTrackedQuery.h"
+#import "FPruneForest.h"
+#import "FClock.h"
+#import "FUtilities.h"
+#import "FCachePolicy.h"
+
+@interface FTrackedQueryManager ()
+
+@property (nonatomic, strong) FImmutableTree *trackedQueryTree;
+@property (nonatomic, strong) id<FStorageEngine> storageEngine;
+@property (nonatomic, strong) id<FClock> clock;
+@property (nonatomic) NSUInteger currentQueryId;
+
+@end
+
+@implementation FTrackedQueryManager
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine clock:(id<FClock>)clock {
+ self = [super init];
+ if (self != nil) {
+ self->_storageEngine = storageEngine;
+ self->_clock = clock;
+ self->_trackedQueryTree = [FImmutableTree empty];
+
+ NSTimeInterval lastUse = [clock currentTime];
+
+ NSArray *trackedQueries = [self.storageEngine loadTrackedQueries];
+ [trackedQueries enumerateObjectsUsingBlock:^(FTrackedQuery *trackedQuery, NSUInteger idx, BOOL *stop) {
+ self.currentQueryId = MAX(trackedQuery.queryId + 1, self.currentQueryId);
+ if (trackedQuery.isActive) {
+ trackedQuery = [[trackedQuery setActiveState:NO] updateLastUse:lastUse];
+ FFDebug(@"I-RDB081001", @"Setting active query %lu from previous app start inactive", (unsigned long)trackedQuery.queryId);
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ }
+ [self cacheTrackedQuery:trackedQuery];
+ }];
+ }
+ return self;
+}
+
++ (void)assertValidTrackedQuery:(FQuerySpec *)query {
+ NSAssert(!query.loadsAllData || query.isDefault, @"Can't have tracked non-default query that loads all data");
+}
+
++ (FQuerySpec *)normalizeQuery:(FQuerySpec *)query {
+ return query.loadsAllData ? [FQuerySpec defaultQueryAtPath:query.path] : query;
+}
+
+- (FTrackedQuery *)findTrackedQuery:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ NSDictionary *set = [self.trackedQueryTree valueAtPath:query.path];
+ return set[query.params];
+}
+
+- (void)removeTrackedQuery:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+ NSAssert(trackedQuery, @"Tracked query must exist to be removed!");
+
+ [self.storageEngine removeTrackedQuery:trackedQuery.queryId];
+ NSMutableDictionary *trackedQueries = [self.trackedQueryTree valueAtPath:query.path];
+ [trackedQueries removeObjectForKey:query.params];
+}
+
+- (void)setQueryActive:(FQuerySpec *)query {
+ [self setQueryActive:YES forQuery:query];
+}
+
+- (void)setQueryInactive:(FQuerySpec *)query {
+ [self setQueryActive:NO forQuery:query];
+}
+
+- (void)setQueryActive:(BOOL)isActive forQuery:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+
+ // Regardless of whether it's now active or no langer active, we update the lastUse time
+ NSTimeInterval lastUse = [self.clock currentTime];
+ if (trackedQuery != nil) {
+ trackedQuery = [[trackedQuery updateLastUse:lastUse] setActiveState:isActive];
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ } else {
+ NSAssert(isActive, @"If we're setting the query to inactive, we should already be tracking it!");
+ trackedQuery = [[FTrackedQuery alloc] initWithId:self.currentQueryId++
+ query:query
+ lastUse:lastUse
+ isActive:isActive];
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ }
+
+ [self cacheTrackedQuery:trackedQuery];
+}
+
+- (void)setQueryComplete:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+ if (!trackedQuery) {
+ // We might have removed a query and pruned it before we got the complete message from the server...
+ FFWarn(@"I-RDB081002", @"Trying to set a query complete that is not tracked!");
+ } else if (!trackedQuery.isComplete) {
+ trackedQuery = [trackedQuery setComplete];
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ [self cacheTrackedQuery:trackedQuery];
+ } else {
+ // Nothing to do, already marked complete
+ }
+}
+
+- (void)setQueriesCompleteAtPath:(FPath *)path {
+ [[self.trackedQueryTree subtreeAtPath:path] forEach:^(FPath *childPath, NSDictionary *trackedQueries) {
+ [trackedQueries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *parms, FTrackedQuery *trackedQuery, BOOL *stop) {
+ if (!trackedQuery.isComplete) {
+ FTrackedQuery *newTrackedQuery = [trackedQuery setComplete];
+ [self.storageEngine saveTrackedQuery:newTrackedQuery];
+ [self cacheTrackedQuery:newTrackedQuery];
+ }
+ }];
+ }];
+}
+
+- (BOOL)isQueryComplete:(FQuerySpec *)query {
+ if ([self isIncludedInDefaultCompleteQuery:query]) {
+ return YES;
+ } else if (query.loadsAllData) {
+ // We didn't find a default complete query, so must not be complete.
+ return NO;
+ } else {
+ NSDictionary *trackedQueries = [self.trackedQueryTree valueAtPath:query.path];
+ return [trackedQueries[query.params] isComplete];
+ }
+}
+
+- (BOOL)hasActiveDefaultQueryAtPath:(FPath *)path {
+ return [self.trackedQueryTree rootMostValueOnPath:path matching:^BOOL(NSDictionary *trackedQueries) {
+ return [trackedQueries[[FQueryParams defaultInstance]] isActive];
+ }] != nil;
+}
+
+- (void)ensureCompleteTrackedQueryAtPath:(FPath *)path {
+ FQuerySpec *query = [FQuerySpec defaultQueryAtPath:path];
+ if (![self isIncludedInDefaultCompleteQuery:query]) {
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+ if (trackedQuery == nil) {
+ trackedQuery = [[FTrackedQuery alloc] initWithId:self.currentQueryId++
+ query:query
+ lastUse:[self.clock currentTime]
+ isActive:NO
+ isComplete:YES];
+ } else {
+ NSAssert(!trackedQuery.isComplete, @"This should have been handled above!");
+ trackedQuery = [trackedQuery setComplete];
+ }
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ [self cacheTrackedQuery:trackedQuery];
+ }
+}
+
+- (BOOL)isIncludedInDefaultCompleteQuery:(FQuerySpec *)query {
+ return [self.trackedQueryTree findRootMostMatchingPath:query.path predicate:^BOOL(NSDictionary *trackedQueries) {
+ return [trackedQueries[[FQueryParams defaultInstance]] isComplete];
+ }] != nil;
+}
+
+- (void)cacheTrackedQuery:(FTrackedQuery *)query {
+ [FTrackedQueryManager assertValidTrackedQuery:query.query];
+ NSMutableDictionary *trackedDict = [self.trackedQueryTree valueAtPath:query.query.path];
+ if (trackedDict == nil) {
+ trackedDict = [NSMutableDictionary dictionary];
+ self.trackedQueryTree = [self.trackedQueryTree setValue:trackedDict atPath:query.query.path];
+ }
+ trackedDict[query.query.params] = query;
+}
+
+- (NSUInteger) numberOfQueriesToPrune:(id<FCachePolicy>)cachePolicy prunableCount:(NSUInteger)numPrunable {
+ NSUInteger numPercent = (NSUInteger)ceilf(numPrunable * [cachePolicy percentOfQueriesToPruneAtOnce]);
+ NSUInteger maxToKeep = [cachePolicy maxNumberOfQueriesToKeep];
+ NSUInteger numMax = (numPrunable > maxToKeep) ? numPrunable - maxToKeep : 0;
+ // Make sure we get below number of max queries to prune
+ return MAX(numMax, numPercent);
+}
+
+- (FPruneForest *)pruneOldQueries:(id<FCachePolicy>)cachePolicy {
+ NSMutableArray *pruneableQueries = [NSMutableArray array];
+ NSMutableArray *unpruneableQueries = [NSMutableArray array];
+ [self.trackedQueryTree forEach:^(FPath *path, NSDictionary *trackedQueries) {
+ [trackedQueries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *params, FTrackedQuery *trackedQuery, BOOL *stop) {
+ if (!trackedQuery.isActive) {
+ [pruneableQueries addObject:trackedQuery];
+ } else {
+ [unpruneableQueries addObject:trackedQuery];
+ }
+ }];
+ }];
+ [pruneableQueries sortUsingComparator:^NSComparisonResult(FTrackedQuery *q1, FTrackedQuery *q2) {
+ if (q1.lastUse < q2.lastUse) {
+ return NSOrderedAscending;
+ } else if (q1.lastUse > q2.lastUse) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ }];
+
+
+ __block FPruneForest *pruneForest = [FPruneForest empty];
+ NSUInteger numToPrune = [self numberOfQueriesToPrune:cachePolicy prunableCount:pruneableQueries.count];
+
+ // TODO: do in transaction
+ for (NSUInteger i = 0; i < numToPrune; i++) {
+ FTrackedQuery *toPrune = pruneableQueries[i];
+ pruneForest = [pruneForest prunePath:toPrune.query.path];
+ [self removeTrackedQuery:toPrune.query];
+ }
+
+ // Keep the rest of the prunable queries
+ for (NSUInteger i = numToPrune; i < pruneableQueries.count; i++) {
+ FTrackedQuery *toKeep = pruneableQueries[i];
+ pruneForest = [pruneForest keepPath:toKeep.query.path];
+ }
+
+ // Also keep unprunable queries
+ [unpruneableQueries enumerateObjectsUsingBlock:^(FTrackedQuery *toKeep, NSUInteger idx, BOOL *stop) {
+ pruneForest = [pruneForest keepPath:toKeep.query.path];
+ }];
+
+ return pruneForest;
+}
+
+- (NSUInteger)numberOfPrunableQueries {
+ __block NSUInteger count = 0;
+ [self.trackedQueryTree forEach:^(FPath *path, NSDictionary *trackedQueries) {
+ [trackedQueries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *params, FTrackedQuery *trackedQuery, BOOL *stop) {
+ if (!trackedQuery.isActive) {
+ count++;
+ }
+ }];
+ }];
+ return count;
+}
+
+- (NSSet *)filteredQueryIdsAtPath:(FPath *)path {
+ NSDictionary *queries = [self.trackedQueryTree valueAtPath:path];
+ if (queries) {
+ NSMutableSet *ids = [NSMutableSet set];
+ [queries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *params, FTrackedQuery *query, BOOL *stop) {
+ if (!query.query.loadsAllData) {
+ [ids addObject:@(query.queryId)];
+ }
+ }];
+ return ids;
+ } else {
+ return [NSSet set];
+ }
+}
+
+- (NSSet *)knownCompleteChildrenAtPath:(FPath *)path {
+ NSAssert(![self isQueryComplete:[FQuerySpec defaultQueryAtPath:path]], @"Path is fully complete");
+
+ NSMutableSet *completeChildren = [NSMutableSet set];
+ // First, get complete children from any queries at this location.
+ NSSet *queryIds = [self filteredQueryIdsAtPath:path];
+ [queryIds enumerateObjectsUsingBlock:^(NSNumber *queryId, BOOL *stop) {
+ NSSet *keys = [self.storageEngine trackedQueryKeysForQuery:[queryId unsignedIntegerValue]];
+ [completeChildren unionSet:keys];
+ }];
+
+ // Second, get any complete default queries immediately below us.
+ [[[self.trackedQueryTree subtreeAtPath:path] children] enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if ([childTree.value[[FQueryParams defaultInstance]] isComplete]) {
+ [completeChildren addObject:childKey];
+ }
+ }];
+
+ return completeChildren;
+}
+
+- (void)verifyCache {
+ NSArray *storedTrackedQueries = [self.storageEngine loadTrackedQueries];
+ NSMutableArray *trackedQueries = [NSMutableArray array];
+
+ [self.trackedQueryTree forEach:^(FPath *path, NSDictionary *queryDict) {
+ [trackedQueries addObjectsFromArray:queryDict.allValues];
+ }];
+ NSComparator comparator = ^NSComparisonResult(FTrackedQuery *q1, FTrackedQuery *q2) {
+ if (q1.queryId < q2.queryId) {
+ return NSOrderedAscending;
+ } else if (q1.queryId > q2.queryId) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ };
+ [trackedQueries sortUsingComparator:comparator];
+ storedTrackedQueries = [storedTrackedQueries sortedArrayUsingComparator:comparator];
+
+ if (![trackedQueries isEqualToArray:storedTrackedQueries]) {
+ [NSException raise:NSInternalInconsistencyException format:@"Tracked queries and queries stored on disk don't match"];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Realtime/FConnection.h b/Firebase/Database/Realtime/FConnection.h
new file mode 100644
index 0000000..ed4879a
--- /dev/null
+++ b/Firebase/Database/Realtime/FConnection.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 <Foundation/Foundation.h>
+#import "FWebSocketConnection.h"
+#import "FTypedefs.h"
+
+@protocol FConnectionDelegate;
+
+@interface FConnection : NSObject <FWebSocketDelegate>
+
+@property (nonatomic, weak) id <FConnectionDelegate> delegate;
+
+- (id)initWith:(FRepoInfo *)aRepoInfo andDispatchQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID;
+
+- (void)open;
+- (void)close;
+- (void)sendRequest:(NSDictionary *)dataMsg sensitive:(BOOL)sensitive;
+
+// FWebSocketDelegate delegate methods
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected;
+
+@end
+
+typedef enum {
+ DISCONNECT_REASON_SERVER_RESET = 0,
+ DISCONNECT_REASON_OTHER = 1
+} FDisconnectReason;
+
+@protocol FConnectionDelegate <NSObject>
+
+- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID;
+- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FConnection *)fconnection withReason:(FDisconnectReason)reason;
+- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason;
+
+@end
+
diff --git a/Firebase/Database/Realtime/FConnection.m b/Firebase/Database/Realtime/FConnection.m
new file mode 100644
index 0000000..1550bfc
--- /dev/null
+++ b/Firebase/Database/Realtime/FConnection.m
@@ -0,0 +1,211 @@
+/*
+ * 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 "FConnection.h"
+#import "FConstants.h"
+
+typedef enum {
+ REALTIME_STATE_CONNECTING = 0,
+ REALTIME_STATE_CONNECTED = 1,
+ REALTIME_STATE_DISCONNECTED = 2,
+} FConnectionState;
+
+@interface FConnection () {
+ FConnectionState state;
+}
+
+@property (nonatomic, strong) FWebSocketConnection* conn;
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+
+@end
+
+#pragma mark -
+#pragma mark FConnection implementation
+
+@implementation FConnection
+
+@synthesize delegate;
+@synthesize conn;
+@synthesize repoInfo;
+
+#pragma mark -
+#pragma mark Initializers
+
+- (id)initWith:(FRepoInfo *)aRepoInfo andDispatchQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID{
+ self = [super init];
+ if (self) {
+ state = REALTIME_STATE_CONNECTING;
+ self.repoInfo = aRepoInfo;
+ self.conn = [[FWebSocketConnection alloc] initWith:self.repoInfo andQueue:queue lastSessionID:lastSessionID];
+ self.conn.delegate = self;
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark Public method implementation
+
+- (void)open {
+ FFLog(@"I-RDB082001", @"Calling open in FConnection");
+ [self.conn open];
+}
+
+- (void) closeWithReason:(FDisconnectReason)reason {
+ if (state != REALTIME_STATE_DISCONNECTED) {
+ FFLog(@"I-RDB082002", @"Closing realtime connection.");
+ state = REALTIME_STATE_DISCONNECTED;
+
+ if (self.conn) {
+ FFLog(@"I-RDB082003", @"Calling close again.");
+ [self.conn close];
+ self.conn = nil;
+ }
+
+ [self.delegate onDisconnect:self withReason:reason];
+ }
+}
+
+- (void) close {
+ [self closeWithReason:DISCONNECT_REASON_OTHER];
+}
+
+- (void) sendRequest:(NSDictionary *)dataMsg sensitive:(BOOL)sensitive {
+ // since this came from the persistent connection, wrap it in a data message envelope
+ NSDictionary* msg = @{
+ kFWPRequestType: kFWPRequestTypeData,
+ kFWPRequestDataPayload: dataMsg
+ };
+ [self sendData:msg sensitive:sensitive];
+}
+
+#pragma mark -
+#pragma mark Helpers
+
+
+- (void) sendData:(NSDictionary *)data sensitive:(BOOL)sensitive {
+ if (state != REALTIME_STATE_CONNECTED) {
+ @throw [[NSException alloc] initWithName:@"InvalidConnectionState" reason:@"Tried to send data on an unconnected FConnection" userInfo:nil];
+ } else {
+ if (sensitive) {
+ FFLog(@"I-RDB082004", @"Sending data (contents hidden)");
+ } else {
+ FFLog(@"I-RDB082005", @"Sending: %@", data);
+ }
+ [self.conn send:data];
+ }
+}
+
+#pragma mark -
+#pragma mark FWebSocketConnectinDelegate implementation
+
+// Corresponds to onConnectionLost in JS
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected {
+
+ self.conn = nil;
+ if (!everConnected && state == REALTIME_STATE_CONNECTING) {
+ FFLog(@"I-RDB082006", @"Realtime connection failed.");
+
+ // Since we failed to connect at all, clear any cached entry for this namespace in case the machine went away
+ [self.repoInfo clearInternalHostCache];
+ } else if (state == REALTIME_STATE_CONNECTED) {
+ FFLog(@"I-RDB082007", @"Realtime connection lost.");
+ }
+
+ [self close];
+}
+
+// Corresponds to onMessageReceived in JS
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message {
+ NSString* rawMessageType = [message objectForKey:kFWPAsyncServerEnvelopeType];
+ if(rawMessageType != nil) {
+ if([rawMessageType isEqualToString:kFWPAsyncServerDataMessage]) {
+ [self onDataMessage:[message objectForKey:kFWPAsyncServerEnvelopeData]];
+ }
+ else if ([rawMessageType isEqualToString:kFWPAsyncServerControlMessage]) {
+ [self onControl:[message objectForKey:kFWPAsyncServerEnvelopeData]];
+ }
+ else {
+ FFLog(@"I-RDB082008", @"Unrecognized server packet type: %@", rawMessageType);
+ }
+ }
+ else {
+ FFLog(@"I-RDB082009", @"Unrecognized raw server packet received: %@", message);
+ }
+}
+
+- (void) onDataMessage:(NSDictionary *)message {
+ // we don't do anything with data messages, just kick them up a level
+ FFLog(@"I-RDB082010", @"Got data message: %@", message);
+ [self.delegate onDataMessage:self withMessage:message];
+}
+
+- (void) onControl:(NSDictionary *)message {
+ FFLog(@"I-RDB082011", @"Got control message: %@", message);
+ NSString* type = [message objectForKey:kFWPAsyncServerControlMessageType];
+ if([type isEqualToString:kFWPAsyncServerControlMessageShutdown]) {
+ NSString* reason = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onConnectionShutdownWithReason:reason];
+ }
+ else if ([type isEqualToString:kFWPAsyncServerControlMessageReset]) {
+ NSString* host = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onReset:host];
+ }
+ else if ([type isEqualToString:kFWPAsyncServerHello]) {
+ NSDictionary* handshakeData = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onHandshake:handshakeData];
+ }
+ else {
+ FFLog(@"I-RDB082012", @"Unknown control message returned from server: %@", message);
+ }
+}
+
+- (void) onConnectionShutdownWithReason:(NSString *)reason {
+ FFLog(@"I-RDB082013", @"Connection shutdown command received. Shutting down...");
+
+ [self.delegate onKill:self withReason:reason];
+ [self close];
+}
+
+- (void) onHandshake:(NSDictionary *)handshake {
+ NSNumber* timestamp = [handshake objectForKey:kFWPAsyncServerHelloTimestamp];
+// NSString* version = [handshake objectForKey:kFWPAsyncServerHelloVersion];
+ NSString* host = [handshake objectForKey:kFWPAsyncServerHelloConnectedHost];
+ NSString* sessionID = [handshake objectForKey:kFWPAsyncServerHelloSession];
+
+ self.repoInfo.internalHost = host;
+
+ if (state == REALTIME_STATE_CONNECTING) {
+ [self.conn start];
+ [self onConnection:self.conn readyAtTime:timestamp sessionID:sessionID];
+ }
+}
+
+- (void) onConnection:(FWebSocketConnection *)conn readyAtTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID {
+ FFLog(@"I-RDB082014", @"Realtime connection established");
+ state = REALTIME_STATE_CONNECTED;
+
+ [self.delegate onReady:self atTime:timestamp sessionID:sessionID];
+}
+
+- (void) onReset:(NSString *)host {
+ FFLog(@"I-RDB082015", @"Got a reset; killing connection to: %@; Updating internalHost to: %@", repoInfo.internalHost, host);
+ self.repoInfo.internalHost = host;
+
+ // Explicitly close the connection with SERVER_RESET so calling code knows to reconnect immediately.
+ [self closeWithReason:DISCONNECT_REASON_SERVER_RESET];
+}
+
+@end
diff --git a/Firebase/Database/Realtime/FWebSocketConnection.h b/Firebase/Database/Realtime/FWebSocketConnection.h
new file mode 100644
index 0000000..6a14d47
--- /dev/null
+++ b/Firebase/Database/Realtime/FWebSocketConnection.h
@@ -0,0 +1,46 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FSRWebSocket.h"
+#import "FUtilities.h"
+
+@protocol FWebSocketDelegate;
+
+@interface FWebSocketConnection : NSObject <FSRWebSocketDelegate>
+
+@property (nonatomic, weak) id <FWebSocketDelegate> delegate;
+
+- (id)initWith:(FRepoInfo *)repoInfo andQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID;
+
+- (void) open;
+- (void) close;
+- (void) start;
+- (void) send:(NSDictionary *)dictionary;
+
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message;
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket;
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error;
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
+
+@end
+
+@protocol FWebSocketDelegate <NSObject>
+
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected;
+
+@end
diff --git a/Firebase/Database/Realtime/FWebSocketConnection.m b/Firebase/Database/Realtime/FWebSocketConnection.m
new file mode 100644
index 0000000..52e2296
--- /dev/null
+++ b/Firebase/Database/Realtime/FWebSocketConnection.m
@@ -0,0 +1,305 @@
+/*
+ * 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.
+ */
+
+// Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build.
+
+#import "FWebSocketConnection.h"
+#import "FConstants.h"
+#import "FIRDatabaseReference.h"
+#import "FStringUtilities.h"
+#import "FIRDatabase_Private.h"
+
+#if TARGET_OS_IPHONE
+#import <UIKit/UIKit.h>
+#endif
+
+@interface FWebSocketConnection () {
+ NSMutableString* frame;
+ BOOL everConnected;
+ BOOL isClosed;
+ NSTimer* keepAlive;
+}
+
+- (void) shutdown;
+- (void) onClosed;
+- (void) closeIfNeverConnected;
+
+@property (nonatomic, strong) FSRWebSocket* webSocket;
+@property (nonatomic, strong) NSNumber* connectionId;
+@property (nonatomic, readwrite) int totalFrames;
+@property (nonatomic, readonly) BOOL buffering;
+@property (nonatomic, readonly) NSString* userAgent;
+@property (nonatomic) dispatch_queue_t dispatchQueue;
+
+- (void)nop:(NSTimer *)timer;
+
+@end
+
+@implementation FWebSocketConnection
+
+@synthesize delegate;
+@synthesize webSocket;
+@synthesize connectionId;
+
+- (id)initWith:(FRepoInfo *)repoInfo andQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID {
+ self = [super init];
+ if (self) {
+ everConnected = NO;
+ isClosed = NO;
+ self.connectionId = [FUtilities LUIDGenerator];
+ self.totalFrames = 0;
+ self.dispatchQueue = queue;
+ frame = nil;
+
+ NSString* connectionUrl = [repoInfo connectionURLWithLastSessionID:lastSessionID];
+ NSString* ua = [self userAgent];
+ FFLog(@"I-RDB083001", @"(wsc:%@) Connecting to: %@ as %@", self.connectionId, connectionUrl, ua);
+
+ NSURLRequest* req = [[NSURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:connectionUrl]];
+ self.webSocket = [[FSRWebSocket alloc] initWithURLRequest:req queue:queue andUserAgent:ua];
+ [self.webSocket setDelegateDispatchQueue:queue];
+ self.webSocket.delegate = self;
+ }
+ return self;
+}
+
+- (NSString *) userAgent {
+ NSString* systemVersion;
+ NSString* deviceName;
+ BOOL hasUiDeviceClass = NO;
+
+ // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build.
+ #if TARGET_OS_IPHONE
+ Class uiDeviceClass = NSClassFromString(@"UIDevice");
+ if (uiDeviceClass) {
+ systemVersion = [uiDeviceClass currentDevice].systemVersion;
+ deviceName = [uiDeviceClass currentDevice].model;
+ hasUiDeviceClass = YES;
+ }
+ #endif
+
+ if (!hasUiDeviceClass) {
+ NSDictionary *systemVersionDictionary = [NSDictionary dictionaryWithContentsOfFile:@"/System/Library/CoreServices/SystemVersion.plist"];
+ systemVersion = [systemVersionDictionary objectForKey:@"ProductVersion"];
+ deviceName = [systemVersionDictionary objectForKey:@"ProductName"];
+ }
+
+ NSString* bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
+
+ // Sanitize '/'s in deviceName and bundleIdentifier for stats
+ deviceName = [FStringUtilities sanitizedForUserAgent:deviceName];
+ bundleIdentifier = [FStringUtilities sanitizedForUserAgent:bundleIdentifier];
+
+ // Firebase/5/<semver>_<build date>_<git hash>/<os version>/{device model / os (Mac OS X, iPhone, etc.}_<bundle id>
+ NSString* ua = [NSString stringWithFormat:@"Firebase/%@/%@/%@/%@_%@", kWebsocketProtocolVersion, [FIRDatabase buildVersion], systemVersion, deviceName, bundleIdentifier];
+ return ua;
+}
+
+- (BOOL) buffering {
+ return frame != nil;
+}
+
+#pragma mark -
+#pragma mark Public FWebSocketConnection methods
+
+- (void) open {
+ FFLog(@"I-RDB083002", @"(wsc:%@) FWebSocketConnection open.", self.connectionId);
+ assert(delegate);
+ everConnected = NO;
+ // TODO Assert url
+ [self.webSocket open];
+ dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, kWebsocketConnectTimeout * NSEC_PER_SEC);
+ dispatch_after(when, self.dispatchQueue, ^{
+ [self closeIfNeverConnected];
+ });
+}
+
+- (void) close {
+ FFLog(@"I-RDB083003", @"(wsc:%@) FWebSocketConnection is being closed.", self.connectionId);
+ isClosed = YES;
+ [self.webSocket close];
+}
+
+- (void) start {
+ // Start is a no-op for websockets.
+}
+
+- (void) send:(NSDictionary *)dictionary {
+
+ [self resetKeepAlive];
+
+ NSData* jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
+ options:kNilOptions error:nil];
+
+ NSString* data = [[NSString alloc] initWithData:jsonData
+ encoding:NSUTF8StringEncoding];
+
+ NSArray* dataSegs = [FUtilities splitString:data intoMaxSize:kWebsocketMaxFrameSize];
+
+ // First send the header so the server knows how many segments are forthcoming
+ if (dataSegs.count > 1) {
+ [self.webSocket send:[NSString stringWithFormat:@"%u", (unsigned int)dataSegs.count]];
+ }
+
+ // Then, actually send the segments.
+ for(NSString * segment in dataSegs) {
+ [self.webSocket send:segment];
+ }
+}
+
+- (void) nop:(NSTimer *)timer {
+ if(self.webSocket) {
+ FFLog(@"I-RDB083004", @"(wsc:%@) nop", self.connectionId);
+ [self.webSocket send:@"0"];
+ }
+ else {
+ FFLog(@"I-RDB083005", @"(wsc:%@) No more websocket; invalidating nop timer.", self.connectionId);
+ [timer invalidate];
+ }
+}
+
+- (void) handleNewFrameCount:(int) numFrames {
+ self.totalFrames = numFrames;
+ frame = [[NSMutableString alloc] initWithString:@""];
+ FFLog(@"I-RDB083006", @"(wsc:%@) handleNewFrameCount: %d", self.connectionId, self.totalFrames);
+}
+
+- (NSString *) extractFrameCount:(NSString *) message {
+ if ([message length] <= 4) {
+ int frameCount = [message intValue];
+ if (frameCount > 0) {
+ [self handleNewFrameCount:frameCount];
+ return nil;
+ }
+ }
+ [self handleNewFrameCount:1];
+ return message;
+}
+
+- (void) appendFrame:(NSString *) message {
+ [frame appendString:message];
+ self.totalFrames = self.totalFrames - 1;
+
+ if (self.totalFrames == 0) {
+ // Call delegate and pass an immutable version of the frame
+ NSDictionary* json = [NSJSONSerialization JSONObjectWithData:[frame dataUsingEncoding:NSUTF8StringEncoding]
+ options:kNilOptions
+ error:nil];
+ frame = nil;
+ FFLog(@"I-RDB083007", @"(wsc:%@) handleIncomingFrame sending complete frame: %d", self.connectionId, self.totalFrames);
+
+ @autoreleasepool {
+ [self.delegate onMessage:self withMessage:json];
+ }
+ }
+}
+
+- (void) handleIncomingFrame:(NSString *) message {
+ [self resetKeepAlive];
+ if (self.buffering) {
+ [self appendFrame:message];
+ } else {
+ NSString *remaining = [self extractFrameCount:message];
+ if (remaining) {
+ [self appendFrame:remaining];
+ }
+ }
+}
+
+#pragma mark -
+#pragma mark SRWebSocketDelegate implementation
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message
+{
+ [self handleIncomingFrame:message];
+}
+
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket
+{
+ FFLog(@"I-RDB083008", @"(wsc:%@) webSocketDidOpen", self.connectionId);
+
+ everConnected = YES;
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ self->keepAlive = [NSTimer scheduledTimerWithTimeInterval:kWebsocketKeepaliveInterval
+ target:self
+ selector:@selector(nop:)
+ userInfo:nil
+ repeats:YES];
+ FFLog(@"I-RDB083009", @"(wsc:%@) nop timer kicked off", self.connectionId);
+ });
+}
+
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error
+{
+ FFLog(@"I-RDB083010", @"(wsc:%@) didFailWithError didFailWithError: %@", self.connectionId, [error description]);
+ [self onClosed];
+}
+
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
+{
+ FFLog(@"I-RDB083011", @"(wsc:%@) didCloseWithCode: %ld %@", self.connectionId, (long)code, reason);
+ [self onClosed];
+}
+
+#pragma mark -
+#pragma mark Private methods
+
+/**
+ * Note that the close / onClosed / shutdown cycle here is a little different from the javascript client.
+ * In order to properly handle deallocation, no close-related action is taken at a higher level until we
+ * have received notification from the websocket itself that it is closed. Otherwise, we end up deallocating
+ * this class and the FConnection class before the websocket has a change to call some of its delegate methods.
+ * So, since close is the external close handler, we just set a flag saying not to call our own delegate method
+ * and close the websocket. That will trigger a callback into this class that can then do things like clean up
+ * the keepalive timer.
+ */
+
+- (void) closeIfNeverConnected {
+ if (!everConnected) {
+ FFLog(@"I-RDB083012", @"(wsc:%@) Websocket timed out on connect", self.connectionId);
+ [self.webSocket close];
+ }
+}
+
+- (void) shutdown {
+ isClosed = YES;
+
+ // Call delegate methods
+ [self.delegate onDisconnect:self wasEverConnected:everConnected];
+
+}
+
+- (void) onClosed {
+ if (!isClosed) {
+ FFLog(@"I-RDB083013", @"Websocket is closing itself");
+ [self shutdown];
+ }
+ self.webSocket = nil;
+ if (keepAlive.isValid) {
+ [keepAlive invalidate];
+ }
+}
+
+- (void) resetKeepAlive {
+ NSDate* newTime = [NSDate dateWithTimeIntervalSinceNow:kWebsocketKeepaliveInterval];
+ // Calling setFireDate is actually kinda' expensive, so wait at least 5 seconds before updating it.
+ if ([newTime timeIntervalSinceDate:keepAlive.fireDate] > 5) {
+ FFLog(@"I-RDB083014", @"(wsc:%@) resetting keepalive, to %@ ; old: %@", self.connectionId, newTime, [keepAlive fireDate]);
+ [keepAlive setFireDate:newTime];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FChildrenNode.h b/Firebase/Database/Snapshot/FChildrenNode.h
new file mode 100644
index 0000000..9eebdae
--- /dev/null
+++ b/Firebase/Database/Snapshot/FChildrenNode.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FNode.h"
+#import "FTypedefs.h"
+#import "FTypedefs_Private.h"
+#import "FImmutableSortedDictionary.h"
+
+@class FNamedNode;
+
+@interface FChildrenNode : NSObject <FNode>
+
+- (id)initWithChildren:(FImmutableSortedDictionary *)someChildren;
+- (id)initWithPriority:(id<FNode>)aPriority children:(FImmutableSortedDictionary *)someChildren;
+
+// FChildrenNode specific methods
+
+- (void) enumerateChildrenAndPriorityUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block;
+
+- (FNamedNode *) firstChild;
+- (FNamedNode *) lastChild;
+
+@property (nonatomic, strong) FImmutableSortedDictionary* children;
+@property (nonatomic, strong) id<FNode> priorityNode;
+
+@end
diff --git a/Firebase/Database/Snapshot/FChildrenNode.m b/Firebase/Database/Snapshot/FChildrenNode.m
new file mode 100644
index 0000000..b5598ad
--- /dev/null
+++ b/Firebase/Database/Snapshot/FChildrenNode.m
@@ -0,0 +1,385 @@
+/*
+ * 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 "FChildrenNode.h"
+#import "FEmptyNode.h"
+#import "FConstants.h"
+#import "FStringUtilities.h"
+#import "FUtilities.h"
+#import "FNamedNode.h"
+#import "FMaxNode.h"
+#import "FTransformedEnumerator.h"
+#import "FSnapshotUtilities.h"
+#import "FTransformedEnumerator.h"
+#import "FPriorityIndex.h"
+#import "FUtilities.h"
+
+@interface FChildrenNode ()
+@property (nonatomic, strong) NSString *lazyHash;
+@end
+
+@implementation FChildrenNode
+
+// Note: The only reason we allow nil priority is to for EmptyNode, since we can't use
+// EmptyNode as the priority of EmptyNode. We might want to consider making EmptyNode its own
+// class instead of an empty ChildrenNode.
+
+- (id)init {
+ return [self initWithPriority:nil children:[FImmutableSortedDictionary dictionaryWithComparator:[FUtilities keyComparator]]];
+}
+
+- (id)initWithChildren:(FImmutableSortedDictionary *)someChildren {
+ return [self initWithPriority:nil children:someChildren];
+}
+
+- (id)initWithPriority:(id<FNode>)aPriority children:(FImmutableSortedDictionary *)someChildren {
+ if (someChildren.isEmpty && aPriority != nil && ![aPriority isEmpty]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create empty node with priority!"];
+ }
+ self = [super init];
+ if(self) {
+ self.children = someChildren;
+ self.priorityNode = aPriority;
+ }
+ return self;
+}
+
+- (NSString *) description {
+ return [[self valForExport:YES] description];
+}
+
+#pragma mark -
+#pragma mark FNode methods
+
+
+- (BOOL) isLeafNode {
+ return NO;
+}
+
+- (id<FNode>) getPriority {
+ if (self.priorityNode) {
+ return self.priorityNode;
+ } else {
+ return [FEmptyNode emptyNode];
+ }
+
+}
+
+- (id<FNode>) updatePriority:(id<FNode>)aPriority {
+ if ([self.children isEmpty]) {
+ return [FEmptyNode emptyNode];
+ } else {
+ return [[FChildrenNode alloc] initWithPriority:aPriority children:self.children];
+ }
+}
+
+- (id<FNode>) getImmediateChild:(NSString *) childName {
+ if ([childName isEqualToString:@".priority"]) {
+ return [self getPriority];
+ } else {
+ id <FNode> child = [self.children objectForKey:childName];
+ return (child == nil) ? [FEmptyNode emptyNode] : child;
+ }
+}
+
+- (id<FNode>) getChild:(FPath *)path {
+ NSString* front = [path getFront];
+ if(front == nil) {
+ return self;
+ }
+ else {
+ return [[self getImmediateChild:front] getChild:[path popFront]];
+ }
+}
+
+- (BOOL)hasChild:(NSString *)childName {
+ return ![self getImmediateChild:childName].isEmpty;
+}
+
+
+- (id<FNode>) updateImmediateChild:(NSString *)childName withNewChild:(id<FNode>)newChildNode {
+ NSAssert(newChildNode != nil, @"Should always be passing nodes.");
+
+ if ([childName isEqualToString:@".priority"]) {
+ return [self updatePriority:newChildNode];
+ } else {
+ FImmutableSortedDictionary *newChildren;
+ if (newChildNode.isEmpty) {
+ newChildren = [self.children removeObjectForKey:childName];
+ } else {
+ newChildren = [self.children setObject:newChildNode forKey:childName];
+ }
+ if (newChildren.isEmpty) {
+ return [FEmptyNode emptyNode];
+ } else {
+ return [[FChildrenNode alloc] initWithPriority:self.getPriority children:newChildren];
+ }
+ }
+}
+
+- (id<FNode>) updateChild:(FPath *)path withNewChild:(id<FNode>)newChildNode {
+ NSString* front = [path getFront];
+ if(front == nil) {
+ return newChildNode;
+ } else {
+ NSAssert(![front isEqualToString:@".priority"] || path.length == 1, @".priority must be the last token in a path.");
+ id<FNode> newImmediateChild = [[self getImmediateChild:front] updateChild:[path popFront] withNewChild:newChildNode];
+ return [self updateImmediateChild:front withNewChild:newImmediateChild];
+ }
+}
+
+- (BOOL) isEmpty {
+ return [self.children isEmpty];
+}
+
+- (int) numChildren {
+ return [self.children count];
+}
+
+- (id) val {
+ return [self valForExport:NO];
+}
+
+- (id) valForExport:(BOOL)exp {
+ if([self isEmpty]) {
+ return [NSNull null];
+ }
+
+ __block int numKeys = 0;
+ __block NSInteger maxKey = 0;
+ __block BOOL allIntegerKeys = YES;
+
+ NSMutableDictionary* obj = [[NSMutableDictionary alloc] initWithCapacity:[self.children count]];
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> childNode, BOOL *stop) {
+ [obj setObject:[childNode valForExport:exp] forKey:key];
+
+ numKeys++;
+
+ // If we already found a string key, don't bother with any of this
+ if (!allIntegerKeys) {
+ return;
+ }
+
+ // Treat leading zeroes that are not exactly "0" as strings
+ NSString* firstChar = [key substringWithRange:NSMakeRange(0, 1)];
+ if ([firstChar isEqualToString:@"0"] && [key length] > 1) {
+ allIntegerKeys = NO;
+ } else {
+ NSNumber *keyAsNum = [FUtilities intForString:key];
+ if (keyAsNum != nil) {
+ NSInteger keyAsInt = [keyAsNum integerValue];
+ if (keyAsInt > maxKey) {
+ maxKey = keyAsInt;
+ }
+ } else {
+ allIntegerKeys = NO;
+ }
+ }
+ }];
+
+ if (!exp && allIntegerKeys && maxKey < 2 * numKeys) {
+ // convert to an array
+ NSMutableArray* array = [[NSMutableArray alloc] initWithCapacity:maxKey + 1];
+ for (int i = 0; i <= maxKey; ++i) {
+ NSString* keyString = [NSString stringWithFormat:@"%i", i];
+ id child = obj[keyString];
+ if (child != nil) {
+ [array addObject:child];
+ } else {
+ [array addObject:[NSNull null]];
+ }
+ }
+ return array;
+ } else {
+
+ if(exp && [self getPriority] != nil && !self.getPriority.isEmpty) {
+ obj[kPayloadPriority] = [self.getPriority val];
+ }
+
+ return obj;
+ }
+}
+
+- (NSString *) dataHash {
+ if (self.lazyHash == nil) {
+ NSMutableString *toHash = [[NSMutableString alloc] init];
+
+ if (!self.getPriority.isEmpty) {
+ [toHash appendString:@"priority:"];
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:(FLeafNode *)self.getPriority
+ toString:toHash
+ hashVersion:FDataHashVersionV1];
+ [toHash appendString:@":"];
+ }
+
+ __block BOOL sawPriority = NO;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ sawPriority = sawPriority || [[node getPriority] isEmpty];
+ *stop = sawPriority;
+ }];
+ if (sawPriority) {
+ NSMutableArray *array = [NSMutableArray array];
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ FNamedNode *namedNode = [[FNamedNode alloc] initWithName:key andNode:node];
+ [array addObject:namedNode];
+ }];
+ [array sortUsingComparator:^NSComparisonResult(FNamedNode *namedNode1, FNamedNode *namedNode2) {
+ return [[FPriorityIndex priorityIndex] compareNamedNode:namedNode1 toNamedNode:namedNode2];
+ }];
+ [array enumerateObjectsUsingBlock:^(FNamedNode *namedNode, NSUInteger idx, BOOL *stop) {
+ NSString *childHash = [namedNode.node dataHash];
+ if (![childHash isEqualToString:@""]) {
+ [toHash appendFormat:@":%@:%@", namedNode.name, childHash];
+ }
+ }];
+ } else {
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ NSString *childHash = [node dataHash];
+ if (![childHash isEqualToString:@""]) {
+ [toHash appendFormat:@":%@:%@", key, childHash];
+ }
+ }];
+ }
+ self.lazyHash = [toHash isEqualToString:@""] ? @"" : [FStringUtilities base64EncodedSha1:toHash];
+ }
+ return self.lazyHash;
+}
+
+- (NSComparisonResult)compare:(id <FNode>)other {
+ // children nodes come last, unless this is actually an empty node, then we come first.
+ if (self.isEmpty) {
+ if (other.isEmpty) {
+ return NSOrderedSame;
+ } else {
+ return NSOrderedAscending;
+ }
+ } else if (other.isLeafNode || other.isEmpty) {
+ return NSOrderedDescending;
+ } else if (other == [FMaxNode maxNode]) {
+ return NSOrderedAscending;
+ } else {
+ // Must be another node with children.
+ return NSOrderedSame;
+ }
+}
+
+- (BOOL)isEqual:(id <FNode>)other {
+ if (other == self) {
+ return YES;
+ } else if (other == nil) {
+ return NO;
+ } else if (other.isLeafNode) {
+ return NO;
+ } else if (self.isEmpty && [other isEmpty]) {
+ // Empty nodes do not have priority
+ return YES;
+ } else {
+ FChildrenNode *otherChildrenNode = other;
+ if (![self.getPriority isEqual:other.getPriority]) {
+ return NO;
+ } else if (self.children.count == otherChildrenNode.children.count) {
+ __block BOOL equal = YES;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ id<FNode> child = [otherChildrenNode getImmediateChild:key];
+ if (![child isEqual:node]) {
+ equal = NO;
+ *stop = YES;
+ }
+ }];
+ return equal;
+ } else {
+ return NO;
+ }
+ }
+}
+
+- (NSUInteger)hash {
+ __block NSUInteger hashCode = 0;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ hashCode = 31 * hashCode + key.hash;
+ hashCode = 17 * hashCode + node.hash;
+ }];
+ return 17 * hashCode + self.priorityNode.hash;
+}
+
+- (void) enumerateChildrenAndPriorityUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ if ([self.getPriority isEmpty]) {
+ [self enumerateChildrenUsingBlock:block];
+ } else {
+ __block BOOL passedPriorityKey = NO;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ if (!passedPriorityKey && [FUtilities compareKey:key toKey:@".priority"] == NSOrderedDescending) {
+ passedPriorityKey = YES;
+ BOOL stopAfterPriority = NO;
+ block(@".priority", [self getPriority], &stopAfterPriority);
+ if (stopAfterPriority) return;
+ }
+ block(key, node, stop);
+ }];
+ }
+}
+
+- (void) enumerateChildrenUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ [self.children enumerateKeysAndObjectsUsingBlock:block];
+}
+
+- (void) enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ [self.children enumerateKeysAndObjectsReverse:reverse usingBlock:block];
+}
+
+- (NSEnumerator *)childEnumerator
+{
+ return [[FTransformedEnumerator alloc] initWithEnumerator:self.children.keyEnumerator andTransform:^id(NSString *key) {
+ return [FNamedNode nodeWithName:key node:[self getImmediateChild:key]];
+ }];
+}
+
+- (NSString *) predecessorChildKey:(NSString *)childKey
+{
+ return [self.children getPredecessorKey:childKey];
+}
+
+#pragma mark -
+#pragma mark FChildrenNode specific methods
+
+- (id) childrenGetter:(id)key {
+ return [self.children objectForKey:key];
+}
+
+- (FNamedNode *)firstChild
+{
+ NSString *childKey = self.children.minKey;
+ if (childKey) {
+ return [[FNamedNode alloc] initWithName:childKey andNode:[self getImmediateChild:childKey]];
+ } else {
+ return nil;
+ }
+}
+
+- (FNamedNode *)lastChild
+{
+ NSString *childKey = self.children.maxKey;
+ if (childKey) {
+ return [[FNamedNode alloc] initWithName:childKey andNode:[self getImmediateChild:childKey]];
+ } else {
+ return nil;
+ }
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FCompoundWrite.h b/Firebase/Database/Snapshot/FCompoundWrite.h
new file mode 100644
index 0000000..c67cfc7
--- /dev/null
+++ b/Firebase/Database/Snapshot/FCompoundWrite.h
@@ -0,0 +1,61 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FImmutableTree;
+@protocol FNode;
+@class FPath;
+
+/**
+* This class holds a collection of writes that can be applied to nodes in unison. It abstracts away the logic with
+* dealing with priority writes and multiple nested writes. At any given path, there is only allowed to be one write
+* modifying that path. Any write to an existing path or shadowing an existing path will modify that existing write to
+* reflect the write added.
+*/
+@interface FCompoundWrite : NSObject
+
+- (id) initWithWriteTree:(FImmutableTree *)tree;
+
+/**
+ * Creates a compound write with NSDictionary from path string to object
+ */
++ (FCompoundWrite *) compoundWriteWithValueDictionary:(NSDictionary *)dictionary;
+/**
+ * Creates a compound write with NSDictionary from path string to node
+ */
++ (FCompoundWrite *) compoundWriteWithNodeDictionary:(NSDictionary *)dictionary;
+
++ (FCompoundWrite *) emptyWrite;
+
+- (FCompoundWrite *) addWrite:(id<FNode>)node atPath:(FPath *)path;
+- (FCompoundWrite *) addWrite:(id<FNode>)node atKey:(NSString *)key;
+- (FCompoundWrite *) addCompoundWrite:(FCompoundWrite *)node atPath:(FPath *)path;
+- (FCompoundWrite *) removeWriteAtPath:(FPath *)path;
+- (id<FNode>)rootWrite;
+- (BOOL) hasCompleteWriteAtPath:(FPath *)path;
+- (id<FNode>) completeNodeAtPath:(FPath *)path;
+- (NSArray *) completeChildren;
+- (NSDictionary *)childCompoundWrites;
+- (FCompoundWrite *) childCompoundWriteAtPath:(FPath *)path;
+- (id<FNode>) applyToNode:(id<FNode>)node;
+- (void)enumerateWrites:(void (^)(FPath *path, id<FNode>node, BOOL *stop))block;
+
+- (NSDictionary *)valForExport:(BOOL)exportFormat;
+
+- (BOOL) isEmpty;
+
+@end
diff --git a/Firebase/Database/Snapshot/FCompoundWrite.m b/Firebase/Database/Snapshot/FCompoundWrite.m
new file mode 100644
index 0000000..8887095
--- /dev/null
+++ b/Firebase/Database/Snapshot/FCompoundWrite.m
@@ -0,0 +1,257 @@
+/*
+ * 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 "FCompoundWrite.h"
+#import "FImmutableTree.h"
+#import "FNode.h"
+#import "FPath.h"
+#import "FNamedNode.h"
+#import "FSnapshotUtilities.h"
+
+@interface FCompoundWrite ()
+@property (nonatomic, strong) FImmutableTree *writeTree;
+@end
+
+@implementation FCompoundWrite
+
+- (id) initWithWriteTree:(FImmutableTree *)tree {
+ self = [super init];
+ if (self) {
+ self.writeTree = tree;
+ }
+ return self;
+}
+
++ (FCompoundWrite *)compoundWriteWithValueDictionary:(NSDictionary *)dictionary {
+ __block FImmutableTree *writeTree = [FImmutableTree empty];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *pathString, id value, BOOL *stop) {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:value];
+ FImmutableTree *tree = [[FImmutableTree alloc] initWithValue:node];
+ writeTree = [writeTree setTree:tree atPath:[[FPath alloc] initWith:pathString]];
+ }];
+ return [[FCompoundWrite alloc] initWithWriteTree:writeTree];
+}
+
++ (FCompoundWrite *)compoundWriteWithNodeDictionary:(NSDictionary *)dictionary {
+ __block FImmutableTree *writeTree = [FImmutableTree empty];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *pathString, id node, BOOL *stop) {
+ FImmutableTree *tree = [[FImmutableTree alloc] initWithValue:node];
+ writeTree = [writeTree setTree:tree atPath:[[FPath alloc] initWith:pathString]];
+ }];
+ return [[FCompoundWrite alloc] initWithWriteTree:writeTree];
+}
+
++ (FCompoundWrite *) emptyWrite {
+ static dispatch_once_t pred = 0;
+ static FCompoundWrite *empty = nil;
+ dispatch_once(&pred, ^{
+ empty = [[FCompoundWrite alloc] initWithWriteTree:[[FImmutableTree alloc] initWithValue:nil]];
+ });
+ return empty;
+}
+
+- (FCompoundWrite *) addWrite:(id<FNode>)node atPath:(FPath *)path {
+ if (path.isEmpty) {
+ return [[FCompoundWrite alloc] initWithWriteTree:[[FImmutableTree alloc] initWithValue:node]];
+ } else {
+ FTuplePathValue *rootMost = [self.writeTree findRootMostValueAndPath:path];
+ if (rootMost != nil) {
+ FPath *relativePath = [FPath relativePathFrom:rootMost.path to:path];
+ id<FNode> value = [rootMost.value updateChild:relativePath withNewChild:node];
+ return [[FCompoundWrite alloc] initWithWriteTree:[self.writeTree setValue:value atPath:rootMost.path]];
+ } else {
+ FImmutableTree *subtree = [[FImmutableTree alloc] initWithValue:node];
+ FImmutableTree *newWriteTree = [self.writeTree setTree:subtree atPath:path];
+ return [[FCompoundWrite alloc] initWithWriteTree:newWriteTree];
+ }
+ }
+}
+
+- (FCompoundWrite *) addWrite:(id<FNode>)node atKey:(NSString *)key {
+ return [self addWrite:node atPath:[[FPath alloc] initWith:key]];
+}
+
+- (FCompoundWrite *) addCompoundWrite:(FCompoundWrite *)compoundWrite atPath:(FPath *)path {
+ __block FCompoundWrite *newWrite = self;
+ [compoundWrite.writeTree forEach:^(FPath *childPath, id<FNode> value) {
+ newWrite = [newWrite addWrite:value atPath:[path child:childPath]];
+ }];
+ return newWrite;
+}
+
+/**
+* Will remove a write at the given path and deeper paths. This will <em>not</em> modify a write at a higher location,
+* which must be removed by calling this method with that path.
+* @param path The path at which a write and all deeper writes should be removed.
+* @return The new FWriteCompound with the removed path.
+*/
+- (FCompoundWrite *) removeWriteAtPath:(FPath *)path {
+ if (path.isEmpty) {
+ return [FCompoundWrite emptyWrite];
+ } else {
+ FImmutableTree *newWriteTree = [self.writeTree setTree:[FImmutableTree empty] atPath:path];
+ return [[FCompoundWrite alloc] initWithWriteTree:newWriteTree];
+ }
+}
+
+/**
+* Returns whether this FCompoundWrite will fully overwrite a node at a given location and can therefore be considered
+* "complete".
+* @param path The path to check for
+* @return Whether there is a complete write at that path.
+*/
+- (BOOL) hasCompleteWriteAtPath:(FPath *)path {
+ return [self completeNodeAtPath:path] != nil;
+}
+
+/**
+* Returns a node for a path if and only if the node is a "complete" overwrite at that path. This will not aggregate
+* writes from depeer paths, but will return child nodes from a more shallow path.
+* @param path The path to get a complete write
+* @return The node if complete at that path, or nil otherwise.
+*/
+- (id<FNode>) completeNodeAtPath:(FPath *)path {
+ FTuplePathValue *rootMost = [self.writeTree findRootMostValueAndPath:path];
+ if (rootMost != nil) {
+ FPath *relativePath = [FPath relativePathFrom:rootMost.path to:path];
+ return [rootMost.value getChild:relativePath];
+ } else {
+ return nil;
+ }
+}
+
+// TODO: change into traversal method...
+- (NSArray *) completeChildren {
+ NSMutableArray *children = [[NSMutableArray alloc] init];
+ if (self.writeTree.value != nil) {
+ id<FNode> node = self.writeTree.value;
+ [node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [children addObject:[[FNamedNode alloc] initWithName:key andNode:node]];
+ }];
+ } else {
+ [self.writeTree.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if (childTree.value != nil) {
+ [children addObject:[[FNamedNode alloc] initWithName:childKey andNode:childTree.value]];
+ }
+ }];
+ }
+ return children;
+}
+
+
+// TODO: change into enumarate method
+- (NSDictionary *)childCompoundWrites {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ [self.writeTree.children enumerateKeysAndObjectsUsingBlock:^(NSString *key, FImmutableTree *childWrite, BOOL *stop) {
+ dict[key] = [[FCompoundWrite alloc] initWithWriteTree:childWrite];
+ }];
+ return dict;
+}
+
+- (FCompoundWrite *) childCompoundWriteAtPath:(FPath *)path {
+ if (path.isEmpty) {
+ return self;
+ } else {
+ id<FNode> shadowingNode = [self completeNodeAtPath:path];
+ if (shadowingNode != nil) {
+ return [[FCompoundWrite alloc] initWithWriteTree:[[FImmutableTree alloc] initWithValue:shadowingNode]];
+ } else {
+ return [[FCompoundWrite alloc] initWithWriteTree:[self.writeTree subtreeAtPath:path]];
+ }
+ }
+}
+
+- (id<FNode>) applySubtreeWrite:(FImmutableTree *)subtreeWrite atPath:(FPath *)relativePath toNode:(id<FNode>)node {
+ if (subtreeWrite.value != nil) {
+ // Since a write there is always a leaf, we're done here.
+ return [node updateChild:relativePath withNewChild:subtreeWrite.value];
+ } else {
+ __block id<FNode> priorityWrite = nil;
+ __block id<FNode> blockNode = node;
+ [subtreeWrite.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if ([childKey isEqualToString:@".priority"]) {
+ // Apply priorities at the end so we don't update priorities for either empty nodes or forget to apply
+ // priorities to empty nodes that are later filled.
+ NSAssert(childTree.value != nil, @"Priority writes must always be leaf nodes");
+ priorityWrite = childTree.value;
+ } else {
+ blockNode = [self applySubtreeWrite:childTree atPath:[relativePath childFromString:childKey] toNode:blockNode];
+ }
+ }];
+ // If there was a priority write, we only apply it if the node is not empty
+ if (![blockNode getChild:relativePath].isEmpty && priorityWrite != nil) {
+ blockNode = [blockNode updateChild:[relativePath childFromString:@".priority"] withNewChild:priorityWrite];
+ }
+ return blockNode;
+ }
+}
+
+- (void)enumerateWrites:(void (^)(FPath *, id<FNode>, BOOL *))block {
+ __block BOOL stop = NO;
+ // TODO: add stop to tree iterator...
+ [self.writeTree forEach:^(FPath *path, id value) {
+ if (!stop) {
+ block(path, value, &stop);
+ }
+ }];
+}
+
+/**
+* Applies this FCompoundWrite to a node. The node is returned with all writes from this FCompoundWrite applied to the node.
+* @param node The node to apply this FCompoundWrite to
+* @return The node with all writes applied
+*/
+- (id<FNode>) applyToNode:(id<FNode>)node {
+ return [self applySubtreeWrite:self.writeTree atPath:[FPath empty] toNode:node];
+}
+
+/**
+* Return true if this CompoundWrite is empty and therefore does not modify any nodes.
+* @return Whether this CompoundWrite is empty
+*/
+- (BOOL) isEmpty {
+ return self.writeTree.isEmpty;
+}
+
+- (id<FNode>) rootWrite {
+ return self.writeTree.value;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FCompoundWrite class]]) {
+ return NO;
+ }
+ FCompoundWrite *other = (FCompoundWrite *)object;
+ return [[self valForExport:YES] isEqualToDictionary:[other valForExport:YES]];
+}
+
+- (NSUInteger)hash {
+ return [[self valForExport:YES] hash];
+}
+
+- (NSDictionary *)valForExport:(BOOL)exportFormat {
+ NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
+ [self.writeTree forEach:^(FPath *path, id<FNode> value) {
+ dictionary[path.wireFormat] = [value valForExport:exportFormat];
+ }];
+ return dictionary;
+}
+
+- (NSString *)description {
+ return [[self valForExport:YES] description];
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FEmptyNode.h b/Firebase/Database/Snapshot/FEmptyNode.h
new file mode 100644
index 0000000..ab404c2
--- /dev/null
+++ b/Firebase/Database/Snapshot/FEmptyNode.h
@@ -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 <Foundation/Foundation.h>
+#import "FNode.h"
+
+@interface FEmptyNode : NSObject
+
++ (id<FNode>) emptyNode;
+
+@end
diff --git a/Firebase/Database/Snapshot/FEmptyNode.m b/Firebase/Database/Snapshot/FEmptyNode.m
new file mode 100644
index 0000000..dd2d9ea
--- /dev/null
+++ b/Firebase/Database/Snapshot/FEmptyNode.m
@@ -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 "FEmptyNode.h"
+#import "FChildrenNode.h"
+
+@implementation FEmptyNode
+
++ (id<FNode>) emptyNode {
+ static FChildrenNode* empty = nil;
+ if (empty == nil) {
+ empty = [[FChildrenNode alloc] init];
+ }
+ return empty;
+}
+@end
diff --git a/Firebase/Database/Snapshot/FIndexedNode.h b/Firebase/Database/Snapshot/FIndexedNode.h
new file mode 100644
index 0000000..fd2db37
--- /dev/null
+++ b/Firebase/Database/Snapshot/FIndexedNode.h
@@ -0,0 +1,49 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FNode.h"
+#import "FIndex.h"
+#import "FNamedNode.h"
+
+/**
+ * Represents a node together with an index. The index and node are updated in unison. In the case where the index
+ * does not affect the ordering (i.e. the ordering is identical to the key ordering) this class uses a fallback index
+ * to save memory. Everything operating on the index must special case the fallback index.
+ */
+@interface FIndexedNode : NSObject
+
+@property (nonatomic, strong, readonly) id<FNode> node;
+
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node;
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node index:(id<FIndex>)index;
+
+- (BOOL)hasIndex:(id<FIndex>)index;
+- (FIndexedNode *)updateChild:(NSString *)key withNewChild:(id<FNode>)newChildNode;
+- (FIndexedNode *)updatePriority:(id<FNode>)priority;
+
+- (FNamedNode *)firstChild;
+- (FNamedNode *)lastChild;
+
+- (NSString *)predecessorForChildKey:(NSString *)childKey childNode:(id<FNode>)childNode index:(id<FIndex>)index;
+
+- (void)enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *key, id<FNode> node, BOOL *stop))block;
+
+- (NSEnumerator *)childEnumerator;
+
+@end
diff --git a/Firebase/Database/Snapshot/FIndexedNode.m b/Firebase/Database/Snapshot/FIndexedNode.m
new file mode 100644
index 0000000..e874dcf
--- /dev/null
+++ b/Firebase/Database/Snapshot/FIndexedNode.m
@@ -0,0 +1,202 @@
+/*
+ * 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 "FIndexedNode.h"
+
+#import "FImmutableSortedSet.h"
+#import "FIndex.h"
+#import "FPriorityIndex.h"
+#import "FKeyIndex.h"
+#import "FChildrenNode.h"
+
+static FImmutableSortedSet *FALLBACK_INDEX;
+
+@interface FIndexedNode ()
+
+@property (nonatomic, strong) id<FNode> node;
+/**
+ * The indexed set is initialized lazily to prevent creation when it is not needed
+ */
+@property (nonatomic, strong) FImmutableSortedSet *indexed;
+@property (nonatomic, strong) id<FIndex> index;
+
+@end
+
+@implementation FIndexedNode
+
++ (FImmutableSortedSet *)fallbackIndex {
+ static FImmutableSortedSet *fallbackIndex;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ fallbackIndex = [[FImmutableSortedSet alloc] init];
+ });
+ return fallbackIndex;
+}
+
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node
+{
+ return [[FIndexedNode alloc] initWithNode:node index:[FPriorityIndex priorityIndex]];
+}
+
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node index:(id<FIndex>)index
+{
+ return [[FIndexedNode alloc] initWithNode:node index:index];
+}
+
+- (id)initWithNode:(id<FNode>)node index:(id<FIndex>)index
+{
+ // Initialize indexed lazily
+ return [self initWithNode:node index:index indexed:nil];
+}
+
+- (id)initWithNode:(id<FNode>)node index:(id<FIndex>)index indexed:(FImmutableSortedSet *)indexed
+{
+ self = [super init];
+ if (self != nil) {
+ self->_node = node;
+ self->_index = index;
+ self->_indexed = indexed;
+ }
+ return self;
+}
+
+- (void)ensureIndexed
+{
+ if (!self.indexed) {
+ if ([self.index isEqual:[FKeyIndex keyIndex]]) {
+ self.indexed = [FIndexedNode fallbackIndex];
+ } else {
+ __block BOOL sawChild;
+ [self.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ sawChild = sawChild || [self.index isDefinedOn:node];
+ *stop = sawChild;
+ }];
+ if (sawChild) {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ [self.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ FNamedNode *namedNode = [[FNamedNode alloc] initWithName:key andNode:node];
+ dict[namedNode] = [NSNull null];
+ }];
+ // Make sure to assign index here, because the comparator will be retained and using self will cause a
+ // cycle
+ id<FIndex> index = self.index;
+ self.indexed = [FImmutableSortedSet setWithKeysFromDictionary:dict
+ comparator:^NSComparisonResult(FNamedNode *namedNode1, FNamedNode *namedNode2) {
+ return [index compareNamedNode:namedNode1 toNamedNode:namedNode2];
+ }];
+ } else {
+ self.indexed = [FIndexedNode fallbackIndex];
+ }
+ }
+ }
+}
+
+- (BOOL)hasIndex:(id<FIndex>)index
+{
+ return [self.index isEqual:index];
+}
+
+- (FIndexedNode *)updateChild:(NSString *)key withNewChild:(id<FNode>)newChildNode
+{
+ id<FNode> newNode = [self.node updateImmediateChild:key withNewChild:newChildNode];
+ if (self.indexed == [FIndexedNode fallbackIndex] && ![self.index isDefinedOn:newChildNode]) {
+ // doesn't affect the index, no need to create an index
+ return [[FIndexedNode alloc] initWithNode:newNode index:self.index indexed:[FIndexedNode fallbackIndex]];
+ } else if (!self.indexed || self.indexed == [FIndexedNode fallbackIndex]) {
+ // No need to index yet, index lazily
+ return [[FIndexedNode alloc] initWithNode:newNode index:self.index];
+ } else {
+ id<FNode> oldChild = [self.node getImmediateChild:key];
+ FImmutableSortedSet *newIndexed = [self.indexed removeObject:[FNamedNode nodeWithName:key node:oldChild]];
+ if (![newChildNode isEmpty]) {
+ newIndexed = [newIndexed addObject:[FNamedNode nodeWithName:key node:newChildNode]];
+ }
+ return [[FIndexedNode alloc] initWithNode:newNode index:self.index indexed:newIndexed];
+ }
+}
+
+- (FIndexedNode *)updatePriority:(id<FNode>)priority
+{
+ return [[FIndexedNode alloc] initWithNode:[self.node updatePriority:priority]
+ index:self.index
+ indexed:self.indexed];
+}
+
+- (FNamedNode *)firstChild
+{
+ if (![self.node isKindOfClass:[FChildrenNode class]]) {
+ return nil;
+ } else {
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [((FChildrenNode *)self.node) firstChild];
+ } else {
+ return self.indexed.firstObject;
+ }
+ }
+}
+
+- (FNamedNode *)lastChild
+{
+ if (![self.node isKindOfClass:[FChildrenNode class]]) {
+ return nil;
+ } else {
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [((FChildrenNode *)self.node) lastChild];
+ } else {
+ return self.indexed.lastObject;
+ }
+ }
+}
+
+- (NSString *)predecessorForChildKey:(NSString *)childKey childNode:(id<FNode>)childNode index:(id<FIndex>)index
+{
+ if (![self.index isEqual:index]) {
+ [NSException raise:NSInvalidArgumentException format:@"Index not available in IndexedNode!"];
+ }
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [self.node predecessorChildKey:childKey];
+ } else {
+ FNamedNode *node = [self.indexed predecessorEntry:[FNamedNode nodeWithName:childKey node:childNode]];
+ return node.name;
+ }
+}
+
+- (void)enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ [self.node enumerateChildrenReverse:reverse usingBlock:block];
+ } else {
+ [self.indexed enumerateObjectsReverse:reverse usingBlock:^(FNamedNode *namedNode, BOOL *stop) {
+ block(namedNode.name, namedNode.node, stop);
+ }];
+ }
+}
+
+- (NSEnumerator *)childEnumerator
+{
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [self.node childEnumerator];
+ } else {
+ return [self.indexed objectEnumerator];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FLeafNode.h b/Firebase/Database/Snapshot/FLeafNode.h
new file mode 100644
index 0000000..15e0132
--- /dev/null
+++ b/Firebase/Database/Snapshot/FLeafNode.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 <Foundation/Foundation.h>
+#import "FNode.h"
+
+
+@interface FLeafNode : NSObject <FNode>
+
+- (id)initWithValue:(id)aValue;
+- (id)initWithValue:(id)aValue withPriority:(id<FNode>)aPriority;
+
+@property (nonatomic, strong) id value;
+
+@end
diff --git a/Firebase/Database/Snapshot/FLeafNode.m b/Firebase/Database/Snapshot/FLeafNode.m
new file mode 100644
index 0000000..a26e057
--- /dev/null
+++ b/Firebase/Database/Snapshot/FLeafNode.m
@@ -0,0 +1,250 @@
+/*
+ * 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 "FLeafNode.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FConstants.h"
+#import "FImmutableSortedDictionary.h"
+#import "FUtilities.h"
+#import "FStringUtilities.h"
+#import "FSnapshotUtilities.h"
+
+@interface FLeafNode ()
+@property (nonatomic, strong) id<FNode> priorityNode;
+@property (nonatomic, strong) NSString *lazyHash;
+
+@end
+
+@implementation FLeafNode
+
+@synthesize value;
+@synthesize priorityNode;
+
+- (id)initWithValue:(id)aValue {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ self.priorityNode = [FEmptyNode emptyNode];
+ }
+ return self;
+}
+
+- (id)initWithValue:(id)aValue withPriority:(id<FNode>)aPriority {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ [FSnapshotUtilities validatePriorityNode:aPriority];
+ self.priorityNode = aPriority;
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark FNode methods
+
+- (BOOL) isLeafNode {
+ return YES;
+}
+
+- (id<FNode>) getPriority {
+ return self.priorityNode;
+}
+
+- (id<FNode>) updatePriority:(id<FNode>)aPriority {
+ return [[FLeafNode alloc] initWithValue:self.value withPriority:aPriority];
+}
+
+- (id<FNode>) getImmediateChild:(NSString *) childName {
+ if ([childName isEqualToString:@".priority"]) {
+ return self.priorityNode;
+ } else {
+ return [FEmptyNode emptyNode];
+ }
+}
+
+- (id<FNode>) getChild:(FPath *)path {
+ if (path.getFront == nil) {
+ return self;
+ } else if ([[path getFront] isEqualToString:@".priority"]) {
+ return [self getPriority];
+ } else {
+ return [FEmptyNode emptyNode];
+ }
+}
+
+- (BOOL)hasChild:(NSString *)childName {
+ return [childName isEqualToString:@".priority"] && ![self getPriority].isEmpty;
+}
+
+
+- (NSString *)predecessorChildKey:(NSString *)childKey
+{
+ return nil;
+}
+
+- (id<FNode>) updateImmediateChild:(NSString *)childName withNewChild:(id<FNode>)newChildNode {
+ if ([childName isEqualToString:@".priority"]) {
+ return [self updatePriority:newChildNode];
+ } else if (newChildNode.isEmpty) {
+ return self;
+ } else {
+ FChildrenNode* childrenNode = [[FChildrenNode alloc] init];
+ childrenNode = [childrenNode updateImmediateChild:childName withNewChild:newChildNode];
+ childrenNode = [childrenNode updatePriority:self.priorityNode];
+ return childrenNode;
+ }
+}
+
+- (id<FNode>) updateChild:(FPath *)path withNewChild:(id<FNode>)newChildNode {
+ NSString* front = [path getFront];
+ if(front == nil) {
+ return newChildNode;
+ } else if (newChildNode.isEmpty && ![front isEqualToString:@".priority"]) {
+ return self;
+ } else {
+ NSAssert(![front isEqualToString:@".priority"] || path.length == 1, @".priority must be the last token in a path.");
+ return [self updateImmediateChild:front withNewChild:
+ [[FEmptyNode emptyNode] updateChild:[path popFront] withNewChild:newChildNode]];
+ }
+}
+
+- (id) val {
+ return [self valForExport:NO];
+}
+
+- (id) valForExport:(BOOL)exp {
+ if(exp && !self.getPriority.isEmpty) {
+ return @{kPayloadValue : self.value,
+ kPayloadPriority : [[self getPriority] val]};
+ }
+ else {
+ return self.value;
+ }
+}
+
+- (BOOL)isEqual:(id <FNode>)other {
+ if(other == self) {
+ return YES;
+ } else if (other.isLeafNode) {
+ FLeafNode *otherLeaf = other;
+ if ([FUtilities getJavascriptType:self.value] != [FUtilities getJavascriptType:otherLeaf.value]) {
+ return NO;
+ }
+ return [otherLeaf.value isEqual:self.value] && [otherLeaf.priorityNode isEqual:self.priorityNode];
+ } else {
+ return NO;
+ }
+}
+
+- (NSUInteger)hash {
+ return [self.value hash] * 17 + self.priorityNode.hash;
+}
+
+- (id <FNode>)withIndex:(id <FIndex>)index {
+ return self;
+}
+
+- (BOOL)isIndexed:(id <FIndex>)index {
+ return YES;
+}
+
+- (BOOL) isEmpty {
+ return NO;
+}
+
+- (int) numChildren {
+ return 0;
+}
+
+- (void) enumerateChildrenUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ // Nothing to iterate over
+}
+
+- (void) enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ // Nothing to iterate over
+}
+
+- (NSEnumerator *)childEnumerator
+{
+ // Nothing to iterate over
+ return [@[] objectEnumerator];
+}
+
+- (NSString *) dataHash {
+ if (self.lazyHash == nil) {
+ NSMutableString *toHash = [[NSMutableString alloc] init];
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:self toString:toHash hashVersion:FDataHashVersionV1];
+
+ self.lazyHash = [FStringUtilities base64EncodedSha1:toHash];
+ }
+ return self.lazyHash;
+}
+
+- (NSComparisonResult)compare:(id <FNode>)other {
+ if (other == [FEmptyNode emptyNode]) {
+ return NSOrderedDescending;
+ } else if ([other isKindOfClass:[FChildrenNode class]]) {
+ return NSOrderedAscending;
+ } else {
+ NSAssert(other.isLeafNode, @"Compared against unknown type of node.");
+ return [self compareToLeafNode:(FLeafNode*)other];
+ }
+}
+
++ (NSArray*) valueTypeOrder {
+ static NSArray* valueOrder = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ valueOrder = @[kJavaScriptObject, kJavaScriptBoolean, kJavaScriptNumber, kJavaScriptString];
+ });
+ return valueOrder;
+}
+
+- (NSComparisonResult) compareToLeafNode:(FLeafNode*)other {
+ NSString* thisLeafType = [FUtilities getJavascriptType:self.value];
+ NSString* otherLeafType = [FUtilities getJavascriptType:other.value];
+ NSUInteger thisIndex = [[FLeafNode valueTypeOrder] indexOfObject:thisLeafType];
+ NSUInteger otherIndex = [[FLeafNode valueTypeOrder] indexOfObject:otherLeafType];
+ assert(thisIndex >= 0 && otherIndex >= 0);
+ if (otherIndex == thisIndex) {
+ // Same type. Compare values.
+ if (thisLeafType == kJavaScriptObject) {
+ // Deferred value nodes are all equal, but we should also never get to this point...
+ return NSOrderedSame;
+ } else if (thisLeafType == kJavaScriptString) {
+ return [self.value compare:other.value options:NSLiteralSearch];
+ } else {
+ return [self.value compare:other.value];
+ }
+ } else {
+ return thisIndex > otherIndex ? NSOrderedDescending : NSOrderedAscending;
+ }
+}
+
+- (NSString *) description {
+ return [[self valForExport:YES] description];
+}
+
+- (void) forEachChildDo:(fbt_bool_nsstring_node)action {
+ // There are no children, so there is nothing to do.
+ return;
+}
+
+
+@end
diff --git a/Firebase/Database/Snapshot/FNode.h b/Firebase/Database/Snapshot/FNode.h
new file mode 100644
index 0000000..1316756
--- /dev/null
+++ b/Firebase/Database/Snapshot/FNode.h
@@ -0,0 +1,46 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FPath.h"
+#import "FTypedefs_Private.h"
+
+@protocol FIndex;
+
+@protocol FNode <NSObject>
+
+- (BOOL) isLeafNode;
+- (id<FNode>) getPriority;
+- (id<FNode>) updatePriority:(id<FNode>)priority;
+- (id<FNode>) getImmediateChild:(NSString *)childKey;
+- (id<FNode>) getChild:(FPath *)path;
+- (NSString *) predecessorChildKey:(NSString *)childKey;
+- (id<FNode>) updateImmediateChild:(NSString *)childKey withNewChild:(id<FNode>)newChildNode;
+- (id<FNode>) updateChild:(FPath *)path withNewChild:(id<FNode>)newChildNode;
+- (BOOL) hasChild:(NSString*)childKey;
+- (BOOL) isEmpty;
+- (int) numChildren;
+- (id) val;
+- (id) valForExport:(BOOL)exp;
+- (NSString *) dataHash;
+- (NSComparisonResult) compare:(id<FNode>)other;
+- (BOOL) isEqual:(id<FNode>)other;
+- (void)enumerateChildrenUsingBlock:(void (^)(NSString *key, id<FNode> node, BOOL *stop))block;
+- (void)enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *key, id<FNode> node, BOOL *stop))block;
+
+- (NSEnumerator *)childEnumerator;
+
+@end
diff --git a/Firebase/Database/Snapshot/FSnapshotUtilities.h b/Firebase/Database/Snapshot/FSnapshotUtilities.h
new file mode 100644
index 0000000..2a28788
--- /dev/null
+++ b/Firebase/Database/Snapshot/FSnapshotUtilities.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FNode.h"
+
+@class FImmutableSortedDictionary;
+@class FCompoundWrite;
+@class FLeafNode;
+@protocol FNode;
+
+typedef NS_ENUM(NSInteger, FDataHashVersion) {
+ FDataHashVersionV1,
+ FDataHashVersionV2,
+};
+
+@interface FSnapshotUtilities : NSObject
+
++ (id<FNode>) nodeFrom:(id)val;
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority;
++ (id<FNode>) nodeFrom:(id)val withValidationFrom:(NSString *)fn;
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority withValidationFrom:(NSString *)fn;
++ (FCompoundWrite *) compoundWriteFromDictionary:(NSDictionary *)values withValidationFrom:(NSString *)fn;
++ (void) validatePriorityNode:(id<FNode>)priorityNode;
++ (void)appendHashRepresentationForLeafNode:(FLeafNode *)val
+ toString:(NSMutableString *)string
+ hashVersion:(FDataHashVersion)hashVersion;
++ (void)appendHashV2RepresentationForString:(NSString *)string toString:(NSMutableString *)mutableString;
+
++ (NSUInteger)estimateSerializedNodeSize:(id<FNode>)node;
+
+@end
diff --git a/Firebase/Database/Snapshot/FSnapshotUtilities.m b/Firebase/Database/Snapshot/FSnapshotUtilities.m
new file mode 100644
index 0000000..1b83430
--- /dev/null
+++ b/Firebase/Database/Snapshot/FSnapshotUtilities.m
@@ -0,0 +1,301 @@
+/*
+ * 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 "FSnapshotUtilities.h"
+#import "FEmptyNode.h"
+#import "FLeafNode.h"
+#import "FConstants.h"
+#import "FUtilities.h"
+#import "FChildrenNode.h"
+#import "FLLRBValueNode.h"
+#import "FValidation.h"
+#import "FMaxNode.h"
+#import "FNamedNode.h"
+#import "FCompoundWrite.h"
+
+@implementation FSnapshotUtilities
+
++ (id<FNode>) nodeFrom:(id)val {
+ return [FSnapshotUtilities nodeFrom:val priority:nil];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority {
+ return [FSnapshotUtilities nodeFrom:val priority:priority withValidationFrom:@"nodeFrom:priority:"];
+}
+
++ (id<FNode>) nodeFrom:(id)val withValidationFrom:(NSString *)fn {
+ return [FSnapshotUtilities nodeFrom:val priority:nil withValidationFrom:fn];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority withValidationFrom:(NSString *)fn {
+ return [FSnapshotUtilities nodeFrom:val priority:priority withValidationFrom:fn atDepth:0 path:[[NSMutableArray alloc] init]];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)aPriority withValidationFrom:(NSString *)fn atDepth:(int)depth path:(NSMutableArray *)path {
+ @autoreleasepool {
+ return [FSnapshotUtilities internalNodeFrom:val priority:aPriority withValidationFrom:fn atDepth:depth path:path];
+ }
+}
+
++ (id<FNode>) internalNodeFrom:(id)val priority:(id)aPriority withValidationFrom:(NSString *)fn atDepth:(int)depth path:(NSMutableArray *)path {
+
+
+ if (depth > kFirebaseMaxObjectDepth) {
+ NSRange range;
+ range.location = 0;
+ range.length = 100;
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Max object depth exceeded: %@...", fn, pathString] userInfo:nil];
+ }
+
+ if (val == nil || val == [NSNull null]) {
+ // Null is a valid type to store
+ return [FEmptyNode emptyNode];
+ }
+
+ [FValidation validateFrom:fn isValidPriorityValue:aPriority withPath:path];
+ id<FNode> priority = [FSnapshotUtilities nodeFrom:aPriority];
+
+ id value = val;
+ BOOL isLeafNode = NO;
+
+ if([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dict = val;
+ if(dict[kPayloadPriority] != nil) {
+ id rawPriority = [dict objectForKey:kPayloadPriority];
+ [FValidation validateFrom:fn isValidPriorityValue:rawPriority withPath:path];
+ priority = [FSnapshotUtilities nodeFrom:rawPriority];
+ }
+
+ if(dict[kPayloadValue] != nil) {
+ value = [dict objectForKey:kPayloadValue];
+ if ([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
+ isLeafNode = YES;
+ } else {
+ @throw [[NSException alloc]
+ initWithName:@"InvalidLeafValueType"
+ reason:[NSString stringWithFormat:@"(%@) Invalid data type used with .value. Can only use "
+ "NSString and NSNumber or be null. Found %@ instead.",
+ fn, [[value class] description]] userInfo:nil];
+ }
+ }
+ }
+
+ if([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
+ isLeafNode = YES;
+ }
+
+ if (isLeafNode) {
+ return [[FLeafNode alloc] initWithValue:value withPriority:priority];
+ }
+
+ // Unlike with JS, we have to handle the dictionary and array cases separately.
+ if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dval = (NSDictionary *)value;
+ NSMutableDictionary *children = [NSMutableDictionary dictionaryWithCapacity:dval.count];
+
+ // Avoid creating a million newPaths by appending to old one
+ for (id keyId in dval) {
+ [FValidation validateFrom:fn validDictionaryKey:keyId withPath:path];
+ NSString* key = (NSString*)keyId;
+
+ if (![key hasPrefix:kPayloadMetadataPrefix]) {
+ [path addObject:key];
+ id<FNode> childNode = [FSnapshotUtilities nodeFrom:dval[key] priority:nil withValidationFrom:fn atDepth:depth + 1 path:path];
+ [path removeLastObject];
+
+ if (![childNode isEmpty]) {
+ children[key] = childNode;
+ }
+ }
+ }
+
+ if ([children count] == 0) {
+ return [FEmptyNode emptyNode];
+ } else {
+ FImmutableSortedDictionary *childrenDict = [FImmutableSortedDictionary fromDictionary:children
+ withComparator:[FUtilities keyComparator]];
+ return [[FChildrenNode alloc] initWithPriority:priority children:childrenDict];
+ }
+ } else if([value isKindOfClass:[NSArray class]]) {
+ NSArray* aval = (NSArray *)value;
+ NSMutableDictionary* children = [NSMutableDictionary dictionaryWithCapacity:aval.count];
+
+ for(int i = 0; i < [aval count]; i++) {
+ NSString* key = [NSString stringWithFormat:@"%i", i];
+ [path addObject:key];
+ id<FNode> childNode = [FSnapshotUtilities nodeFrom:[aval objectAtIndex:i] priority:nil withValidationFrom:fn atDepth:depth + 1 path:path];
+ [path removeLastObject];
+
+ if (![childNode isEmpty]) {
+ children[key] = childNode;
+ }
+ }
+
+ if ([children count] == 0) {
+ return [FEmptyNode emptyNode];
+ } else {
+ FImmutableSortedDictionary *childrenDict = [FImmutableSortedDictionary fromDictionary:children
+ withComparator:[FUtilities keyComparator]];
+ return [[FChildrenNode alloc] initWithPriority:priority children:childrenDict];
+ }
+ } else {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData"
+ reason:[NSString stringWithFormat:@"(%@) Cannot store object of type %@ at %@. "
+ "Can only store objects of type NSNumber, NSString, NSDictionary, and NSArray.",
+ fn, [[value class] description], pathString] userInfo:nil];
+ }
+}
+
++ (FCompoundWrite *) compoundWriteFromDictionary:(NSDictionary *)values withValidationFrom:(NSString *)fn {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+
+ NSMutableArray *updatePaths = [NSMutableArray arrayWithCapacity:values.count];
+ for (NSString *keyId in values) {
+ id value = values[keyId];
+ [FValidation validateFrom:fn validUpdateDictionaryKey:keyId withValue:value];
+
+ FPath* path = [FPath pathWithString:keyId];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:value withValidationFrom:fn];
+
+ [updatePaths addObject:path];
+ compoundWrite = [compoundWrite addWrite:node atPath:path];
+ }
+
+ // Check that the update paths are not descendants of each other.
+ [updatePaths sortUsingComparator:^NSComparisonResult(FPath* left, FPath* right) {
+ return [left compare:right];
+ }];
+ FPath *prevPath = nil;
+ for (FPath *path in updatePaths) {
+ if (prevPath != nil && [prevPath contains:path]) {
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid path in object. Path (%@) is an ancestor of (%@).", fn, prevPath, path] userInfo:nil];
+ }
+ prevPath = path;
+ }
+
+ return compoundWrite;
+}
+
++ (void)validatePriorityNode:(id <FNode>)priorityNode {
+ assert(priorityNode != nil);
+ if (priorityNode.isLeafNode) {
+ id val = priorityNode.val;
+ if ([val isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* valDict __unused = (NSDictionary*)val;
+ NSAssert(valDict[kServerValueSubKey] != nil, @"Priority can't be object unless it's a deferred value");
+ } else {
+ NSString *jsType __unused = [FUtilities getJavascriptType:val];
+ NSAssert(jsType == kJavaScriptString || jsType == kJavaScriptNumber, @"Priority of unexpected type.");
+ }
+ } else {
+ NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.isEmpty, @"Priority of unexpected type.");
+ }
+ // Don't call getPriority() on MAX_NODE to avoid hitting assertion.
+ NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.getPriority.isEmpty,
+ @"Priority nodes can't have a priority of their own.");
+}
+
++ (void)appendHashRepresentationForLeafNode:(FLeafNode *)leafNode
+ toString:(NSMutableString *)string
+ hashVersion:(FDataHashVersion)hashVersion {
+ NSAssert(hashVersion == FDataHashVersionV1 || hashVersion == FDataHashVersionV2,
+ @"Unknown hash version: %lu", (unsigned long)hashVersion);
+ if (!leafNode.getPriority.isEmpty) {
+ [string appendString:@"priority:"];
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:leafNode.getPriority toString:string hashVersion:hashVersion];
+ [string appendString:@":"];
+ }
+
+ NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
+ [string appendString:jsType];
+ [string appendString:@":"];
+
+
+ if (jsType == kJavaScriptBoolean) {
+ NSString *boolString = [leafNode.val boolValue] ? kJavaScriptTrue : kJavaScriptFalse;
+ [string appendString:boolString];
+ } else if (jsType == kJavaScriptNumber) {
+ NSString *numberString = [FUtilities ieee754StringForNumber:leafNode.val];
+ [string appendString:numberString];
+ } else if (jsType == kJavaScriptString) {
+ if (hashVersion == FDataHashVersionV1) {
+ [string appendString:leafNode.val];
+ } else {
+ NSAssert(hashVersion == FDataHashVersionV2, @"Invalid hash version found");
+ [FSnapshotUtilities appendHashV2RepresentationForString:leafNode.val toString:string];
+ }
+ } else {
+ [NSException raise:NSInvalidArgumentException format:@"Unknown value for hashing: %@", leafNode];
+ }
+}
+
++ (void)appendHashV2RepresentationForString:(NSString *)string
+ toString:(NSMutableString *)mutableString {
+ string = [string stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
+ string = [string stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
+ [mutableString appendString:@"\""];
+ [mutableString appendString:string];
+ [mutableString appendString:@"\""];
+}
+
++ (NSUInteger)estimateLeafNodeSize:(FLeafNode *)leafNode {
+ NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
+ // These values are somewhat arbitrary, but we don't need an exact value so prefer performance over exact value
+ NSUInteger valueSize;
+ if (jsType == kJavaScriptNumber) {
+ valueSize = 8; // estimate each float with 8 bytes
+ } else if (jsType == kJavaScriptBoolean) {
+ valueSize = 4; // true or false need roughly 4 bytes
+ } else if (jsType == kJavaScriptString) {
+ valueSize = 2 + [leafNode.val length]; // add 2 for quotes
+ } else {
+ [NSException raise:NSInvalidArgumentException format:@"Unknown leaf type: %@", leafNode];
+ return 0;
+ }
+
+ if (leafNode.getPriority.isEmpty) {
+ return valueSize;
+ } else {
+ // Account for extra overhead due to the extra JSON object and the ".value" and ".priority" keys, colons, comma
+ NSUInteger leafPriorityOverhead = 2 + 8 + 11 + 2 + 1;
+ return leafPriorityOverhead + valueSize + [FSnapshotUtilities estimateLeafNodeSize:leafNode.getPriority];
+ }
+}
+
++ (NSUInteger)estimateSerializedNodeSize:(id<FNode>)node {
+ if ([node isEmpty]) {
+ return 4; // null keyword
+ } else if ([node isLeafNode]) {
+ return [FSnapshotUtilities estimateLeafNodeSize:node];
+ } else {
+ NSAssert([node isKindOfClass:[FChildrenNode class]], @"Unexpected node type: %@", [node class]);
+ __block NSUInteger sum = 1; // opening brackets
+ [((FChildrenNode *)node) enumerateChildrenAndPriorityUsingBlock:^(NSString *key, id<FNode>child, BOOL *stop) {
+ sum += key.length;
+ sum += 4; // quotes around key and colon and (comma or closing bracket)
+ sum += [FSnapshotUtilities estimateSerializedNodeSize:child];
+ }];
+ return sum;
+ }
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FAtomicNumber.h b/Firebase/Database/Utilities/FAtomicNumber.h
new file mode 100644
index 0000000..589dc25
--- /dev/null
+++ b/Firebase/Database/Utilities/FAtomicNumber.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 <Foundation/Foundation.h>
+
+@interface FAtomicNumber : NSObject
+
+- (NSNumber *) getAndIncrement;
+
+@end
diff --git a/Firebase/Database/Utilities/FAtomicNumber.m b/Firebase/Database/Utilities/FAtomicNumber.m
new file mode 100644
index 0000000..be0e537
--- /dev/null
+++ b/Firebase/Database/Utilities/FAtomicNumber.m
@@ -0,0 +1,54 @@
+/*
+ * 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 "FAtomicNumber.h"
+
+@interface FAtomicNumber() {
+ unsigned long number;
+}
+
+@property (nonatomic, strong) NSLock* lock;
+
+@end
+
+@implementation FAtomicNumber
+
+@synthesize lock;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ number = 1;
+ self.lock = [[NSLock alloc] init];
+ }
+ return self;
+}
+
+- (NSNumber *) getAndIncrement {
+ NSNumber* result;
+
+ // See: http://developer.apple.com/library/ios/#DOCUMENTATION/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html#//apple_ref/doc/uid/10000057i-CH8-SW14 to improve, etc.
+
+ [self.lock lock];
+ result = [NSNumber numberWithUnsignedLong:number];
+ number = number + 1;
+ [self.lock unlock];
+
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FEventEmitter.h b/Firebase/Database/Utilities/FEventEmitter.h
new file mode 100644
index 0000000..069e10f
--- /dev/null
+++ b/Firebase/Database/Utilities/FEventEmitter.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 "FIRDatabaseQuery.h"
+#import "FIRDatabaseConfig.h"
+#import "FTypedefs_Private.h"
+
+@interface FEventEmitter : NSObject
+
+- (id) initWithAllowedEvents:(NSArray *)theAllowedEvents queue:(dispatch_queue_t)queue;
+
+- (id) getInitialEventForType:(NSString *)eventType;
+- (void) triggerEventType:(NSString *)eventType data:(id)data;
+
+- (FIRDatabaseHandle)observeEventType:(NSString *)eventType withBlock:(fbt_void_id)block;
+- (void) removeObserverForEventType:(NSString *)eventType withHandle:(FIRDatabaseHandle)handle;
+
+- (void) validateEventType:(NSString *)eventType;
+
+@end
diff --git a/Firebase/Database/Utilities/FEventEmitter.m b/Firebase/Database/Utilities/FEventEmitter.m
new file mode 100644
index 0000000..f7c569b
--- /dev/null
+++ b/Firebase/Database/Utilities/FEventEmitter.m
@@ -0,0 +1,145 @@
+/*
+ * 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 "FEventEmitter.h"
+#import "FUtilities.h"
+#import "FRepoManager.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@interface FEventListener : NSObject
+
+@property (nonatomic, copy) fbt_void_id userCallback;
+@property (nonatomic) FIRDatabaseHandle handle;
+
+@end
+
+@implementation FEventListener
+
+@synthesize userCallback;
+@synthesize handle;
+
+@end
+
+
+@interface FEventEmitter ()
+
+@property (nonatomic, strong) NSArray *allowedEvents;
+@property (nonatomic, strong) NSMutableDictionary *listeners;
+@property (nonatomic, strong) dispatch_queue_t queue;
+
+@end
+
+
+@implementation FEventEmitter
+
+@synthesize allowedEvents;
+@synthesize listeners;
+
+- (id) initWithAllowedEvents:(NSArray *)theAllowedEvents queue:(dispatch_queue_t)queue {
+ if (theAllowedEvents == nil || [theAllowedEvents count] == 0) {
+ @throw [NSException exceptionWithName:@"AllowedEventsValidation" reason:@"FEventEmitters must be initialized with at least one valid event." userInfo:nil];
+ }
+
+ self = [super init];
+
+ if (self) {
+ self.allowedEvents = [theAllowedEvents copy];
+ self.listeners = [[NSMutableDictionary alloc] init];
+ self.queue = queue;
+ }
+
+ return self;
+}
+
+- (id) getInitialEventForType:(NSString *)eventType {
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"You must override getInitialEvent: when subclassing FEventEmitter" userInfo:nil];
+}
+
+- (void) triggerEventType:(NSString *)eventType data:(id)data {
+ [self validateEventType:eventType];
+ NSMutableDictionary *eventTypeListeners = [self.listeners objectForKey:eventType];
+ for (FEventListener *listener in eventTypeListeners) {
+ [self triggerListener:listener withData:data];
+ }
+}
+
+- (void) triggerListener:(FEventListener *)listener withData:(id)data {
+ // TODO, should probably get this from FRepo or something although it ends up being the same. (Except maybe for testing)
+ if (listener.userCallback) {
+ dispatch_async(self.queue, ^{
+ listener.userCallback(data);
+ });
+ }
+}
+
+- (FIRDatabaseHandle)observeEventType:(NSString *)eventType withBlock:(fbt_void_id)block {
+ [self validateEventType:eventType];
+
+ // Create listener
+ FEventListener *listener = [[FEventListener alloc] init];
+ listener.handle = [[FUtilities LUIDGenerator] integerValue];
+ listener.userCallback = block; // copies block automatically
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self addEventListener:listener forEventType:eventType];
+ });
+
+ return listener.handle;
+}
+
+- (void) addEventListener:(FEventListener *)listener forEventType:(NSString *)eventType {
+ // Get or initializer listeners map [FIRDatabaseHandle -> callback block] for eventType
+ NSMutableArray *eventTypeListeners = [self.listeners objectForKey:eventType];
+ if (eventTypeListeners == nil) {
+ eventTypeListeners = [[NSMutableArray alloc] init];
+ [self.listeners setObject:eventTypeListeners forKey:eventType];
+ }
+
+ // Add listener and fire the current event for this listener
+ [eventTypeListeners addObject:listener];
+ id initialData = [self getInitialEventForType:eventType];
+ [self triggerListener:listener withData:initialData];
+}
+
+- (void) removeObserverForEventType:(NSString *)eventType withHandle:(FIRDatabaseHandle)handle {
+ [self validateEventType:eventType];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self removeEventListenerWithHandle:handle forEventType:eventType];
+ });
+}
+
+- (void)removeEventListenerWithHandle:(FIRDatabaseHandle)handle forEventType:(NSString *)eventType {
+ NSMutableArray *eventTypeListeners = [self.listeners objectForKey:eventType];
+ for (FEventListener *listener in [eventTypeListeners copy]) {
+ if (handle == NSNotFound || handle == listener.handle) {
+ [eventTypeListeners removeObject:listener];
+ }
+ }
+}
+
+
+- (void) validateEventType:(NSString *)eventType {
+ if ([self.allowedEvents indexOfObject:eventType] == NSNotFound) {
+ @throw [NSException exceptionWithName:@"InvalidEventType"
+ reason:[NSString stringWithFormat:@"%@ is not a valid event type. %@ is the list of valid events.",
+ eventType, self.allowedEvents]
+ userInfo:nil];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FNextPushId.h b/Firebase/Database/Utilities/FNextPushId.h
new file mode 100644
index 0000000..2da54f0
--- /dev/null
+++ b/Firebase/Database/Utilities/FNextPushId.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 <Foundation/Foundation.h>
+
+@interface FNextPushId : NSObject
+
++ (NSString *) get:(NSTimeInterval)now;
+
+@end
diff --git a/Firebase/Database/Utilities/FNextPushId.m b/Firebase/Database/Utilities/FNextPushId.m
new file mode 100644
index 0000000..af54e3d
--- /dev/null
+++ b/Firebase/Database/Utilities/FNextPushId.m
@@ -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 "FNextPushId.h"
+#import "FUtilities.h"
+
+static NSString *const PUSH_CHARS = @"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
+
+@implementation FNextPushId
+
++ (NSString *) get:(NSTimeInterval)currentTime {
+ static long long lastPushTime = 0;
+ static int lastRandChars[12];
+
+ long long now = (long long)(currentTime * 1000);
+
+ BOOL duplicateTime = now == lastPushTime;
+ lastPushTime = now;
+
+ unichar timeStampChars[8];
+ for(int i = 7; i >= 0; i--) {
+ timeStampChars[i] = [PUSH_CHARS characterAtIndex:(now % 64)];
+ now = (long long)floor(now / 64);
+ }
+
+ NSMutableString* id = [[NSMutableString alloc] init];
+ [id appendString:[NSString stringWithCharacters:timeStampChars length:8]];
+
+
+ if(!duplicateTime) {
+ for(int i = 0; i < 12; i++) {
+ lastRandChars[i] = (int)floor(arc4random() % 64);
+ }
+ }
+ else {
+ int i = 0;
+ for(i = 11; i >= 0 && lastRandChars[i] == 63; i--) {
+ lastRandChars[i] = 0;
+ }
+ lastRandChars[i]++;
+ }
+
+ for(int i = 0; i < 12; i++) {
+ [id appendFormat:@"%C", [PUSH_CHARS characterAtIndex:lastRandChars[i]]];
+ }
+
+ return [NSString stringWithString:id];
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FParsedUrl.h b/Firebase/Database/Utilities/FParsedUrl.h
new file mode 100644
index 0000000..7145f86
--- /dev/null
+++ b/Firebase/Database/Utilities/FParsedUrl.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 "FRepoInfo.h"
+#import "FPath.h"
+
+@interface FParsedUrl : NSObject
+
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+@property (nonatomic, strong) FPath* path;
+
+@end
diff --git a/Firebase/Database/Utilities/FParsedUrl.m b/Firebase/Database/Utilities/FParsedUrl.m
new file mode 100644
index 0000000..eb83330
--- /dev/null
+++ b/Firebase/Database/Utilities/FParsedUrl.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 "FParsedUrl.h"
+
+@implementation FParsedUrl
+
+@synthesize repoInfo;
+@synthesize path;
+
+@end
diff --git a/Firebase/Database/Utilities/FStringUtilities.h b/Firebase/Database/Utilities/FStringUtilities.h
new file mode 100644
index 0000000..34ac9af
--- /dev/null
+++ b/Firebase/Database/Utilities/FStringUtilities.h
@@ -0,0 +1,26 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@interface FStringUtilities : NSObject
+
++ (NSString *) base64EncodedSha1:(NSString *)str;
++ (NSString *) urlDecoded:(NSString *)url;
++ (NSString *) urlEncoded:(NSString *)url;
++ (NSString *) sanitizedForUserAgent:(NSString *)str;
+
+@end
diff --git a/Firebase/Database/Utilities/FStringUtilities.m b/Firebase/Database/Utilities/FStringUtilities.m
new file mode 100644
index 0000000..dff58e0
--- /dev/null
+++ b/Firebase/Database/Utilities/FStringUtilities.m
@@ -0,0 +1,61 @@
+/*
+ * 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 <CommonCrypto/CommonDigest.h>
+#import "FStringUtilities.h"
+#import "NSData+SRB64Additions.h"
+
+@implementation FStringUtilities
+
+// http://stackoverflow.com/questions/3468268/objective-c-sha1
+// http://stackoverflow.com/questions/7310457/ios-objective-c-sha-1-and-base64-problem
++ (NSString *) base64EncodedSha1:(NSString *)str {
+ const char *cstr = [str cStringUsingEncoding:NSUTF8StringEncoding];
+ // NSString reports length in characters, but we want it in bytes, which strlen will give us.
+ unsigned long dataLen = strlen(cstr);
+ NSData *data = [NSData dataWithBytes:cstr length:dataLen];
+ uint8_t digest[CC_SHA1_DIGEST_LENGTH];
+ CC_SHA1(data.bytes, (unsigned int)data.length, digest);
+ NSData* output = [[NSData alloc] initWithBytes:digest length:CC_SHA1_DIGEST_LENGTH];
+ return [FSRUtilities base64EncodedStringFromData:output];
+}
+
++ (NSString *) urlDecoded:(NSString *)url {
+ NSString* replaced = [url stringByReplacingOccurrencesOfString:@"+" withString:@" "];
+ NSString* decoded = [replaced stringByRemovingPercentEncoding];
+ // This is kind of a hack, but is generally how the js client works. We could run into trouble if
+ // some piece is a correctly escaped %-sequence, and another isn't. But, that's bad input anyways...
+ if (decoded) {
+ return decoded;
+ } else {
+ return replaced;
+ }
+}
+
++ (NSString *) urlEncoded:(NSString *)url {
+ // Didn't seem like there was an Apple NSCharacterSet that had our version of the encoding
+ // So I made my own, following RFC 2396 https://www.ietf.org/rfc/rfc2396.txt
+ // allowedCharacters = alphanum | "-" | "_" | "~"
+ NSCharacterSet *allowedCharacters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~"];
+ return [url stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];
+}
+
++ (NSString *) sanitizedForUserAgent:(NSString *)str {
+ return [str stringByReplacingOccurrencesOfString:@"/|_" withString:@"|" options:NSRegularExpressionSearch range:NSMakeRange(0, [str length])];
+}
+
+
+@end
diff --git a/Firebase/Database/Utilities/FTypedefs.h b/Firebase/Database/Utilities/FTypedefs.h
new file mode 100644
index 0000000..4a24ca5
--- /dev/null
+++ b/Firebase/Database/Utilities/FTypedefs.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#ifndef Firebase_FTypedefs_h
+#define Firebase_FTypedefs_h
+
+/**
+ * Stub...
+ */
+@class FIRDataSnapshot;
+@class FIRDatabaseReference;
+@class FAuthData;
+@protocol FNode;
+
+// fbt = Firebase Block Typedef
+
+typedef void (^fbt_void_void)(void);
+typedef void (^fbt_void_datasnapshot_nsstring) (FIRDataSnapshot *snapshot, NSString *prevName);
+typedef void (^fbt_void_datasnapshot) (FIRDataSnapshot *snapshot);
+typedef void (^fbt_void_user)(FAuthData *user);
+typedef void (^fbt_void_nsstring_id)(NSString* status, id data);
+typedef void (^fbt_void_nserror_id)(NSError* error, id data);
+typedef void (^fbt_void_nserror)(NSError *error);
+typedef void (^fbt_void_nserror_ref)(NSError* error, FIRDatabaseReference * ref);
+typedef void (^fbt_void_nserror_user)(NSError* error, FAuthData * user);
+typedef void (^fbt_void_nserror_json)(NSError* error, NSDictionary* json);
+typedef void (^fbt_void_nsdictionary)(NSDictionary *data);
+typedef id (^fbt_id_node_nsstring)(id<FNode> node, NSString* childName);
+
+#endif
diff --git a/Firebase/Database/Utilities/FUtilities.h b/Firebase/Database/Utilities/FUtilities.h
new file mode 100644
index 0000000..f5e312f
--- /dev/null
+++ b/Firebase/Database/Utilities/FUtilities.h
@@ -0,0 +1,76 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FIRLogger.h"
+#import "FParsedUrl.h"
+
+@interface FUtilities : NSObject
+
++ (NSArray *) splitString:(NSString *)str intoMaxSize:(const unsigned int)size;
++ (NSNumber *) LUIDGenerator;
++ (FParsedUrl *) parseUrl:(NSString *)url;
++ (NSString *) getJavascriptType:(id)obj;
++ (NSError *) errorForStatus:(NSString *)status andReason:(NSString *)reason;
++ (NSNumber *) intForString:(NSString *)string;
++ (NSString *) ieee754StringForNumber:(NSNumber *)val;
++ (void) setLoggingEnabled:(BOOL)enabled;
++ (BOOL) getLoggingEnabled;
+
++ (NSString*) minName;
++ (NSString*) maxName;
++ (NSComparisonResult) compareKey:(NSString *)a toKey:(NSString *)b;
++ (NSComparator) stringComparator;
++ (NSComparator) keyComparator;
+
++ (double)randomDouble;
+
+@end
+
+typedef enum {
+ FLogLevelDebug = 1,
+ FLogLevelInfo = 2,
+ FLogLevelWarn = 3,
+ FLogLevelError = 4,
+ FLogLevelNone = 5
+} FLogLevel;
+
+// Log tags
+FOUNDATION_EXPORT NSString *const kFPersistenceLogTag;
+
+#define FFLog(code, format, ...) FFDebug((code), (format), ##__VA_ARGS__)
+
+#define FFDebug(code, format, ...) do { \
+ if (FFIsLoggingEnabled(FLogLevelDebug)) { \
+ FIRLogDebug(kFIRLoggerDatabase, (code), (format), ##__VA_ARGS__); \
+ } \
+} while(0)
+
+#define FFInfo(code, format, ...) do { \
+ if (FFIsLoggingEnabled(FLogLevelInfo)) { \
+ FIRLogError(kFIRLoggerDatabase, (code), (format), ##__VA_ARGS__); \
+ } \
+} while(0)
+
+#define FFWarn(code, format, ...) do { \
+ if (FFIsLoggingEnabled(FLogLevelWarn)) { \
+ FIRLogWarning(kFIRLoggerDatabase, (code), (format), ##__VA_ARGS__); \
+ } \
+} while(0)
+
+BOOL FFIsLoggingEnabled(FLogLevel logLevel);
+void firebaseUncaughtExceptionHandler(NSException *exception);
+void firebaseJobsTroll(void);
diff --git a/Firebase/Database/Utilities/FUtilities.m b/Firebase/Database/Utilities/FUtilities.m
new file mode 100644
index 0000000..7c25e3b
--- /dev/null
+++ b/Firebase/Database/Utilities/FUtilities.m
@@ -0,0 +1,389 @@
+/*
+ * 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 "FUtilities.h"
+#import "FStringUtilities.h"
+#import "FConstants.h"
+#import "FAtomicNumber.h"
+
+#define ARC4RANDOM_MAX 0x100000000
+#define INTEGER_32_MIN (-2147483648)
+#define INTEGER_32_MAX 2147483647
+
+#pragma mark -
+#pragma mark C functions
+
+static FLogLevel logLevel = FLogLevelInfo; // Default log level is info
+static NSMutableDictionary* options = nil;
+
+BOOL FFIsLoggingEnabled(FLogLevel level) {
+ return level >= logLevel;
+}
+
+void firebaseJobsTroll(void) {
+ FFLog(@"I-RDB095001", @"password super secret; JFK conspiracy; Hello there! Having fun digging through Firebase? We're always hiring! jobs@firebase.com");
+}
+
+#pragma mark -
+#pragma mark Private property and singleton specification
+
+@interface FUtilities() {
+
+}
+
+@property (nonatomic, strong) FAtomicNumber* localUid;
+
++ (FUtilities*)singleton;
+
+@end
+
+@implementation FUtilities
+
+@synthesize localUid;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ self.localUid = [[FAtomicNumber alloc] init];
+ }
+ return self;
+}
+
+// TODO: We really want to be able to set the log level
++ (void) setLoggingEnabled:(BOOL)enabled {
+ logLevel = enabled ? FLogLevelDebug : FLogLevelInfo;
+}
+
++ (BOOL) getLoggingEnabled {
+ return logLevel == FLogLevelDebug;
+}
+
++ (FUtilities*) singleton
+{
+ static dispatch_once_t pred = 0;
+ __strong static id _sharedObject = nil;
+ dispatch_once(&pred, ^{
+ _sharedObject = [[self alloc] init]; // or some other init method
+ });
+ return _sharedObject;
+}
+
+// Refactor as a category of NSString
++ (NSArray *) splitString:(NSString *) str intoMaxSize:(const unsigned int) size {
+ if(str.length <= size) {
+ return [NSArray arrayWithObject:str];
+ }
+
+ NSMutableArray* dataSegs = [[NSMutableArray alloc] init];
+ for(int c = 0; c < str.length; c += size) {
+ if (c + size > str.length) {
+ int rangeStart = c;
+ unsigned long rangeLength = size - ((c + size) - str.length);
+ [dataSegs addObject:[str substringWithRange:NSMakeRange(rangeStart, rangeLength)]];
+ }
+ else {
+ int rangeStart = c;
+ int rangeLength = size;
+ [dataSegs addObject:[str substringWithRange:NSMakeRange(rangeStart, rangeLength)]];
+ }
+ }
+ return dataSegs;
+}
+
++ (NSNumber *) LUIDGenerator {
+ FUtilities* f = [FUtilities singleton];
+ return [f.localUid getAndIncrement];
+}
+
++ (NSString *) decodePath:(NSString *)pathString {
+ NSMutableArray* decodedPieces = [[NSMutableArray alloc] init];
+ NSArray* pieces = [pathString componentsSeparatedByString:@"/"];
+ for (NSString* piece in pieces) {
+ if (piece.length > 0) {
+ [decodedPieces addObject:[FStringUtilities urlDecoded:piece]];
+ }
+ }
+ return [NSString stringWithFormat:@"/%@", [decodedPieces componentsJoinedByString:@"/"]];
+}
+
++ (FParsedUrl *) parseUrl:(NSString *)url {
+ NSString* original = url;
+ //NSURL* n = [[NSURL alloc] initWithString:url]
+
+ NSString* host;
+ NSString* namespace;
+ bool secure;
+
+ NSString* scheme = nil;
+ FPath* path = nil;
+ NSRange colonIndex = [url rangeOfString:@"//"];
+ if (colonIndex.location != NSNotFound) {
+ scheme = [url substringToIndex:colonIndex.location - 1];
+ url = [url substringFromIndex:colonIndex.location + 2];
+ }
+ NSInteger slashIndex = [url rangeOfString:@"/"].location;
+ if (slashIndex == NSNotFound) {
+ slashIndex = url.length;
+ }
+
+ host = [[url substringToIndex:slashIndex] lowercaseString];
+ if (slashIndex >= url.length) {
+ url = @"";
+ } else {
+ url = [url substringFromIndex:slashIndex + 1];
+ }
+
+ NSArray *parts = [host componentsSeparatedByString:@"."];
+ if([parts count] == 3) {
+ NSInteger colonIndex = [[parts objectAtIndex:2] rangeOfString:@":"].location;
+ if (colonIndex != NSNotFound) {
+ // we have a port, use the provided scheme
+ secure = [scheme isEqualToString:@"https"];
+ } else {
+ secure = YES;
+ }
+
+ namespace = [[parts objectAtIndex:0] lowercaseString];
+ NSString* pathString = [self decodePath:[NSString stringWithFormat:@"/%@", url]];
+ path = [[FPath alloc] initWith:pathString];
+ }
+ else {
+ [NSException raise:@"No Firebase database specified." format:@"No Firebase database found for input: %@", url];
+ }
+
+ FRepoInfo* repoInfo = [[FRepoInfo alloc] initWithHost:host isSecure:secure withNamespace:namespace];
+
+ FFLog(@"I-RDB095002", @"---> Parsed (%@) to: (%@,%@); ns=(%@); path=(%@)", original, [repoInfo description], [repoInfo connectionURL], repoInfo.namespace, [path description]);
+
+ FParsedUrl* parsedUrl = [[FParsedUrl alloc] init];
+ parsedUrl.repoInfo = repoInfo;
+ parsedUrl.path = path;
+
+ return parsedUrl;
+}
+
+/*
+ case str: JString => priString + "string:" + str.s;
+ case bool: JBool => priString + "boolean:" + bool.value;
+ case double: JDouble => priString + "number:" + double.num;
+ case int: JInt => priString + "number:" + int.num;
+ case _ => {
+ error("Leaf node has value '" + data.value + "' of invalid type '" + data.value.getClass.toString + "'");
+ "";
+ }
+ */
+
++ (NSString *) getJavascriptType:(id)obj {
+ if ([obj isKindOfClass:[NSDictionary class]]) {
+ return kJavaScriptObject;
+ } else if([obj isKindOfClass:[NSString class]]) {
+ return kJavaScriptString;
+ }
+ else if ([obj isKindOfClass:[NSNumber class]]) {
+ // We used to just compare to @encode(BOOL) as suggested at
+ // http://stackoverflow.com/questions/2518761/get-type-of-nsnumber, but on arm64, @encode(BOOL) returns "B"
+ // instead of "c" even though objCType still returns 'c' (signed char). So check both.
+ if(strcmp([obj objCType], @encode(BOOL)) == 0 ||
+ strcmp([obj objCType], @encode(signed char)) == 0) {
+ return kJavaScriptBoolean;
+ }
+ else {
+ return kJavaScriptNumber;
+ }
+ }
+ else {
+ return kJavaScriptNull;
+ }
+}
+
++ (NSError *) errorForStatus:(NSString *)status andReason:(NSString *)reason {
+ static dispatch_once_t pred = 0;
+ __strong static NSDictionary* errorMap = nil;
+ __strong static NSDictionary* errorCodes = nil;
+ dispatch_once(&pred, ^{
+ errorMap = @{
+ @"permission_denied": @"Permission Denied",
+ @"unavailable": @"Service is unavailable",
+ kFErrorWriteCanceled: @"Write cancelled by user"
+ };
+ errorCodes = @{
+ @"permission_denied": @1,
+ @"unavailable": @2,
+ kFErrorWriteCanceled: @3
+ };
+ });
+
+ if ([status isEqualToString:kFWPResponseForActionStatusOk]) {
+ return nil;
+ } else {
+ NSInteger code;
+ NSString* desc = nil;
+ if (reason) {
+ desc = reason;
+ } else if ([errorMap objectForKey:status] != nil) {
+ desc = [errorMap objectForKey:status];
+ } else {
+ desc = status;
+ }
+
+ if ([errorCodes objectForKey:status] != nil) {
+ NSNumber* num = [errorCodes objectForKey:status];
+ code = [num integerValue];
+ } else {
+ // XXX what to do here?
+ code = 9999;
+ }
+
+ return [[NSError alloc] initWithDomain:kFErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: desc}];
+ }
+}
+
++ (NSNumber *) intForString:(NSString *)string {
+ static NSCharacterSet *notDigits = nil;
+ if (!notDigits) {
+ notDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
+ }
+ if ([string rangeOfCharacterFromSet:notDigits].length == 0) {
+ NSInteger num;
+ NSScanner* scanner = [NSScanner scannerWithString:string];
+ if ([scanner scanInteger:&num]) {
+ return [NSNumber numberWithInteger:num];
+ }
+ }
+ return nil;
+}
+
++ (NSString *) ieee754StringForNumber:(NSNumber *)val {
+ double d = [val doubleValue];
+ NSData* data = [NSData dataWithBytes:&d length:sizeof(double)];
+ NSMutableString* str = [[NSMutableString alloc] init];
+ const unsigned char* buffer = (const unsigned char*)[data bytes];
+ for (int i = 0; i < data.length; i++) {
+ unsigned char byte = buffer[7 - i];
+ [str appendFormat:@"%02x", byte];
+ }
+ return str;
+}
+
+static inline BOOL tryParseStringToInt(__unsafe_unretained NSString* str, NSInteger* integer) {
+ // First do some cheap checks (NOTE: The below checks are significantly faster than an equivalent regex :-( ).
+ NSUInteger length = str.length;
+ if (length > 11 || length == 0) {
+ return NO;
+ }
+ long long value = 0;
+ BOOL negative = NO;
+ NSUInteger i = 0;
+ if ([str characterAtIndex:0] == '-') {
+ if (length == 1) {
+ return NO;
+ }
+ negative = YES;
+ i = 1;
+ }
+ for(; i < length; i++) {
+ unichar c = [str characterAtIndex:i];
+ // Must be a digit, or '-' if it's the first char.
+ if (c < '0' || c > '9') {
+ return NO;
+ } else {
+ int charValue = c - '0';
+ value = value*10 + charValue;
+ }
+ }
+
+ value = (negative) ? -value : value;
+
+ if (value < INTEGER_32_MIN || value > INTEGER_32_MAX) {
+ return NO;
+ } else {
+ *integer = (NSInteger)value;
+ return YES;
+ }
+}
+
++ (NSString *) maxName {
+ static dispatch_once_t once;
+ static NSString *maxName;
+ dispatch_once(&once, ^{
+ maxName = [[NSString alloc] initWithFormat:@"[MAX_NAME]"];
+ });
+ return maxName;
+}
+
++ (NSString *) minName {
+ static dispatch_once_t once;
+ static NSString *minName;
+ dispatch_once(&once, ^{
+ minName = [[NSString alloc] initWithFormat:@"[MIN_NAME]"];
+ });
+ return minName;
+}
+
++ (NSComparisonResult) compareKey:(NSString *)a toKey:(NSString *)b {
+ if (a == b) {
+ return NSOrderedSame;
+ } else if (a == [FUtilities minName] || b == [FUtilities maxName]) {
+ return NSOrderedAscending;
+ } else if (b == [FUtilities minName] || a == [FUtilities maxName]) {
+ return NSOrderedDescending;
+ } else {
+ NSInteger aAsInt, bAsInt;
+ if (tryParseStringToInt(a, &aAsInt)) {
+ if (tryParseStringToInt(b, &bAsInt)) {
+ if (aAsInt > bAsInt) {
+ return NSOrderedDescending;
+ } else if (aAsInt < bAsInt) {
+ return NSOrderedAscending;
+ } else if (a.length > b.length) {
+ return NSOrderedDescending;
+ } else if (a.length < b.length) {
+ return NSOrderedAscending;
+ } else {
+ return NSOrderedSame;
+ }
+ } else {
+ return (NSComparisonResult) NSOrderedAscending;
+ }
+ } else if (tryParseStringToInt(b, &bAsInt)) {
+ return (NSComparisonResult) NSOrderedDescending;
+ } else {
+ // Perform literal character by character search to prevent a > b && b > a issues.
+ // Note that calling -(NSString *)decomposedStringWithCanonicalMapping also works.
+ return [a compare:b options:NSLiteralSearch];
+ }
+ }
+}
+
++ (NSComparator) keyComparator {
+ return ^NSComparisonResult(__unsafe_unretained NSString *a, __unsafe_unretained NSString *b) {
+ return [FUtilities compareKey:a toKey:b];
+ };
+}
+
++ (NSComparator) stringComparator {
+ return ^NSComparisonResult(__unsafe_unretained NSString *a, __unsafe_unretained NSString *b) {
+ return [a compare:b];
+ };
+}
+
++ (double) randomDouble {
+ return ((double) arc4random() / ARC4RANDOM_MAX);
+}
+
+@end
+
diff --git a/Firebase/Database/Utilities/FValidation.h b/Firebase/Database/Utilities/FValidation.h
new file mode 100644
index 0000000..faa8f76
--- /dev/null
+++ b/Firebase/Database/Utilities/FValidation.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FPath.h"
+#import "FIRDataEventType.h"
+#import "FParsedUrl.h"
+#import "FTypedefs.h"
+
+@interface FValidation : NSObject
+
++ (void) validateFrom:(NSString *)fn writablePath:(FPath *)path;
++ (void) validateFrom:(NSString *)fn knownEventType:(FIRDataEventType)event;
++ (void) validateFrom:(NSString *)fn validPathString:(NSString *)pathString;
++ (void) validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString;
++ (void) validateFrom:(NSString *)fn validKey:(NSString *)key;
++ (void) validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl;
+
++ (void) validateToken:(NSString *)token;
+
+// Functions for handling passing errors back
++ (void) handleError:(NSError *)error withUserCallback:(fbt_void_nserror_id)userCallback;
++ (void) handleError:(NSError *)error withSuccessCallback:(fbt_void_nserror)userCallback;
+
+// Functions used for validating while creating snapshots in FSnapshotUtilities
++ (BOOL) validateFrom:(NSString*)fn isValidLeafValue:(id)value withPath:(NSArray*)path;
++ (void) validateFrom:(NSString*)fn validDictionaryKey:(id)keyId withPath:(NSArray*)path;
++ (void) validateFrom:(NSString*)fn validUpdateDictionaryKey:(id)keyId withValue:(id)value;
++ (void) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path;
++ (BOOL) validatePriorityValue:value;
+
+@end
diff --git a/Firebase/Database/Utilities/FValidation.m b/Firebase/Database/Utilities/FValidation.m
new file mode 100644
index 0000000..c4c6b2b
--- /dev/null
+++ b/Firebase/Database/Utilities/FValidation.m
@@ -0,0 +1,312 @@
+/*
+ * 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 "FValidation.h"
+#import "FConstants.h"
+#import "FParsedUrl.h"
+#import "FTypedefs.h"
+
+
+// Have to escape: * ? + [ ( ) { } ^ $ | \ . /
+// See: https://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSRegularExpression_Class/Reference/Reference.html
+
+NSString *const kInvalidPathCharacters = @"[].#$";
+NSString *const kInvalidKeyCharacters = @"[].#$/";
+
+@implementation FValidation
+
++ (void) validateFrom:(NSString *)fn writablePath:(FPath *)path {
+ if([[path getFront] isEqualToString:kDotInfoPrefix]) {
+ @throw [[NSException alloc] initWithName:@"WritablePathValidation" reason:[NSString stringWithFormat:@"(%@) failed to path %@: Can't modify data under %@", fn, [path description], kDotInfoPrefix] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString*)fn knownEventType:(FIRDataEventType)event {
+ switch (event) {
+ case FIRDataEventTypeValue:
+ case FIRDataEventTypeChildAdded:
+ case FIRDataEventTypeChildChanged:
+ case FIRDataEventTypeChildMoved:
+ case FIRDataEventTypeChildRemoved:
+ return;
+ break;
+ default:
+ @throw [[NSException alloc] initWithName:@"KnownEventTypeValidation" reason:[NSString stringWithFormat:@"(%@) Unknown event type: %d", fn, (int) event] userInfo:nil];
+ break;
+ }
+}
+
++ (BOOL) isValidPathString:(NSString *)pathString {
+ static dispatch_once_t token;
+ static NSCharacterSet *badPathChars = nil;
+ dispatch_once(&token, ^{
+ badPathChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidPathCharacters];
+ });
+ return pathString != nil && [pathString length] != 0 &&
+ [pathString rangeOfCharacterFromSet:badPathChars].location == NSNotFound;
+}
+
++ (void) validateFrom:(NSString *)fn validPathString:(NSString *)pathString {
+ if(! [self isValidPathString:pathString]) {
+ @throw [[NSException alloc] initWithName:@"InvalidPathValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString {
+ static dispatch_once_t token;
+ static NSRegularExpression *dotInfoRegex = nil;
+ dispatch_once(&token, ^{
+ dotInfoRegex = [NSRegularExpression regularExpressionWithPattern:@"^\\/*\\.info(\\/|$)" options:0 error:nil];
+ });
+
+ NSString *tempPath = pathString;
+ // HACK: Obj-C regex are kinda' slow. Do a plain string search first before bothering with the regex.
+ if ([pathString rangeOfString:@".info"].location != NSNotFound) {
+ tempPath = [dotInfoRegex stringByReplacingMatchesInString:pathString options:0 range:NSMakeRange(0, pathString.length) withTemplate:@"/"];
+ }
+ [self validateFrom:fn validPathString:tempPath];
+}
+
++ (BOOL) isValidKey:(NSString *)key {
+ static dispatch_once_t token;
+ static NSCharacterSet *badKeyChars = nil;
+ dispatch_once(&token, ^{
+ badKeyChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidKeyCharacters];
+ });
+ return key != nil && key.length > 0 && [key rangeOfCharacterFromSet:badKeyChars].location == NSNotFound;
+}
+
++ (void) validateFrom:(NSString *)fn validKey:(NSString *)key {
+ if (![self isValidKey:key]) {
+ @throw [[NSException alloc] initWithName:@"InvalidKeyValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '/' '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl {
+ NSString* pathString = [parsedUrl.path description];
+ [self validateFrom:fn validRootPathString:pathString];
+}
+
+#pragma mark -
+#pragma mark Authentication validation
+
++ (BOOL) stringNonempty:(NSString *)str {
+ return str != nil && ![str isKindOfClass:[NSNull class]] && str.length > 0;
+}
+
++ (void) validateToken:(NSString *)token {
+ if (![FValidation stringNonempty:token]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't have empty string or nil for custom token"];
+ }
+}
+
+#pragma mark -
+#pragma mark Handling authentication errors
+
+/**
+* This function immediately calls the callback.
+* It assumes that it is not on FirebaseWorker thread.
+* It assumes it's on a user-controlled thread.
+*/
++ (void) handleError:(NSError *)error withUserCallback:(fbt_void_nserror_id)userCallback {
+ if (userCallback) {
+ userCallback(error, nil);
+ }
+}
+
+/**
+* This function immediately calls the callback.
+* It assumes that it is not on FirebaseWorker thread.
+* It assumes it's on a user-controlled thread.
+*/
++ (void) handleError:(NSError *)error withSuccessCallback:(fbt_void_nserror)userCallback {
+ if (userCallback) {
+ userCallback(error);
+ }
+}
+
+#pragma mark -
+#pragma mark Snapshot validation
+
++ (BOOL) validateFrom:(NSString*)fn isValidLeafValue:(id)value withPath:(NSArray*)path {
+ if ([value isKindOfClass:[NSString class]]) {
+ // Try to avoid conversion to bytes if possible
+ NSString* theString = value;
+ if ([theString maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize &&
+ [theString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) String exceeds max size of %u utf8 bytes: %@", fn, (int)kFirebaseMaxLeafSize, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+
+ else if ([value isKindOfClass:[NSNumber class]]) {
+ // Cannot store NaN, but otherwise can store NSNumbers.
+ if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN at path: %@.", fn, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+
+ else if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dval = value;
+ if (dval[kServerValueSubKey] != nil) {
+ if ([dval count] > 1) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys.%@.", fn, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+ return NO;
+ }
+
+ else if (value == [NSNull null] || value == nil) {
+ // Null is valid type to store at leaf
+ return YES;
+ }
+
+ return NO;
+}
+
++ (NSString*) parseAndValidateKey:(id)keyId fromFunction:(NSString*)fn path:(NSArray*)path {
+ if (![keyId isKindOfClass:[NSString class]]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Non-string keys are not allowed in object at path: %@", fn, pathString] userInfo:nil];
+ }
+ return (NSString*)keyId;
+}
+
++ (void) validateFrom:(NSString*)fn validDictionaryKey:(id)keyId withPath:(NSArray*)path {
+ NSString *key = [self parseAndValidateKey:keyId fromFunction:fn path:path];
+ if (![key isEqualToString:kPayloadPriority] && ![key isEqualToString:kPayloadValue] && ![key isEqualToString:kServerValueSubKey] && ![FValidation isValidKey:key]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object at path: %@. Keys must be non-empty and cannot contain '/' '.' '#' '$' '[' or ']'", fn, pathString] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString*)fn validUpdateDictionaryKey:(id)keyId withValue:(id)value {
+ FPath *path = [FPath pathWithString:[self parseAndValidateKey:keyId fromFunction:fn path:@[]]];
+ __block NSInteger keyNum = 0;
+ [path enumerateComponentsUsingBlock:^void (NSString *key, BOOL *stop) {
+ if ([key isEqualToString:kPayloadPriority] && keyNum == [path length] - 1) {
+ [self validateFrom:fn isValidPriorityValue:value withPath:@[]];
+ } else {
+ keyNum++;
+
+ if (![FValidation isValidKey:key]) {
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object. Keys must be non-empty and cannot contain '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+ }
+ }];
+}
+
++ (void) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path {
+ [self validateFrom:fn isValidPriorityValue:value withPath:path throwError:YES];
+}
+
+/**
+* Returns YES if priority is valid.
+*/
++ (BOOL)validatePriorityValue:value {
+ return [self validateFrom:nil isValidPriorityValue:value withPath:nil throwError:NO];
+}
+
+/**
+* Helper for validating priorities. If passed YES for throwError, it'll throw descriptive errors on validation
+* problems. Else, it'll just return YES/NO.
+*/
++ (BOOL) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path throwError:(BOOL)throwError {
+ if ([value isKindOfClass:[NSNumber class]]) {
+ if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ } else if (value == (id) kCFBooleanFalse || value == (id) kCFBooleanTrue) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store true/false as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ }
+ else if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *dval = value;
+ if (dval[kServerValueSubKey] != nil) {
+ if ([dval count] > 1) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ } else {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSDictionary as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ }
+ else if ([value isKindOfClass:[NSArray class]]) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSArray as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+
+ // It's valid!
+ return YES;
+}
+@end
diff --git a/Firebase/Database/Utilities/NSString+FURLUtils.h b/Firebase/Database/Utilities/NSString+FURLUtils.h
new file mode 100644
index 0000000..7bd39bc
--- /dev/null
+++ b/Firebase/Database/Utilities/NSString+FURLUtils.h
@@ -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 <Foundation/Foundation.h>
+
+@interface NSString (FURLUtils)
+
+- (NSString *) urlEncoded;
+- (NSString *) urlDecoded;
+
+@end
diff --git a/Firebase/Database/Utilities/NSString+FURLUtils.m b/Firebase/Database/Utilities/NSString+FURLUtils.m
new file mode 100644
index 0000000..2e018c8
--- /dev/null
+++ b/Firebase/Database/Utilities/NSString+FURLUtils.m
@@ -0,0 +1,38 @@
+/*
+ * 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 "NSString+FURLUtils.h"
+
+@implementation NSString (FURLUtils)
+
+- (NSString *) urlDecoded {
+ NSString* replaced = [self stringByReplacingOccurrencesOfString:@"+" withString:@" "];
+ NSString* decoded = [replaced stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+ // This is kind of a hack, but is generally how the js client works. We could run into trouble if
+ // some piece is a correctly escaped %-sequence, and another isn't. But, that's bad input anyways...
+ if (decoded) {
+ return decoded;
+ } else {
+ return replaced;
+ }
+}
+
+- (NSString *) urlEncoded {
+ CFStringRef urlString = CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)self, NULL, (CFStringRef)@"!*'\"();:@&=+$,/?%#[]% ", kCFStringEncodingUTF8);
+ return (__bridge NSString *) urlString;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.h b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.h
new file mode 100644
index 0000000..bceeed2
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.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 <Foundation/Foundation.h>
+#import "FTypedefs.h"
+
+@interface FTupleBoolBlock : NSObject
+
+@property (nonatomic, readwrite) BOOL boolean;
+@property (nonatomic, copy) fbt_void_void block;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m
new file mode 100644
index 0000000..c4cd8bf
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.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 "FTupleBoolBlock.h"
+
+@implementation FTupleBoolBlock
+
+@synthesize boolean;
+@synthesize block;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h
new file mode 100644
index 0000000..6ec2375
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h
@@ -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 <Foundation/Foundation.h>
+#import "FTypedefs_Private.h"
+
+@interface FTupleCallbackStatus : NSObject
+@property (nonatomic, copy) fbt_void_nsstring_nsstring block;
+@property (nonatomic) NSString* status;
+@property (nonatomic) NSString* errorReason;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m
new file mode 100644
index 0000000..05914bf
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.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 "FTupleCallbackStatus.h"
+
+@implementation FTupleCallbackStatus
+@synthesize block;
+@synthesize status;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleFirebase.h b/Firebase/Database/Utilities/Tuples/FTupleFirebase.h
new file mode 100644
index 0000000..ff84bbb
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleFirebase.h
@@ -0,0 +1,26 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FIRDatabaseReference.h"
+
+@interface FTupleFirebase : NSObject
+
+@property (nonatomic, strong) FIRDatabaseReference * one;
+@property (nonatomic, strong) FIRDatabaseReference * two;
+@property (nonatomic, strong) FIRDatabaseReference * three;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleFirebase.m b/Firebase/Database/Utilities/Tuples/FTupleFirebase.m
new file mode 100644
index 0000000..3956f8b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleFirebase.m
@@ -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 "FTupleFirebase.h"
+
+@implementation FTupleFirebase
+
+@synthesize one;
+@synthesize two;
+@synthesize three;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleNodePath.h b/Firebase/Database/Utilities/Tuples/FTupleNodePath.h
new file mode 100644
index 0000000..fbf62c7
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleNodePath.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 <Foundation/Foundation.h>
+#import "FPath.h"
+#import "FNode.h"
+
+@interface FTupleNodePath : NSObject
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) id<FNode> node;
+
+- (id) initWithNode:(id<FNode>)aNode andPath:(FPath *)aPath;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleNodePath.m b/Firebase/Database/Utilities/Tuples/FTupleNodePath.m
new file mode 100644
index 0000000..eefc0c2
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleNodePath.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 "FTupleNodePath.h"
+
+@implementation FTupleNodePath
+
+@synthesize path;
+@synthesize node;
+
+- (id) initWithNode:(id<FNode>)aNode andPath:(FPath *)aPath {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.node = aNode;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjectNode.h b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.h
new file mode 100644
index 0000000..6fcb746
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FNode.h"
+
+@interface FTupleObjectNode : NSObject
+
+- (id)initWithObject:(id)aObj andNode:(id<FNode>)aNode;
+
+@property (nonatomic, strong) id<FNode> node;
+@property (nonatomic, strong) id obj;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjectNode.m b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.m
new file mode 100644
index 0000000..4c533b0
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.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 "FTupleObjectNode.h"
+
+@implementation FTupleObjectNode
+
+@synthesize obj;
+@synthesize node;
+
+- (id)initWithObject:(id)aObj andNode:(id<FNode>)aNode {
+ self = [super init];
+ if (self) {
+ self.obj = aObj;
+ self.node = aNode;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjects.h b/Firebase/Database/Utilities/Tuples/FTupleObjects.h
new file mode 100644
index 0000000..4ff1fcf
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjects.h
@@ -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 <Foundation/Foundation.h>
+
+@interface FTupleObjects : NSObject
+
+@property (nonatomic, strong) id objA;
+@property (nonatomic, strong) id objB;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjects.m b/Firebase/Database/Utilities/Tuples/FTupleObjects.m
new file mode 100644
index 0000000..a9e4c88
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjects.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 "FTupleObjects.h"
+
+@implementation FTupleObjects
+
+@synthesize objA;
+@synthesize objB;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h
new file mode 100644
index 0000000..91ad5e4
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FTypedefs_Private.h"
+
+@interface FTupleOnDisconnect : NSObject
+
+@property (strong, nonatomic) NSString* pathString;
+@property (strong, nonatomic) NSString* action;
+@property (strong, nonatomic) id data;
+@property (strong, nonatomic) fbt_void_nsstring_nsstring onComplete;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m
new file mode 100644
index 0000000..bd45822
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m
@@ -0,0 +1,26 @@
+/*
+ * 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 "FTupleOnDisconnect.h"
+
+@implementation FTupleOnDisconnect
+
+@synthesize pathString;
+@synthesize action;
+@synthesize data;
+@synthesize onComplete;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTuplePathValue.h b/Firebase/Database/Utilities/Tuples/FTuplePathValue.h
new file mode 100644
index 0000000..f7ed423
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTuplePathValue.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 <Foundation/Foundation.h>
+
+@class FPath;
+
+@interface FTuplePathValue : NSObject
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, strong, readonly) id value;
+- (id) initWithPath:(FPath *)aPath value:(id)aValue;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTuplePathValue.m b/Firebase/Database/Utilities/Tuples/FTuplePathValue.m
new file mode 100644
index 0000000..49240aa
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTuplePathValue.m
@@ -0,0 +1,38 @@
+/*
+ * 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 "FTuplePathValue.h"
+#import "FPath.h"
+
+@interface FTuplePathValue ()
+@property (nonatomic, strong, readwrite) id value;
+@property (nonatomic, strong, readwrite) FPath *path;
+@end
+
+@implementation FTuplePathValue
+@synthesize path;
+@synthesize value;
+
+- (id) initWithPath:(FPath *)aPath value:(id)aValue {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ self.path = aPath;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.h b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.h
new file mode 100644
index 0000000..f986916
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.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 <Foundation/Foundation.h>
+
+@interface FTupleRemovedQueriesEvents : NSObject
+/**
+* `FIRDatabaseQuery`s removed with [SyncPoint removeEventRegistration:]
+*/
+@property (nonatomic, strong, readonly) NSArray *removedQueries;
+/**
+* cancel events as FEvent
+*/
+@property (nonatomic, strong, readonly) NSArray *cancelEvents;
+
+- (id) initWithRemovedQueries:(NSArray *)removed cancelEvents:(NSArray *)events;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m
new file mode 100644
index 0000000..818d16b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m
@@ -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 "FTupleRemovedQueriesEvents.h"
+
+@interface FTupleRemovedQueriesEvents ()
+@property (nonatomic, strong, readwrite) NSArray *removedQueries;
+@property (nonatomic, strong, readwrite) NSArray *cancelEvents;
+@end
+
+@implementation FTupleRemovedQueriesEvents
+@synthesize removedQueries;
+@synthesize cancelEvents;
+
+- (id) initWithRemovedQueries:(NSArray *)removed cancelEvents:(NSArray *)events {
+ self = [super init];
+ if (self) {
+ self.removedQueries = removed;
+ self.cancelEvents = events;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h
new file mode 100644
index 0000000..5133d6d
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FPath.h"
+
+@interface FTupleSetIdPath : NSObject
+
+- (id) initWithSetId:(NSNumber *)aSetId andPath:(FPath *)aPath;
+
+@property (strong, nonatomic) NSNumber* setId;
+@property (strong, nonatomic) FPath* path;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m
new file mode 100644
index 0000000..5d3312b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.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 "FTupleSetIdPath.h"
+
+@implementation FTupleSetIdPath
+
+@synthesize path;
+@synthesize setId;
+
+- (id) initWithSetId:(NSNumber *)aSetId andPath:(FPath *)aPath {
+ self = [super init];
+ if (self) {
+ self.setId = aSetId;
+ self.path = aPath;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleStringNode.h b/Firebase/Database/Utilities/Tuples/FTupleStringNode.h
new file mode 100644
index 0000000..e3fec80
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleStringNode.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FNode.h"
+
+@interface FTupleStringNode : NSObject
+
+- (id)initWithString:(NSString *)aString andNode:(id<FNode>)aNode;
+
+@property (nonatomic, strong) id<FNode> node;
+@property (nonatomic, strong) NSString* string;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleStringNode.m b/Firebase/Database/Utilities/Tuples/FTupleStringNode.m
new file mode 100644
index 0000000..f058a8e
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleStringNode.m
@@ -0,0 +1,34 @@
+/*
+ * 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 "FTupleStringNode.h"
+
+@implementation FTupleStringNode
+
+@synthesize string;
+@synthesize node;
+
+- (id)initWithString:(NSString *)aString andNode:(id<FNode>)aNode {
+ self = [super init];
+ if (self) {
+ self.string = aString;
+ self.node = aNode;
+ }
+ return self;
+}
+
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTSN.h b/Firebase/Database/Utilities/Tuples/FTupleTSN.h
new file mode 100644
index 0000000..bc62b2d
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTSN.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 <Foundation/Foundation.h>
+#import "FTupleStringNode.h"
+
+@interface FTupleTSN : NSObject
+
+@property (nonatomic, strong) FTupleStringNode* from;
+@property (nonatomic, strong) FTupleStringNode* to;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTSN.m b/Firebase/Database/Utilities/Tuples/FTupleTSN.m
new file mode 100644
index 0000000..348c319
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTSN.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 "FTupleTSN.h"
+
+@implementation FTupleTSN
+
+@synthesize from;
+@synthesize to;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTransaction.h b/Firebase/Database/Utilities/Tuples/FTupleTransaction.h
new file mode 100644
index 0000000..c9dcf4b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTransaction.h
@@ -0,0 +1,74 @@
+/*
+ * 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 <Foundation/Foundation.h>
+#import "FPath.h"
+#import "FTypedefs_Private.h"
+#import "FTypedefs.h"
+
+@interface FTupleTransaction : NSObject
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, copy) fbt_transactionresult_mutabledata update;
+@property (nonatomic, copy) fbt_void_nserror_bool_datasnapshot onComplete;
+@property (nonatomic) FTransactionStatus status;
+
+/**
+* Used when combining transaction at different locations to figure out which one goes first.
+*/
+@property (nonatomic, strong) NSNumber* order;
+/**
+* Whether to raise local events for this transaction
+*/
+@property (nonatomic) BOOL applyLocally;
+
+/**
+* Count how many times we've retried the transaction
+*/
+@property (nonatomic) int retryCount;
+
+/**
+* Function to call to clean up our listener
+*/
+@property (nonatomic, copy) fbt_void_void unwatcher;
+
+/**
+* Stores why a transaction was aborted
+*/
+@property (nonatomic, strong, readonly) NSString* abortStatus;
+@property (nonatomic, strong, readonly) NSString* abortReason;
+
+- (void)setAbortStatus:(NSString *)abortStatus reason:(NSString *)reason;
+- (NSError *)abortError;
+
+@property (nonatomic, strong) NSNumber *currentWriteId;
+
+/**
+* Stores the input snapshot, before the update
+*/
+@property (nonatomic, strong) id<FNode> currentInputSnapshot;
+
+/**
+* Stores the unresolved (for server values) output snapshot, after the update
+*/
+@property (nonatomic, strong) id<FNode> currentOutputSnapshotRaw;
+
+/**
+ * Stores the resolved (for server values) output snapshot, after the update
+ */
+@property (nonatomic, strong) id<FNode> currentOutputSnapshotResolved;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTransaction.m b/Firebase/Database/Utilities/Tuples/FTupleTransaction.m
new file mode 100644
index 0000000..bcff54e
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTransaction.m
@@ -0,0 +1,38 @@
+/*
+ * 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 "FTupleTransaction.h"
+#import "FUtilities.h"
+
+@interface FTupleTransaction ()
+
+@property (nonatomic, strong) NSString *abortStatus;
+@property (nonatomic, strong) NSString *abortReason;
+
+@end
+
+@implementation FTupleTransaction
+
+- (void)setAbortStatus:(NSString *)abortStatus reason:(NSString *)reason {
+ self.abortStatus = abortStatus;
+ self.abortReason = reason;
+}
+
+- (NSError *)abortError {
+ return (self.abortStatus != nil) ? [FUtilities errorForStatus:self.abortStatus andReason:self.abortReason] : nil;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleUserCallback.h b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.h
new file mode 100644
index 0000000..d598217
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.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 <Foundation/Foundation.h>
+#import "FTypedefs.h"
+#import "FQueryParams.h"
+
+@interface FTupleUserCallback : NSObject
+
+- (id) initWithHandle:(NSUInteger)handle;
+
+@property (nonatomic, copy) fbt_void_datasnapshot_nsstring datasnapshotPrevnameCallback;
+@property (nonatomic, copy) fbt_void_datasnapshot datasnapshotCallback;
+@property (nonatomic, copy) fbt_void_nserror cancelCallback;
+@property (nonatomic, copy) FQueryParams* queryParams;
+@property (nonatomic) NSUInteger handle;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleUserCallback.m b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.m
new file mode 100644
index 0000000..dc33bbd
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.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 "FTupleUserCallback.h"
+
+@implementation FTupleUserCallback
+
+@synthesize datasnapshotCallback;
+@synthesize datasnapshotPrevnameCallback;
+@synthesize cancelCallback;
+@synthesize queryParams;
+@synthesize handle;
+
+- (id) initWithHandle:(NSUInteger)theHandle {
+ self = [super init];
+ if (self) {
+ self.handle = theHandle;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/module.modulemap b/Firebase/Database/module.modulemap
new file mode 100644
index 0000000..28b323e
--- /dev/null
+++ b/Firebase/Database/module.modulemap
@@ -0,0 +1,13 @@
+framework module FirebaseDatabase {
+ umbrella header "FirebaseDatabase.h"
+
+ export *
+ module * { export * }
+
+ link framework "CFNetwork"
+ link framework "Security"
+ link framework "SystemConfiguration"
+
+ link "c++"
+ link "icucore"
+}
diff --git a/Firebase/Database/third_party/SocketRocket/FSRWebSocket.h b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.h
new file mode 100644
index 0000000..dfda376
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.h
@@ -0,0 +1,107 @@
+//
+// 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.
+//
+
+#import <Foundation/Foundation.h>
+#import <Security/SecCertificate.h>
+
+typedef enum {
+ SR_CONNECTING = 0,
+ SR_OPEN = 1,
+ SR_CLOSING = 2,
+ SR_CLOSED = 3,
+
+} FSRReadyState;
+
+@class FSRWebSocket;
+
+extern NSString *const FSRWebSocketErrorDomain;
+
+@protocol FSRWebSocketDelegate;
+
+@interface FSRWebSocket : NSObject <NSStreamDelegate>
+
+@property (nonatomic, weak) id <FSRWebSocketDelegate> delegate;
+
+@property (nonatomic, readonly) FSRReadyState readyState;
+@property (nonatomic, readonly, retain) NSURL *url;
+
+// This returns the negotiated protocol.
+// It will be niluntil after the handshake completes.
+@property (nonatomic, readonly, copy) NSString *protocol;
+
+// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
+- (id)initWithURLRequest:(NSURLRequest *)request queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+- (id)initWithURLRequest:(NSURLRequest *)request;
+
+// Some helper constructors
+- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
+- (id)initWithURL:(NSURL *)url;
+
+// Delegate queue will be dispatch_main_queue by default.
+// You cannot set both OperationQueue and dispatch_queue.
+- (void)setDelegateOperationQueue:(NSOperationQueue*) queue;
+- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue;
+
+// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes.
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+
+// SRWebSockets are intended one-time-use only. Open should be called once and only once
+- (void)open;
+
+- (void)close;
+- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
+
+// Send a UTF8 String or Data
+- (void)send:(id)data;
+
+@end
+
+@protocol FSRWebSocketDelegate <NSObject>
+
+// message will either be an NSString if the server is using text
+// or NSData if the server is using binary
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message;
+
+@optional
+
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket;
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error;
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
+
+@end
+
+
+@interface NSURLRequest (FCertificateAdditions)
+
+@property (nonatomic, retain, readonly) NSArray *FSR_SSLPinnedCertificates;
+
+@end
+
+
+@interface NSMutableURLRequest (FCertificateAdditions)
+
+@property (nonatomic, retain) NSArray *FSR_SSLPinnedCertificates;
+
+@end
+
+@interface NSRunLoop (FSRWebSocket)
+
++ (NSRunLoop *)FSR_networkRunLoop;
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m
new file mode 100644
index 0000000..c2b395c
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m
@@ -0,0 +1,1848 @@
+//
+// 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.
+//
+
+#import "FSRWebSocket.h"
+
+#if TARGET_OS_IPHONE
+#define HAS_ICU
+#endif
+
+#import <sys/socket.h>
+
+#ifdef HAS_ICU
+#import <unicode/utf8.h>
+#endif
+
+#if TARGET_OS_IPHONE
+#import <Endian.h>
+#else
+#import <CoreServices/CoreServices.h>
+#endif
+
+#import <CommonCrypto/CommonDigest.h>
+#import <Security/SecRandom.h>
+#import "fbase64.h"
+#import "NSData+SRB64Additions.h"
+
+#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE
+#define sr_dispatch_retain(x)
+#define sr_dispatch_release(x)
+#define maybe_bridge(x) ((__bridge void *) x)
+#else
+#define sr_dispatch_retain(x) dispatch_retain(x)
+#define sr_dispatch_release(x) dispatch_release(x)
+#define maybe_bridge(x) (x)
+#endif
+
+typedef enum {
+ SROpCodeTextFrame = 0x1,
+ SROpCodeBinaryFrame = 0x2,
+ //3-7Reserved
+ SROpCodeConnectionClose = 0x8,
+ SROpCodePing = 0x9,
+ SROpCodePong = 0xA,
+ //B-F reserved
+} FSROpCode;
+
+typedef enum {
+ SRStatusCodeNormal = 1000,
+ SRStatusCodeGoingAway = 1001,
+ SRStatusCodeProtocolError = 1002,
+ SRStatusCodeUnhandledType = 1003,
+ // 1004 reserved
+ SRStatusNoStatusReceived = 1005,
+ // 1004-1006 reserved
+ SRStatusCodeInvalidUTF8 = 1007,
+ SRStatusCodePolicyViolated = 1008,
+ SRStatusCodeMessageTooBig = 1009,
+} FSRStatusCode;
+
+typedef struct {
+ BOOL fin;
+// BOOL rsv1;
+// BOOL rsv2;
+// BOOL rsv3;
+ uint8_t opcode;
+ BOOL masked;
+ uint64_t payload_length;
+} frame_header;
+
+static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+static inline int32_t validate_dispatch_data_partial_string(NSData *data);
+static inline void SRFastLog(NSString *format, ...);
+
+@interface NSData (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+
+@end
+
+
+@interface NSString (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+
+@end
+
+
+@interface NSURL (FSRWebSocket)
+
+// The origin isn't really applicable for a native application
+// So instead, just map ws -> http and wss -> https
+- (NSString *)SR_origin;
+
+@end
+
+@interface _FSRRunLoopThread : NSThread
+
+@property (nonatomic, readonly) NSRunLoop *runLoop;
+
+@end
+
+static NSString *newSHA1String(const char *bytes, size_t length) {
+ uint8_t md[CC_SHA1_DIGEST_LENGTH];
+
+ CC_SHA1(bytes, (int)length, md);
+
+ size_t buffer_size = ((sizeof(md) * 3 + 2) / 2);
+
+ char *buffer = (char *)malloc(buffer_size);
+
+ int len = f_b64_ntop(md, CC_SHA1_DIGEST_LENGTH, buffer, buffer_size);
+ if (len == -1) {
+ free(buffer);
+ return nil;
+ } else{
+ return [[NSString alloc] initWithBytesNoCopy:buffer length:len encoding:NSASCIIStringEncoding freeWhenDone:YES];
+ }
+}
+
+@implementation NSData (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+{
+ return newSHA1String(self.bytes, self.length);
+}
+
+@end
+
+
+@implementation NSString (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+{
+ return newSHA1String(self.UTF8String, self.length);
+}
+
+@end
+
+NSString *const FSRWebSocketErrorDomain = @"FSRWebSocketErrorDomain";
+
+// Returns number of bytes consumed. returning 0 means you didn't match.
+// Sends bytes to callback handler;
+typedef size_t (^stream_scanner)(NSData *collected_data);
+
+typedef void (^data_callback)(FSRWebSocket *webSocket, NSData *data);
+
+@interface FSRIOConsumer : NSObject {
+ stream_scanner _scanner;
+ data_callback _handler;
+ size_t _bytesNeeded;
+ BOOL _readToCurrentFrame;
+ BOOL _unmaskBytes;
+}
+@property (nonatomic, copy, readonly) stream_scanner consumer;
+@property (nonatomic, copy, readonly) data_callback handler;
+@property (nonatomic, assign) size_t bytesNeeded;
+@property (nonatomic, assign, readonly) BOOL readToCurrentFrame;
+@property (nonatomic, assign, readonly) BOOL unmaskBytes;
+
+@end
+
+// This class is not thread-safe, and is expected to always be run on the same queue.
+@interface FSRIOConsumerPool : NSObject
+
+- (id)initWithBufferCapacity:(NSUInteger)poolSize;
+
+- (FSRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+- (void)returnConsumer:(FSRIOConsumer *)consumer;
+
+@end
+
+@interface FSRWebSocket () <NSStreamDelegate>
+
+- (void)_writeData:(NSData *)data;
+- (void)_closeWithProtocolError:(NSString *)message;
+- (void)_failWithError:(NSError *)error;
+
+- (void)_disconnect;
+
+- (void)_readFrameNew;
+- (void)_readFrameContinue;
+
+- (void)_pumpScanner;
+
+- (void)_pumpWriting;
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback;
+- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength;
+- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler;
+- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler;
+
+- (void)_sendFrameWithOpcode:(FSROpCode)opcode data:(id)data;
+
+- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;
+- (void)_SR_commonInit;
+
+- (void)_initializeStreams;
+- (void)_connect;
+
+@property (nonatomic) FSRReadyState readyState;
+
+@property (nonatomic) NSOperationQueue *delegateOperationQueue;
+@property (nonatomic) dispatch_queue_t delegateDispatchQueue;
+
+@end
+
+
+@implementation FSRWebSocket {
+ NSInteger _webSocketVersion;
+
+ NSOperationQueue *_delegateOperationQueue;
+ dispatch_queue_t _delegateDispatchQueue;
+ dispatch_queue_t _workQueue;
+ NSMutableArray *_consumers;
+
+ NSInputStream *_inputStream;
+ NSOutputStream *_outputStream;
+
+ NSMutableData *_readBuffer;
+ NSInteger _readBufferOffset;
+
+ NSMutableData *_outputBuffer;
+ NSInteger _outputBufferOffset;
+
+ uint8_t _currentFrameOpcode;
+ size_t _currentFrameCount;
+ size_t _readOpCount;
+ uint32_t _currentStringScanPosition;
+ NSMutableData *_currentFrameData;
+
+ NSString *_closeReason;
+
+ NSString *_secKey;
+
+ BOOL _pinnedCertFound;
+
+ uint8_t _currentReadMaskKey[4];
+ size_t _currentReadMaskOffset;
+
+ BOOL _consumerStopped;
+
+ BOOL _closeWhenFinishedWriting;
+ BOOL _failed;
+
+ BOOL _secure;
+ NSURLRequest *_urlRequest;
+ NSString *_userAgent;
+
+ CFHTTPMessageRef _receivedHTTPHeaders;
+
+ BOOL _sentClose;
+ BOOL _didFail;
+ BOOL _cleanupScheduled;
+ int _closeCode;
+
+ BOOL _isPumping;
+
+ NSMutableSet *_scheduledRunloops;
+
+ // We use this to retain ourselves.
+ __strong FSRWebSocket *_selfRetain;
+
+ NSArray *_requestedProtocols;
+ FSRIOConsumerPool *_consumerPool;
+}
+
+@synthesize delegate = _delegate;
+@synthesize url = _url;
+@synthesize readyState = _readyState;
+@synthesize protocol = _protocol;
+
+static __strong NSData *CRLFCRLF;
+
++ (void)initialize;
+{
+ CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+{
+ self = [super init];
+ if (self) {
+ assert(request.URL);
+ _url = request.URL;
+ NSString *scheme = [_url scheme];
+
+ _requestedProtocols = [protocols copy];
+ _userAgent = userAgent;
+
+ assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);
+ _urlRequest = request;
+
+ if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) {
+ _secure = YES;
+ }
+
+ if (!queue) {
+ _delegateDispatchQueue = dispatch_get_main_queue();
+ } else {
+ _delegateDispatchQueue = queue;
+ }
+
+ [self _SR_commonInit];
+ }
+
+ return self;
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
+{
+ return [self initWithURLRequest:request protocols:nil queue:nil andUserAgent:nil];
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+{
+ return [self initWithURLRequest:request protocols:nil queue:queue andUserAgent:userAgent];
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request;
+{
+ return [self initWithURLRequest:request protocols:nil];
+}
+
+- (id)initWithURL:(NSURL *)url;
+{
+ return [self initWithURL:url protocols:nil];
+}
+
+- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
+{
+ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
+ return [self initWithURLRequest:request protocols:protocols];
+}
+
+- (void)_SR_commonInit;
+{
+ _readyState = SR_CONNECTING;
+
+ _consumerStopped = YES;
+
+ _webSocketVersion = 13;
+
+ _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
+
+ // Going to set a specific on the queue so we can validate we're on the work queue
+ dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);
+
+ sr_dispatch_retain(_delegateDispatchQueue);
+
+ _readBuffer = [[NSMutableData alloc] init];
+ _outputBuffer = [[NSMutableData alloc] init];
+
+ _currentFrameData = [[NSMutableData alloc] init];
+
+ _consumers = [[NSMutableArray alloc] init];
+
+ _consumerPool = [[FSRIOConsumerPool alloc] init];
+
+ _scheduledRunloops = [[NSMutableSet alloc] init];
+
+ [self _initializeStreams];
+
+ // default handlers
+}
+
+- (void)assertOnWorkQueue;
+{
+ assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue));
+}
+
+- (void)dealloc
+{
+ _inputStream.delegate = nil;
+ _outputStream.delegate = nil;
+
+ [_inputStream close];
+ [_outputStream close];
+
+ sr_dispatch_release(_workQueue);
+ _workQueue = NULL;
+
+ if (_receivedHTTPHeaders) {
+ CFRelease(_receivedHTTPHeaders);
+ _receivedHTTPHeaders = NULL;
+ }
+
+ if (_delegateDispatchQueue) {
+ sr_dispatch_release(_delegateDispatchQueue);
+ _delegateDispatchQueue = NULL;
+ }
+}
+
+#ifndef NDEBUG
+
+- (void)setReadyState:(FSRReadyState)aReadyState;
+{
+ [self willChangeValueForKey:@"readyState"];
+ assert(aReadyState > _readyState);
+ _readyState = aReadyState;
+ [self didChangeValueForKey:@"readyState"];
+}
+
+#endif
+
+- (void)open;
+{
+ assert(_url);
+ NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");
+
+ _selfRetain = self;
+
+ [self _connect];
+}
+
+// Calls block on delegate queue
+- (void)_performDelegateBlock:(dispatch_block_t)block;
+{
+ if (_delegateOperationQueue) {
+ [_delegateOperationQueue addOperationWithBlock:block];
+ } else {
+ assert(_delegateDispatchQueue);
+ dispatch_async(_delegateDispatchQueue, block);
+ }
+}
+
+- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue;
+{
+ if (queue) {
+ sr_dispatch_retain(queue);
+ }
+
+ if (_delegateDispatchQueue) {
+ sr_dispatch_release(_delegateDispatchQueue);
+ }
+
+ _delegateDispatchQueue = queue;
+}
+
+- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;
+{
+ NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept")));
+
+ if (acceptHeader == nil) {
+ return NO;
+ }
+
+ NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString];
+ NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding];
+
+ return [acceptHeader isEqualToString:expectedAccept];
+}
+
+- (void)_HTTPHeadersDidFinish;
+{
+ NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders);
+
+ if (responseCode >= 400) {
+ SRFastLog(@"Request failed with response code %d", responseCode);
+ [self _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:2132 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"received bad response code from server %u", (int)responseCode] forKey:NSLocalizedDescriptionKey]]];
+ return;
+
+ }
+
+ if(![self _checkHandshake:_receivedHTTPHeaders]) {
+ [self _failWithError:[NSError errorWithDomain:FSRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]];
+ return;
+ }
+
+ NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol")));
+ if (negotiatedProtocol) {
+ // Make sure we requested the protocol
+ if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) {
+ [self _failWithError:[NSError errorWithDomain:FSRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]];
+ return;
+ }
+
+ _protocol = negotiatedProtocol;
+ }
+
+ self.readyState = SR_OPEN;
+
+ if (!_didFail) {
+ [self _readFrameNew];
+ }
+
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) {
+ [self.delegate webSocketDidOpen:self];
+ };
+ }];
+}
+
+
+- (void)_readHTTPHeader;
+{
+ if (_receivedHTTPHeaders == NULL) {
+ _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO);
+ }
+
+ [self _readUntilHeaderCompleteWithCallback:^(FSRWebSocket *self, NSData *data) {
+ CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length);
+
+ if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) {
+ SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders)));
+ [self _HTTPHeadersDidFinish];
+ } else {
+ [self _readHTTPHeader];
+ }
+ }];
+}
+
+- (void)didConnect
+{
+ SRFastLog(@"Connected");
+ CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1);
+
+ // Set host first so it defaults
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host));
+
+ NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16];
+ int result = SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes);
+ assert(result == 0);
+ _secKey = [FSRUtilities base64EncodedStringFromData:keyBytes];
+ assert([_secKey length] == 24);
+
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket"));
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade"));
+ if (_userAgent) {
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("User-Agent"), (__bridge CFStringRef)_userAgent);
+ }
+
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey);
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%u", (int)_webSocketVersion]);
+
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin);
+
+ if (_requestedProtocols) {
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]);
+ }
+
+ [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
+ }];
+
+ NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request));
+
+ CFRelease(request);
+
+ [self _writeData:message];
+ [self _readHTTPHeader];
+}
+
+//- (void)_connectToHost:(NSString *)host port:(NSInteger)port;
+- (void)_initializeStreams;
+{
+ NSInteger port = _url.port.integerValue;
+ if (port == 0) {
+ if (!_secure) {
+ port = 80;
+ } else {
+ port = 443;
+ }
+ }
+ NSString *host = _url.host;
+
+ CFReadStreamRef readStream = NULL;
+ CFWriteStreamRef writeStream = NULL;
+
+ CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, (int)port, &readStream, &writeStream);
+
+ // XXX
+ CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeBackground);
+ CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeBackground);
+
+ _outputStream = CFBridgingRelease(writeStream);
+ _inputStream = CFBridgingRelease(readStream);
+
+
+ if (_secure) {
+ NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init];
+
+ [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel];
+
+ // If we're using pinned certs, don't validate the certificate chain
+ if ([_urlRequest FSR_SSLPinnedCertificates].count) {
+ [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain];
+ }
+
+ [_outputStream setProperty:SSLOptions
+ forKey:(__bridge id)kCFStreamPropertySSLSettings];
+ }
+
+ _inputStream.delegate = self;
+ _outputStream.delegate = self;
+
+ [_outputStream open];
+ [_inputStream open];
+}
+
+- (void)_connect;
+{
+ if (!_scheduledRunloops.count) {
+ [self scheduleInRunLoop:[NSRunLoop FSR_networkRunLoop] forMode:NSDefaultRunLoopMode];
+ }
+
+
+ [_outputStream open];
+ [_inputStream open];
+}
+
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+{
+ [_outputStream scheduleInRunLoop:aRunLoop forMode:mode];
+ [_inputStream scheduleInRunLoop:aRunLoop forMode:mode];
+
+ [_scheduledRunloops addObject:@[aRunLoop, mode]];
+}
+
+- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+{
+ [_outputStream removeFromRunLoop:aRunLoop forMode:mode];
+ [_inputStream removeFromRunLoop:aRunLoop forMode:mode];
+
+ [_scheduledRunloops removeObject:@[aRunLoop, mode]];
+}
+
+- (void)close;
+{
+ [self closeWithCode:-1 reason:nil];
+}
+
+- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
+{
+ assert(code);
+ dispatch_async(_workQueue, ^{
+ if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) {
+ return;
+ }
+
+ BOOL wasConnecting = self.readyState == SR_CONNECTING;
+
+ self.readyState = SR_CLOSING;
+
+ SRFastLog(@"Closing with code %d reason %@", code, reason);
+
+ if (wasConnecting) {
+ [self _disconnect];
+ return;
+ }
+
+ size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+ NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize];
+ NSData *payload = mutablePayload;
+
+ ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(code);
+
+ if (reason) {
+ NSRange remainingRange = {0};
+
+ NSUInteger usedLength = 0;
+
+ BOOL success = [reason getBytes:(char *)mutablePayload.mutableBytes + sizeof(uint16_t) maxLength:payload.length - sizeof(uint16_t) usedLength:&usedLength encoding:NSUTF8StringEncoding options:NSStringEncodingConversionExternalRepresentation range:NSMakeRange(0, reason.length) remainingRange:&remainingRange];
+
+ assert(success);
+ assert(remainingRange.length == 0);
+
+ if (usedLength != maxMsgSize) {
+ payload = [payload subdataWithRange:NSMakeRange(0, usedLength + sizeof(uint16_t))];
+ }
+ }
+
+
+ [self _sendFrameWithOpcode:SROpCodeConnectionClose data:payload];
+ });
+}
+
+- (void)_closeWithProtocolError:(NSString *)message;
+{
+ // Need to shunt this on the _callbackQueue first to see if they received any messages
+ [self _performDelegateBlock:^{
+ [self closeWithCode:SRStatusCodeProtocolError reason:message];
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+ }];
+}
+
+- (void)_failWithError:(NSError *)error;
+{
+ dispatch_async(_workQueue, ^{
+ if (self.readyState != SR_CLOSED) {
+ _failed = YES;
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) {
+ [self.delegate webSocket:self didFailWithError:error];
+ }
+ }];
+
+ self.readyState = SR_CLOSED;
+
+ SRFastLog(@"Failing with error %@", error.localizedDescription);
+
+ [self _disconnect];
+ [self _scheduleCleanup];
+ }
+ });
+}
+
+- (void)_writeData:(NSData *)data;
+{
+ [self assertOnWorkQueue];
+
+ if (_closeWhenFinishedWriting) {
+ return;
+ }
+ [_outputBuffer appendData:data];
+ [self _pumpWriting];
+}
+- (void)send:(id)data;
+{
+ SRFastLog(@"Sending data %@", data);
+ NSAssert(self.readyState != SR_CONNECTING, @"Invalid State: Cannot call send: until connection is open");
+ // TODO: maybe not copy this for performance
+ data = [data copy];
+ dispatch_async(_workQueue, ^{
+ if ([data isKindOfClass:[NSString class]]) {
+ [self _sendFrameWithOpcode:SROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]];
+ } else if ([data isKindOfClass:[NSData class]]) {
+ [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data];
+ } else if (data == nil) {
+ [self _sendFrameWithOpcode:SROpCodeTextFrame data:data];
+ } else {
+ assert(NO);
+ }
+ });
+}
+
+- (void)handlePing:(NSData *)pingData;
+{
+ // Need to pingpong this off _callbackQueue first to make sure messages happen in order
+ [self _performDelegateBlock:^{
+ dispatch_async(_workQueue, ^{
+ [self _sendFrameWithOpcode:SROpCodePong data:pingData];
+ });
+ }];
+}
+
+- (void)handlePong;
+{
+ // NOOP
+}
+
+- (void)_handleMessage:(id)message
+{
+ SRFastLog(@"Received message");
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didReceiveMessage:)]) {
+ [self.delegate webSocket:self didReceiveMessage:message];
+ }
+ }];
+}
+
+
+static inline BOOL closeCodeIsValid(int closeCode) {
+ if (closeCode < 1000) {
+ return NO;
+ }
+
+ if (closeCode >= 1000 && closeCode <= 1011) {
+ if (closeCode == 1004 ||
+ closeCode == 1005 ||
+ closeCode == 1006) {
+ return NO;
+ }
+ return YES;
+ }
+
+ if (closeCode >= 3000 && closeCode <= 3999) {
+ return YES;
+ }
+
+ if (closeCode >= 4000 && closeCode <= 4999) {
+ return YES;
+ }
+
+ return NO;
+}
+
+// Note from RFC:
+//
+// If there is a body, the first two
+// bytes of the body MUST be a 2-byte unsigned integer (in network byte
+// order) representing a status code with value /code/ defined in
+// Section 7.4. Following the 2-byte integer the body MAY contain UTF-8
+// encoded data with value /reason/, the interpretation of which is not
+// defined by this specification.
+
+- (void)handleCloseWithData:(NSData *)data;
+{
+ size_t dataSize = data.length;
+ __block uint16_t closeCode = 0;
+
+ SRFastLog(@"Received close frame");
+
+ if (dataSize == 1) {
+ // TODO handle error
+ [self _closeWithProtocolError:@"Payload for close must be larger than 2 bytes"];
+ return;
+ } else if (dataSize >= 2) {
+ [data getBytes:&closeCode length:sizeof(closeCode)];
+ _closeCode = EndianU16_BtoN(closeCode);
+ if (!closeCodeIsValid(_closeCode)) {
+ [self _closeWithProtocolError:[NSString stringWithFormat:@"Cannot have close code of %d", _closeCode]];
+ return;
+ }
+ if (dataSize > 2) {
+ _closeReason = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(2, dataSize - 2)] encoding:NSUTF8StringEncoding];
+ if (!_closeReason) {
+ [self _closeWithProtocolError:@"Close reason MUST be valid UTF-8"];
+ return;
+ }
+ }
+ } else {
+ _closeCode = SRStatusNoStatusReceived;
+ }
+
+ [self assertOnWorkQueue];
+
+ if (self.readyState == SR_OPEN) {
+ [self closeWithCode:1000 reason:nil];
+ }
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+}
+
+- (void)_disconnect;
+{
+ [self assertOnWorkQueue];
+ SRFastLog(@"Trying to disconnect");
+ _closeWhenFinishedWriting = YES;
+ [self _pumpWriting];
+}
+
+- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode;
+{
+ // Check that the current data is valid UTF8
+
+ BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose);
+ if (!isControlFrame) {
+ [self _readFrameNew];
+ } else {
+ dispatch_async(_workQueue, ^{
+ [self _readFrameContinue];
+ });
+ }
+
+ switch (opcode) {
+ case SROpCodeTextFrame: {
+ NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding];
+ if (str == nil && frameData) {
+ [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"];
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+
+ return;
+ }
+ [self _handleMessage:str];
+ break;
+ }
+ case SROpCodeBinaryFrame:
+ [self _handleMessage:[frameData copy]];
+ break;
+ case SROpCodeConnectionClose:
+ [self handleCloseWithData:frameData];
+ break;
+ case SROpCodePing:
+ [self handlePing:frameData];
+ break;
+ case SROpCodePong:
+ [self handlePong];
+ break;
+ default:
+ [self _closeWithProtocolError:[NSString stringWithFormat:@"Unknown opcode %u", (int)opcode]];
+ // TODO: Handle invalid opcode
+ break;
+ }
+}
+
+- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData;
+{
+ assert(frame_header.opcode != 0);
+
+ if (self.readyState != SR_OPEN) {
+ return;
+ }
+
+
+ BOOL isControlFrame = (frame_header.opcode == SROpCodePing || frame_header.opcode == SROpCodePong || frame_header.opcode == SROpCodeConnectionClose);
+
+ if (isControlFrame && !frame_header.fin) {
+ [self _closeWithProtocolError:@"Fragmented control frames not allowed"];
+ return;
+ }
+
+ if (isControlFrame && frame_header.payload_length >= 126) {
+ [self _closeWithProtocolError:@"Control frames cannot have payloads larger than 126 bytes"];
+ return;
+ }
+
+ if (!isControlFrame) {
+ _currentFrameOpcode = frame_header.opcode;
+ _currentFrameCount += 1;
+ }
+
+ if (frame_header.payload_length == 0) {
+ if (isControlFrame) {
+ [self _handleFrameWithData:curData opCode:frame_header.opcode];
+ } else {
+ if (frame_header.fin) {
+ [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode];
+ } else {
+ // TODO add assert that opcode is not a control;
+ [self _readFrameContinue];
+ }
+ }
+ } else {
+ [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(FSRWebSocket *self, NSData *newData) {
+ if (isControlFrame) {
+ [self _handleFrameWithData:newData opCode:frame_header.opcode];
+ } else {
+ if (frame_header.fin) {
+ [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode];
+ } else {
+ // TODO add assert that opcode is not a control;
+ [self _readFrameContinue];
+ }
+
+ }
+ } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked];
+ }
+}
+
+/* From RFC:
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-------+-+-------------+-------------------------------+
+ |F|R|R|R| opcode|M| Payload len | Extended payload length |
+ |I|S|S|S| (4) |A| (7) | (16/64) |
+ |N|V|V|V| |S| | (if payload len==126/127) |
+ | |1|2|3| |K| | |
+ +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+ | Extended payload length continued, if payload len == 127 |
+ + - - - - - - - - - - - - - - - +-------------------------------+
+ | |Masking-key, if MASK set to 1 |
+ +-------------------------------+-------------------------------+
+ | Masking-key (continued) | Payload Data |
+ +-------------------------------- - - - - - - - - - - - - - - - +
+ : Payload Data continued ... :
+ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ | Payload Data continued ... |
+ +---------------------------------------------------------------+
+ */
+
+static const uint8_t SRFinMask = 0x80;
+static const uint8_t SROpCodeMask = 0x0F;
+static const uint8_t SRRsvMask = 0x70;
+static const uint8_t SRMaskMask = 0x80;
+static const uint8_t SRPayloadLenMask = 0x7F;
+
+
+- (void)_readFrameContinue;
+{
+ assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0));
+
+ [self _addConsumerWithDataLength:2 callback:^(FSRWebSocket *self, NSData *data) {
+ __block frame_header header = {0};
+
+ const uint8_t *headerBuffer = data.bytes;
+ assert(data.length >= 2);
+
+ if (headerBuffer[0] & SRRsvMask) {
+ [self _closeWithProtocolError:@"Server used RSV bits"];
+ return;
+ }
+
+ uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]);
+
+ BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose);
+
+ if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) {
+ [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"];
+ return;
+ }
+
+ if (receivedOpcode == 0 && self->_currentFrameCount == 0) {
+ [self _closeWithProtocolError:@"cannot continue a message"];
+ return;
+ }
+
+ header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode;
+
+ header.fin = !!(SRFinMask & headerBuffer[0]);
+
+
+ header.masked = !!(SRMaskMask & headerBuffer[1]);
+ header.payload_length = SRPayloadLenMask & headerBuffer[1];
+
+ headerBuffer = NULL;
+
+ if (header.masked) {
+ [self _closeWithProtocolError:@"Client must receive unmasked data"];
+ }
+
+ size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0;
+
+ if (header.payload_length == 126) {
+ extra_bytes_needed += sizeof(uint16_t);
+ } else if (header.payload_length == 127) {
+ extra_bytes_needed += sizeof(uint64_t);
+ }
+
+ if (extra_bytes_needed == 0) {
+ [self _handleFrameHeader:header curData:self->_currentFrameData];
+ } else {
+ [self _addConsumerWithDataLength:extra_bytes_needed callback:^(FSRWebSocket *self, NSData *data) {
+ size_t mapped_size = data.length;
+ const void *mapped_buffer = data.bytes;
+ size_t offset = 0;
+
+ if (header.payload_length == 126) {
+ assert(mapped_size >= sizeof(uint16_t));
+ uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer));
+ header.payload_length = newLen;
+ offset += sizeof(uint16_t);
+ } else if (header.payload_length == 127) {
+ assert(mapped_size >= sizeof(uint64_t));
+ header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer));
+ offset += sizeof(uint64_t);
+ } else {
+ assert(header.payload_length < 126 && header.payload_length >= 0);
+ }
+
+
+ if (header.masked) {
+ assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset);
+ memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey));
+ }
+
+ [self _handleFrameHeader:header curData:self->_currentFrameData];
+ } readToCurrentFrame:NO unmaskBytes:NO];
+ }
+ } readToCurrentFrame:NO unmaskBytes:NO];
+}
+
+- (void)_readFrameNew;
+{
+ dispatch_async(_workQueue, ^{
+ [_currentFrameData setLength:0];
+
+ _currentFrameOpcode = 0;
+ _currentFrameCount = 0;
+ _readOpCount = 0;
+ _currentStringScanPosition = 0;
+
+ [self _readFrameContinue];
+ });
+}
+
+- (void)_pumpWriting;
+{
+ [self assertOnWorkQueue];
+
+ NSUInteger dataLength = _outputBuffer.length;
+ if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) {
+ NSUInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset];
+ if (bytesWritten == -1) {
+ [self _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]];
+ return;
+ }
+
+ _outputBufferOffset += bytesWritten;
+
+ if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) {
+ _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset];
+ _outputBufferOffset = 0;
+ }
+ }
+
+ if (_closeWhenFinishedWriting &&
+ _outputBuffer.length - _outputBufferOffset == 0 &&
+ (_inputStream.streamStatus != NSStreamStatusNotOpen &&
+ _inputStream.streamStatus != NSStreamStatusClosed) &&
+ !_sentClose) {
+ _sentClose = YES;
+
+ @synchronized (self) {
+ [_outputStream close];
+ [_inputStream close];
+
+ // TODO: Why are we missing the SocketRocket code to call unscheduleFromRunLoop???
+ }
+
+ if (!_failed) {
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
+ [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES];
+ }
+ }];
+ }
+ [self _scheduleCleanup];
+ }
+}
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback;
+{
+ [self assertOnWorkQueue];
+ [self _addConsumerWithScanner:consumer callback:callback dataLength:0];
+}
+
+- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+ [self assertOnWorkQueue];
+ assert(dataLength);
+
+ [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]];
+ [self _pumpScanner];
+}
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength;
+{
+ [self assertOnWorkQueue];
+ [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]];
+ [self _pumpScanner];
+}
+
+
+- (void)_scheduleCleanup
+{
+ @synchronized(self) {
+ if (_cleanupScheduled) {
+ return;
+ }
+
+ _cleanupScheduled = YES;
+
+ // Cleanup NSStream delegate's in the same RunLoop used by the streams themselves:
+ // This way we'll prevent race conditions between handleEvent and SRWebsocket's dealloc
+ NSTimer *timer = [NSTimer timerWithTimeInterval:(0.0f) target:self selector:@selector(_cleanupSelfReference:) userInfo:nil repeats:NO];
+ [[NSRunLoop FSR_networkRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
+ }
+}
+
+- (void)_cleanupSelfReference:(NSTimer *)timer
+{
+ @synchronized(self) {
+ // Nuke NSStream delegate's
+ _inputStream.delegate = nil;
+ _outputStream.delegate = nil;
+
+ // Remove the streams, right now, from the networkRunLoop
+ [_inputStream close];
+ [_outputStream close];
+ }
+
+ // Cleanup selfRetain in the same GCD queue as usual
+ dispatch_async(_workQueue, ^{
+ _selfRetain = nil;
+ });
+}
+
+
+static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'};
+
+- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler;
+{
+ [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler];
+}
+
+- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler;
+{
+ // TODO optimize so this can continue from where we last searched
+ stream_scanner consumer = ^size_t(NSData *data) {
+ __block size_t found_size = 0;
+ __block size_t match_count = 0;
+
+ size_t size = data.length;
+ const unsigned char *buffer = data.bytes;
+ for (int i = 0; i < size; i++ ) {
+ if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) {
+ match_count += 1;
+ if (match_count == length) {
+ found_size = i + 1;
+ break;
+ }
+ } else {
+ match_count = 0;
+ }
+ }
+ return found_size;
+ };
+ [self _addConsumerWithScanner:consumer callback:dataHandler];
+}
+
+
+// Returns true if did work
+- (BOOL)_innerPumpScanner {
+
+ BOOL didWork = NO;
+
+ if (self.readyState >= SR_CLOSING) {
+ return didWork;
+ }
+
+ if (!_consumers.count) {
+ return didWork;
+ }
+
+ size_t curSize = _readBuffer.length - _readBufferOffset;
+ if (!curSize) {
+ return didWork;
+ }
+
+ FSRIOConsumer *consumer = [_consumers objectAtIndex:0];
+
+ size_t bytesNeeded = consumer.bytesNeeded;
+
+ size_t foundSize = 0;
+ if (consumer.consumer) {
+ NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO];
+ foundSize = consumer.consumer(tempView);
+ } else {
+ assert(consumer.bytesNeeded);
+ if (curSize >= bytesNeeded) {
+ foundSize = bytesNeeded;
+ } else if (consumer.readToCurrentFrame) {
+ foundSize = curSize;
+ }
+ }
+
+ NSData *slice = nil;
+ if (consumer.readToCurrentFrame || foundSize) {
+ NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize);
+ slice = [_readBuffer subdataWithRange:sliceRange];
+
+ _readBufferOffset += foundSize;
+
+ if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) {
+ _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset]; _readBufferOffset = 0;
+ }
+
+ if (consumer.unmaskBytes) {
+ NSMutableData *mutableSlice = [slice mutableCopy];
+
+ NSUInteger len = mutableSlice.length;
+ uint8_t *bytes = mutableSlice.mutableBytes;
+
+ for (int i = 0; i < len; i++) {
+ bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)];
+ _currentReadMaskOffset += 1;
+ }
+
+ slice = mutableSlice;
+ }
+
+ if (consumer.readToCurrentFrame) {
+ [_currentFrameData appendData:slice];
+
+ _readOpCount += 1;
+
+ if (_currentFrameOpcode == SROpCodeTextFrame) {
+ // Validate UTF8 stuff.
+ size_t currentDataSize = _currentFrameData.length;
+ if (_currentFrameOpcode == SROpCodeTextFrame && currentDataSize > 0) {
+ // TODO: Optimize the crap out of this. Don't really have to copy all the data each time
+
+ size_t scanSize = currentDataSize - _currentStringScanPosition;
+
+ NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)];
+ int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data);
+
+ if (valid_utf8_size == -1) {
+ [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"];
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+ return didWork;
+ } else {
+ _currentStringScanPosition += valid_utf8_size;
+ }
+ }
+
+ }
+
+ consumer.bytesNeeded -= foundSize;
+
+ if (consumer.bytesNeeded == 0) {
+ [_consumers removeObjectAtIndex:0];
+ consumer.handler(self, nil);
+ didWork = YES;
+ }
+ } else if (foundSize) {
+ [_consumers removeObjectAtIndex:0];
+ consumer.handler(self, slice);
+ didWork = YES;
+ }
+ }
+ return didWork;
+}
+
+-(void)_pumpScanner;
+{
+ [self assertOnWorkQueue];
+
+ if (!_isPumping) {
+ _isPumping = YES;
+ } else {
+ return;
+ }
+
+ while ([self _innerPumpScanner]) {
+
+ }
+
+ _isPumping = NO;
+}
+
+//#define NOMASK
+
+static const size_t SRFrameHeaderOverhead = 32;
+
+- (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");
+
+ size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length];
+
+ NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead];
+ if (!frame) {
+ [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"];
+ return;
+ }
+ uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes];
+
+ // set fin
+ frame_buffer[0] = SRFinMask | opcode;
+
+ BOOL useMask = YES;
+#ifdef NOMASK
+ useMask = NO;
+#endif
+
+ if (useMask) {
+ // set the mask and header
+ frame_buffer[1] |= SRMaskMask;
+ }
+
+ size_t frame_buffer_size = 2;
+
+ const uint8_t *unmasked_payload = NULL;
+ if ([data isKindOfClass:[NSData class]]) {
+ unmasked_payload = (uint8_t *)[data bytes];
+ } else if ([data isKindOfClass:[NSString class]]) {
+ unmasked_payload = (const uint8_t *)[data UTF8String];
+ } else {
+ assert(NO);
+ }
+
+ if (payloadLength < 126) {
+ frame_buffer[1] |= payloadLength;
+ } else if (payloadLength <= UINT16_MAX) {
+ frame_buffer[1] |= 126;
+ *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength);
+ frame_buffer_size += sizeof(uint16_t);
+ } else {
+ frame_buffer[1] |= 127;
+ *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength);
+ frame_buffer_size += sizeof(uint64_t);
+ }
+
+ if (!useMask) {
+ for (int i = 0; i < payloadLength; i++) {
+ frame_buffer[frame_buffer_size] = unmasked_payload[i];
+ frame_buffer_size += 1;
+ }
+ } else {
+ uint8_t *mask_key = frame_buffer + frame_buffer_size;
+ int result = SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key);
+ assert(result == 0);
+ frame_buffer_size += sizeof(uint32_t);
+
+ // TODO: could probably optimize this with SIMD
+ for (int i = 0; i < payloadLength; i++) {
+ frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)];
+ frame_buffer_size += 1;
+ }
+ }
+
+ assert(frame_buffer_size <= [frame length]);
+ frame.length = frame_buffer_size;
+
+ [self _writeData:frame];
+}
+
+- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;
+{
+ __weak __typeof__(self) weakSelf = self;
+
+ // turn on keep-alive for the output stream.
+ if (eventCode == NSStreamEventOpenCompleted && aStream == _outputStream) {
+ CFDataRef socketData = CFWriteStreamCopyProperty((CFWriteStreamRef)_outputStream, kCFStreamPropertySocketNativeHandle);
+ // In rare cases socketData might be nil (there are crash reports out there), in which case we'll have to just
+ // live without keep-alive :(
+ if (socketData != nil) {
+ CFSocketNativeHandle socket;
+ CFDataGetBytes(socketData, CFRangeMake(0, sizeof(CFSocketNativeHandle)), (UInt8 *)&socket);
+ CFRelease(socketData);
+
+ int keepAliveOn = 1;
+ if (setsockopt(socket, SOL_SOCKET, SO_KEEPALIVE, &keepAliveOn, sizeof(keepAliveOn)) == -1) {
+ SRFastLog(@"Failed to turn on TCP keepalive for websocket");
+ }
+ }
+ }
+
+ if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) {
+
+ NSArray *sslCerts = [_urlRequest FSR_SSLPinnedCertificates];
+ if (sslCerts) {
+ SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust];
+ if (secTrust) {
+ NSInteger numCerts = SecTrustGetCertificateCount(secTrust);
+ for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) {
+ SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i);
+ NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert));
+
+ for (id ref in sslCerts) {
+ SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref;
+ NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert));
+
+ if ([trustedCertData isEqualToData:certData]) {
+ _pinnedCertFound = YES;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!_pinnedCertFound) {
+ dispatch_async(_workQueue, ^{
+ NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : @"Invalid server cert" };
+ [weakSelf _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:23556 userInfo:userInfo]];
+ });
+ return;
+ }
+ }
+ }
+
+ // SRFastLog(@"%@ Got stream event %d", aStream, eventCode);
+ dispatch_async(_workQueue, ^{
+ [weakSelf safeHandleEvent:eventCode stream:aStream];
+ });
+}
+
+- (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream
+{
+ switch (eventCode) {
+ case NSStreamEventOpenCompleted: {
+ SRFastLog(@"NSStreamEventOpenCompleted %@", aStream);
+ if (self.readyState >= SR_CLOSING) {
+ return;
+ }
+
+
+ assert(_readBuffer);
+
+ if (self.readyState == SR_CONNECTING && aStream == _inputStream) {
+ [self didConnect];
+ }
+ [self _pumpWriting];
+ [self _pumpScanner];
+ break;
+ }
+
+ case NSStreamEventErrorOccurred: {
+ SRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]);
+ /// TODO specify error better!
+ [self _failWithError:aStream.streamError];
+ _readBufferOffset = 0;
+ [_readBuffer setLength:0];
+ break;
+
+ }
+
+ case NSStreamEventEndEncountered: {
+ [self _pumpScanner];
+ SRFastLog(@"NSStreamEventEndEncountered %@", aStream);
+ if (aStream.streamError) {
+ [self _failWithError:aStream.streamError];
+ } else {
+ dispatch_async(_workQueue, ^{
+ if (self.readyState != SR_CLOSED) {
+ self.readyState = SR_CLOSED;
+ [self _scheduleCleanup];
+ }
+
+ if (!_sentClose && !_failed) {
+ _sentClose = YES;
+ // If we get closed in this state it's probably not clean because we should be sending this when we send messages
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
+ [self.delegate webSocket:self didCloseWithCode:0 reason:@"Stream end encountered" wasClean:NO];
+ }
+ }];
+ }
+ });
+ }
+
+ break;
+ }
+
+ case NSStreamEventHasBytesAvailable: {
+ SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream);
+ const NSUInteger bufferSize = 2048;
+ uint8_t buffer[bufferSize];
+
+ while (_inputStream.hasBytesAvailable) {
+ NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize];
+
+ if (bytes_read > 0) {
+ [_readBuffer appendBytes:buffer length:bytes_read];
+ } else if (bytes_read < 0) {
+ [self _failWithError:_inputStream.streamError];
+ }
+
+ if (bytes_read != bufferSize) {
+ break;
+ }
+ };
+ [self _pumpScanner];
+ break;
+ }
+
+ case NSStreamEventHasSpaceAvailable: {
+ SRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream);
+ [self _pumpWriting];
+ break;
+ }
+
+ default:
+ SRFastLog(@"(default) %@", aStream);
+ break;
+ }
+}
+
+@end
+
+
+@implementation FSRIOConsumer
+
+@synthesize bytesNeeded = _bytesNeeded;
+@synthesize consumer = _scanner;
+@synthesize handler = _handler;
+@synthesize readToCurrentFrame = _readToCurrentFrame;
+@synthesize unmaskBytes = _unmaskBytes;
+
+- (void)setupWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+ _scanner = [scanner copy];
+ _handler = [handler copy];
+ _bytesNeeded = bytesNeeded;
+ _readToCurrentFrame = readToCurrentFrame;
+ _unmaskBytes = unmaskBytes;
+ assert(_scanner || _bytesNeeded);
+}
+
+@end
+
+@implementation FSRIOConsumerPool {
+ NSUInteger _poolSize;
+ NSMutableArray *_bufferedConsumers;
+}
+
+- (id)initWithBufferCapacity:(NSUInteger)poolSize;
+{
+ self = [super init];
+ if (self) {
+ _poolSize = poolSize;
+ _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize];
+ }
+ return self;
+}
+
+- (id)init
+{
+ return [self initWithBufferCapacity:8];
+}
+
+- (FSRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+ FSRIOConsumer *consumer = nil;
+ if (_bufferedConsumers.count) {
+ consumer = [_bufferedConsumers lastObject];
+ [_bufferedConsumers removeLastObject];
+ } else {
+ consumer = [[FSRIOConsumer alloc] init];
+ }
+
+ [consumer setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes];
+
+ return consumer;
+}
+
+- (void)returnConsumer:(FSRIOConsumer *)consumer;
+{
+ if (_bufferedConsumers.count < _poolSize) {
+ [_bufferedConsumers addObject:consumer];
+ }
+}
+
+@end
+
+@implementation NSURLRequest (FCertificateAdditions)
+
+- (NSArray *)FSR_SSLPinnedCertificates;
+{
+ return [NSURLProtocol propertyForKey:@"FSR_SSLPinnedCertificates" inRequest:self];
+}
+
+@end
+
+@implementation NSMutableURLRequest (FCertificateAdditions)
+
+- (NSArray *)FSR_SSLPinnedCertificates;
+{
+ return [NSURLProtocol propertyForKey:@"FSR_SSLPinnedCertificates" inRequest:self];
+}
+
+- (void)setFSR_SSLPinnedCertificates:(NSArray *)FSR_SSLPinnedCertificates;
+{
+ [NSURLProtocol setProperty:FSR_SSLPinnedCertificates forKey:@"FSR_SSLPinnedCertificates" inRequest:self];
+}
+
+@end
+
+@implementation NSURL (FSRWebSocket)
+
+- (NSString *)SR_origin;
+{
+ NSString *scheme = [self.scheme lowercaseString];
+
+ if ([scheme isEqualToString:@"wss"]) {
+ scheme = @"https";
+ } else if ([scheme isEqualToString:@"ws"]) {
+ scheme = @"http";
+ }
+
+ if (self.port) {
+ return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port];
+ } else {
+ return [NSString stringWithFormat:@"%@://%@/", scheme, self.host];
+ }
+}
+
+@end
+
+// #define SR_ENABLE_LOG
+
+static inline void SRFastLog(NSString *format, ...) {
+#ifdef SR_ENABLE_LOG
+ __block va_list arg_list;
+ va_start (arg_list, format);
+
+ NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list];
+
+ va_end(arg_list);
+
+ NSLog(@"[SR] %@", formattedString);
+#endif
+}
+
+
+#ifdef HAS_ICU
+
+static inline int32_t validate_dispatch_data_partial_string(NSData *data) {
+
+ const void * contents = [data bytes];
+ long size = [data length];
+
+ const uint8_t *str = (const uint8_t *)contents;
+
+
+ UChar32 codepoint = 1;
+ int32_t offset = 0;
+ int32_t lastOffset = 0;
+ while(offset < size && codepoint > 0) {
+ lastOffset = offset;
+ U8_NEXT(str, offset, size, codepoint);
+ }
+
+ if (codepoint == -1) {
+ // Check to see if the last byte is valid or whether it was just continuing
+ if (!U8_IS_LEAD(str[lastOffset]) || U8_COUNT_TRAIL_BYTES(str[lastOffset]) + lastOffset < (int32_t)size) {
+
+ size = -1;
+ } else {
+ uint8_t leadByte = str[lastOffset];
+ U8_MASK_LEAD_BYTE(leadByte, U8_COUNT_TRAIL_BYTES(leadByte));
+
+ for (int i = lastOffset + 1; i < offset; i++) {
+
+ if (U8_IS_SINGLE(str[i]) || U8_IS_LEAD(str[i]) || !U8_IS_TRAIL(str[i])) {
+ size = -1;
+ }
+ }
+
+ if (size != -1) {
+ size = lastOffset;
+ }
+ }
+ }
+
+ if (size != -1 && ![[NSString alloc] initWithBytesNoCopy:(char *)[data bytes] length:size encoding:NSUTF8StringEncoding freeWhenDone:NO]) {
+ size = -1;
+ }
+
+ return (int32_t)size;
+}
+
+#else
+
+// This is a hack, and probably not optimal
+static inline int32_t validate_dispatch_data_partial_string(NSData *data) {
+ static const int maxCodepointSize = 3;
+
+ for (int i = 0; i < maxCodepointSize; i++) {
+ NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO];
+ if (str) {
+ return (int)(data.length - i);
+ }
+ }
+
+ return -1;
+}
+
+#endif
+
+static _FSRRunLoopThread *networkThread = nil;
+static NSRunLoop *networkRunLoop = nil;
+
+@implementation NSRunLoop (FSRWebSocket)
+
++ (NSRunLoop *)FSR_networkRunLoop {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ networkThread = [[_FSRRunLoopThread alloc] init];
+ networkThread.name = @"com.squareup.SocketRocket.NetworkThread";
+ [networkThread start];
+ networkRunLoop = networkThread.runLoop;
+ });
+
+ return networkRunLoop;
+}
+
+@end
+
+
+@implementation _FSRRunLoopThread {
+ dispatch_group_t _waitGroup;
+}
+
+@synthesize runLoop = _runLoop;
+
+- (void)dealloc
+{
+ sr_dispatch_release(_waitGroup);
+}
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ _waitGroup = dispatch_group_create();
+ dispatch_group_enter(_waitGroup);
+ }
+ return self;
+}
+
+
+/**
+ * This is the main method of the thread on which the socket events are scheduled in a run loop.
+ */
+- (void)main;
+{
+ @autoreleasepool {
+ _runLoop = [NSRunLoop currentRunLoop];
+ dispatch_group_leave(_waitGroup);
+
+ // Add an empty run loop source to prevent runloop from spinning.
+ CFRunLoopSourceContext sourceCtx = {
+ .version = 0,
+ .info = NULL,
+ .retain = NULL,
+ .release = NULL,
+ .copyDescription = NULL,
+ .equal = NULL,
+ .hash = NULL,
+ .schedule = NULL,
+ .cancel = NULL,
+ .perform = NULL
+ };
+ CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &sourceCtx);
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
+ CFRelease(source);
+
+ while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
+
+ }
+ assert(NO);
+ }
+}
+
+- (NSRunLoop *)runLoop;
+{
+ dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER);
+ return _runLoop;
+}
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h
new file mode 100644
index 0000000..bac393b
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h
@@ -0,0 +1,23 @@
+//
+// 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.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface FSRUtilities : NSObject
+
++ (NSString *)base64EncodedStringFromData:(NSData *)data;
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m
new file mode 100644
index 0000000..2be1d84
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m
@@ -0,0 +1,37 @@
+//
+// 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.
+//
+
+#import "NSData+SRB64Additions.h"
+#import "fbase64.h"
+
+@implementation FSRUtilities
+
++ (NSString *)base64EncodedStringFromData:(NSData *)data {
+ size_t buffer_size = ((data.length * 3 + 2) / 2);
+
+ char *buffer = (char *)malloc(buffer_size);
+
+ int len = f_b64_ntop(data.bytes, data.length, buffer, buffer_size);
+
+ if (len == -1) {
+ free(buffer);
+ return nil;
+ } else{
+ return [[NSString alloc] initWithBytesNoCopy:buffer length:len encoding:NSUTF8StringEncoding freeWhenDone:YES];
+ }
+}
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb3 b/Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb3
new file mode 100644
index 0000000..152c47c
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb3
@@ -0,0 +1,3 @@
+Fri Aug 3 15:45:39 PDT 2012
+Github commit: aa2297808c225710e267afece4439c256f6efdb3
+
diff --git a/Firebase/Database/third_party/SocketRocket/fbase64.c b/Firebase/Database/third_party/SocketRocket/fbase64.c
new file mode 100644
index 0000000..1750673
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/fbase64.c
@@ -0,0 +1,318 @@
+/* $OpenBSD: base64.c,v 1.5 2006/10/21 09:55:03 otto Exp $ */
+
+/*
+ * 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 */
+
+
+//
+// Distributed with modifications by Firebase ( https://www.firebase.com )
+//
+
+#if (!defined(HAVE_B64_NTOP) && !defined(HAVE___B64_NTOP)) || (!defined(HAVE_B64_PTON) && !defined(HAVE___B64_PTON))
+
+#include <sys/types.h>
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <ctype.h>
+#include <stdio.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "fbase64.h"
+
+static const char Base64[] =
+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+static const char Pad64 = '=';
+
+/* (From RFC1521 and draft-ietf-dnssec-secext-03.txt)
+ The following encoding technique is taken from RFC 1521 by Borenstein
+ and Freed. It is reproduced here in a slightly edited form for
+ convenience.
+
+ A 65-character subset of US-ASCII is used, enabling 6 bits to be
+ represented per printable character. (The extra 65th character, "=",
+ is used to signify a special processing function.)
+
+ The encoding process represents 24-bit groups of input bits as output
+ strings of 4 encoded characters. Proceeding from left to right, a
+ 24-bit input group is formed by concatenating 3 8-bit input groups.
+ These 24 bits are then treated as 4 concatenated 6-bit groups, each
+ of which is translated into a single digit in the base64 alphabet.
+
+ Each 6-bit group is used as an index into an array of 64 printable
+ characters. The character referenced by the index is placed in the
+ output string.
+
+ Table 1: The Base64 Alphabet
+
+ Value Encoding Value Encoding Value Encoding Value Encoding
+ 0 A 17 R 34 i 51 z
+ 1 B 18 S 35 j 52 0
+ 2 C 19 T 36 k 53 1
+ 3 D 20 U 37 l 54 2
+ 4 E 21 V 38 m 55 3
+ 5 F 22 W 39 n 56 4
+ 6 G 23 X 40 o 57 5
+ 7 H 24 Y 41 p 58 6
+ 8 I 25 Z 42 q 59 7
+ 9 J 26 a 43 r 60 8
+ 10 K 27 b 44 s 61 9
+ 11 L 28 c 45 t 62 +
+ 12 M 29 d 46 u 63 /
+ 13 N 30 e 47 v
+ 14 O 31 f 48 w (pad) =
+ 15 P 32 g 49 x
+ 16 Q 33 h 50 y
+
+ Special processing is performed if fewer than 24 bits are available
+ at the end of the data being encoded. A full encoding quantum is
+ always completed at the end of a quantity. When fewer than 24 input
+ bits are available in an input group, zero bits are added (on the
+ right) to form an integral number of 6-bit groups. Padding at the
+ end of the data is performed using the '=' character.
+
+ Since all base64 input is an integral number of octets, only the
+ -------------------------------------------------
+ following cases can arise:
+
+ (1) the final quantum of encoding input is an integral
+ multiple of 24 bits; here, the final unit of encoded
+ output will be an integral multiple of 4 characters
+ with no "=" padding,
+ (2) the final quantum of encoding input is exactly 8 bits;
+ here, the final unit of encoded output will be two
+ characters followed by two "=" padding characters, or
+ (3) the final quantum of encoding input is exactly 16 bits;
+ here, the final unit of encoded output will be three
+ characters followed by one "=" padding character.
+ */
+
+#if !defined(HAVE_B64_NTOP) && !defined(HAVE___B64_NTOP)
+int
+f_b64_ntop(u_char const *src, size_t srclength, char *target, size_t targsize)
+{
+ size_t datalength = 0;
+ u_char input[3];
+ u_char output[4];
+ u_int i;
+
+ while (2 < srclength) {
+ input[0] = *src++;
+ input[1] = *src++;
+ input[2] = *src++;
+ srclength -= 3;
+
+ output[0] = input[0] >> 2;
+ output[1] = ((input[0] & 0x03) << 4) + (input[1] >> 4);
+ output[2] = ((input[1] & 0x0f) << 2) + (input[2] >> 6);
+ output[3] = input[2] & 0x3f;
+
+ if (datalength + 4 > targsize)
+ return (-1);
+ target[datalength++] = Base64[output[0]];
+ target[datalength++] = Base64[output[1]];
+ target[datalength++] = Base64[output[2]];
+ target[datalength++] = Base64[output[3]];
+ }
+
+ /* Now we worry about padding. */
+ if (0 != srclength) {
+ /* Get what's left. */
+ input[0] = input[1] = input[2] = '\0';
+ for (i = 0; i < srclength; i++)
+ input[i] = *src++;
+
+ output[0] = input[0] >> 2;
+ output[1] = ((input[0] & 0x03) << 4) + (input[1] >> 4);
+ output[2] = ((input[1] & 0x0f) << 2) + (input[2] >> 6);
+
+ if (datalength + 4 > targsize)
+ return (-1);
+ target[datalength++] = Base64[output[0]];
+ target[datalength++] = Base64[output[1]];
+ if (srclength == 1)
+ target[datalength++] = Pad64;
+ else
+ target[datalength++] = Base64[output[2]];
+ target[datalength++] = Pad64;
+ }
+ if (datalength >= targsize)
+ return (-1);
+ target[datalength] = '\0'; /* Returned value doesn't count \0. */
+ return (int)(datalength);
+}
+#endif /* !defined(HAVE_B64_NTOP) && !defined(HAVE___B64_NTOP) */
+
+#if !defined(HAVE_B64_PTON) && !defined(HAVE___B64_PTON)
+
+/* skips all whitespace anywhere.
+ converts characters, four at a time, starting at (or after)
+ src from base - 64 numbers into three 8 bit bytes in the target area.
+ it returns the number of data bytes stored at the target, or -1 on error.
+ */
+
+int
+f_b64_pton(char const *src, u_char *target, size_t targsize)
+{
+ u_int tarindex, state;
+ int ch;
+ char *pos;
+
+ state = 0;
+ tarindex = 0;
+
+ while ((ch = *src++) != '\0') {
+ if (isspace(ch)) /* Skip whitespace anywhere. */
+ continue;
+
+ if (ch == Pad64)
+ break;
+
+ pos = strchr(Base64, ch);
+ if (pos == 0) /* A non-base64 character. */
+ return (-1);
+
+ switch (state) {
+ case 0:
+ if (target) {
+ if (tarindex >= targsize)
+ return (-1);
+ target[tarindex] = (pos - Base64) << 2;
+ }
+ state = 1;
+ break;
+ case 1:
+ if (target) {
+ if (tarindex + 1 >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64) >> 4;
+ target[tarindex+1] = ((pos - Base64) & 0x0f)
+ << 4 ;
+ }
+ tarindex++;
+ state = 2;
+ break;
+ case 2:
+ if (target) {
+ if (tarindex + 1 >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64) >> 2;
+ target[tarindex+1] = ((pos - Base64) & 0x03)
+ << 6;
+ }
+ tarindex++;
+ state = 3;
+ break;
+ case 3:
+ if (target) {
+ if (tarindex >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64);
+ }
+ tarindex++;
+ state = 0;
+ break;
+ }
+ }
+
+ /*
+ * We are done decoding Base-64 chars. Let's see if we ended
+ * on a byte boundary, and/or with erroneous trailing characters.
+ */
+
+ if (ch == Pad64) { /* We got a pad char. */
+ ch = *src++; /* Skip it, get next. */
+ switch (state) {
+ case 0: /* Invalid = in first position */
+ case 1: /* Invalid = in second position */
+ return (-1);
+
+ case 2: /* Valid, means one byte of info */
+ /* Skip any number of spaces. */
+ for (; ch != '\0'; ch = *src++)
+ if (!isspace(ch))
+ break;
+ /* Make sure there is another trailing = sign. */
+ if (ch != Pad64)
+ return (-1);
+ ch = *src++; /* Skip the = */
+ /* Fall through to "single trailing =" case. */
+ /* FALLTHROUGH */
+
+ case 3: /* Valid, means two bytes of info */
+ /*
+ * We know this char is an =. Is there anything but
+ * whitespace after it?
+ */
+ for (; ch != '\0'; ch = *src++)
+ if (!isspace(ch))
+ return (-1);
+
+ /*
+ * Now make sure for cases 2 and 3 that the "extra"
+ * bits that slopped past the last full byte were
+ * zeros. If we don't check them, they become a
+ * subliminal channel.
+ */
+ if (target && target[tarindex] != 0)
+ return (-1);
+ }
+ } else {
+ /*
+ * We ended by seeing the end of the string. Make sure we
+ * have no partial bytes lying around.
+ */
+ if (state != 0)
+ return (-1);
+ }
+
+ return (tarindex);
+}
+
+#endif /* !defined(HAVE_B64_PTON) && !defined(HAVE___B64_PTON) */
+#endif \ No newline at end of file
diff --git a/Firebase/Database/third_party/SocketRocket/fbase64.h b/Firebase/Database/third_party/SocketRocket/fbase64.h
new file mode 100644
index 0000000..a9c55c9
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/fbase64.h
@@ -0,0 +1,33 @@
+// 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.
+//
+
+#ifndef FSocketRocket_base64_h
+#define FSocketRocket_base64_h
+
+#include <sys/types.h>
+
+extern int
+f_b64_ntop(u_char const *src,
+ size_t srclength,
+ char *target,
+ size_t targsize);
+
+extern int
+f_b64_pton(char const *src,
+ u_char *target,
+ size_t targsize);
+
+
+#endif
diff --git a/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h
new file mode 100644
index 0000000..c0baa22
--- /dev/null
+++ b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h
@@ -0,0 +1,105 @@
+//
+// APLevelDB.h
+//
+// Created by Adam Preble on 1/23/12.
+// 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, 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.
+
+#import <Foundation/Foundation.h>
+
+extern NSString * const APLevelDBErrorDomain;
+
+@class APLevelDBIterator;
+@protocol APLevelDBWriteBatch;
+
+@interface APLevelDB : NSObject
+
+@property (nonatomic, readonly, strong) NSString *path;
+
++ (APLevelDB *)levelDBWithPath:(NSString *)path error:(NSError *__autoreleasing*)errorOut;
+- (void)close;
+
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key;
+- (BOOL)setString:(NSString *)str forKey:(NSString *)key;
+
+- (NSData *)dataForKey:(NSString *)key;
+- (NSString *)stringForKey:(NSString *)key;
+
+- (BOOL)removeKey:(NSString *)key;
+
+- (NSArray *)allKeys;
+
+- (void)enumerateKeys:(void (^)(NSString *key, BOOL *stop))block;
+- (void)enumerateKeysWithPrefix:(NSString *)prefix usingBlock:(void (^)(NSString *key, BOOL *stop))block;
+
+- (void)enumerateKeysAndValuesAsStrings:(void (^)(NSString *key, NSString *value, BOOL *stop))block;
+- (void)enumerateKeysWithPrefix:(NSString *)prefix asStrings:(void (^)(NSString *key, NSString *value, BOOL *stop))block;
+
+- (void)enumerateKeysAndValuesAsData:(void (^)(NSString *key, NSData *value, BOOL *stop))block;
+- (void)enumerateKeysWithPrefix:(NSString *)prefix asData:(void (^)(NSString *key, NSData *value, BOOL *stop))block;
+
+- (NSUInteger)approximateSizeFrom:(NSString *)from to:(NSString *)to;
+- (NSUInteger)exactSizeFrom:(NSString *)from to:(NSString *)to;
+
+// Objective-C Subscripting Support:
+// The database object supports subscripting for string-string and string-data key-value access and assignment.
+// Examples:
+// db[@"key"] = @"value";
+// db[@"key"] = [NSData data];
+// NSString *s = db[@"key"];
+// An NSInvalidArgumentException is raised if the key is not an NSString, or if the assigned object is not an
+// instance of NSString or NSData.
+- (id)objectForKeyedSubscript:(id)key;
+- (void)setObject:(id)object forKeyedSubscript:(id<NSCopying>)key;
+
+// Batch write/atomic update support:
+- (id<APLevelDBWriteBatch>)beginWriteBatch;
+
+@end
+
+
+@interface APLevelDBIterator : NSObject
+
++ (id)iteratorWithLevelDB:(APLevelDB *)db;
+
+// Designated initializer:
+- (id)initWithLevelDB:(APLevelDB *)db;
+
+- (BOOL)seekToKey:(NSString *)key;
+- (NSString *)nextKey;
+- (NSString *)key;
+- (NSString *)valueAsString;
+- (NSData *)valueAsData;
+
+@end
+
+
+@protocol APLevelDBWriteBatch <NSObject>
+
+- (void)setData:(NSData *)data forKey:(NSString *)key;
+- (void)setString:(NSString *)str forKey:(NSString *)key;
+
+- (void)removeKey:(NSString *)key;
+
+// Remove all of the buffered sets and removes:
+- (void)clear;
+- (BOOL)commit;
+
+@end
diff --git a/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm
new file mode 100644
index 0000000..cdecce6
--- /dev/null
+++ b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm
@@ -0,0 +1,500 @@
+//
+// APLevelDB.m
+//
+// Created by Adam Preble on 1/23/12.
+// 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, 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.
+
+//
+// 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.
+//
+
+#import "APLevelDB.h"
+
+#import "leveldb/db.h"
+#import "leveldb/options.h"
+#import "leveldb/write_batch.h"
+
+NSString * const APLevelDBErrorDomain = @"APLevelDBErrorDomain";
+
+#define SliceFromString(_string_) (leveldb::Slice((char *)[_string_ UTF8String], [_string_ lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))
+#define StringFromSlice(_slice_) ([[NSString alloc] initWithBytes:_slice_.data() length:_slice_.size() encoding:NSUTF8StringEncoding])
+
+
+@interface APLevelDBWriteBatch : NSObject <APLevelDBWriteBatch> {
+ @package
+ leveldb::WriteBatch _batch;
+}
+
+@property (nonatomic, strong) APLevelDB *levelDB;
+
+- (id)initWithLevelDB:(APLevelDB *)levelDB;
+@end
+
+
+#pragma mark - APLevelDB
+
+@interface APLevelDB () {
+ leveldb::DB *_db;
+ leveldb::ReadOptions _readOptions;
+ leveldb::WriteOptions _writeOptions;
+}
+- (id)initWithPath:(NSString *)path error:(NSError **)errorOut;
++ (leveldb::Options)defaultCreateOptions;
+@property (nonatomic, readonly) leveldb::DB *db;
+@end
+
+
+@implementation APLevelDB
+
+@synthesize path = _path;
+@synthesize db = _db;
+
++ (APLevelDB *)levelDBWithPath:(NSString *)path error:(NSError *__autoreleasing *)errorOut
+{
+ return [[APLevelDB alloc] initWithPath:path error:errorOut];
+}
+
+- (id)initWithPath:(NSString *)path error:(NSError *__autoreleasing *)errorOut
+{
+ if ((self = [super init]))
+ {
+ _path = path;
+
+ leveldb::Options options = [[self class] defaultCreateOptions];
+
+ leveldb::Status status = leveldb::DB::Open(options, [_path UTF8String], &_db);
+
+ if (!status.ok())
+ {
+ if (errorOut)
+ {
+ NSString *statusString = [[NSString alloc] initWithCString:status.ToString().c_str() encoding:NSUTF8StringEncoding];
+ *errorOut = [NSError errorWithDomain:APLevelDBErrorDomain
+ code:0
+ userInfo:[NSDictionary dictionaryWithObjectsAndKeys:statusString, NSLocalizedDescriptionKey, nil]];
+ }
+ return nil;
+ }
+
+ _writeOptions.sync = false;
+ }
+ return self;
+}
+
+- (void)close {
+ if (_db != NULL) {
+ delete _db;
+ _db = NULL;
+ }
+}
+
+- (void)dealloc
+{
+ if (_db != NULL) {
+ delete _db;
+ _db = NULL;
+ }
+}
+
++ (leveldb::Options)defaultCreateOptions
+{
+ leveldb::Options options;
+ options.create_if_missing = true;
+ return options;
+}
+
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = leveldb::Slice((const char *)[data bytes], (size_t)[data length]);
+ leveldb::Status status = _db->Put(_writeOptions, keySlice, valueSlice);
+ return (status.ok() == true);
+}
+
+- (BOOL)setString:(NSString *)str forKey:(NSString *)key
+{
+ // This could have been based on
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = SliceFromString(str);
+ leveldb::Status status = _db->Put(_writeOptions, keySlice, valueSlice);
+ return (status.ok() == true);
+}
+
+- (NSData *)dataForKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ std::string valueCPPString;
+ leveldb::Status status = _db->Get(_readOptions, keySlice, &valueCPPString);
+
+ if (!status.ok())
+ return nil;
+ else
+ return [NSData dataWithBytes:valueCPPString.data() length:valueCPPString.size()];
+}
+
+- (NSString *)stringForKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ std::string valueCPPString;
+ leveldb::Status status = _db->Get(_readOptions, keySlice, &valueCPPString);
+
+ // We assume (dangerously?) UTF-8 string encoding:
+ if (!status.ok())
+ return nil;
+ else
+ return [[NSString alloc] initWithBytes:valueCPPString.data() length:valueCPPString.size() encoding:NSUTF8StringEncoding];
+}
+
+- (BOOL)removeKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Status status = _db->Delete(_writeOptions, keySlice);
+ return (status.ok() == true);
+}
+
+- (NSArray *)allKeys
+{
+ NSMutableArray *keys = [NSMutableArray array];
+ [self enumerateKeys:^(NSString *key, BOOL *stop) {
+ [keys addObject:key];
+ }];
+ return keys;
+}
+
+- (void)enumerateKeysAndValuesAsStrings:(void (^)(NSString *key, NSString *value, BOOL *stop))block
+{
+ [self enumerateKeysWithPrefix:@"" asStrings:block];
+}
+
+- (void)enumerateKeysWithPrefix:(NSString *)prefixString asStrings:(void (^)(NSString *, NSString *, BOOL *))block
+{
+ @autoreleasepool {
+ BOOL stop = NO;
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ leveldb::Slice prefix = SliceFromString(prefixString);
+ for (iter->Seek(prefix); iter->Valid(); iter->Next()) {
+ leveldb::Slice key = iter->key(), value = iter->value();
+ if (key.starts_with(prefix)) {
+ NSString *k = StringFromSlice(key);
+ NSString *v = [[NSString alloc] initWithBytes:value.data() length:value.size() encoding:NSUTF8StringEncoding];
+ block(k, v, &stop);
+ if (stop)
+ break;
+ } else {
+ break;
+ }
+ }
+
+ delete iter;
+ }
+}
+
+- (void)enumerateKeys:(void (^)(NSString *key, BOOL *stop))block
+{
+ [self enumerateKeysWithPrefix:@"" usingBlock:block];
+}
+
+- (void)enumerateKeysWithPrefix:(NSString *)prefixString usingBlock:(void (^)(NSString *key, BOOL *stop))block;
+{
+ @autoreleasepool {
+ BOOL stop = NO;
+ leveldb::Slice prefix = SliceFromString(prefixString);
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ for (iter->Seek(prefix); iter->Valid(); iter->Next()) {
+ leveldb::Slice key = iter->key();
+ if (key.starts_with(prefix)) {
+ NSString *k = StringFromSlice(key);
+ block(k, &stop);
+ if (stop)
+ break;
+ } else {
+ break;
+ }
+ }
+
+ delete iter;
+ }
+}
+
+- (void)enumerateKeysAndValuesAsData:(void (^)(NSString *key, NSData *data, BOOL *stop))block
+{
+ [self enumerateKeysWithPrefix:@"" asData:block];
+}
+
+- (void)enumerateKeysWithPrefix:(NSString *)prefixString asData:(void (^)(NSString *, NSData *, BOOL *))block
+{
+ @autoreleasepool {
+ BOOL stop = NO;
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ leveldb::Slice prefix = SliceFromString(prefixString);
+ for (iter->Seek(prefix); iter->Valid(); iter->Next()) {
+ leveldb::Slice key = iter->key(), value = iter->value();
+ if (key.starts_with(prefix)) {
+ NSString *k = StringFromSlice(key);
+ NSData *data = [NSData dataWithBytes:value.data() length:value.size()];
+ block(k, data, &stop);
+ if (stop)
+ break;
+ } else {
+ break;
+ }
+ }
+
+ delete iter;
+ }
+}
+
+- (NSUInteger)exactSizeFrom:(NSString *)from to:(NSString *)to {
+ NSUInteger size = 0;
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ leveldb::Slice fromSlice = SliceFromString(from);
+ leveldb::Slice toSlice = SliceFromString(to);
+ iter->Seek(fromSlice);
+ while (iter->Valid() && iter->key().compare(toSlice) <= 0) {
+ size += iter->value().size();
+ iter->Next();
+ }
+ delete iter;
+ return size;
+}
+
+
+- (NSUInteger)approximateSizeFrom:(NSString *)from to:(NSString *)to {
+ leveldb::Range ranges[1];
+ leveldb::Slice fromSlice = SliceFromString(from);
+ leveldb::Slice toSlice = SliceFromString(to);
+ ranges[0] = leveldb::Range(fromSlice, toSlice);
+ uint64_t sizes[1];
+ _db->GetApproximateSizes(ranges, 1, sizes);
+ return (NSUInteger)sizes[0];
+}
+
+#pragma mark - Subscripting Support
+
+- (id)objectForKeyedSubscript:(id)key
+{
+ if (![key respondsToSelector: @selector(componentsSeparatedByString:)])
+ {
+ [NSException raise:NSInvalidArgumentException format:@"key must be an NSString"];
+ }
+ return [self stringForKey:key];
+}
+- (void)setObject:(id)thing forKeyedSubscript:(id<NSCopying>)key
+{
+ id idKey = (id) key;
+ if (![idKey respondsToSelector: @selector(componentsSeparatedByString:)])
+ {
+ [NSException raise:NSInvalidArgumentException format:@"key must be NSString or NSData"];
+ }
+
+ if ([thing respondsToSelector:@selector(componentsSeparatedByString:)])
+ [self setString:thing forKey:(NSString *)key];
+ else if ([thing respondsToSelector:@selector(subdataWithRange:)])
+ [self setData:thing forKey:(NSString *)key];
+ else
+ [NSException raise:NSInvalidArgumentException format:@"object must be NSString or NSData"];
+}
+
+#pragma mark - Atomic Updates
+
+- (id<APLevelDBWriteBatch>)beginWriteBatch
+{
+ APLevelDBWriteBatch *batch = [[APLevelDBWriteBatch alloc] initWithLevelDB:self];
+ return batch;
+}
+
+- (BOOL)commitWriteBatch:(id<APLevelDBWriteBatch>)theBatch
+{
+ if (!theBatch)
+ return NO;
+
+ APLevelDBWriteBatch *batch = theBatch;
+
+ leveldb::Status status;
+ status = _db->Write(_writeOptions, &batch->_batch);
+ return (status.ok() == true);
+}
+
+@end
+
+
+#pragma mark - APLevelDBIterator
+
+@interface APLevelDBIterator () {
+ leveldb::Iterator *_iter;
+}
+
+@property (nonatomic, strong) APLevelDB *levelDB;
+@end
+
+
+
+@implementation APLevelDBIterator
+
++ (id)iteratorWithLevelDB:(APLevelDB *)db
+{
+ APLevelDBIterator *iter = [[[self class] alloc] initWithLevelDB:db];
+ return iter;
+}
+
+- (id)initWithLevelDB:(APLevelDB *)db
+{
+ if ((self = [super init]))
+ {
+ // Hold on to the database so it doesn't get deallocated before the iterator is deallocated
+ self->_levelDB = db;
+ _iter = db.db->NewIterator(leveldb::ReadOptions());
+ _iter->SeekToFirst();
+ if (!_iter->Valid())
+ return nil;
+ }
+ return self;
+}
+
+- (id)init
+{
+ [NSException raise:@"BadInitializer" format:@"Use the designated initializer, -initWithLevelDB:, instead."];
+ return nil;
+}
+
+- (void)dealloc
+{
+ self->_levelDB = nil;
+ delete _iter;
+ _iter = NULL;
+}
+
+- (BOOL)seekToKey:(NSString *)key
+{
+ leveldb::Slice target = SliceFromString(key);
+ _iter->Seek(target);
+ return _iter->Valid() == true;
+}
+
+- (void)seekToFirst
+{
+ _iter->SeekToFirst();
+}
+
+- (void)seekToLast
+{
+ _iter->SeekToLast();
+}
+
+- (NSString *)nextKey
+{
+ _iter->Next();
+ return [self key];
+}
+
+- (NSString *)key
+{
+ if (_iter->Valid() == false)
+ return nil;
+ leveldb::Slice value = _iter->key();
+ return StringFromSlice(value);
+}
+
+- (NSString *)valueAsString
+{
+ if (_iter->Valid() == false)
+ return nil;
+ leveldb::Slice value = _iter->value();
+ return StringFromSlice(value);
+}
+
+- (NSData *)valueAsData
+{
+ if (_iter->Valid() == false)
+ return nil;
+ leveldb::Slice value = _iter->value();
+ return [NSData dataWithBytes:value.data() length:value.size()];
+}
+
+@end
+
+
+
+#pragma mark - APLevelDBWriteBatch
+
+@implementation APLevelDBWriteBatch
+
+- (id)initWithLevelDB:(APLevelDB *)levelDB {
+ self = [super init];
+ if (self != nil) {
+ self->_levelDB = levelDB;
+ }
+ return self;
+}
+
+- (void)setData:(NSData *)data forKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = leveldb::Slice((const char *)[data bytes], (size_t)[data length]);
+ _batch.Put(keySlice, valueSlice);
+}
+- (void)setString:(NSString *)str forKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = SliceFromString(str);
+ _batch.Put(keySlice, valueSlice);
+}
+
+- (void)removeKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ _batch.Delete(keySlice);
+}
+
+- (void)clear
+{
+ _batch.Clear();
+}
+
+- (BOOL)commit {
+ return [self.levelDB commitWriteBatch:self];
+}
+
+@end
+
diff --git a/Firebase/Firebase/Firebase.h b/Firebase/Firebase/Firebase.h
new file mode 100644
index 0000000..f74e49f
--- /dev/null
+++ b/Firebase/Firebase/Firebase.h
@@ -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.
+ */
+
+#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(<Firebase/FirebaseCore.h>)
+ #import <Firebase/FirebaseCore.h>
+ #endif
+
+ #if __has_include(<FirebaseAnalytics/FirebaseAnalytics.h>)
+ #import <FirebaseAnalytics/FirebaseAnalytics.h>
+ #endif
+
+ #if __has_include(<FirebaseAppIndexing/FirebaseAppIndexing.h>)
+ #import <FirebaseAppIndexing/FirebaseAppIndexing.h>
+ #endif
+
+ #if __has_include(<Firebase/FirebaseAuth.h>)
+ #import <Firebase/FirebaseAuth.h>
+ #endif
+
+ #if __has_include(<FirebaseCrash/FirebaseCrash.h>)
+ #import <FirebaseCrash/FirebaseCrash.h>
+ #endif
+
+ #if __has_include(<Firebase/FirebaseDatabase.h>)
+ #import <Firebase/FirebaseDatabase.h>
+ #endif
+
+ #if __has_include(<FirebaseDynamicLinks/FirebaseDynamicLinks.h>)
+ #import <FirebaseDynamicLinks/FirebaseDynamicLinks.h>
+ #endif
+
+ #if __has_include(<Firebase/FirebaseInstanceID.h>)
+ #import <Firebase/FirebaseInstanceID.h>
+ #endif
+
+ #if __has_include(<FirebaseInvites/FirebaseInvites.h>)
+ #import <FirebaseInvites/FirebaseInvites.h>
+ #endif
+
+ #if __has_include(<Firebase/FirebaseMessaging.h>)
+ #import <Firebase/FirebaseMessaging.h>
+ #endif
+
+ #if __has_include(<FirebaseRemoteConfig/FirebaseRemoteConfig.h>)
+ #import <FirebaseRemoteConfig/FirebaseRemoteConfig.h>
+ #endif
+
+ #if __has_include(<Firebase/FirebaseStorage.h>)
+ #import <Firebase/FirebaseStorage.h>
+ #endif
+
+ #if __has_include(<GoogleMobileAds/GoogleMobileAds.h>)
+ #import <GoogleMobileAds/GoogleMobileAds.h>
+ #endif
+
+#endif // defined(__has_include)
diff --git a/Firebase/Firebase/module.modulemap b/Firebase/Firebase/module.modulemap
new file mode 100755
index 0000000..3685b54
--- /dev/null
+++ b/Firebase/Firebase/module.modulemap
@@ -0,0 +1,4 @@
+module Firebase {
+ export *
+ header "Firebase.h"
+} \ No newline at end of file
diff --git a/Firebase/Messaging/FIRMMessageCode.h b/Firebase/Messaging/FIRMMessageCode.h
new file mode 100644
index 0000000..dc381ee
--- /dev/null
+++ b/Firebase/Messaging/FIRMMessageCode.h
@@ -0,0 +1,169 @@
+/*
+ * 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.
+ */
+
+typedef NS_ENUM(NSInteger, FIRMessagingMessageCode) {
+ // FIRMessaging+FIRApp.m
+ kFIRMessagingMessageCodeFIRApp000 = 1000, // I-FCM001000
+ // FIRMessaging.m
+ kFIRMessagingMessageCodeMessaging000 = 2000, // I-FCM002000
+ kFIRMessagingMessageCodeMessaging001 = 2001, // I-FCM002001
+ kFIRMessagingMessageCodeMessaging002 = 2002, // I-FCM002002 - no longer used
+ kFIRMessagingMessageCodeMessaging003 = 2003, // I-FCM002003
+ kFIRMessagingMessageCodeMessaging004 = 2004, // I-FCM002004
+ kFIRMessagingMessageCodeMessaging005 = 2005, // I-FCM002005
+ kFIRMessagingMessageCodeMessaging006 = 2006, // I-FCM002006 - no longer used
+ kFIRMessagingMessageCodeMessaging007 = 2007, // I-FCM002007 - no longer used
+ kFIRMessagingMessageCodeMessaging008 = 2008, // I-FCM002008 - no longer used
+ kFIRMessagingMessageCodeMessaging009 = 2009, // I-FCM002009
+ kFIRMessagingMessageCodeMessaging010 = 2010, // I-FCM002010
+ kFIRMessagingMessageCodeMessaging011 = 2011, // I-FCM002011
+ kFIRMessagingMessageCodeMessaging012 = 2012, // I-FCM002012
+ kFIRMessagingMessageCodeMessaging013 = 2013, // I-FCM002013
+ kFIRMessagingMessageCodeMessaging014 = 2014, // I-FCM002014
+ kFIRMessagingMessageCodeMessaging015 = 2015, // I-FCM002015
+ kFIRMessagingMessageCodeMessaging016 = 2016, // I-FCM002016
+ kFIRMessagingMessageCodeMessaging017 = 2017, // I-FCM002017
+ kFIRMessagingMessageCodeMessaging018 = 2018, // I-FCM002018
+ kFIRMessagingMessageCodeRemoteMessageDelegateMethodNotImplemented = 2019, // I-FCM002019
+ kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenFetch = 2020, // I-FCM002020
+ kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenDelete = 2021, // I-FCM002021
+ kFIRMessagingMessageCodeAPNSTokenNotAvailableDuringTokenFetch = 2022, // I-FCM002022
+ // FIRMessagingClient.m
+ kFIRMessagingMessageCodeClient000 = 4000, // I-FCM004000
+ kFIRMessagingMessageCodeClient001 = 4001, // I-FCM004001
+ kFIRMessagingMessageCodeClient002 = 4002, // I-FCM004002
+ kFIRMessagingMessageCodeClient003 = 4003, // I-FCM004003
+ kFIRMessagingMessageCodeClient004 = 4004, // I-FCM004004
+ kFIRMessagingMessageCodeClient005 = 4005, // I-FCM004005
+ kFIRMessagingMessageCodeClient006 = 4006, // I-FCM004006
+ kFIRMessagingMessageCodeClient007 = 4007, // I-FCM004007
+ kFIRMessagingMessageCodeClient008 = 4008, // I-FCM004008
+ kFIRMessagingMessageCodeClient009 = 4009, // I-FCM004009
+ kFIRMessagingMessageCodeClient010 = 4010, // I-FCM004010
+ kFIRMessagingMessageCodeClient011 = 4011, // I-FCM004011
+ // FIRMessagingConnection.m
+ kFIRMessagingMessageCodeConnection000 = 5000, // I-FCM005000
+ kFIRMessagingMessageCodeConnection001 = 5001, // I-FCM005001
+ kFIRMessagingMessageCodeConnection002 = 5002, // I-FCM005002
+ kFIRMessagingMessageCodeConnection003 = 5003, // I-FCM005003
+ kFIRMessagingMessageCodeConnection004 = 5004, // I-FCM005004
+ kFIRMessagingMessageCodeConnection005 = 5005, // I-FCM005005
+ kFIRMessagingMessageCodeConnection006 = 5006, // I-FCM005006
+ kFIRMessagingMessageCodeConnection007 = 5007, // I-FCM005007
+ kFIRMessagingMessageCodeConnection008 = 5008, // I-FCM005008
+ kFIRMessagingMessageCodeConnection009 = 5009, // I-FCM005009
+ kFIRMessagingMessageCodeConnection010 = 5010, // I-FCM005010
+ kFIRMessagingMessageCodeConnection011 = 5011, // I-FCM005011
+ kFIRMessagingMessageCodeConnection012 = 5012, // I-FCM005012
+ kFIRMessagingMessageCodeConnection013 = 5013, // I-FCM005013
+ kFIRMessagingMessageCodeConnection014 = 5014, // I-FCM005014
+ kFIRMessagingMessageCodeConnection015 = 5015, // I-FCM005015
+ kFIRMessagingMessageCodeConnection016 = 5016, // I-FCM005016
+ kFIRMessagingMessageCodeConnection017 = 5017, // I-FCM005017
+ kFIRMessagingMessageCodeConnection018 = 5018, // I-FCM005018
+ kFIRMessagingMessageCodeConnection019 = 5019, // I-FCM005019
+ kFIRMessagingMessageCodeConnection020 = 5020, // I-FCM005020
+ kFIRMessagingMessageCodeConnection021 = 5021, // I-FCM005021
+ kFIRMessagingMessageCodeConnection022 = 5022, // I-FCM005022
+ kFIRMessagingMessageCodeConnection023 = 5023, // I-FCM005023
+ // FIRMessagingContextManagerService.m
+ kFIRMessagingMessageCodeContextManagerService000 = 6000, // I-FCM006000
+ kFIRMessagingMessageCodeContextManagerService001 = 6001, // I-FCM006001
+ kFIRMessagingMessageCodeContextManagerService002 = 6002, // I-FCM006002
+ kFIRMessagingMessageCodeContextManagerService003 = 6003, // I-FCM006003
+ kFIRMessagingMessageCodeContextManagerService004 = 6004, // I-FCM006004
+ kFIRMessagingMessageCodeContextManagerService005 = 6005, // I-FCM006005
+ // FIRMessagingDataMessageManager.m
+ kFIRMessagingMessageCodeDataMessageManager000 = 7000, // I-FCM007000
+ kFIRMessagingMessageCodeDataMessageManager001 = 7001, // I-FCM007001
+ kFIRMessagingMessageCodeDataMessageManager002 = 7002, // I-FCM007002
+ kFIRMessagingMessageCodeDataMessageManager003 = 7003, // I-FCM007003
+ kFIRMessagingMessageCodeDataMessageManager004 = 7004, // I-FCM007004
+ kFIRMessagingMessageCodeDataMessageManager005 = 7005, // I-FCM007005
+ kFIRMessagingMessageCodeDataMessageManager006 = 7006, // I-FCM007006
+ kFIRMessagingMessageCodeDataMessageManager007 = 7007, // I-FCM007007
+ kFIRMessagingMessageCodeDataMessageManager008 = 7008, // I-FCM007008
+ kFIRMessagingMessageCodeDataMessageManager009 = 7009, // I-FCM007009
+ kFIRMessagingMessageCodeDataMessageManager010 = 7010, // I-FCM007010
+ kFIRMessagingMessageCodeDataMessageManager011 = 7011, // I-FCM007011
+ kFIRMessagingMessageCodeDataMessageManager012 = 7012, // I-FCM007012
+ // FIRMessagingPendingTopicsList.m
+ kFIRMessagingMessageCodePendingTopicsList000 = 8000, // I-FCM008000
+ // FIRMessagingPubSub.m
+ kFIRMessagingMessageCodePubSub000 = 9000, // I-FCM009000
+ kFIRMessagingMessageCodePubSub001 = 9001, // I-FCM009001
+ kFIRMessagingMessageCodePubSub002 = 9002, // I-FCM009002
+ kFIRMessagingMessageCodePubSub003 = 9003, // I-FCM009003
+ // FIRMessagingReceiver.m
+ kFIRMessagingMessageCodeReceiver000 = 10000, // I-FCM010000
+ kFIRMessagingMessageCodeReceiver001 = 10001, // I-FCM010001
+ kFIRMessagingMessageCodeReceiver002 = 10002, // I-FCM010002
+ kFIRMessagingMessageCodeReceiver003 = 10003, // I-FCM010003
+ kFIRMessagingMessageCodeReceiver004 = 10004, // I-FCM010004 - no longer used
+ kFIRMessagingMessageCodeReceiver005 = 10005, // I-FCM010005
+ // FIRMessagingRegistrar.m
+ kFIRMessagingMessageCodeRegistrar000 = 11000, // I-FCM011000
+ // FIRMessagingRemoteNotificationsProxy.m
+ kFIRMessagingMessageCodeRemoteNotificationsProxy000 = 12000, // I-FCM012000
+ kFIRMessagingMessageCodeRemoteNotificationsProxy001 = 12001, // I-FCM012001
+ kFIRMessagingMessageCodeRemoteNotificationsProxyAPNSFailed = 12002, // I-FCM012002
+ // FIRMessagingRmq2PersistentStore.m
+ kFIRMessagingMessageCodeRmq2PersistentStore000 = 13000, // I-FCM013000
+ kFIRMessagingMessageCodeRmq2PersistentStore001 = 13001, // I-FCM013001
+ kFIRMessagingMessageCodeRmq2PersistentStore002 = 13002, // I-FCM013002
+ kFIRMessagingMessageCodeRmq2PersistentStore003 = 13003, // I-FCM013003
+ kFIRMessagingMessageCodeRmq2PersistentStore004 = 13004, // I-FCM013004
+ kFIRMessagingMessageCodeRmq2PersistentStore005 = 13005, // I-FCM013005
+ kFIRMessagingMessageCodeRmq2PersistentStore006 = 13006, // I-FCM013006
+ // FIRMessagingRmqManager.m
+ kFIRMessagingMessageCodeRmqManager000 = 14000, // I-FCM014000
+ // FIRMessagingSecureSocket.m
+ kFIRMessagingMessageCodeSecureSocket000 = 15000, // I-FCM015000
+ kFIRMessagingMessageCodeSecureSocket001 = 15001, // I-FCM015001
+ kFIRMessagingMessageCodeSecureSocket002 = 15002, // I-FCM015002
+ kFIRMessagingMessageCodeSecureSocket003 = 15003, // I-FCM015003
+ kFIRMessagingMessageCodeSecureSocket004 = 15004, // I-FCM015004
+ kFIRMessagingMessageCodeSecureSocket005 = 15005, // I-FCM015005
+ kFIRMessagingMessageCodeSecureSocket006 = 15006, // I-FCM015006
+ kFIRMessagingMessageCodeSecureSocket007 = 15007, // I-FCM015007
+ kFIRMessagingMessageCodeSecureSocket008 = 15008, // I-FCM015008
+ kFIRMessagingMessageCodeSecureSocket009 = 15009, // I-FCM015009
+ kFIRMessagingMessageCodeSecureSocket010 = 15010, // I-FCM015010
+ kFIRMessagingMessageCodeSecureSocket011 = 15011, // I-FCM015011
+ kFIRMessagingMessageCodeSecureSocket012 = 15012, // I-FCM015012
+ kFIRMessagingMessageCodeSecureSocket013 = 15013, // I-FCM015013
+ kFIRMessagingMessageCodeSecureSocket014 = 15014, // I-FCM015014
+ kFIRMessagingMessageCodeSecureSocket015 = 15015, // I-FCM015015
+ kFIRMessagingMessageCodeSecureSocket016 = 15016, // I-FCM015016
+ // FIRMessagingSyncMessageManager.m
+ kFIRMessagingMessageCodeSyncMessageManager000 = 16000, // I-FCM016000
+ kFIRMessagingMessageCodeSyncMessageManager001 = 16001, // I-FCM016001
+ kFIRMessagingMessageCodeSyncMessageManager002 = 16002, // I-FCM016002
+ kFIRMessagingMessageCodeSyncMessageManager003 = 16003, // I-FCM016003
+ kFIRMessagingMessageCodeSyncMessageManager004 = 16004, // I-FCM016004
+ kFIRMessagingMessageCodeSyncMessageManager005 = 16005, // I-FCM016005
+ kFIRMessagingMessageCodeSyncMessageManager006 = 16006, // I-FCM016006
+ kFIRMessagingMessageCodeSyncMessageManager007 = 16007, // I-FCM016007
+ kFIRMessagingMessageCodeSyncMessageManager008 = 16008, // I-FCM016008
+ // FIRMessagingTopicOperation.m
+ kFIRMessagingMessageCodeTopicOption000 = 17000, // I-FCM017000
+ kFIRMessagingMessageCodeTopicOption001 = 17001, // I-FCM017001
+ kFIRMessagingMessageCodeTopicOption002 = 17002, // I-FCM017002
+ // FIRMessagingUtilities.m
+ kFIRMessagingMessageCodeUtilities000 = 18000, // I-FCM018000
+ kFIRMessagingMessageCodeUtilities001 = 18001, // I-FCM018001
+ kFIRMessagingMessageCodeUtilities002 = 18002, // I-FCM018002
+};
diff --git a/Firebase/Messaging/FIRMessaging+FIRApp.h b/Firebase/Messaging/FIRMessaging+FIRApp.h
new file mode 100644
index 0000000..743b0f4
--- /dev/null
+++ b/Firebase/Messaging/FIRMessaging+FIRApp.h
@@ -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 "FIRMessaging.h"
+
+/**
+ * This category extends FIRMessaging with the configuration for using Cloud Messaging.
+ */
+@interface FIRMessaging (FIRApp)
+
+@end
diff --git a/Firebase/Messaging/FIRMessaging+FIRApp.m b/Firebase/Messaging/FIRMessaging+FIRApp.m
new file mode 100644
index 0000000..fc53286
--- /dev/null
+++ b/Firebase/Messaging/FIRMessaging+FIRApp.m
@@ -0,0 +1,111 @@
+/*
+ * 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 "FIRMessaging+FIRApp.h"
+
+#import "FIRAppInternal.h"
+#import "FIROptionsInternal.h"
+
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPubSub.h"
+#import "FIRMessagingRemoteNotificationsProxy.h"
+#import "FIRMessagingVersionUtilities.h"
+#import "FIRMessaging_Private.h"
+
+@interface FIRMessaging ()
+
+@property(nonatomic, readwrite, strong) NSString *fcmSenderID;
+
+@end
+
+@implementation FIRMessaging (FIRApp)
+
++ (void)load {
+ // FIRMessaging by default removes itself from observing any notifications.
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(didReceiveConfigureSDKNotification:)
+ name:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]];
+}
+
++ (void)didReceiveConfigureSDKNotification:(NSNotification *)notification {
+ NSDictionary *appInfoDict = notification.userInfo;
+ NSString *appName = appInfoDict[kFIRAppNameKey];
+ FIRApp *app = [FIRApp appNamed:appName];
+ [[FIRMessaging messaging] configureMessaging:app];
+}
+
+- (void)configureMessaging:(FIRApp *)app {
+ FIROptions *options = app.options;
+ NSError *error;
+ if (!options.GCMSenderID.length) {
+ error =
+ [FIRApp errorForSubspecConfigurationFailureWithDomain:kFirebaseCloudMessagingErrorDomain
+ errorCode:FIRErrorCodeCloudMessagingFailed
+ service:kFIRServiceMessaging
+ reason:@"Google Sender ID must not be nil"
+ @" or empty."];
+ [self exitApp:app withError:error];
+ return;
+ }
+
+ self.fcmSenderID = [options.GCMSenderID copy];
+
+ // Swizzle remote-notification-related methods (app delegate and UNUserNotificationCenter)
+ if ([FIRMessagingRemoteNotificationsProxy canSwizzleMethods]) {
+ FIRMessagingLoggerNotice(kFIRMessagingMessageCodeFIRApp000,
+ @"FIRMessaging Remote Notifications proxy enabled, will swizzle "
+ @"remote notification receiver handlers. Add \"%@\" to your "
+ @"Info.plist and set it to NO",
+ kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey);
+ [FIRMessagingRemoteNotificationsProxy swizzleMethods];
+ }
+}
+
+- (void)exitApp:(FIRApp *)app withError:(NSError *)error {
+ [app sendLogsWithServiceName:kFIRServiceMessaging
+ version:FIRMessagingCurrentLibraryVersion()
+ error:error];
+ if (error) {
+ NSString *message = nil;
+ if (app.options.usingOptionsFromDefaultPlist) {
+ // Configured using plist file
+ message = [NSString stringWithFormat:@"Firebase Messaging has stopped your project because "
+ @"there are missing or incorrect values provided in %@.%@ that may prevent "
+ @"your app from behaving as expected:\n\n"
+ @"Error: %@\n\n"
+ @"Please fix these issues to ensure that Firebase is correctly configured in "
+ @"your project.",
+ kServiceInfoFileName,
+ kServiceInfoFileType,
+ error.localizedFailureReason];
+ } else {
+ // Configured manually
+ message = [NSString stringWithFormat:@"Firebase Messaging has stopped your project because "
+ @"there are missing or incorrect values in Firebase's configuration options "
+ @"that may prevent your app from behaving as expected:\n\n"
+ @"Error:%@\n\n"
+ @"Please fix these issues to ensure that Firebase is correctly configured in "
+ @"your project.",
+ error.localizedFailureReason];
+ }
+ [NSException raise:kFirebaseCloudMessagingErrorDomain format:@"%@", message];
+ }
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessaging.m b/Firebase/Messaging/FIRMessaging.m
new file mode 100644
index 0000000..94347c8
--- /dev/null
+++ b/Firebase/Messaging/FIRMessaging.m
@@ -0,0 +1,1071 @@
+/*
+ * 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.
+ */
+
+#if !__has_feature(objc_arc)
+#error FIRMessagingLib should be compiled with ARC.
+#endif
+
+#import "FIRMessaging.h"
+#import "FIRMessaging_Private.h"
+
+#import <UIKit/UIKit.h>
+
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConfig.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingContextManagerService.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingInstanceIDProxy.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPubSub.h"
+#import "FIRMessagingReceiver.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSyncMessageManager.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRMessagingVersionUtilities.h"
+
+#import "FIRReachabilityChecker.h"
+
+#import "NSError+FIRMessaging.h"
+
+static NSString *const kFIRMessagingMessageViaAPNSRootKey = @"aps";
+static NSString *const kFIRMessagingReachabilityHostname = @"www.google.com";
+static NSString *const kFIRMessagingDefaultTokenScope = @"*";
+static NSString *const kFIRMessagingFCMTokenFetchAPNSOption = @"apns_token";
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+const NSNotificationName FIRMessagingSendSuccessNotification =
+ @"com.firebase.messaging.notif.send-success";
+const NSNotificationName FIRMessagingSendErrorNotification =
+ @"com.firebase.messaging.notif.send-error";
+const NSNotificationName FIRMessagingMessagesDeletedNotification =
+ @"com.firebase.messaging.notif.messages-deleted";
+const NSNotificationName FIRMessagingConnectionStateChangedNotification =
+ @"com.firebase.messaging.notif.connection-state-changed";
+const NSNotificationName FIRMessagingRegistrationTokenRefreshedNotification =
+ @"com.firebase.messaging.notif.fcm-token-refreshed";
+#else
+NSString *const FIRMessagingSendSuccessNotification =
+ @"com.firebase.messaging.notif.send-success";
+NSString *const FIRMessagingSendErrorNotification =
+ @"com.firebase.messaging.notif.send-error";
+NSString * const FIRMessagingMessagesDeletedNotification =
+ @"com.firebase.messaging.notif.messages-deleted";
+NSString * const FIRMessagingConnectionStateChangedNotification =
+ @"com.firebase.messaging.notif.connection-state-changed";
+NSString * const FIRMessagingRegistrationTokenRefreshedNotification =
+ @"com.firebase.messaging.notif.fcm-token-refreshed";
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+// Copied from Apple's header in case it is missing in some cases (e.g. pre-Xcode 8 builds).
+#ifndef NSFoundationVersionNumber_iOS_8_x_Max
+#define NSFoundationVersionNumber_iOS_8_x_Max 1199
+#endif
+
+@interface FIRMessagingMessageInfo ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingMessageStatus status;
+
+@end
+
+@implementation FIRMessagingMessageInfo
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithStatus:(FIRMessagingMessageStatus)status {
+ self = [super init];
+ if (self) {
+ _status = status;
+ }
+ return self;
+}
+
+@end
+
+#pragma mark - for iOS 10 compatibility
+@implementation FIRMessagingRemoteMessage
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _appData = [[NSMutableDictionary alloc] init];
+ }
+
+ return self;
+}
+
+- (instancetype)initWithMessage:(FIRMessagingRemoteMessage *)message {
+ self = [self init];
+ if (self) {
+ _appData = [message.appData copy];
+ }
+
+ return self;
+}
+
+@end
+
+@interface FIRMessaging ()
+ <FIRMessagingClientDelegate, FIRMessagingReceiverDelegate, FIRReachabilityDelegate>
+
+// FIRApp properties
+@property(nonatomic, readwrite, copy) NSString *fcmSenderID;
+@property(nonatomic, readwrite, strong) NSData *apnsTokenData;
+@property(nonatomic, readwrite, strong) NSString *defaultFcmToken;
+
+// This object is used as a proxy for reflection-based calls to FIRInstanceID.
+// Due to our packaging requirements, we can't directly depend on FIRInstanceID currently.
+@property(nonatomic, readwrite, strong) FIRMessagingInstanceIDProxy *instanceIDProxy;
+
+@property(nonatomic, readwrite, strong) FIRMessagingConfig *config;
+@property(nonatomic, readwrite, assign) BOOL isClientSetup;
+
+@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
+@property(nonatomic, readwrite, strong) FIRReachabilityChecker *reachability;
+@property(nonatomic, readwrite, strong) FIRMessagingDataMessageManager *dataMessageManager;
+@property(nonatomic, readwrite, strong) FIRMessagingPubSub *pubsub;
+@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmq2Manager;
+@property(nonatomic, readwrite, strong) FIRMessagingReceiver *receiver;
+@property(nonatomic, readwrite, strong) FIRMessagingSyncMessageManager *syncMessageManager;
+
+/// Message ID's logged for analytics. This prevents us from logging the same message twice
+/// which can happen if the user inadvertently calls `appDidReceiveMessage` along with us
+/// calling it implicitly during swizzling.
+@property(nonatomic, readwrite, strong) NSMutableSet *loggedMessageIDs;
+
+@end
+
+@implementation FIRMessaging
+
++ (FIRMessaging *)messaging {
+ static FIRMessaging *messaging;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ // Start Messaging (Fully initialize in one place).
+ FIRMessagingConfig *config = [FIRMessagingConfig defaultConfig];
+ messaging = [[FIRMessaging alloc] initWithConfig:config];
+ [messaging start];
+ });
+ return messaging;
+}
+
+- (instancetype)initWithConfig:(FIRMessagingConfig *)config {
+ self = [super init];
+ if (self) {
+ _config = config;
+ _loggedMessageIDs = [NSMutableSet set];
+ _instanceIDProxy = [[FIRMessagingInstanceIDProxy alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self.reachability stop];
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [self teardown];
+}
+
+- (void)setRemoteMessageDelegate:(id<FIRMessagingDelegate>)delegate {
+ _delegate = delegate;
+}
+
+- (id<FIRMessagingDelegate>)remoteMessageDelegate {
+ return self.delegate;
+}
+
+#pragma mark - Config
+
+- (void)start {
+ _FIRMessagingDevAssert(self.config, @"Invalid nil config in FIRMessagingService");
+
+ [self saveLibraryVersion];
+ [self setupLogger:self.config.logLevel];
+ [self setupReceiverWithConfig:self.config];
+
+ NSString *hostname = kFIRMessagingReachabilityHostname;
+ self.reachability = [[FIRReachabilityChecker alloc] initWithReachabilityDelegate:self
+ loggerDelegate:nil
+ withHost:hostname];
+ [self.reachability start];
+
+ [self setupApplicationSupportSubDirectory];
+ // setup FIRMessaging objects
+ [self setupRmqManager];
+ [self setupClient];
+ [self setupSyncMessageManager];
+ [self setupDataMessageManager];
+ [self setupTopics];
+
+ self.isClientSetup = YES;
+ [self setupNotificationListeners];
+}
+
+- (void)setupApplicationSupportSubDirectory {
+ NSString *messagingSubDirectory = kFIRMessagingApplicationSupportSubDirectory;
+ if (![[self class] hasApplicationSupportSubDirectory:messagingSubDirectory]) {
+ [[self class] createApplicationSupportSubDirectory:messagingSubDirectory];
+ }
+}
+
+- (void)setupNotificationListeners {
+ // To prevent multiple notifications remove self as observer for all events.
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center removeObserver:self];
+
+ [center addObserver:self
+ selector:@selector(didReceiveDefaultInstanceIDToken:)
+ name:kFIRMessagingFCMTokenNotification
+ object:nil];
+ [center addObserver:self
+ selector:@selector(defaultInstanceIDTokenWasRefreshed:)
+ name:kFIRMessagingInstanceIDTokenRefreshNotification
+ object:nil];
+ [center addObserver:self
+ selector:@selector(didReceiveAPNSToken:)
+ name:kFIRMessagingAPNSTokenNotification
+ object:nil];
+
+ [center addObserver:self
+ selector:@selector(applicationStateChanged)
+ name:UIApplicationDidBecomeActiveNotification
+ object:nil];
+ [center addObserver:self
+ selector:@selector(applicationStateChanged)
+ name:UIApplicationDidEnterBackgroundNotification
+ object:nil];
+}
+
+- (void)saveLibraryVersion {
+ NSString *currentLibraryVersion = FIRMessagingCurrentLibraryVersion();
+ [[NSUserDefaults standardUserDefaults] setObject:currentLibraryVersion
+ forKey:kFIRMessagingLibraryVersion];
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeMessaging000, @"FIRMessaging library version %@",
+ currentLibraryVersion);
+}
+
+- (void)setupLogger:(FIRMessagingLogLevel)loggerLevel {
+#if FIRMessaging_PROBER
+ // do nothing
+#else
+ FIRMessagingLogger *logger = FIRMessagingSharedLogger();
+ FIRMessagingLogLevelFilter *filter =
+ [[FIRMessagingLogLevelFilter alloc] initWithLevel:loggerLevel];
+ [logger setFilter:filter];
+#endif
+}
+
+- (void)setupReceiverWithConfig:(FIRMessagingConfig *)config {
+ self.receiver = [[FIRMessagingReceiver alloc] init];
+ self.receiver.delegate = self;
+}
+
+- (void)setupClient {
+ self.client = [[FIRMessagingClient alloc] initWithDelegate:self
+ reachability:self.reachability
+ rmq2Manager:self.rmq2Manager];
+}
+
+- (void)setupDataMessageManager {
+ self.dataMessageManager =
+ [[FIRMessagingDataMessageManager alloc] initWithDelegate:self.receiver
+ client:self.client
+ rmq2Manager:self.rmq2Manager
+ syncMessageManager:self.syncMessageManager];
+
+ [self.dataMessageManager refreshDelayedMessages];
+ [self.client setDataMessageManager:self.dataMessageManager];
+}
+
+- (void)setupRmqManager {
+ self.rmq2Manager = [[FIRMessagingRmqManager alloc] initWithDatabaseName:@"rmq2"];
+ [self.rmq2Manager loadRmqId];
+}
+
+- (void)setupTopics {
+ _FIRMessagingDevAssert(self.client, @"Invalid nil client before init pubsub.");
+ self.pubsub = [[FIRMessagingPubSub alloc] initWithClient:self.client];
+}
+
+- (void)setupSyncMessageManager {
+ self.syncMessageManager =
+ [[FIRMessagingSyncMessageManager alloc] initWithRmqManager:self.rmq2Manager];
+
+ // Delete the expired messages with a delay. We don't want to block startup with a somewhat
+ // expensive db call.
+ FIRMessaging_WEAKIFY(self);
+ dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
+ dispatch_after(time, dispatch_get_main_queue(), ^{
+ FIRMessaging_STRONGIFY(self);
+ [self.syncMessageManager removeExpiredSyncMessages];
+ });
+}
+
+- (void)teardown {
+ _FIRMessagingDevAssert([NSThread isMainThread],
+ @"FIRMessaging should be called from main thread only.");
+ [self.client teardown];
+ self.pubsub = nil;
+ self.syncMessageManager = nil;
+ self.rmq2Manager = nil;
+ self.dataMessageManager = nil;
+ self.client = nil;
+ self.isClientSetup = NO;
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeMessaging001, @"Did successfully teardown");
+}
+
+#pragma mark - Messages
+
+- (FIRMessagingMessageInfo *)appDidReceiveMessage:(NSDictionary *)message {
+ if (!message.count) {
+ return [[FIRMessagingMessageInfo alloc] initWithStatus:FIRMessagingMessageStatusUnknown];
+ }
+
+ // For downstream messages that go via MCS we should strip out this key before sending
+ // the message to the device.
+ BOOL isOldMessage = NO;
+ NSString *messageID = message[kFIRMessagingMessageIDKey];
+ if ([messageID length]) {
+ [self.rmq2Manager saveS2dMessageWithRmqId:messageID];
+
+ BOOL isSyncMessage = [[self class] isAPNSSyncMessage:message];
+ if (isSyncMessage) {
+ isOldMessage = [self.syncMessageManager didReceiveAPNSSyncMessage:message];
+ }
+ }
+ // Prevent duplicates by keeping a cache of all the logged messages during each session.
+ // The duplicates only happen when the 3P app calls `appDidReceiveMessage:` along with
+ // us swizzling their implementation to call the same method implicitly.
+ if (!isOldMessage && messageID.length) {
+ isOldMessage = [self.loggedMessageIDs containsObject:messageID];
+ if (!isOldMessage) {
+ [self.loggedMessageIDs addObject:messageID];
+ }
+ }
+
+ if (!isOldMessage) {
+ Class firMessagingLogClass = NSClassFromString(@"FIRMessagingLog");
+ SEL logMessageSelector = NSSelectorFromString(@"logMessage:");
+
+ if ([firMessagingLogClass respondsToSelector:logMessageSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ [firMessagingLogClass performSelector:logMessageSelector
+ withObject:message];
+ }
+#pragma clang diagnostic pop
+ [self handleContextManagerMessage:message];
+ [self handleIncomingLinkIfNeededFromMessage:message];
+ }
+ return [[FIRMessagingMessageInfo alloc] initWithStatus:FIRMessagingMessageStatusNew];
+}
+
+- (BOOL)handleContextManagerMessage:(NSDictionary *)message {
+ if ([FIRMessagingContextManagerService isContextManagerMessage:message]) {
+ return [FIRMessagingContextManagerService handleContextManagerMessage:message];
+ }
+ return NO;
+}
+
++ (BOOL)isAPNSSyncMessage:(NSDictionary *)message {
+ if ([message[kFIRMessagingMessageViaAPNSRootKey] isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *aps = message[kFIRMessagingMessageViaAPNSRootKey];
+ return [aps[kFIRMessagingMessageAPNSContentAvailableKey] boolValue];
+ }
+ return NO;
+}
+
+- (void)handleIncomingLinkIfNeededFromMessage:(NSDictionary *)message {
+ NSURL *url = [self linkURLFromMessage:message];
+ if (url == nil) {
+ return;
+ }
+ if (![NSThread isMainThread]) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self handleIncomingLinkIfNeededFromMessage:message];
+
+ });
+ return;
+ }
+ UIApplication *application = [UIApplication sharedApplication];
+ id<UIApplicationDelegate> appDelegate = application.delegate;
+ SEL continueUserActivitySelector =
+ @selector(application:continueUserActivity:restorationHandler:);
+ SEL openURLWithOptionsSelector = @selector(application:openURL:options:);
+ SEL openURLWithSourceApplicationSelector =
+ @selector(application:openURL:sourceApplication:annotation:);
+ SEL handleOpenURLSelector = @selector(application:handleOpenURL:);
+ // Due to FIRAAppDelegateProxy swizzling, this selector will most likely get chosen, whether or
+ // not the actual application has implemented
+ // |application:continueUserActivity:restorationHandler:|. A warning will be displayed to the user
+ // if they haven't implemented it.
+ if ([NSUserActivity class] != nil &&
+ [appDelegate respondsToSelector:continueUserActivitySelector]) {
+ NSUserActivity *userActivity =
+ [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
+ userActivity.webpageURL = url;
+ [appDelegate application:application
+ continueUserActivity:userActivity
+ restorationHandler:^(NSArray * _Nullable restorableObjects) {
+ // Do nothing, as we don't support the app calling this block
+ }];
+
+ } else if ([appDelegate respondsToSelector:openURLWithOptionsSelector]) {
+ [appDelegate application:application openURL:url options:@{}];
+
+ // 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]) {
+ [appDelegate application:application
+ openURL:url
+ sourceApplication:FIRMessagingAppIdentifier()
+ annotation:@{}];
+
+ } else if ([appDelegate respondsToSelector:handleOpenURLSelector]) {
+ [appDelegate application:application handleOpenURL:url];
+ }
+}
+
+- (NSURL *)linkURLFromMessage:(NSDictionary *)message {
+ NSString *urlString = message[kFIRMessagingMessageLinkKey];
+ if (urlString == nil || ![urlString isKindOfClass:[NSString class]] || urlString.length == 0) {
+ return nil;
+ }
+ NSURL *url = [NSURL URLWithString:urlString];
+ return url;
+}
+
+#pragma mark - APNS
+
+- (NSData *)APNSToken {
+ return self.apnsTokenData;
+}
+
+- (void)setAPNSToken:(NSData *)APNSToken {
+ [self setAPNSToken:APNSToken type:FIRMessagingAPNSTokenTypeUnknown];
+}
+
+- (void)setAPNSToken:(NSData *)apnsToken type:(FIRMessagingAPNSTokenType)type {
+ if ([apnsToken isEqual:self.apnsTokenData]) {
+ return;
+ }
+ self.apnsTokenData = apnsToken;
+ [self.instanceIDProxy setAPNSToken:apnsToken type:(FIRMessagingInstanceIDProxyAPNSTokenType)type];
+}
+
+#pragma mark - FCM
+
+- (NSString *)FCMToken {
+ NSString *token = self.defaultFcmToken;
+ if (!token) {
+ // We may not have received it from Instance ID yet (via NSNotification), so extract it directly
+ token = [self.instanceIDProxy token];
+ }
+ return token;
+}
+
+- (void)retrieveFCMTokenForSenderID:(nonnull NSString *)senderID
+ completion:(nonnull FIRMessagingFCMTokenFetchCompletion)completion {
+ if (!senderID.length) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenFetch,
+ @"Sender ID not supplied. It is required for a token fetch, "
+ @"to identify the sender.");
+ if (completion) {
+ NSString *description = @"Couldn't fetch token because a Sender ID was not supplied. A valid "
+ @"Sender ID is required to fetch an FCM token";
+ NSError *error = [NSError fcm_errorWithCode:FIRMessagingErrorInvalidRequest
+ userInfo:@{NSLocalizedDescriptionKey : description}];
+ completion(nil, error);
+ }
+ return;
+ }
+ NSDictionary *options = nil;
+ if (self.APNSToken) {
+ options = @{kFIRMessagingFCMTokenFetchAPNSOption : self.APNSToken};
+ } else {
+ FIRMessagingLoggerWarn(kFIRMessagingMessageCodeAPNSTokenNotAvailableDuringTokenFetch,
+ @"APNS device token not set before retrieving FCM Token for Sender ID "
+ @"'%@'. Notifications to this FCM Token will not be delivered over APNS."
+ @"Be sure to re-retrieve the FCM token once the APNS device token is "
+ @"set.", senderID);
+ }
+ [self.instanceIDProxy tokenWithAuthorizedEntity:senderID
+ scope:kFIRMessagingDefaultTokenScope
+ options:options
+ handler:completion];
+}
+
+- (void)deleteFCMTokenForSenderID:(nonnull NSString *)senderID
+ completion:(nonnull FIRMessagingDeleteFCMTokenCompletion)completion {
+ if (!senderID.length) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenDelete,
+ @"Sender ID not supplied. It is required to delete an FCM token.");
+ if (completion) {
+ NSString *description = @"Couldn't delete token because a Sender ID was not supplied. A "
+ @"valid Sender ID is required to delete an FCM token";
+ NSError *error = [NSError fcm_errorWithCode:FIRMessagingErrorInvalidRequest
+ userInfo:@{NSLocalizedDescriptionKey : description}];
+ completion(error);
+ }
+ return;
+ }
+ [self.instanceIDProxy deleteTokenWithAuthorizedEntity:senderID
+ scope:kFIRMessagingDefaultTokenScope
+ handler:completion];
+}
+
+#pragma mark - Application State Changes
+
+- (void)applicationStateChanged {
+ if (self.shouldEstablishDirectChannel) {
+ [self updateAutomaticClientConnection];
+ }
+}
+
+#pragma mark - Direct Channel
+
+- (void)setShouldEstablishDirectChannel:(BOOL)shouldEstablishDirectChannel {
+ if (_shouldEstablishDirectChannel == shouldEstablishDirectChannel) {
+ return;
+ }
+ _shouldEstablishDirectChannel = shouldEstablishDirectChannel;
+ [self updateAutomaticClientConnection];
+}
+
+- (BOOL)isDirectChannelEstablished {
+ return self.client.isConnectionActive;
+}
+
+- (BOOL)shouldBeConnectedAutomatically {
+ // We require a token from Instance ID
+ NSString *token = self.defaultFcmToken;
+ // Only on foreground connections
+ UIApplicationState applicationState = [UIApplication sharedApplication].applicationState;
+ BOOL shouldBeConnected = _shouldEstablishDirectChannel &&
+ (token.length > 0) &&
+ applicationState == UIApplicationStateActive;
+ return shouldBeConnected;
+}
+
+- (void)updateAutomaticClientConnection {
+ BOOL shouldBeConnected = [self shouldBeConnectedAutomatically];
+ if (shouldBeConnected && !self.client.isConnected) {
+ [self.client connectWithHandler:^(NSError *error) {
+ if (!error) {
+ // It means we connected. Fire connection change notification
+ [self notifyOfDirectChannelConnectionChange];
+ }
+ }];
+ } else if (!shouldBeConnected && self.client.isConnected) {
+ [self.client disconnect];
+ [self notifyOfDirectChannelConnectionChange];
+ }
+}
+
+- (void)notifyOfDirectChannelConnectionChange {
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center postNotificationName:FIRMessagingConnectionStateChangedNotification object:self];
+}
+
+#pragma mark - Connect
+
+- (void)connectWithCompletion:(FIRMessagingConnectCompletion)handler {
+ _FIRMessagingDevAssert([NSThread isMainThread],
+ @"FIRMessaging connect should be called from main thread only.");
+ _FIRMessagingDevAssert(self.isClientSetup, @"FIRMessaging client not setup.");
+ [self.client connectWithHandler:^(NSError *error) {
+ if (handler) {
+ handler(error);
+ }
+ if (!error) {
+ // It means we connected. Fire connection change notification
+ [self notifyOfDirectChannelConnectionChange];
+ }
+ }];
+
+}
+
+- (void)disconnect {
+ _FIRMessagingDevAssert([NSThread isMainThread],
+ @"FIRMessaging should be called from main thread only.");
+ if ([self.client isConnected]) {
+ [self.client disconnect];
+ [self notifyOfDirectChannelConnectionChange];
+ }
+}
+
+#pragma mark - Topics
+
++ (NSString *)normalizeTopic:(NSString *)topic {
+ if (![FIRMessagingPubSub hasTopicsPrefix:topic]) {
+ topic = [FIRMessagingPubSub addPrefixToTopic:topic];
+ }
+ if ([FIRMessagingPubSub isValidTopicWithPrefix:topic]) {
+ return [topic copy];
+ }
+ return nil;
+}
+
+- (void)subscribeToTopic:(NSString *)topic {
+ if (self.defaultFcmToken.length && topic.length) {
+ NSString *normalizeTopic = [[self class ] normalizeTopic:topic];
+ if (normalizeTopic.length) {
+ [self.pubsub subscribeToTopic:normalizeTopic];
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging009,
+ @"Cannot parse topic name %@. Will not subscribe.", topic);
+ }
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging010,
+ @"Cannot subscribe to topic: %@ with token: %@", topic,
+ self.defaultFcmToken);
+ }
+}
+
+- (void)unsubscribeFromTopic:(NSString *)topic {
+ if (self.defaultFcmToken.length && topic.length) {
+ NSString *normalizeTopic = [[self class] normalizeTopic:topic];
+ if (normalizeTopic.length) {
+ [self.pubsub unsubscribeFromTopic:normalizeTopic];
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging011,
+ @"Cannot parse topic name %@. Will not unsubscribe.", topic);
+ }
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging012,
+ @"Cannot unsubscribe to topic: %@ with token: %@", topic,
+ self.defaultFcmToken);
+ }
+}
+
+#pragma mark - Send
+
+- (void)sendMessage:(NSDictionary *)message
+ to:(NSString *)to
+ withMessageID:(NSString *)messageID
+ timeToLive:(int64_t)ttl {
+ _FIRMessagingDevAssert([to length] != 0, @"Invalid receiver id for FIRMessaging-message");
+
+ NSMutableDictionary *fcmMessage = [[self class] createFIRMessagingMessageWithMessage:message
+ to:to
+ withID:messageID
+ timeToLive:ttl
+ delay:0];
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeMessaging013, @"Sending message: %@ with id: %@",
+ message, messageID);
+ [self.dataMessageManager sendDataMessageStanza:fcmMessage];
+}
+
++ (NSMutableDictionary *)createFIRMessagingMessageWithMessage:(NSDictionary *)message
+ to:(NSString *)to
+ withID:(NSString *)msgID
+ timeToLive:(int64_t)ttl
+ delay:(int)delay {
+ NSMutableDictionary *fcmMessage = [NSMutableDictionary dictionary];
+ fcmMessage[kFIRMessagingSendTo] = [to copy];
+ fcmMessage[kFIRMessagingSendMessageID] = msgID ? [msgID copy] : @"";
+ fcmMessage[kFIRMessagingSendTTL] = @(ttl);
+ fcmMessage[kFIRMessagingSendDelay] = @(delay);
+ fcmMessage[KFIRMessagingSendMessageAppData] =
+ [NSMutableDictionary dictionaryWithDictionary:message];
+ return fcmMessage;
+}
+
+#pragma mark - IID dependencies
+
+// FIRMessagingInternalUtilities.h to see usage.
++ (NSString *)FIRMessagingSDKVersion {
+ NSString *semanticVersion = FIRMessagingCurrentLibraryVersion();
+ // Use prefix fcm for all FCM libs. This allows us to differentiate b/w
+ // the new and old FCM registrations.
+ return [NSString stringWithFormat:@"fcm-%@", semanticVersion];
+}
+
++ (NSString *)FIRMessagingSDKCurrentLocale {
+ return [self currentLocale];
+}
+
+- (void)setAPNSToken:(NSData *)apnsToken error:(NSError *)error {
+ if (apnsToken) {
+ self.apnsTokenData = [apnsToken copy];
+ }
+}
+
+#pragma mark - FIRMessagingReceiverDelegate
+
+- (void)receiver:(FIRMessagingReceiver *)receiver
+ receivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage {
+ if ([self.delegate respondsToSelector:@selector(messaging:didReceiveMessage:)]) {
+ [self.delegate messaging:self didReceiveMessage:remoteMessage];
+ } else if ([self.delegate respondsToSelector:@selector(applicationReceivedRemoteMessage:)]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [self.delegate applicationReceivedRemoteMessage:remoteMessage];
+#pragma clang diagnostic pop
+ } else {
+ // Delegate methods weren't implemented, so messages are being dropped, log a warning
+ FIRMessagingLoggerWarn(kFIRMessagingMessageCodeRemoteMessageDelegateMethodNotImplemented,
+ @"FIRMessaging received data-message, but FIRMessagingDelegate's"
+ @"-messaging:didReceiveMessage: not implemented");
+ }
+}
+
+#pragma mark - FIRReachabilityDelegate
+
+- (void)reachability:(FIRReachabilityChecker *)reachability
+ statusChanged:(FIRReachabilityStatus)status {
+ [self onNetworkStatusChanged];
+}
+
+#pragma mark - Network
+
+- (BOOL)isNetworkAvailable {
+ FIRReachabilityStatus status = self.reachability.reachabilityStatus;
+ return (status == kFIRReachabilityViaCellular || status == kFIRReachabilityViaWifi);
+}
+
+- (FIRMessagingNetworkStatus)networkType {
+ FIRReachabilityStatus status = self.reachability.reachabilityStatus;
+ if (![self isNetworkAvailable]) {
+ return kFIRMessagingReachabilityNotReachable;
+ } else if (status == kFIRReachabilityViaCellular) {
+ return kFIRMessagingReachabilityReachableViaWWAN;
+ } else {
+ return kFIRMessagingReachabilityReachableViaWiFi;
+ }
+}
+
+#pragma mark - Notifications
+
+- (void)onNetworkStatusChanged {
+ if (![self.client isConnected] && [self isNetworkAvailable]) {
+ if (self.client.shouldStayConnected) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeMessaging014,
+ @"Attempting to establish direct channel.");
+ [self.client retryConnectionImmediately:YES];
+ }
+ [self.pubsub scheduleSync:YES];
+ }
+}
+
+#pragma mark - Notifications
+
+- (void)didReceiveDefaultInstanceIDToken:(NSNotification *)notification {
+ if (![notification.object isKindOfClass:[NSString class]]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeMessaging015,
+ @"Invalid default FCM token type %@",
+ NSStringFromClass([notification.object class]));
+ return;
+ }
+ self.defaultFcmToken = [(NSString *)notification.object copy];
+ [self.pubsub scheduleSync:YES];
+ if (self.shouldEstablishDirectChannel) {
+ [self updateAutomaticClientConnection];
+ }
+}
+
+- (void)defaultInstanceIDTokenWasRefreshed:(NSNotification *)notification {
+ // Retrieve the Instance ID default token, and if it is non-nil, post it
+ NSString *token = [self.instanceIDProxy token];
+ // Sometimes Instance ID doesn't yet have a token, so wait until the default
+ // token is fetched, and then notify. This ensures that this token should not
+ // be nil when the developer accesses it.
+ if (token != nil) {
+ self.defaultFcmToken = [token copy];
+ [self.delegate messaging:self didRefreshRegistrationToken:token];
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center postNotificationName:FIRMessagingRegistrationTokenRefreshedNotification object:nil];
+ }
+}
+
+- (void)didReceiveAPNSToken:(NSNotification *)notification {
+ NSData *apnsToken = notification.object;
+ if (![apnsToken isKindOfClass:[NSData class]]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeMessaging016, @"Invalid APNS token type %@",
+ NSStringFromClass([notification.object class]));
+ return;
+ }
+ // Set this value directly, and since this came from InstanceID, don't set it back to InstanceID
+ self.apnsTokenData = [apnsToken copy];
+}
+
+#pragma mark - Application Support Directory
+
++ (BOOL)hasApplicationSupportSubDirectory:(NSString *)subDirectoryName {
+ NSString *subDirectoryPath = [self pathForApplicationSupportSubDirectory:subDirectoryName];
+ BOOL isDirectory;
+ if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
+ isDirectory:&isDirectory]) {
+ return NO;
+ } else if (!isDirectory) {
+ return NO;
+ }
+ return YES;
+}
+
++ (NSString *)pathForApplicationSupportSubDirectory:(NSString *)subDirectoryName {
+ NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+ NSUserDomainMask, YES);
+ NSString *applicationSupportDirPath = directoryPaths.lastObject;
+ NSArray *components = @[applicationSupportDirPath, subDirectoryName];
+ return [NSString pathWithComponents:components];
+}
+
++ (BOOL)createApplicationSupportSubDirectory:(NSString *)subDirectoryName {
+ NSString *subDirectoryPath = [self pathForApplicationSupportSubDirectory:subDirectoryName];
+ BOOL hasSubDirectory;
+
+ if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
+ isDirectory:&hasSubDirectory]) {
+ NSError *error;
+ [[NSFileManager defaultManager] createDirectoryAtPath:subDirectoryPath
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&error];
+ if (error) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging017,
+ @"Cannot create directory %@, error: %@", subDirectoryPath, error);
+ return NO;
+ }
+ } else {
+ if (!hasSubDirectory) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeMessaging018,
+ @"Found file instead of directory at %@", subDirectoryPath);
+ return NO;
+ }
+ }
+ return YES;
+}
+
+#pragma mark - Locales
+
++ (NSString *)currentLocale {
+ NSArray *locales = [self firebaseLocales];
+ NSArray *preferredLocalizations =
+ [NSBundle preferredLocalizationsFromArray:locales
+ forPreferences:[NSLocale preferredLanguages]];
+ NSString *legalDocsLanguage = [preferredLocalizations firstObject];
+ // Use en as the default language
+ return legalDocsLanguage ? legalDocsLanguage : @"en";
+}
+
++ (NSArray *)firebaseLocales {
+ NSMutableArray *locales = [NSMutableArray array];
+ NSDictionary *localesMap = [self firebaselocalesMap];
+ for (NSString *key in localesMap) {
+ [locales addObjectsFromArray:localesMap[key]];
+ }
+ return locales;
+}
+
++ (NSDictionary *)firebaselocalesMap {
+ return @{
+ // Albanian
+ @"sq" : @[ @"sq_AL" ],
+ // Belarusian
+ @"be" : @[ @"be_BY" ],
+ // Bulgarian
+ @"bg" : @[ @"bg_BG" ],
+ // Catalan
+ @"ca" : @[ @"ca", @"ca_ES" ],
+ // Croatian
+ @"hr" : @[ @"hr", @"hr_HR" ],
+ // Czech
+ @"cs" : @[ @"cs", @"cs_CZ" ],
+ // Danish
+ @"da" : @[ @"da", @"da_DK" ],
+ // Estonian
+ @"et" : @[ @"et_EE" ],
+ // Finnish
+ @"fi" : @[ @"fi", @"fi_FI" ],
+ // Hebrew
+ @"he" : @[ @"he", @"iw_IL" ],
+ // Hindi
+ @"hi" : @[ @"hi_IN" ],
+ // Hungarian
+ @"hu" : @[ @"hu", @"hu_HU" ],
+ // Icelandic
+ @"is" : @[ @"is_IS" ],
+ // Indonesian
+ @"id" : @[ @"id", @"in_ID", @"id_ID" ],
+ // Irish
+ @"ga" : @[ @"ga_IE" ],
+ // Korean
+ @"ko" : @[ @"ko", @"ko_KR", @"ko-KR" ],
+ // Latvian
+ @"lv" : @[ @"lv_LV" ],
+ // Lithuanian
+ @"lt" : @[ @"lt_LT" ],
+ // Macedonian
+ @"mk" : @[ @"mk_MK" ],
+ // Malay
+ @"ms" : @[ @"ms_MY" ],
+ // Maltese
+ @"ms" : @[ @"mt_MT" ],
+ // Polish
+ @"pl" : @[ @"pl", @"pl_PL", @"pl-PL" ],
+ // Romanian
+ @"ro" : @[ @"ro", @"ro_RO" ],
+ // Russian
+ @"ru" : @[ @"ru_RU", @"ru", @"ru_BY", @"ru_KZ", @"ru-RU" ],
+ // Slovak
+ @"sk" : @[ @"sk", @"sk_SK" ],
+ // Slovenian
+ @"sl" : @[ @"sl_SI" ],
+ // Swedish
+ @"sv" : @[ @"sv", @"sv_SE", @"sv-SE" ],
+ // Turkish
+ @"tr" : @[ @"tr", @"tr-TR", @"tr_TR" ],
+ // Ukrainian
+ @"uk" : @[ @"uk", @"uk_UA" ],
+ // Vietnamese
+ @"vi" : @[ @"vi", @"vi_VN" ],
+ // The following are groups of locales or locales that sub-divide a
+ // language).
+ // Arabic
+ @"ar" : @[
+ @"ar",
+ @"ar_DZ",
+ @"ar_BH",
+ @"ar_EG",
+ @"ar_IQ",
+ @"ar_JO",
+ @"ar_KW",
+ @"ar_LB",
+ @"ar_LY",
+ @"ar_MA",
+ @"ar_OM",
+ @"ar_QA",
+ @"ar_SA",
+ @"ar_SD",
+ @"ar_SY",
+ @"ar_TN",
+ @"ar_AE",
+ @"ar_YE",
+ @"ar_GB",
+ @"ar-IQ",
+ @"ar_US"
+ ],
+ // Simplified Chinese
+ @"zh_Hans" : @[ @"zh_CN", @"zh_SG", @"zh-Hans" ],
+ // Traditional Chinese
+ @"zh_Hant" : @[ @"zh_HK", @"zh_TW", @"zh-Hant", @"zh-HK", @"zh-TW" ],
+ // Dutch
+ @"nl" : @[ @"nl", @"nl_BE", @"nl_NL", @"nl-NL" ],
+ // English
+ @"en" : @[
+ @"en",
+ @"en_AU",
+ @"en_CA",
+ @"en_IN",
+ @"en_IE",
+ @"en_MT",
+ @"en_NZ",
+ @"en_PH",
+ @"en_SG",
+ @"en_ZA",
+ @"en_GB",
+ @"en_US",
+ @"en_AE",
+ @"en-AE",
+ @"en_AS",
+ @"en-AU",
+ @"en_BD",
+ @"en-CA",
+ @"en_EG",
+ @"en_ES",
+ @"en_GB",
+ @"en-GB",
+ @"en_HK",
+ @"en_ID",
+ @"en-IN",
+ @"en_NG",
+ @"en-PH",
+ @"en_PK",
+ @"en-SG",
+ @"en-US"
+ ],
+ // French
+
+ @"fr" : @[
+ @"fr",
+ @"fr_BE",
+ @"fr_CA",
+ @"fr_FR",
+ @"fr_LU",
+ @"fr_CH",
+ @"fr-CA",
+ @"fr-FR",
+ @"fr_MA"
+ ],
+ // German
+ @"de" : @[ @"de", @"de_AT", @"de_DE", @"de_LU", @"de_CH", @"de-DE" ],
+ // Greek
+ @"el" : @[ @"el", @"el_CY", @"el_GR" ],
+ // Italian
+ @"it" : @[ @"it", @"it_IT", @"it_CH", @"it-IT" ],
+ // Japanese
+ @"ja" : @[ @"ja", @"ja_JP", @"ja_JP_JP", @"ja-JP" ],
+ // Norwegian
+ @"no" : @[ @"nb", @"no_NO", @"no_NO_NY", @"nb_NO" ],
+ // Brazilian Portuguese
+ @"pt_BR" : @[ @"pt_BR", @"pt-BR" ],
+ // European Portuguese
+ @"pt_PT" : @[ @"pt", @"pt_PT", @"pt-PT" ],
+ // Serbian
+ @"sr" : @[
+ @"sr_BA",
+ @"sr_ME",
+ @"sr_RS",
+ @"sr_Latn_BA",
+ @"sr_Latn_ME",
+ @"sr_Latn_RS"
+ ],
+ // European Spanish
+ @"es_ES" : @[ @"es", @"es_ES", @"es-ES" ],
+ // Mexican Spanish
+ @"es_MX" : @[ @"es-MX", @"es_MX", @"es_US", @"es-US" ],
+ // Latin American Spanish
+ @"es_419" : @[
+ @"es_AR",
+ @"es_BO",
+ @"es_CL",
+ @"es_CO",
+ @"es_CR",
+ @"es_DO",
+ @"es_EC",
+ @"es_SV",
+ @"es_GT",
+ @"es_HN",
+ @"es_NI",
+ @"es_PA",
+ @"es_PY",
+ @"es_PE",
+ @"es_PR",
+ @"es_UY",
+ @"es_VE",
+ @"es-AR",
+ @"es-CL",
+ @"es-CO"
+ ],
+ // Thai
+ @"th" : @[ @"th", @"th_TH", @"th_TH_TH" ],
+ };
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingCheckinService.h b/Firebase/Messaging/FIRMessagingCheckinService.h
new file mode 100644
index 0000000..155143a
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingCheckinService.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 <Foundation/Foundation.h>
+
+/**
+ * Register the device with Checkin Service and get back the `authID`, `secret token` etc. for the
+ * client. Checkin results are cached in the `FIRMessagingDefaultsManager` and periodically refreshed to
+ * prevent them from being stale. Each client needs to register with checkin before registering
+ * with FIRMessaging.
+ */
+@interface FIRMessagingCheckinService : NSObject
+
+@property(nonatomic, readonly, strong) NSString *deviceAuthID;
+@property(nonatomic, readonly, strong) NSString *secretToken;
+@property(nonatomic, readonly, strong) NSString *versionInfo;
+@property(nonatomic, readonly, assign) BOOL hasValidCheckinInfo;
+
+/**
+ * Verify if valid checkin preferences have been loaded in memory.
+ *
+ * @return YES if valid checkin preferences exist in memory else NO.
+ */
+- (BOOL)hasValidCheckinInfo;
+
+/**
+ * Try to load prefetched checkin preferences from the cache. This supports the use case where
+ * InstanceID library has already obtained a valid checkin and we should be using that.
+ *
+ * This should be used as a last gasp effort to retreive any cached checkin preferences before
+ * hitting the FIRMessaging backend to retrieve new preferences.
+ *
+ * Note this is only required because InstanceID and FIRMessaging both require checkin preferences which
+ * need to be synced with each other.
+ *
+ * @return YES if successfully loaded cached checkin preferences into memory else NO.
+ */
+- (BOOL)tryToLoadPrefetchedCheckinPreferences;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingCheckinService.m b/Firebase/Messaging/FIRMessagingCheckinService.m
new file mode 100644
index 0000000..9dad847
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingCheckinService.m
@@ -0,0 +1,132 @@
+/*
+ * 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 "FIRMessagingCheckinService.h"
+
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+@interface FIRMessagingCheckinService ()
+
+// This property is of type FIRInstanceIDCheckinPreferences, if InstanceID was directly linkable
+@property(nonatomic, readwrite, strong) id checkinPreferences;
+
+@end
+
+@implementation FIRMessagingCheckinService;
+
+#pragma mark - Reflection-Based Getter Functions
+
+// Encapsulates the -hasValidCheckinInfo method of FIRInstanceIDCheckinPreferences
+BOOL FIRMessagingCheckinService_hasValidCheckinInfo(id checkinPreferences) {
+ SEL hasValidCheckinInfoSelector = NSSelectorFromString(@"hasValidCheckinInfo");
+ if (![checkinPreferences respondsToSelector:hasValidCheckinInfoSelector]) {
+ // Can't check hasValidCheckinInfo
+ return NO;
+ }
+
+ // Since hasValidCheckinInfo returns a BOOL, use NSInvocation
+ NSMethodSignature *methodSignature =
+ [[checkinPreferences class] instanceMethodSignatureForSelector:hasValidCheckinInfoSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
+ invocation.selector = hasValidCheckinInfoSelector;
+ invocation.target = checkinPreferences;
+ [invocation invoke];
+ BOOL returnValue;
+ [invocation getReturnValue:&returnValue];
+ return returnValue;
+}
+
+// Returns a non-scalar (id) object based on the property name
+id FIRMessagingCheckinService_propertyNamed(id checkinPreferences, NSString *propertyName) {
+ SEL propertyGetterSelector = NSSelectorFromString(propertyName);
+ if ([checkinPreferences respondsToSelector:propertyGetterSelector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ return [checkinPreferences performSelector:propertyGetterSelector];
+#pragma clang diagnostic pop
+ }
+ return nil;
+}
+
+#pragma mark - Methods
+
+- (BOOL)tryToLoadPrefetchedCheckinPreferences {
+ Class instanceIDClass = NSClassFromString(@"FIRInstanceID");
+ if (!instanceIDClass) {
+ // InstanceID is not linked
+ return NO;
+ }
+
+ // [FIRInstanceID instanceID]
+ SEL instanceIDSelector = NSSelectorFromString(@"instanceID");
+ if (![instanceIDClass respondsToSelector:instanceIDSelector]) {
+ return NO;
+ }
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ id instanceID = [instanceIDClass performSelector:instanceIDSelector];
+#pragma clang diagnostic pop
+ if (!instanceID) {
+ // Instance ID singleton not available
+ return NO;
+ }
+
+ // [[FIRInstanceID instanceID] cachedCheckinPreferences]
+ SEL cachedCheckinPrefsSelector = NSSelectorFromString(@"cachedCheckinPreferences");
+ if (![instanceID respondsToSelector:cachedCheckinPrefsSelector]) {
+ // cachedCheckinPreferences is not accessible
+ return NO;
+ }
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ id checkinPreferences = [instanceID performSelector:cachedCheckinPrefsSelector];
+#pragma clang diagnostic pop
+ if (!checkinPreferences) {
+ // No cached checkin prefs
+ return NO;
+ }
+
+ BOOL hasValidInfo = FIRMessagingCheckinService_hasValidCheckinInfo(checkinPreferences);
+ if (hasValidInfo) {
+ self.checkinPreferences = checkinPreferences;
+ }
+ return hasValidInfo;
+}
+
+#pragma mark - API
+
+- (NSString *)deviceAuthID {
+ return FIRMessagingCheckinService_propertyNamed(self.checkinPreferences, @"deviceID");
+}
+
+- (NSString *)secretToken {
+ return FIRMessagingCheckinService_propertyNamed(self.checkinPreferences, @"secretToken");
+}
+
+- (NSString *)versionInfo {
+ return FIRMessagingCheckinService_propertyNamed(self.checkinPreferences, @"versionInfo");
+}
+
+- (NSString *)digest {
+ return FIRMessagingCheckinService_propertyNamed(self.checkinPreferences, @"digest");
+}
+
+- (BOOL)hasValidCheckinInfo {
+ return FIRMessagingCheckinService_hasValidCheckinInfo(self.checkinPreferences);
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingClient.h b/Firebase/Messaging/FIRMessagingClient.h
new file mode 100644
index 0000000..1726428
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingClient.h
@@ -0,0 +1,156 @@
+/*
+ * 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 "FIRMessagingTopicsCommon.h"
+
+@class FIRReachabilityChecker;
+@class GPBMessage;
+
+@class FIRMessagingConnection;
+@class FIRMessagingDataMessageManager;
+@class FIRMessagingRmqManager;
+
+/**
+ * Callback to handle MCS connection requests.
+ *
+ * @param error The error object if any while trying to connect with MCS else nil.
+ */
+typedef void(^FIRMessagingConnectCompletionHandler)(NSError *error);
+
+@protocol FIRMessagingClientDelegate <NSObject>
+
+@end
+
+/**
+ * The client handles the subscribe/unsubscribe for an unregistered senderID
+ * and device. It also manages the FIRMessaging data connection, the exponential backoff
+ * algorithm in case of registration failures, sign in failures and unregister
+ * failures. It also handles the reconnect logic if the FIRMessaging connection is
+ * broken off by some error during an active session.
+ */
+@interface FIRMessagingClient : NSObject
+
+@property(nonatomic, readonly, strong) FIRMessagingConnection *connection;
+@property(nonatomic, readwrite, weak) FIRMessagingDataMessageManager *dataMessageManager;
+
+// Designated initializer
+- (instancetype)initWithDelegate:(id<FIRMessagingClientDelegate>)delegate
+ reachability:(FIRReachabilityChecker *)reachability
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager;
+
+- (void)teardown;
+
+- (void)cancelAllRequests;
+
+#pragma mark - FIRMessaging subscribe
+
+/**
+ * Update the subscription associated with the given token and topic.
+ *
+ * For a to-be-created subscription we check if the client is already
+ * subscribed to the topic or not. If subscribed we should have the
+ * subscriptionID in the cache and we return from there itself, else we call
+ * the FIRMessaging backend to create a new subscription for the topic for this client.
+ *
+ * For delete subscription requests we delete the stored subscription in the
+ * client and then invoke the FIRMessaging backend to delete the existing subscription
+ * completely.
+ *
+ * @param token The token associated with the device.
+ * @param topic The topic for which the subscription should be updated.
+ * @param options The options to be passed in to the subscription request.
+ * @param shouldDelete If YES this would delete the subscription from the cache
+ * and also let the FIRMessaging backend know that we need to delete
+ * the subscriptionID associated with this topic.
+ * If NO we try to create a new subscription for the given
+ * token and topic.
+ * @param handler The handler to invoke once the subscription request
+ * finishes.
+ */
+- (void)updateSubscriptionWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler;
+
+#pragma mark - MCS Connection
+
+/**
+ * Create a MCS connection.
+ *
+ * @param handler The handler to be invokend once the connection is setup. If
+ * setting up the connection fails we invoke the handler with
+ * an appropriate error object.
+ */
+- (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler;
+
+/**
+ * Disconnect the current MCS connection. If there is no valid connection this
+ * should be a NO-OP.
+ */
+- (void)disconnect;
+
+#pragma mark - MCS Connection State
+
+/**
+ * If we are connected to MCS or not. This doesn't take into account the fact if
+ * the client has been signed in(verified) by MCS.
+ *
+ * @return YES if we are signed in or connecting and trying to sign-in else NO.
+ */
+@property(nonatomic, readonly) BOOL isConnected;
+
+/**
+ * If we have an active MCS connection
+ *
+ * @return YES if we have an active MCS connection else NO.
+ */
+@property(nonatomic, readonly) BOOL isConnectionActive;
+
+/**
+ * If we should be connected to MCS
+ *
+ * @return YES if we have attempted a connection and not requested to disconect.
+ */
+@property(nonatomic, readonly) BOOL shouldStayConnected;
+
+/**
+ * Schedule a retry to connect to MCS. If `immediately` is `YES` try to
+ * schedule a retry now else retry with some delay.
+ *
+ * @param immediately Should retry right now.
+ */
+- (void)retryConnectionImmediately:(BOOL)immediately;
+
+#pragma mark - Messages
+
+/**
+ * Send a message over the MCS connection.
+ *
+ * @param message Message to be sent.
+ */
+- (void)sendMessage:(GPBMessage *)message;
+
+/**
+ * Send message if we have an active MCS connection. If not cache the message
+ * for this session and in case we are able to re-establish the connection try
+ * again else drop it. This should only be used for TTL=0 messages for now.
+ *
+ * @param message Message to be sent.
+ */
+- (void)sendOnConnectOrDrop:(GPBMessage *)message;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingClient.m b/Firebase/Messaging/FIRMessagingClient.m
new file mode 100644
index 0000000..c01aecc
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingClient.m
@@ -0,0 +1,490 @@
+/*
+ * 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 "FIRMessagingClient.h"
+
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingRegistrar.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingTopicsCommon.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRReachabilityChecker.h"
+#import "NSError+FIRMessaging.h"
+
+static const NSTimeInterval kConnectTimeoutInterval = 40.0;
+static const NSTimeInterval kReconnectDelayInSeconds = 2 * 60; // 2 minutes
+
+static const NSUInteger kMaxRetryExponent = 10; // 2^10 = 1024 seconds ~= 17 minutes
+
+static NSString *const kFIRMessagingMCSServerHost = @"mtalk.google.com";
+static NSUInteger const kFIRMessagingMCSServerPort = 5228;
+
+// register device with checkin
+typedef void(^FIRMessagingRegisterDeviceHandler)(NSError *error);
+
+static NSString *FIRMessagingServerHost() {
+ static NSString *serverHost = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSDictionary *environment = [[NSProcessInfo processInfo] environment];
+ NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
+ NSString *host = [customServerHostAndPort componentsSeparatedByString:@":"].firstObject;
+ if (host) {
+ serverHost = host;
+ } else {
+ serverHost = kFIRMessagingMCSServerHost;
+ }
+ });
+ return serverHost;
+}
+
+static NSUInteger FIRMessagingServerPort() {
+ static NSUInteger serverPort = kFIRMessagingMCSServerPort;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSDictionary *environment = [[NSProcessInfo processInfo] environment];
+ NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
+ NSArray<NSString *> *components = [customServerHostAndPort componentsSeparatedByString:@":"];
+ NSUInteger port = (NSUInteger)[components.lastObject integerValue];
+ if (port != 0) {
+ serverPort = port;
+ }
+ });
+ return serverPort;
+}
+
+@interface FIRMessagingClient () <FIRMessagingConnectionDelegate>
+
+@property(nonatomic, readwrite, weak) id<FIRMessagingClientDelegate> clientDelegate;
+@property(nonatomic, readwrite, strong) FIRMessagingConnection *connection;
+@property(nonatomic, readwrite, strong) FIRMessagingRegistrar *registrar;
+
+@property(nonatomic, readwrite, strong) NSString *senderId;
+
+// FIRMessagingService owns these instances
+@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
+@property(nonatomic, readwrite, weak) FIRReachabilityChecker *reachability;
+
+@property(nonatomic, readwrite, assign) int64_t lastConnectedTimestamp;
+@property(nonatomic, readwrite, assign) int64_t lastDisconnectedTimestamp;
+@property(nonatomic, readwrite, assign) NSUInteger connectRetryCount;
+
+// Should we stay connected to MCS or not. Should be YES throughout the lifetime
+// of a MCS connection. If set to NO it signifies that an existing MCS connection
+// should be disconnected.
+@property(nonatomic, readwrite, assign) BOOL stayConnected;
+@property(nonatomic, readwrite, assign) NSTimeInterval connectionTimeoutInterval;
+
+// Used if the MCS connection suddenly breaksdown in the middle and we want to reconnect
+// with some permissible delay we schedule a reconnect and set it to YES and when it's
+// scheduled this will be set back to NO.
+@property(nonatomic, readwrite, assign) BOOL didScheduleReconnect;
+
+// handlers
+@property(nonatomic, readwrite, copy) FIRMessagingConnectCompletionHandler connectHandler;
+
+@end
+
+@implementation FIRMessagingClient
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithDelegate:(id<FIRMessagingClientDelegate>)delegate
+ reachability:(FIRReachabilityChecker *)reachability
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager {
+ self = [super init];
+ if (self) {
+ _reachability = reachability;
+ _clientDelegate = delegate;
+ _rmq2Manager = rmq2Manager;
+ _registrar = [[FIRMessagingRegistrar alloc] init];
+ _connectionTimeoutInterval = kConnectTimeoutInterval;
+ }
+ return self;
+}
+
+- (void)teardown {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient000, @"");
+ self.stayConnected = NO;
+
+ // Clear all the handlers
+ self.connectHandler = nil;
+
+ [self.connection teardown];
+
+ // Stop all subscription requests
+ [self.registrar cancelAllRequests];
+
+ _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected, @"Did not disconnect");
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+}
+
+- (void)cancelAllRequests {
+ // Stop any checkin requests or any subscription requests
+ [self.registrar cancelAllRequests];
+
+ // Stop any future connection requests to MCS
+ if (self.stayConnected && self.isConnected && !self.isConnectionActive) {
+ self.stayConnected = NO;
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ }
+}
+
+#pragma mark - FIRMessaging subscribe
+
+- (void)updateSubscriptionWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler {
+
+ _FIRMessagingDevAssert(handler != nil, @"Invalid handler to FIRMessaging subscribe");
+
+ FIRMessagingTopicOperationCompletion completion =
+ ^void(FIRMessagingTopicOperationResult result, NSError * error) {
+ if (error) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001, @"Failed to subscribe to topic %@",
+ error);
+ } else {
+ if (shouldDelete) {
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002,
+ @"Successfully unsubscribed from topic %@", topic);
+ } else {
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003,
+ @"Successfully subscribed to topic %@", topic);
+ }
+ }
+ handler(result, error);
+ };
+
+ [self.registrar tryToLoadValidCheckinInfo];
+ [self.registrar updateSubscriptionToTopic:topic
+ withToken:token
+ options:options
+ shouldDelete:shouldDelete
+ handler:completion];
+}
+
+#pragma mark - MCS Connection
+
+- (BOOL)isConnected {
+ return self.stayConnected && self.connection.state != kFIRMessagingConnectionNotConnected;
+}
+
+- (BOOL)isConnectionActive {
+ return self.stayConnected && self.connection.state == kFIRMessagingConnectionSignedIn;
+}
+
+- (BOOL)shouldStayConnected {
+ return self.stayConnected;
+}
+
+- (void)retryConnectionImmediately:(BOOL)immediately {
+ // Do not connect to an invalid host or an invalid port
+ if (!self.stayConnected || !self.connection.host || self.connection.port == 0) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient004,
+ @"FIRMessaging connection will not reconnect to MCS. "
+ @"Stay connected: %d",
+ self.stayConnected);
+ return;
+ }
+ if (self.isConnectionActive) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient005,
+ @"FIRMessaging Connection skip retry, active");
+ // already connected and logged in.
+ // Heartbeat alarm is set and will force close the connection
+ return;
+ }
+ if (self.isConnected) {
+ // already connected and logged in.
+ // Heartbeat alarm is set and will force close the connection
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient006,
+ @"FIRMessaging Connection skip retry, connected");
+ return;
+ }
+
+ if (immediately) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient007,
+ @"Try to connect to MCS immediately");
+ [self tryToConnect];
+ } else {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient008, @"Try to connect to MCS lazily");
+ // Avoid all the other logic that we have in other clients, since this would always happen
+ // when the app is in the foreground and since the FIRMessaging connection isn't shared with any other
+ // app we can be more aggressive in reconnections
+ if (!self.didScheduleReconnect) {
+ FIRMessaging_WEAKIFY(self);
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
+ (int64_t)(kReconnectDelayInSeconds * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ FIRMessaging_STRONGIFY(self);
+ self.didScheduleReconnect = NO;
+ [self tryToConnect];
+ });
+
+ self.didScheduleReconnect = YES;
+ }
+ }
+}
+
+- (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler {
+ if (self.isConnected) {
+ NSError *error = [NSError fcm_errorWithCode:kFIRMessagingErrorCodeAlreadyConnected
+ userInfo:@{
+ NSLocalizedFailureReasonErrorKey: @"FIRMessaging is already connected",
+ }];
+ handler(error);
+ return;
+ }
+ self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
+ self.connectHandler = handler;
+ [self.registrar tryToLoadValidCheckinInfo];
+ [self connect];
+}
+
+- (void)connect {
+ // reset retry counts
+ self.connectRetryCount = 0;
+
+ if (self.isConnected) {
+ return;
+ }
+
+ self.stayConnected = YES;
+ BOOL isRegistrationComplete = [self.registrar hasValidCheckinInfo];
+
+ if (!isRegistrationComplete) {
+ if (![self.registrar tryToLoadValidCheckinInfo]) {
+ if (self.connectHandler) {
+ NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID];
+ self.connectHandler(error);
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient009,
+ @"Failed to connect to MCS. No deviceID and secret found.");
+ return;
+ }
+ }
+ [self setupConnectionAndConnect];
+}
+
+- (void)disconnect {
+ // user called disconnect
+ // We don't want to connect later even if no network is available.
+ [self disconnectWithTryToConnectLater:NO];
+}
+
+/**
+ * Disconnect the current client connection. Also explicitly stop and connction retries.
+ *
+ * @param tryToConnectLater If YES will try to connect later when sending upstream messages
+ * else if NO do not connect again until user explicitly calls
+ * connect.
+ */
+- (void)disconnectWithTryToConnectLater:(BOOL)tryToConnectLater {
+
+ self.stayConnected = tryToConnectLater;
+ [self.connection signOut];
+ _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected,
+ @"FIRMessaging connection did not disconnect");
+
+ // since we can disconnect while still trying to establish the connection it's required to
+ // cancel all performSelectors else the object might be retained
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(tryToConnect)
+ object:nil];
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(didConnectTimeout)
+ object:nil];
+ self.connectHandler = nil;
+}
+
+
+#pragma mark - Messages
+
+- (void)sendMessage:(GPBMessage *)message {
+ [self.connection sendProto:message];
+}
+
+- (void)sendOnConnectOrDrop:(GPBMessage *)message {
+ [self.connection sendOnConnectOrDrop:message];
+}
+
+#pragma mark - FIRMessagingConnectionDelegate
+
+- (void)connection:(FIRMessagingConnection *)fcmConnection
+ didCloseForReason:(FIRMessagingConnectionCloseReason)reason {
+
+ self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
+
+ if (reason == kFIRMessagingConnectionCloseReasonSocketDisconnected) {
+ // Cancel the not-yet-triggered timeout task before rescheduling, in case the previous sign in
+ // failed, due to a connection error caused by bad network.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(didConnectTimeout)
+ object:nil];
+ }
+ if (self.stayConnected) {
+ [self scheduleConnectRetry];
+ }
+}
+
+- (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection {
+ // Cancel the not-yet-triggered timeout task.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(didConnectTimeout)
+ object:nil];
+ self.connectRetryCount = 0;
+ self.lastConnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
+
+
+ [self.dataMessageManager setDeviceAuthID:self.registrar.deviceAuthID
+ secretToken:self.registrar.secretToken];
+ if (self.connectHandler) {
+ self.connectHandler(nil);
+ // notified the third party app with the registrationId.
+ // we don't want them to know about the connection status and how it changes
+ // so remove this handler
+ self.connectHandler = nil;
+ }
+}
+
+- (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message {
+ NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+ if ([parsedMessage count]) {
+ [self.dataMessageManager didReceiveParsedMessage:parsedMessage];
+ }
+}
+
+- (int)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds {
+ NSSet *rmqIDSet = [NSSet setWithArray:rmqIds];
+ NSMutableArray *messagesSent = [NSMutableArray arrayWithCapacity:rmqIds.count];
+ [self.rmq2Manager scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ NSString *rmqIdString = [NSString stringWithFormat:@"%lld", rmqId];
+ if ([rmqIDSet containsObject:rmqIdString]) {
+ [messagesSent addObject:stanza];
+ }
+ }];
+ for (GtalkDataMessageStanza *message in messagesSent) {
+ [self.dataMessageManager didSendDataMessageStanza:message];
+ }
+ return [self.rmq2Manager removeRmqMessagesWithRmqIds:rmqIds];
+}
+
+#pragma mark - Private
+
+- (void)setupConnectionAndConnect {
+ [self setupConnection];
+ [self tryToConnect];
+}
+
+- (void)setupConnection {
+ NSString *host = FIRMessagingServerHost();
+ NSUInteger port = FIRMessagingServerPort();
+ _FIRMessagingDevAssert([host length] > 0 && port != 0, @"Invalid port or host");
+
+ if (self.connection != nil) {
+ // if there is an old connection, explicitly sign it off.
+ [self.connection signOut];
+ self.connection.delegate = nil;
+ }
+ self.connection = [[FIRMessagingConnection alloc] initWithAuthID:self.registrar.deviceAuthID
+ token:self.registrar.secretToken
+ host:host
+ port:port
+ runLoop:[NSRunLoop mainRunLoop]
+ rmq2Manager:self.rmq2Manager
+ fcmManager:self.dataMessageManager];
+ self.connection.delegate = self;
+}
+
+- (void)tryToConnect {
+ if (!self.stayConnected) {
+ return;
+ }
+
+ // Cancel any other pending signin requests.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(tryToConnect)
+ object:nil];
+
+ // Do not re-sign in if there is already a connection in progress.
+ if (self.connection.state != kFIRMessagingConnectionNotConnected) {
+ return;
+ }
+
+ _FIRMessagingDevAssert(self.registrar.deviceAuthID.length > 0 &&
+ self.registrar.secretToken.length > 0 &&
+ self.connection != nil,
+ @"Invalid state cannot connect");
+
+ self.connectRetryCount = MIN(kMaxRetryExponent, self.connectRetryCount + 1);
+ [self performSelector:@selector(didConnectTimeout)
+ withObject:nil
+ afterDelay:self.connectionTimeoutInterval];
+ [self.connection signIn];
+}
+
+- (void)didConnectTimeout {
+ _FIRMessagingDevAssert(self.connection.state != kFIRMessagingConnectionSignedIn,
+ @"Invalid state for MCS connection");
+
+ if (self.stayConnected) {
+ [self.connection signOut];
+ [self scheduleConnectRetry];
+ }
+}
+
+#pragma mark - Schedulers
+
+- (void)scheduleConnectRetry {
+ FIRReachabilityStatus status = self.reachability.reachabilityStatus;
+ BOOL isReachable = (status == kFIRReachabilityViaWifi || status == kFIRReachabilityViaCellular);
+ if (!isReachable) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient010,
+ @"Internet not reachable when signing into MCS during a retry");
+
+ FIRMessagingConnectCompletionHandler handler = [self.connectHandler copy];
+ // disconnect before issuing a callback
+ [self disconnectWithTryToConnectLater:YES];
+ NSError *error = [NSError errorWithDomain:@"No internet available, cannot connect to FIRMessaging"
+ code:kFIRMessagingErrorCodeNetwork
+ userInfo:nil];
+ if (handler) {
+ handler(error);
+ self.connectHandler = nil;
+ }
+ return;
+ }
+
+ NSUInteger retryInterval = [self nextRetryInterval];
+
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient011,
+ @"Failed to sign in to MCS, retry in %lu seconds",
+ _FIRMessaging_UL(retryInterval));
+ [self performSelector:@selector(tryToConnect) withObject:nil afterDelay:retryInterval];
+}
+
+- (NSUInteger)nextRetryInterval {
+ return 1u << self.connectRetryCount;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingCodedInputStream.h b/Firebase/Messaging/FIRMessagingCodedInputStream.h
new file mode 100644
index 0000000..8f22290
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingCodedInputStream.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 <Foundation/Foundation.h>
+
+@interface FIRMessagingCodedInputStream : NSObject
+
+@property(nonatomic, readonly, assign) size_t offset;
+
+- (instancetype)initWithData:(NSData *)data;
+- (BOOL)readTag:(int8_t *)tag;
+- (BOOL)readLength:(int32_t *)length;
+- (NSData *)readDataWithLength:(uint32_t)length;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingCodedInputStream.m b/Firebase/Messaging/FIRMessagingCodedInputStream.m
new file mode 100644
index 0000000..82c0677
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingCodedInputStream.m
@@ -0,0 +1,142 @@
+/*
+ * 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 "FIRMessagingCodedInputStream.h"
+#import "FIRMessagingDefines.h"
+
+typedef struct {
+ const void *bytes;
+ size_t bufferSize;
+ size_t bufferPos;
+} BufferState;
+
+static BOOL CheckSize(BufferState *state, size_t size) {
+ size_t newSize = state->bufferPos + size;
+ if (newSize > state->bufferSize) {
+ return NO;
+ }
+ return YES;
+}
+
+static BOOL ReadRawByte(BufferState *state, int8_t *output) {
+ _FIRMessagingDevAssert(output != NULL && state != NULL, @"Invalid parameters");
+
+ if (CheckSize(state, sizeof(int8_t))) {
+ *output = ((int8_t *)state->bytes)[state->bufferPos++];
+ return YES;
+ }
+ return NO;
+}
+
+static BOOL ReadRawVarInt32(BufferState *state, int32_t *output) {
+ _FIRMessagingDevAssert(output != NULL && state != NULL, @"Invalid parameters");
+
+ int8_t tmp = 0;
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ if (tmp >= 0) {
+ *output = tmp;
+ return YES;
+ }
+ int32_t result = tmp & 0x7f;
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ if (tmp >= 0) {
+ result |= tmp << 7;
+ } else {
+ result |= (tmp & 0x7f) << 7;
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ if (tmp >= 0) {
+ result |= tmp << 14;
+ } else {
+ result |= (tmp & 0x7f) << 14;
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ if (tmp >= 0) {
+ result |= tmp << 21;
+ } else {
+ result |= (tmp & 0x7f) << 21;
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ result |= tmp << 28;
+ if (tmp < 0) {
+ // Discard upper 32 bits.
+ for (int i = 0; i < 5; ++i) {
+ if (!ReadRawByte(state, &tmp)) {
+ return NO;
+ }
+ if (tmp >= 0) {
+ *output = result;
+ return YES;
+ }
+ }
+ return NO;
+ }
+ }
+ }
+ }
+ *output = result;
+ return YES;
+}
+
+@interface FIRMessagingCodedInputStream()
+
+@property(nonatomic, readwrite, strong) NSData *buffer;
+@property(nonatomic, readwrite, assign) BufferState state;
+
+@end
+
+@implementation FIRMessagingCodedInputStream;
+
+- (instancetype)initWithData:(NSData *)data {
+ self = [super init];
+ if (self) {
+ _buffer = data;
+ _state.bytes = _buffer.bytes;
+ _state.bufferSize = _buffer.length;
+ }
+ return self;
+}
+
+- (size_t)offset {
+ return _state.bufferPos;
+}
+
+- (BOOL)readTag:(int8_t *)tag {
+ return ReadRawByte(&_state, tag);
+}
+
+- (BOOL)readLength:(int32_t *)length {
+ return ReadRawVarInt32(&_state, length);
+}
+
+- (NSData *)readDataWithLength:(uint32_t)length {
+ if (!CheckSize(&_state, length)) {
+ return nil;
+ }
+ const void *bytesToRead = _state.bytes + _state.bufferPos;
+ NSData *result = [NSData dataWithBytes:bytesToRead length:length];
+ _state.bufferPos += length;
+ return result;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingConfig.h b/Firebase/Messaging/FIRMessagingConfig.h
new file mode 100644
index 0000000..09a9ec7
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConfig.h
@@ -0,0 +1,46 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+typedef NS_ENUM(int8_t, FIRMessagingLogLevel) {
+ kFIRMessagingLogLevelDebug,
+ kFIRMessagingLogLevelInfo,
+ kFIRMessagingLogLevelError,
+ kFIRMessagingLogLevelAssert,
+};
+
+/**
+ * Config used to set different options in Firebase Messaging.
+ */
+@interface FIRMessagingConfig : NSObject
+
+/**
+ * The log level for the FIRMessaging library. Valid values are `kFIRMessagingLogLevelDebug`,
+ * `kFIRMessagingLogLevelInfo`, `kFIRMessagingLogLevelError`, and `kFIRMessagingLogLevelAssert`.
+ */
+@property(nonatomic, readwrite, assign) FIRMessagingLogLevel logLevel;
+
+/**
+ * Get default configuration for FIRMessaging. The default config has logLevel set to
+ * `kFIRMessagingLogLevelError` and `receiverDelegate` is set to nil.
+ *
+ * @return FIRMessagingConfig sharedInstance.
+ */
++ (instancetype)defaultConfig;
+
+@end
+
diff --git a/Firebase/Messaging/FIRMessagingConfig.m b/Firebase/Messaging/FIRMessagingConfig.m
new file mode 100644
index 0000000..e7674c3
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConfig.m
@@ -0,0 +1,38 @@
+/*
+ * 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 "FIRMessagingConfig.h"
+#import "FIRMessagingDefines.h"
+
+@implementation FIRMessagingConfig
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
++ (instancetype)defaultConfig {
+ return [[FIRMessagingConfig alloc] initWithDefaultConfig];
+}
+
+- (instancetype)initWithDefaultConfig {
+ self = [super init];
+ if (self) {
+ self.logLevel = kFIRMessagingLogLevelError;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingConnection.h b/Firebase/Messaging/FIRMessagingConnection.h
new file mode 100644
index 0000000..e78adbf
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConnection.h
@@ -0,0 +1,107 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FIRMessagingConnection;
+@class FIRMessagingDataMessageManager;
+@class FIRMessagingRmqManager;
+
+@class GtalkDataMessageStanza;
+@class GPBMessage;
+
+typedef void (^FIRMessagingMessageHandler)(NSDictionary *);
+
+typedef NS_ENUM(NSUInteger, FIRMessagingConnectionState) {
+ kFIRMessagingConnectionNotConnected = 0,
+ kFIRMessagingConnectionConnecting,
+ kFIRMessagingConnectionConnected,
+ kFIRMessagingConnectionSignedIn,
+};
+
+typedef NS_ENUM(NSUInteger, FIRMessagingConnectionCloseReason) {
+ kFIRMessagingConnectionCloseReasonSocketDisconnected = 0,
+ kFIRMessagingConnectionCloseReasonTimeout,
+ kFIRMessagingConnectionCloseReasonUserDisconnect,
+};
+
+@protocol FIRMessagingConnectionDelegate<NSObject>
+
+- (void)connection:(FIRMessagingConnection *)fcmConnection
+ didCloseForReason:(FIRMessagingConnectionCloseReason)reason;
+- (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection;
+- (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message;
+/**
+ * Called when a stream ACK or a selective ACK are received - this indicates the
+ * message has been received by MCS.
+ * @return The count of rmqIds deleted from the client RMQ store.
+ */
+- (int)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds;
+
+@end
+
+
+/**
+ * This class maintains the actual FIRMessaging connection that we use to receive and send messages
+ * while the app is in foreground. Once we have a registrationID from the FIRMessaging backend we
+ * are able to set up this connection which is used for any further communication with FIRMessaging
+ * backend. In case the connection breaks off while the app is still being used we try to rebuild
+ * the connection with an exponential backoff.
+ *
+ * This class also notifies the delegate about the main events happening in the lifcycle of the
+ * FIRMessaging connection (read FIRMessagingConnectionDelegate). All of the `on-the-wire`
+ * interactions with FIRMessaging are channelled through here.
+ */
+@interface FIRMessagingConnection : NSObject
+
+@property(nonatomic, readwrite, assign) int64_t lastHeartbeatPingTimestamp;
+@property(nonatomic, readonly, assign) FIRMessagingConnectionState state;
+@property(nonatomic, readonly, copy) NSString *host;
+@property(nonatomic, readonly, assign) NSUInteger port;
+@property(nonatomic, readwrite, weak) id<FIRMessagingConnectionDelegate> delegate;
+
+- (instancetype)initWithAuthID:(NSString *)authId
+ token:(NSString *)token
+ host:(NSString *)host
+ port:(NSUInteger)port
+ runLoop:(NSRunLoop *)runLoop
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
+ fcmManager:(FIRMessagingDataMessageManager *)dataMessageManager;
+
+- (void)signIn; // connect
+- (void)signOut; // disconnect
+
+/**
+ * Teardown the FIRMessaging connection and deallocate the resources being held up by the
+ * connection.
+ */
+- (void)teardown;
+
+/**
+ * Send proto to the wire. The message will be cached before we try to send so that in case of
+ * failure we can send it again later on when we have connection.
+ */
+- (void)sendProto:(GPBMessage *)proto;
+
+/**
+ * Send a message after the currently in progress connection succeeds, otherwise drop it.
+ *
+ * This should be used for TTL=0 messages that force a reconnect. They shouldn't be persisted
+ * in the RMQ, but they should be sent if the reconnect is successful.
+ */
+- (void)sendOnConnectOrDrop:(GPBMessage *)message;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingConnection.m b/Firebase/Messaging/FIRMessagingConnection.m
new file mode 100644
index 0000000..afbd0ba
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConnection.m
@@ -0,0 +1,711 @@
+/*
+ * 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 "FIRMessagingConnection.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+#import "Protos/GtalkExtensions.pbobjc.h"
+
+#import "FIRMessaging.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingSecureSocket.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRMessagingVersionUtilities.h"
+#import "FIRMessaging_Private.h"
+
+static NSInteger const kIqSelectiveAck = 12;
+static NSInteger const kIqStreamAck = 13;
+static int const kInvalidStreamId = -1;
+// Threshold for number of messages removed that we will ack, for short lived connections
+static int const kMessageRemoveAckThresholdCount = 5;
+
+static NSTimeInterval const kHeartbeatInterval = 30.0;
+static NSTimeInterval const kConnectionTimeout = 20.0;
+static int32_t const kAckingInterval = 10;
+
+static NSString *const kUnackedS2dIdKey = @"FIRMessagingUnackedS2dIdKey";
+static NSString *const kAckedS2dIdMapKey = @"FIRMessagingAckedS2dIdMapKey";
+
+static NSString *const kRemoteFromAddress = @"from";
+
+@interface FIRMessagingD2SInfo : NSObject
+
+@property(nonatomic, readwrite, assign) int streamId;
+@property(nonatomic, readwrite, strong) NSString *d2sID;
+- (instancetype)initWithStreamId:(int)streamId d2sId:(NSString *)d2sID;
+
+@end
+
+@implementation FIRMessagingD2SInfo
+
+- (instancetype)initWithStreamId:(int)streamId d2sId:(NSString *)d2sID {
+ self = [super init];
+ if (self) {
+ _streamId = streamId;
+ _d2sID = [d2sID copy];
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)object {
+ if ([object isKindOfClass:[self class]]) {
+ FIRMessagingD2SInfo *other = (FIRMessagingD2SInfo *)object;
+ return self.streamId == other.streamId && [self.d2sID isEqualToString:other.d2sID];
+ }
+ return NO;
+}
+
+- (NSUInteger)hash {
+ return [self.d2sID hash];
+}
+
+@end
+
+@interface FIRMessagingConnection ()<FIRMessagingSecureSocketDelegate>
+
+@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
+@property(nonatomic, readwrite, weak) FIRMessagingDataMessageManager *dataMessageManager;
+
+@property(nonatomic, readwrite, assign) FIRMessagingConnectionState state;
+@property(nonatomic, readwrite, copy) NSString *host;
+@property(nonatomic, readwrite, assign) NSUInteger port;
+
+@property(nonatomic, readwrite, strong) NSString *authId;
+@property(nonatomic, readwrite, strong) NSString *token;
+
+@property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;
+
+@property(nonatomic, readwrite, assign) int64_t lastLoginServerTimestamp;
+@property(nonatomic, readwrite, assign) int lastStreamIdAcked;
+@property(nonatomic, readwrite, assign) int inStreamId;
+@property(nonatomic, readwrite, assign) int outStreamId;
+
+@property(nonatomic, readwrite, strong) NSMutableArray *unackedS2dIds;
+@property(nonatomic, readwrite, strong) NSMutableDictionary *ackedS2dMap;
+@property(nonatomic, readwrite, strong) NSMutableArray *d2sInfos;
+// ttl=0 messages that need to be sent as soon as we establish a connection
+@property(nonatomic, readwrite, strong) NSMutableArray *sendOnConnectMessages;
+
+@property(nonatomic, readwrite, strong) NSRunLoop *runLoop;
+
+@end
+
+
+@implementation FIRMessagingConnection;
+
+- (instancetype)initWithAuthID:(NSString *)authId
+ token:(NSString *)token
+ host:(NSString *)host
+ port:(NSUInteger)port
+ runLoop:(NSRunLoop *)runLoop
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
+ fcmManager:(FIRMessagingDataMessageManager *)dataMessageManager {
+ self = [super init];
+ if (self) {
+ _authId = [authId copy];
+ _token = [token copy];
+ _host = [host copy];
+ _port = port;
+ _runLoop = runLoop;
+ _rmq2Manager = rmq2Manager;
+ _dataMessageManager = dataMessageManager;
+
+ _d2sInfos = [NSMutableArray array];
+
+ _unackedS2dIds = [NSMutableArray arrayWithArray:[_rmq2Manager unackedS2dRmqIds]];
+ _ackedS2dMap = [NSMutableDictionary dictionary];
+ _sendOnConnectMessages = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"host: %@, port: %lu, stream id in: %d, stream id out: %d",
+ self.host,
+ _FIRMessaging_UL(self.port),
+ self.inStreamId,
+ self.outStreamId];
+}
+
+- (void)signIn {
+ _FIRMessagingDevAssert(self.state == kFIRMessagingConnectionNotConnected, @"Invalid connection state.");
+ if (self.state != kFIRMessagingConnectionNotConnected) {
+ return;
+ }
+
+ // break it up for testing
+ [self setupConnectionSocket];
+ [self connectToSocket:self.socket];
+}
+
+- (void)setupConnectionSocket {
+ self.socket = [[FIRMessagingSecureSocket alloc] init];
+ self.socket.delegate = self;
+}
+
+- (void)connectToSocket:(FIRMessagingSecureSocket *)socket {
+ self.state = kFIRMessagingConnectionConnecting;
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection000,
+ @"Start connecting to FIRMessaging service.");
+ [socket connectToHost:self.host port:self.port onRunLoop:self.runLoop];
+}
+
+- (void)signOut {
+ // Clear the list of messages to be sent on connect. This will only
+ // have messages in it if an error happened before receiving the LoginResponse.
+ [self.sendOnConnectMessages removeAllObjects];
+
+ if (self.state == kFIRMessagingConnectionSignedIn) {
+ [self sendClose];
+ }
+ if (self.state != kFIRMessagingConnectionNotConnected) {
+ [self disconnect];
+ }
+}
+
+- (void)teardown {
+ if (self.state != kFIRMessagingConnectionNotConnected) {
+ [self disconnect];
+ }
+}
+
+#pragma mark - FIRMessagingSecureSocketDelegate
+
+- (void)secureSocketDidConnect:(FIRMessagingSecureSocket *)socket {
+ self.state = kFIRMessagingConnectionConnected;
+ self.lastStreamIdAcked = 0;
+ self.inStreamId = 0;
+ self.outStreamId = 0;
+
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection001,
+ @"Connected to FIRMessaging service.");
+ [self resetUnconfirmedAcks];
+ [self sendLoginRequest:self.authId token:self.token];
+}
+
+- (void)didDisconnectWithSecureSocket:(FIRMessagingSecureSocket *)socket {
+ _FIRMessagingDevAssert(self.socket == socket, @"Invalid socket");
+ _FIRMessagingDevAssert(self.socket.state == kFIRMessagingSecureSocketClosed, @"Socket already closed");
+
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection002,
+ @"Secure socket disconnected from FIRMessaging service.");
+ [self disconnect];
+ [self.delegate connection:self didCloseForReason:kFIRMessagingConnectionCloseReasonSocketDisconnected];
+}
+
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didReceiveData:(NSData *)data
+ withTag:(int8_t)tag {
+ if (tag < 0) {
+ // Invalid proto tag
+ return;
+ }
+
+ Class klassForTag = FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag);
+ if ([klassForTag isSubclassOfClass:[NSNull class]]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeConnection003, @"Invalid tag %d for proto",
+ tag);
+ return;
+ }
+
+ GPBMessage *proto = [klassForTag parseFromData:data error:NULL];
+ if (tag == kFIRMessagingProtoTagLoginResponse && self.state != kFIRMessagingConnectionConnected) {
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeConnection004,
+ @"Should not receive generated message when the connection is not connected.");
+ return;
+ } else if (tag != kFIRMessagingProtoTagLoginResponse && self.state != kFIRMessagingConnectionSignedIn) {
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeConnection005,
+ @"Should not receive generated message when the connection is not signed in.");
+ return;
+ }
+
+ // If traffic is received after a heartbeat it is safe to assume the connection is healthy.
+ [self cancelConnectionTimeoutTask];
+ [self performSelector:@selector(sendHeartbeatPing)
+ withObject:nil
+ afterDelay:kHeartbeatInterval];
+
+ [self willProcessProto:proto];
+ switch (tag) {
+ case kFIRMessagingProtoTagLoginResponse:
+ [self didReceiveLoginResponse:(GtalkLoginResponse *)proto];
+ break;
+ case kFIRMessagingProtoTagDataMessageStanza:
+ [self didReceiveDataMessageStanza:(GtalkDataMessageStanza *)proto];
+ break;
+ case kFIRMessagingProtoTagHeartbeatPing:
+ [self didReceiveHeartbeatPing:(GtalkHeartbeatPing *)proto];
+ break;
+ case kFIRMessagingProtoTagHeartbeatAck:
+ [self didReceiveHeartbeatAck:(GtalkHeartbeatAck *)proto];
+ break;
+ case kFIRMessagingProtoTagClose:
+ [self didReceiveClose:(GtalkClose *)proto];
+ break;
+ case kFIRMessagingProtoTagIqStanza:
+ [self handleIqStanza:(GtalkIqStanza *)proto];
+ break;
+ default:
+ [self didReceiveUnhandledProto:proto];
+ break;
+ }
+}
+
+// Called from secure socket once we have send the proto with given rmqId over the wire
+// since we are mostly concerned with user facing messages which certainly have a rmqId
+// we can retrieve them from the Rmq if necessary to look at stuff but for now we just
+// log it.
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didSendProtoWithTag:(int8_t)tag
+ rmqId:(NSString *)rmqId {
+ // log the message
+ [self logMessage:rmqId messageType:tag isOut:YES];
+}
+
+#pragma mark - FIRMessagingTestConnection
+
+- (void)sendProto:(GPBMessage *)proto {
+ FIRMessagingProtoTag tag = FIRMessagingGetTagForProto(proto);
+ if (tag == kFIRMessagingProtoTagLoginRequest && self.state != kFIRMessagingConnectionConnected) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection006,
+ @"Cannot send generated message when the connection is not connected.");
+ return;
+ } else if (tag != kFIRMessagingProtoTagLoginRequest && self.state != kFIRMessagingConnectionSignedIn) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection007,
+ @"Cannot send generated message when the connection is not signed in.");
+ return;
+ }
+
+ _FIRMessagingDevAssert(self.socket != nil, @"Socket shouldn't be nil");
+ if (self.socket == nil) {
+ return;
+ }
+
+ [self willSendProto:proto];
+
+ [self.socket sendData:proto.data withTag:tag rmqId:FIRMessagingGetRmq2Id(proto)];
+}
+
+- (void)sendOnConnectOrDrop:(GPBMessage *)message {
+ if (self.state == kFIRMessagingConnectionSignedIn) {
+ // If a connection has already been established, send normally
+ [self sendProto:message];
+ } else {
+ // Otherwise add them to the list of messages to send after login
+ [self.sendOnConnectMessages addObject:message];
+ }
+}
+
++ (GtalkLoginRequest *)loginRequestWithToken:(NSString *)token authID:(NSString *)authID {
+ GtalkLoginRequest *login = [[GtalkLoginRequest alloc] init];
+ login.accountId = 1000000;
+ login.authService = GtalkLoginRequest_AuthService_AndroidId;
+ login.authToken = token;
+ login.id_p = [NSString stringWithFormat:@"%@-%@", @"ios", FIRMessagingCurrentLibraryVersion()];
+ login.domain = @"mcs.android.com";
+ login.deviceId = [NSString stringWithFormat:@"android-%llx", authID.longLongValue];
+ login.networkType = [self currentNetworkType];
+ login.resource = authID;
+ login.user = authID;
+ login.useRmq2 = YES;
+ login.lastRmqId = 1; // Sending not enabled yet so this stays as 1.
+ return login;
+}
+
++ (int32_t)currentNetworkType {
+ // http://developer.android.com/reference/android/net/ConnectivityManager.html
+ int32_t fcmNetworkType;
+ FIRMessagingNetworkStatus type = [[FIRMessaging messaging] networkType];
+ switch (type) {
+ case kFIRMessagingReachabilityReachableViaWiFi:
+ fcmNetworkType = 1;
+ break;
+
+ case kFIRMessagingReachabilityReachableViaWWAN:
+ fcmNetworkType = 0;
+ break;
+
+ default:
+ fcmNetworkType = -1;
+ break;
+ }
+ return fcmNetworkType;
+}
+
+- (void)sendLoginRequest:(NSString *)authId
+ token:(NSString *)token {
+ GtalkLoginRequest *login = [[self class] loginRequestWithToken:token authID:authId];
+
+ // clear the messages sent during last connection
+ if ([self.d2sInfos count]) {
+ [self.d2sInfos removeAllObjects];
+ }
+
+ if (self.unackedS2dIds.count > 0) {
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeConnection008,
+ @"There are unacked persistent Ids in the login request: %@",
+ [self.unackedS2dIds.description stringByReplacingOccurrencesOfString:@"%"
+ withString:@"%%"]);
+ }
+ // Send out acks.
+ for (NSString *unackedPersistentS2dId in self.unackedS2dIds) {
+ [login.receivedPersistentIdArray addObject:unackedPersistentS2dId];
+ }
+
+ GtalkSetting *setting = [[GtalkSetting alloc] init];
+ setting.name = @"new_vc";
+ setting.value = @"1";
+ [login.settingArray addObject:setting];
+
+ [self sendProto:login];
+}
+
+- (void)sendHeartbeatAck {
+ [self sendProto:[[GtalkHeartbeatAck alloc] init]];
+}
+
+- (void)sendHeartbeatPing {
+ // cancel the previous heartbeat request.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(sendHeartbeatPing)
+ object:nil];
+ [self scheduleConnectionTimeoutTask];
+ [self sendProto:[[GtalkHeartbeatPing alloc] init]];
+}
+
++ (GtalkIqStanza *)createStreamAck {
+ GtalkIqStanza *iq = [[GtalkIqStanza alloc] init];
+ iq.type = GtalkIqStanza_IqType_Set;
+ iq.id_p = @"";
+ GtalkExtension *ext = [[GtalkExtension alloc] init];
+ ext.id_p = kIqStreamAck;
+ ext.data_p = @"";
+ iq.extension = ext;
+ return iq;
+}
+
+- (void)sendStreamAck {
+ GtalkIqStanza *iq = [[self class] createStreamAck];
+ [self sendProto:iq];
+}
+
+- (void)sendClose {
+ [self sendProto:[[GtalkClose alloc] init]];
+}
+
+- (void)handleIqStanza:(GtalkIqStanza *)iq {
+ if (iq.hasExtension) {
+ if (iq.extension.id_p == kIqStreamAck) {
+ [self didReceiveStreamAck:iq];
+ return;
+ }
+ if (iq.extension.id_p == kIqSelectiveAck) {
+ [self didReceiveSelectiveAck:iq];
+ return;
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection009, @"Unknown ack extension id %d.",
+ iq.extension.id_p);
+ } else {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection010, @"Ip stanza without extension.");
+ }
+ [self didReceiveUnhandledProto:iq];
+}
+
+- (void)didReceiveLoginResponse:(GtalkLoginResponse *)loginResponse {
+ if (loginResponse.hasError) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection011,
+ @"Login error with type: %@, message: %@.", loginResponse.error.type,
+ loginResponse.error.message);
+ return;
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection012, @"Logged onto MCS service.");
+ // We sent the persisted list of unack'd messages with login so we can assume they have been ack'd
+ // by the server.
+ _FIRMessagingDevAssert(self.unackedS2dIds.count == 0, @"No ids present");
+ _FIRMessagingDevAssert(self.outStreamId == 1, @"Login should be the first stream id");
+
+ self.state = kFIRMessagingConnectionSignedIn;
+ self.lastLoginServerTimestamp = loginResponse.serverTimestamp;
+ [self.delegate didLoginWithConnection:self];
+ [self sendHeartbeatPing];
+
+ // Add all the TTL=0 messages on connect
+ for (GPBMessage *message in self.sendOnConnectMessages) {
+ [self sendProto:message];
+ }
+ [self.sendOnConnectMessages removeAllObjects];
+}
+
+- (void)didReceiveHeartbeatPing:(GtalkHeartbeatPing *)heartbeatPing {
+ [self sendHeartbeatAck];
+}
+
+- (void)didReceiveHeartbeatAck:(GtalkHeartbeatAck *)heartbeatAck {
+#if FIRMessaging_PROBER
+ self.lastHeartbeatPingTimestamp = FIRMessagingCurrentTimestampInSeconds();
+#endif
+}
+
+- (void)didReceiveDataMessageStanza:(GtalkDataMessageStanza *)dataMessageStanza {
+ // TODO: Maybe add support raw data later
+ [self.delegate connectionDidRecieveMessage:dataMessageStanza];
+}
+
+- (void)didReceiveUnhandledProto:(GPBMessage *)proto {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection013, @"Received unhandled proto");
+}
+
+- (void)didReceiveStreamAck:(GtalkIqStanza *)iq {
+ // Server received some stuff from us we don't really need to do anything special
+}
+
+- (void)didReceiveSelectiveAck:(GtalkIqStanza *)iq {
+ GtalkExtension *extension = iq.extension;
+ if (extension) {
+ int extensionId = extension.id_p;
+ if (extensionId == kIqSelectiveAck) {
+
+ NSString *dataString = extension.data_p;
+ GtalkSelectiveAck *selectiveAck = [[GtalkSelectiveAck alloc] init];
+ [selectiveAck mergeFromData:[dataString dataUsingEncoding:NSUTF8StringEncoding]
+ extensionRegistry:nil];
+
+ NSArray <NSString *>*acks = [selectiveAck idArray];
+
+ // we've received ACK's
+ [self.delegate connectionDidReceiveAckForRmqIds:acks];
+
+ // resend unacked messages
+ [self.dataMessageManager resendMessagesWithConnection:self];
+ }
+ }
+}
+
+- (void)didReceiveClose:(GtalkClose *)close {
+ [self disconnect];
+}
+
+- (void)willProcessProto:(GPBMessage *)proto {
+ self.inStreamId++;
+
+ if ([proto isKindOfClass:GtalkDataMessageStanza.class]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection014,
+ @"RMQ: Receiving %@ with rmq_id: %@ incoming stream Id: %d",
+ proto.class, FIRMessagingGetRmq2Id(proto), self.inStreamId);
+ } else {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection015,
+ @"RMQ: Receiving %@ with incoming stream Id: %d.", proto.class,
+ self.inStreamId);
+ }
+ int streamId = FIRMessagingGetLastStreamId(proto);
+ if (streamId != kInvalidStreamId) {
+ // confirm the D2S messages that were sent by us
+ [self confirmAckedD2sIdsWithStreamId:streamId];
+
+ // We can now confirm that our ack was received by the server and start our unack'd list fresh
+ // with the proto we just received.
+ [self confirmAckedS2dIdsWithStreamId:streamId];
+ }
+ NSString *rmq2Id = FIRMessagingGetRmq2Id(proto);
+ if (rmq2Id != nil) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection016,
+ @"RMQ: Add unacked persistent Id: %@.",
+ [rmq2Id stringByReplacingOccurrencesOfString:@"%" withString:@"%%"]);
+ [self.unackedS2dIds addObject:rmq2Id];
+ [self.rmq2Manager saveS2dMessageWithRmqId:rmq2Id]; // RMQ save
+ }
+ BOOL explicitAck = ([proto isKindOfClass:[GtalkDataMessageStanza class]] &&
+ [(GtalkDataMessageStanza *)proto immediateAck]);
+ // If we have not sent anything and the ack threshold has been reached then explicitly send one
+ // to notify the server that we have received messages.
+ if (self.inStreamId - self.lastStreamIdAcked >= kAckingInterval || explicitAck) {
+ [self sendStreamAck];
+ }
+}
+
+- (void)willSendProto:(GPBMessage *)proto {
+ self.outStreamId++;
+
+ NSString *rmq2Id = FIRMessagingGetRmq2Id(proto);
+ if ([rmq2Id length]) {
+ FIRMessagingD2SInfo *d2sInfo = [[FIRMessagingD2SInfo alloc] initWithStreamId:self.outStreamId d2sId:rmq2Id];
+ [self.d2sInfos addObject:d2sInfo];
+ }
+
+ // each time we send a d2s message, it acks previously received
+ // s2d messages via the last (s2d) stream id received.
+
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection017,
+ @"RMQ: Sending %@ with outgoing stream Id: %d.", proto.class,
+ self.outStreamId);
+ // We have received messages since last time we sent something - send ack info to server.
+ if (self.inStreamId > self.lastStreamIdAcked) {
+ FIRMessagingSetLastStreamId(proto, self.inStreamId);
+ self.lastStreamIdAcked = self.inStreamId;
+ }
+
+ if (self.unackedS2dIds.count > 0) {
+ // Move all 'unack'd' messages to the ack'd map so they can be removed once the
+ // ack is confirmed.
+ NSArray *ackedS2dIds = [NSArray arrayWithArray:self.unackedS2dIds];
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeConnection018, @"RMQ: Mark persistent Ids as acked: %@.",
+ [ackedS2dIds.description stringByReplacingOccurrencesOfString:@"%" withString:@"%%"]);
+ [self.unackedS2dIds removeAllObjects];
+ self.ackedS2dMap[[@(self.outStreamId) stringValue]] = ackedS2dIds;
+ }
+}
+
+#pragma mark - Private
+
+/**
+ * This processes the s2d message received in reference to the d2s messages
+ * that we have sent before.
+ */
+- (void)confirmAckedD2sIdsWithStreamId:(int)lastReceivedStreamId {
+ NSMutableArray *d2sIdsAcked = [NSMutableArray array];
+ for (FIRMessagingD2SInfo *d2sInfo in self.d2sInfos) {
+ if (lastReceivedStreamId < d2sInfo.streamId) {
+ break;
+ }
+ [d2sIdsAcked addObject:d2sInfo];
+ }
+
+ NSMutableArray *rmqIds = [NSMutableArray arrayWithCapacity:[d2sIdsAcked count]];
+ // remove ACK'ed messages
+ for (FIRMessagingD2SInfo *d2sInfo in d2sIdsAcked) {
+ if ([d2sInfo.d2sID length]) {
+ [rmqIds addObject:d2sInfo.d2sID];
+ }
+ [self.d2sInfos removeObject:d2sInfo];
+ }
+ [self.delegate connectionDidReceiveAckForRmqIds:rmqIds];
+ int count = [self.delegate connectionDidReceiveAckForRmqIds:rmqIds];
+ if (kMessageRemoveAckThresholdCount > 0 && count >= kMessageRemoveAckThresholdCount) {
+ // For short lived connections, if a large number of messages are removed, send an
+ // ack straight away so the server knows that this message was received.
+ [self sendStreamAck];
+ }
+}
+
+/**
+ * Called when a stream ACK or a selective ACK are received - this indicates the message has
+ * been received by MCS.
+ */
+- (void)didReceiveAckForRmqIds:(NSArray *)rmqIds {
+ // TODO: let the user know that the following messages were received by the server
+}
+
+- (void)confirmAckedS2dIdsWithStreamId:(int)lastReceivedStreamId {
+ // If the server hasn't received the streamId yet.
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection019,
+ @"RMQ: Server last received stream Id: %d.", lastReceivedStreamId);
+ if (lastReceivedStreamId < self.outStreamId) {
+ // TODO: This could be a good indicator that we need to re-send something (acks)?
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection020,
+ @"RMQ: There are unsent messages that should be send...\n"
+ "server received: %d\nlast stream id sent: %d",
+ lastReceivedStreamId, self.outStreamId);
+ }
+
+ NSSet *ackedStreamIds =
+ [self.ackedS2dMap keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
+ NSString *streamId = key;
+ return streamId.intValue <= lastReceivedStreamId;
+ }];
+ NSMutableArray *s2dIdsToDelete = [NSMutableArray array];
+
+ for (NSString *streamId in ackedStreamIds) {
+ NSArray *ackedS2dIds = self.ackedS2dMap[streamId];
+ if (ackedS2dIds.count > 0) {
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeConnection021,
+ @"RMQ: Mark persistent Ids as confirmed by stream id %@: %@.", streamId,
+ [ackedS2dIds.description stringByReplacingOccurrencesOfString:@"%" withString:@"%%"]);
+ [self.ackedS2dMap removeObjectForKey:streamId];
+ }
+
+ [s2dIdsToDelete addObjectsFromArray:ackedS2dIds];
+ }
+
+ // clean up s2d ids that the server knows we've received.
+ // we let the server know via a s2d last stream id received in a
+ // d2s message. the server lets us know it has received our d2s
+ // message via a d2s last stream id received in a s2d message.
+ [self.rmq2Manager removeS2dIds:s2dIdsToDelete];
+}
+
+- (void)resetUnconfirmedAcks {
+ [self.ackedS2dMap enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ [self.unackedS2dIds addObjectsFromArray:obj];
+ }];
+ [self.ackedS2dMap removeAllObjects];
+}
+
+- (void)disconnect {
+ _FIRMessagingDevAssert(self.state != kFIRMessagingConnectionNotConnected, @"Connection already not connected");
+ // cancel pending timeout tasks.
+ [self cancelConnectionTimeoutTask];
+ // cancel pending heartbeat.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(sendHeartbeatPing)
+ object:nil];
+ // Unset the delegate. FIRMessagingConnection will not receive further events from the socket from now on.
+ self.socket.delegate = nil;
+ [self.socket disconnect];
+ self.state = kFIRMessagingConnectionNotConnected;
+}
+
+- (void)connectionTimedOut {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection022,
+ @"Connection to FIRMessaging service timed out.");
+ [self disconnect];
+ [self.delegate connection:self didCloseForReason:kFIRMessagingConnectionCloseReasonTimeout];
+}
+
+- (void)scheduleConnectionTimeoutTask {
+ // cancel the previous heartbeat timeout event and schedule a new one.
+ [self cancelConnectionTimeoutTask];
+ [self performSelector:@selector(connectionTimedOut)
+ withObject:nil
+ afterDelay:[self connectionTimeoutInterval]];
+}
+
+- (void)cancelConnectionTimeoutTask {
+ // cancel pending timeout tasks.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(connectionTimedOut)
+ object:nil];
+}
+
+- (void)logMessage:(NSString *)description messageType:(int)messageType isOut:(BOOL)isOut {
+ messageType = isOut ? -messageType : messageType;
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection023,
+ @"Send msg: %@ type: %d inStreamId: %d outStreamId: %d", description,
+ messageType, self.inStreamId, self.outStreamId);
+}
+
+- (NSTimeInterval)connectionTimeoutInterval {
+ return kConnectionTimeout;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingConstants.h b/Firebase/Messaging/FIRMessagingConstants.h
new file mode 100644
index 0000000..0e244a5
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConstants.h
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+/**
+ * Global constants to be put here.
+ *
+ */
+#import <Foundation/Foundation.h>
+
+#ifndef _FIRMessaging_CONSTANTS_H
+#define _FIRMessaging_CONSTANTS_H
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingRawDataKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingCollapseKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingFromKey;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingSendTo;
+FOUNDATION_EXPORT NSString *const kFIRMessagingSendTTL;
+FOUNDATION_EXPORT NSString *const kFIRMessagingSendDelay;
+FOUNDATION_EXPORT NSString *const kFIRMessagingSendMessageID;
+FOUNDATION_EXPORT NSString *const KFIRMessagingSendMessageAppData;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageInternalReservedKeyword;
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessagePersistentIDKey;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageIDKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageAPNSContentAvailableKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageSyncViaMCSKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageSyncMessageTTLKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingMessageLinkKey;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingLibraryVersion;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey;
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingApplicationSupportSubDirectory;
+
+// Notifications
+FOUNDATION_EXPORT NSString *const kFIRMessagingAPNSTokenNotification;
+FOUNDATION_EXPORT NSString *const kFIRMessagingFCMTokenNotification;
+FOUNDATION_EXPORT NSString *const kFIRMessagingInstanceIDTokenRefreshNotification;
+
+FOUNDATION_EXPORT const int kFIRMessagingSendTtlDefault; // 24 hours
+
+#endif
diff --git a/Firebase/Messaging/FIRMessagingConstants.m b/Firebase/Messaging/FIRMessagingConstants.m
new file mode 100644
index 0000000..f8e420c
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingConstants.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 "FIRMessagingConstants.h"
+
+NSString *const kFIRMessagingRawDataKey = @"rawData";
+NSString *const kFIRMessagingCollapseKey = @"collapse_key";
+NSString *const kFIRMessagingFromKey = @"from";
+
+NSString *const kFIRMessagingSendTo = @"google." @"to";
+NSString *const kFIRMessagingSendTTL = @"google." @"ttl";
+NSString *const kFIRMessagingSendDelay = @"google." @"delay";
+NSString *const kFIRMessagingSendMessageID = @"google." @"msg_id";
+NSString *const KFIRMessagingSendMessageAppData = @"google." @"data";
+
+NSString *const kFIRMessagingMessageInternalReservedKeyword = @"gcm.";
+NSString *const kFIRMessagingMessagePersistentIDKey = @"persistent_id";
+
+NSString *const kFIRMessagingMessageIDKey = @"gcm." @"message_id";
+NSString *const kFIRMessagingMessageAPNSContentAvailableKey = @"content-available";
+NSString *const kFIRMessagingMessageSyncViaMCSKey = @"gcm." @"duplex";
+NSString *const kFIRMessagingMessageSyncMessageTTLKey = @"gcm." @"ttl";
+NSString *const kFIRMessagingMessageLinkKey = @"gcm." @"app_link";
+
+NSString *const kFIRMessagingLibraryVersion = @"FIRMessaging-version";
+
+NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey =
+ @"FirebaseAppDelegateProxyEnabled";
+
+NSString *const kFIRMessagingApplicationSupportSubDirectory = @"Google/FirebaseMessaging";
+
+// Notifications
+NSString *const kFIRMessagingAPNSTokenNotification = @"com.firebase.iid.notif.apns-token";
+NSString *const kFIRMessagingFCMTokenNotification = @"com.firebase.iid.notif.fcm-token";
+NSString *const kFIRMessagingInstanceIDTokenRefreshNotification =
+ @"com.firebase.iid.notif.refresh-token";
+
+const int kFIRMessagingSendTtlDefault = 24 * 60 * 60; // 24 hours
diff --git a/Firebase/Messaging/FIRMessagingContextManagerService.h b/Firebase/Messaging/FIRMessagingContextManagerService.h
new file mode 100644
index 0000000..83e6444
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingContextManagerService.h
@@ -0,0 +1,44 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerCategory;
+FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerLocalTimeStart;
+FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerLocalTimeEnd;
+FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerBodyKey;
+
+@interface FIRMessagingContextManagerService : NSObject
+
+/**
+ * Check if the message is a context manager message or not.
+ *
+ * @param message The message to verify.
+ *
+ * @return YES if the message is a context manager message else NO.
+ */
++ (BOOL)isContextManagerMessage:(NSDictionary *)message;
+
+/**
+ * Handle context manager message.
+ *
+ * @param message The message to handle.
+ *
+ * @return YES if the message was handled successfully else NO.
+ */
++ (BOOL)handleContextManagerMessage:(NSDictionary *)message;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingContextManagerService.m b/Firebase/Messaging/FIRMessagingContextManagerService.m
new file mode 100644
index 0000000..1c9f653
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingContextManagerService.m
@@ -0,0 +1,189 @@
+/*
+ * 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 "FIRMessagingContextManagerService.h"
+
+#import <UIKit/UIKit.h>
+
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+
+#define kFIRMessagingContextManagerPrefixKey @"google.c.cm."
+#define kFIRMessagingContextManagerNotificationKeyPrefix @"gcm.notification."
+
+static NSString *const kLogTag = @"FIRMessagingAnalytics";
+
+static NSString *const kLocalTimeFormatString = @"yyyy-MM-dd HH:mm:ss";
+
+static NSString *const kContextManagerPrefixKey = kFIRMessagingContextManagerPrefixKey;
+
+// Local timed messages (format yyyy-mm-dd HH:mm:ss)
+NSString *const kFIRMessagingContextManagerLocalTimeStart = kFIRMessagingContextManagerPrefixKey @"lt_start";
+NSString *const kFIRMessagingContextManagerLocalTimeEnd = kFIRMessagingContextManagerPrefixKey @"lt_end";
+
+// Local Notification Params
+NSString *const kFIRMessagingContextManagerBodyKey = kFIRMessagingContextManagerNotificationKeyPrefix @"body";
+NSString *const kFIRMessagingContextManagerTitleKey = kFIRMessagingContextManagerNotificationKeyPrefix @"title";
+NSString *const kFIRMessagingContextManagerBadgeKey = kFIRMessagingContextManagerNotificationKeyPrefix @"badge";
+NSString *const kFIRMessagingContextManagerCategoryKey =
+ kFIRMessagingContextManagerNotificationKeyPrefix @"click_action";
+NSString *const kFIRMessagingContextManagerSoundKey = kFIRMessagingContextManagerNotificationKeyPrefix @"sound";
+NSString *const kFIRMessagingContextManagerContentAvailableKey =
+ kFIRMessagingContextManagerNotificationKeyPrefix @"content-available";
+
+typedef NS_ENUM(NSUInteger, FIRMessagingContextManagerMessageType) {
+ FIRMessagingContextManagerMessageTypeNone,
+ FIRMessagingContextManagerMessageTypeLocalTime,
+};
+
+@implementation FIRMessagingContextManagerService
+
++ (BOOL)isContextManagerMessage:(NSDictionary *)message {
+ // For now we only support local time in ContextManager.
+ if (![message[kFIRMessagingContextManagerLocalTimeStart] length]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeContextManagerService000,
+ @"Received message missing local start time, dropped.");
+ return NO;
+ }
+
+ return YES;
+}
+
++ (BOOL)handleContextManagerMessage:(NSDictionary *)message {
+ NSString *startTimeString = message[kFIRMessagingContextManagerLocalTimeStart];
+ if (startTimeString.length) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeContextManagerService001,
+ @"%@ Received context manager message with local time %@", kLogTag,
+ startTimeString);
+ return [self handleContextManagerLocalTimeMessage:message];
+ }
+
+ return NO;
+}
+
++ (BOOL)handleContextManagerLocalTimeMessage:(NSDictionary *)message {
+ NSString *startTimeString = message[kFIRMessagingContextManagerLocalTimeStart];
+ NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
+ [dateFormatter setDateFormat:kLocalTimeFormatString];
+ NSDate *startDate = [dateFormatter dateFromString:startTimeString];
+
+ _FIRMessagingDevAssert(startDate, @"Invalid local start date format %@", startTimeString);
+ if (!startTimeString) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerService002,
+ @"Invalid local start date format %@. Message dropped",
+ startTimeString);
+ return NO;
+ }
+
+ NSDate *currentDate = [NSDate date];
+
+ if ([currentDate compare:startDate] == NSOrderedAscending) {
+ [self scheduleLocalNotificationForMessage:message
+ atDate:startDate];
+ } else {
+ // check end time has not passed
+ NSString *endTimeString = message[kFIRMessagingContextManagerLocalTimeEnd];
+ if (!endTimeString) {
+ FIRMessagingLoggerInfo(
+ kFIRMessagingMessageCodeContextManagerService003,
+ @"No end date specified for message, start date elapsed. Message dropped.");
+ return YES;
+ }
+
+ NSDate *endDate = [dateFormatter dateFromString:endTimeString];
+
+ _FIRMessagingDevAssert(endDate, @"Invalid local end date format %@", endTimeString);
+ if (!endTimeString) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerService004,
+ @"Invalid local end date format %@. Message dropped", endTimeString);
+ return NO;
+ }
+
+ if ([endDate compare:currentDate] == NSOrderedAscending) {
+ // end date has already passed drop the message
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeContextManagerService005,
+ @"End date %@ has already passed. Message dropped.", endTimeString);
+ return YES;
+ }
+
+ // schedule message right now (buffer 10s)
+ [self scheduleLocalNotificationForMessage:message
+ atDate:[currentDate dateByAddingTimeInterval:10]];
+ }
+ return YES;
+}
+
++ (void)scheduleLocalNotificationForMessage:(NSDictionary *)message
+ atDate:(NSDate *)date {
+ NSDictionary *apsDictionary = message;
+ UILocalNotification *notification = [[UILocalNotification alloc] init];
+
+ // A great way to understand timezones and UILocalNotifications
+ // http://stackoverflow.com/questions/18424569/understanding-uilocalnotification-timezone
+ notification.timeZone = [NSTimeZone defaultTimeZone];
+ notification.fireDate = date;
+
+ // In the current solution all of the display stuff goes into a special "aps" dictionary
+ // being sent in the message.
+ if ([apsDictionary[kFIRMessagingContextManagerBodyKey] length]) {
+ notification.alertBody = apsDictionary[kFIRMessagingContextManagerBodyKey];
+ }
+ if ([apsDictionary[kFIRMessagingContextManagerTitleKey] length]) {
+ // |alertTitle| is iOS 8.2+, so check if we can set it
+ if ([notification respondsToSelector:@selector(setAlertTitle:)]) {
+ notification.alertTitle = apsDictionary[kFIRMessagingContextManagerTitleKey];
+ }
+ }
+
+ if (apsDictionary[kFIRMessagingContextManagerSoundKey]) {
+ notification.soundName = apsDictionary[kFIRMessagingContextManagerSoundKey];
+ }
+ if (apsDictionary[kFIRMessagingContextManagerBadgeKey]) {
+ notification.applicationIconBadgeNumber =
+ [apsDictionary[kFIRMessagingContextManagerBadgeKey] integerValue];
+ }
+ if (apsDictionary[kFIRMessagingContextManagerCategoryKey]) {
+ // |category| is iOS 8.0+, so check if we can set it
+ if ([notification respondsToSelector:@selector(setCategory:)]) {
+ notification.category = apsDictionary[kFIRMessagingContextManagerCategoryKey];
+ }
+ }
+
+ NSDictionary *userInfo = [self parseDataFromMessage:message];
+ if (userInfo.count) {
+ notification.userInfo = userInfo;
+ }
+
+ [[UIApplication sharedApplication] scheduleLocalNotification:notification];
+}
+
++ (NSDictionary *)parseDataFromMessage:(NSDictionary *)message {
+ NSMutableDictionary *data = [NSMutableDictionary dictionary];
+ for (NSObject<NSCopying> *key in message) {
+ if ([key isKindOfClass:[NSString class]]) {
+ NSString *keyString = (NSString *)key;
+ if ([keyString isEqualToString:kFIRMessagingContextManagerContentAvailableKey]) {
+ continue;
+ } else if ([keyString hasPrefix:kContextManagerPrefixKey]) {
+ continue;
+ }
+ }
+ data[[key copy]] = message[key];
+ }
+ return [data copy];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingDataMessageManager.h b/Firebase/Messaging/FIRMessagingDataMessageManager.h
new file mode 100644
index 0000000..8eaecc1
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDataMessageManager.h
@@ -0,0 +1,101 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class GtalkDataMessageStanza;
+
+@class FIRMessagingClient;
+@class FIRMessagingConnection;
+@class FIRMessagingReceiver;
+@class FIRMessagingRmqManager;
+@class FIRMessagingSyncMessageManager;
+
+@protocol FIRMessagingDataMessageManagerDelegate <NSObject>
+
+#pragma mark - Downstream Callbacks
+
+/**
+ * Invoked when FIRMessaging receives a downstream message via the MCS connection.
+ * Let's the user know that they have received a new message by invoking the
+ * App's remoteNotification callback.
+ *
+ * @param message The downstream message received by the MCS connection.
+ */
+- (void)didReceiveMessage:(nonnull NSDictionary *)message
+ withIdentifier:(nullable NSString *)messageID;
+
+#pragma mark - Upstream Callbacks
+
+/**
+ * Notify the app that FIRMessaging will soon be sending the upstream message requested by the app.
+ *
+ * @param messageID The messageId passed in by the app to track this particular message.
+ * @param error The error in case FIRMessaging cannot send the message upstream.
+ */
+- (void)willSendDataMessageWithID:(nonnull NSString *)messageID error:(nullable NSError *)error;
+
+/**
+ * Notify the app that FIRMessaging did successfully send it's message via the MCS
+ * connection and the message was successfully delivered.
+ *
+ * @param messageId The messageId passed in by the app to track this particular
+ * message.
+ */
+- (void)didSendDataMessageWithID:(nonnull NSString *)messageId;
+
+#pragma mark - Server Callbacks
+
+/**
+ * Notify the app that FIRMessaging server deleted some messages which exceeded storage limits. This
+ * indicates the "deleted_messages" message type we received from the server.
+ */
+- (void)didDeleteMessagesOnServer;
+
+@end
+
+/**
+ * This manages all of the data messages being sent by the client and also the messages that
+ * were received from the server.
+ */
+@interface FIRMessagingDataMessageManager : NSObject
+
+NS_ASSUME_NONNULL_BEGIN
+
+- (instancetype)initWithDelegate:(id<FIRMessagingDataMessageManagerDelegate>)delegate
+ client:(FIRMessagingClient *)client
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
+ syncMessageManager:(FIRMessagingSyncMessageManager *)syncMessageManager;
+
+- (void)setDeviceAuthID:(NSString *)deviceAuthID secretToken:(NSString *)secretToken;
+
+- (void)refreshDelayedMessages;
+
+#pragma mark - Receive
+
+- (NSDictionary *)processPacket:(GtalkDataMessageStanza *)packet;
+- (void)didReceiveParsedMessage:(NSDictionary *)message;
+
+#pragma mark - Send
+
+- (void)sendDataMessageStanza:(NSMutableDictionary *)dataMessage;
+- (void)didSendDataMessageStanza:(GtalkDataMessageStanza *)message;
+
+- (void)resendMessagesWithConnection:(FIRMessagingConnection *)connection;
+
+NS_ASSUME_NONNULL_END
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingDataMessageManager.m b/Firebase/Messaging/FIRMessagingDataMessageManager.m
new file mode 100644
index 0000000..2433bd4
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDataMessageManager.m
@@ -0,0 +1,545 @@
+/*
+ * 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 "FIRMessagingDataMessageManager.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingClient.h"
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingDelayedMessageQueue.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingReceiver.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessaging_Private.h"
+#import "FIRMessagingSyncMessageManager.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+// The Notification used to send InstanceID messages that FIRMessaging receives.
+static NSString *const NOTIFICATION_IID_MESSAGE = @"com.google.gcm/notification/iid";
+
+static const int kMaxAppDataSizeDefault = 4 * 1024; // 4k
+static const int kMinDelaySeconds = 1; // 1 second
+static const int kMaxDelaySeconds = 60 * 60; // 1 hour
+
+static NSString *const kFromForInstanceIDMessages = @"google.com/iid";
+static NSString *const kFromForFIRMessagingMessages = @"mcs.android.com";
+static NSString *const kGSFMessageCategory = @"com.google.android.gsf.gtalkservice";
+// TODO: Update Gcm to FIRMessaging in the constants below
+static NSString *const kFCMMessageCategory = @"com.google.gcm";
+static NSString *const kMessageReservedPrefix = @"google.";
+
+static NSString *const kFCMMessageSpecialMessage = @"message_type";
+
+// special messages sent by the server
+static NSString *const kFCMMessageTypeDeletedMessages = @"deleted_messages";
+
+static NSString *const kMCSNotificationPrefix = @"gcm.notification.";
+static NSString *const kDataMessageNotificationKey = @"notification";
+
+
+typedef NS_ENUM(int8_t, UpstreamForceReconnect) {
+ // Never force reconnect on upstream messages
+ kUpstreamForceReconnectOff = 0,
+ // Force reconnect for TTL=0 upstream messages
+ kUpstreamForceReconnectTTL0 = 1,
+ // Force reconnect for all upstream messages
+ kUpstreamForceReconnectAll = 2,
+};
+
+@interface FIRMessagingDataMessageManager ()
+
+@property(nonatomic, readwrite, weak) FIRMessagingClient *client;
+@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
+@property(nonatomic, readwrite, weak) FIRMessagingSyncMessageManager *syncMessageManager;
+@property(nonatomic, readwrite, weak) id<FIRMessagingDataMessageManagerDelegate> delegate;
+@property(nonatomic, readwrite, strong) FIRMessagingDelayedMessageQueue *delayedMessagesQueue;
+
+@property(nonatomic, readwrite, assign) int ttl;
+@property(nonatomic, readwrite, copy) NSString *deviceAuthID;
+@property(nonatomic, readwrite, copy) NSString *secretToken;
+@property(nonatomic, readwrite, assign) int maxAppDataSize;
+@property(nonatomic, readwrite, assign) UpstreamForceReconnect upstreamForceReconnect;
+
+@end
+
+@implementation FIRMessagingDataMessageManager
+
+- (instancetype)initWithDelegate:(id<FIRMessagingDataMessageManagerDelegate>)delegate
+ client:(FIRMessagingClient *)client
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
+ syncMessageManager:(FIRMessagingSyncMessageManager *)syncMessageManager {
+ self = [super init];
+ if (self) {
+ _delegate = delegate;
+ _client = client;
+ _rmq2Manager = rmq2Manager;
+ _syncMessageManager = syncMessageManager;
+ _ttl = kFIRMessagingSendTtlDefault;
+ _maxAppDataSize = kMaxAppDataSizeDefault;
+ // on by default
+ _upstreamForceReconnect = kUpstreamForceReconnectAll;
+ }
+ return self;
+}
+
+- (void)setDeviceAuthID:(NSString *)deviceAuthID secretToken:(NSString *)secretToken {
+ _FIRMessagingDevAssert([deviceAuthID length] && [secretToken length],
+ @"Invalid credentials for FIRMessaging");
+ self.deviceAuthID = deviceAuthID;
+ self.secretToken = secretToken;
+}
+
+- (void)refreshDelayedMessages {
+ FIRMessaging_WEAKIFY(self);
+ self.delayedMessagesQueue =
+ [[FIRMessagingDelayedMessageQueue alloc] initWithRmqScanner:self.rmq2Manager
+ sendDelayedMessagesHandler:^(NSArray *messages) {
+ FIRMessaging_STRONGIFY(self);
+ [self sendDelayedMessages:messages];
+ }];
+}
+
+- (NSDictionary *)processPacket:(GtalkDataMessageStanza *)dataMessage {
+ NSString *category = dataMessage.category;
+ NSString *from = dataMessage.from;
+ if ([kFCMMessageCategory isEqualToString:category] ||
+ [kGSFMessageCategory isEqualToString:category]) {
+ [self handleMCSDataMessage:dataMessage];
+ return nil;
+ } else if ([kFromForFIRMessagingMessages isEqualToString:from]) {
+ [self handleMCSDataMessage:dataMessage];
+ return nil;
+ } else if ([kFromForInstanceIDMessages isEqualToString:from]) {
+ // send message to InstanceID library.
+ NSMutableDictionary *message = [NSMutableDictionary dictionary];
+ for (GtalkAppData *item in dataMessage.appDataArray) {
+ _FIRMessagingDevAssert(item.key && item.value, @"Invalid app data item");
+ if (item.key && item.value) {
+ message[item.key] = item.value;
+ }
+ }
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_IID_MESSAGE
+ object:message];
+ return nil;
+ }
+
+ return [self parseDataMessage:dataMessage];
+}
+
+- (void)handleMCSDataMessage:(GtalkDataMessageStanza *)dataMessage {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager000,
+ @"Received message for FIRMessaging from downstream %@", dataMessage);
+}
+
+- (NSDictionary *)parseDataMessage:(GtalkDataMessageStanza *)dataMessage {
+ NSMutableDictionary *message = [NSMutableDictionary dictionary];
+ NSString *from = [dataMessage from];
+ if ([from length]) {
+ message[kFIRMessagingFromKey] = from;
+ }
+
+ // raw data
+ NSData *rawData = [dataMessage rawData];
+ if ([rawData length]) {
+ message[kFIRMessagingRawDataKey] = rawData;
+ }
+
+ NSString *token = [dataMessage token];
+ if ([token length]) {
+ message[kFIRMessagingCollapseKey] = token;
+ }
+
+ // Add the persistent_id. This would be removed later before sending the message to the device.
+ NSString *persistentID = [dataMessage persistentId];
+ _FIRMessagingDevAssert([persistentID length], @"Invalid MCS message without persistentID");
+ if ([persistentID length]) {
+ message[kFIRMessagingMessageIDKey] = persistentID;
+ }
+
+ // third-party data
+ for (GtalkAppData *item in dataMessage.appDataArray) {
+ _FIRMessagingDevAssert(item.hasKey && item.hasValue, @"Invalid AppData");
+
+ // do not process the "from" key -- is not useful
+ if ([kFIRMessagingFromKey isEqualToString:item.key]) {
+ continue;
+ }
+
+ // Filter the "gcm.notification." keys in the message
+ if ([item.key hasPrefix:kMCSNotificationPrefix]) {
+ NSString *key = [item.key substringFromIndex:[kMCSNotificationPrefix length]];
+ if ([key length]) {
+ if (!message[kDataMessageNotificationKey]) {
+ message[kDataMessageNotificationKey] = [NSMutableDictionary dictionary];
+ }
+ message[kDataMessageNotificationKey][key] = item.value;
+ } else {
+ _FIRMessagingDevAssert([key length], @"Invalid key in MCS message: %@", key);
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager001,
+ @"Invalid key in MCS message: %@", key);
+ }
+ continue;
+ }
+
+ // Filter the "gcm.duplex" key
+ if ([item.key isEqualToString:kFIRMessagingMessageSyncViaMCSKey]) {
+ BOOL value = [item.value boolValue];
+ message[kFIRMessagingMessageSyncViaMCSKey] = @(value);
+ continue;
+ }
+
+ // do not allow keys with "reserved" keyword
+ if ([[item.key lowercaseString] hasPrefix:kMessageReservedPrefix]) {
+ continue;
+ }
+
+ [message setObject:item.value forKey:item.key];
+ }
+ // TODO: Add support for encrypting raw data later
+ return [NSDictionary dictionaryWithDictionary:message];
+}
+
+- (void)didReceiveParsedMessage:(NSDictionary *)message {
+ if ([message[kFCMMessageSpecialMessage] length]) {
+ NSString *messageType = message[kFCMMessageSpecialMessage];
+ if ([kFCMMessageTypeDeletedMessages isEqualToString:messageType]) {
+ // TODO: Maybe trim down message to remove some unnecessary fields.
+ // tell the FCM receiver of deleted messages
+ [self.delegate didDeleteMessagesOnServer];
+ return;
+ }
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager002,
+ @"Invalid message type received: %@", messageType);
+ } else if (message[kFIRMessagingMessageSyncViaMCSKey]) {
+ // Update SYNC_RMQ with the message
+ BOOL isDuplicate = [self.syncMessageManager didReceiveMCSSyncMessage:message];
+ if (isDuplicate) {
+ return;
+ }
+ }
+ NSString *messageId = message[kFIRMessagingMessageIDKey];
+ NSDictionary *filteredMessage = [self filterInternalFIRMessagingKeysFromMessage:message];
+ [self.delegate didReceiveMessage:filteredMessage withIdentifier:messageId];
+}
+
+- (NSDictionary *)filterInternalFIRMessagingKeysFromMessage:(NSDictionary *)message {
+ NSMutableDictionary *newMessage = [NSMutableDictionary dictionaryWithDictionary:message];
+ for (NSString *key in message) {
+ if ([key hasPrefix:kFIRMessagingMessageInternalReservedKeyword]) {
+ [newMessage removeObjectForKey:key];
+ }
+ }
+ return [newMessage copy];
+}
+
+- (void)sendDataMessageStanza:(NSMutableDictionary *)dataMessage {
+ NSNumber *ttlNumber = dataMessage[kFIRMessagingSendTTL];
+ NSString *to = dataMessage[kFIRMessagingSendTo];
+ NSString *msgId = dataMessage[kFIRMessagingSendMessageID];
+ NSString *appPackage = [self categoryForUpstreamMessages];
+ GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
+
+ // TODO: enforce TTL (right now only ttl=0 is special, means no storage)
+ int ttl = [ttlNumber intValue];
+ if (ttl < 0 || ttl > self.ttl) {
+ ttl = self.ttl;
+ }
+ [stanza setTtl:ttl];
+ [stanza setSent:FIRMessagingCurrentTimestampInSeconds()];
+
+ int delay = [self delayForMessage:dataMessage];
+ if (delay > 0) {
+ [stanza setMaxDelay:delay];
+ }
+
+ if (msgId) {
+ [stanza setId_p:msgId];
+ }
+
+ // collapse key as given by the sender
+ NSString *token = dataMessage[KFIRMessagingSendMessageAppData][kFIRMessagingCollapseKey];
+ if ([token length]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager003,
+ @"FIRMessaging using %@ as collapse key", token);
+ [stanza setToken:token];
+ }
+
+ if (!self.secretToken) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager004,
+ @"Trying to send data message without a secret token. "
+ @"Authentication failed.");
+ [self willSendDataMessageFail:stanza
+ withMessageId:msgId
+ error:kFIRMessagingErrorCodeMissingDeviceID];
+ return;
+ }
+
+ if (![to length]) {
+ [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorMissingTo];
+ return;
+ }
+ [stanza setTo:to];
+ [stanza setCategory:appPackage];
+ // required field in the proto this is set by the server
+ // set it to a sentinel so the runtime doesn't throw an exception
+ [stanza setFrom:@""];
+
+ // MCS itself would set the registration ID
+ // [stanza setRegId:nil];
+
+ int size = [self addData:dataMessage[KFIRMessagingSendMessageAppData] toStanza:stanza];
+ if (size > kMaxAppDataSizeDefault) {
+ [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSizeExceeded];
+ return;
+ }
+
+ BOOL useRmq = (ttl != 0) && (msgId != nil);
+ if (useRmq) {
+ if (!self.client.isConnected) {
+ // do nothing assuming rmq save is enabled
+ }
+
+ NSError *error;
+ if (![self.rmq2Manager saveRmqMessage:stanza error:&error]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager005, @"%@", error);
+ [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSave];
+ return;
+ }
+
+ [self willSendDataMessageSuccess:stanza withMessageId:msgId];
+ }
+
+ // if delay > 0 we don't really care about sending the message right now
+ // so we piggy-back on any other urgent(delay = 0) message that we are sending
+ if (delay > 0 && [self delayMessage:stanza]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager006, @"Delaying Message %@",
+ dataMessage);
+ return;
+ }
+ // send delayed messages
+ [self sendDelayedMessages:[self.delayedMessagesQueue removeDelayedMessages]];
+
+ BOOL sending = [self tryToSendDataMessageStanza:stanza];
+ if (!sending) {
+ if (useRmq) {
+ NSString *event __unused = [NSString stringWithFormat:@"Queued message: %@", [stanza id_p]];
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager007, @"%@", event);
+ } else {
+ [self willSendDataMessageFail:stanza
+ withMessageId:msgId
+ error:kFIRMessagingErrorCodeNetwork];
+ return;
+ }
+ }
+}
+
+- (void)sendDelayedMessages:(NSArray *)delayedMessages {
+ for (GtalkDataMessageStanza *message in delayedMessages) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager008,
+ @"%@ Sending delayed message %@", @"DMM", message);
+ [message setActualDelay:(int)(FIRMessagingCurrentTimestampInSeconds() - message.sent)];
+ [self tryToSendDataMessageStanza:message];
+ }
+}
+
+- (void)didSendDataMessageStanza:(GtalkDataMessageStanza *)message {
+ NSString *msgId = [message id_p] ?: @"";
+ [self.delegate didSendDataMessageWithID:msgId];
+}
+
+- (void)addParamWithKey:(NSString *)key
+ value:(NSString *)val
+ toStanza:(GtalkDataMessageStanza *)stanza {
+ if (!key || !val) {
+ return;
+ }
+ GtalkAppData *appData = [[GtalkAppData alloc] init];
+ [appData setKey:key];
+ [appData setValue:val];
+ [[stanza appDataArray] addObject:appData];
+}
+
+/**
+ @return The size of the data being added to stanza.
+ */
+- (int)addData:(NSDictionary *)data toStanza:(GtalkDataMessageStanza *)stanza {
+ int size = 0;
+ for (NSString *key in data) {
+ NSObject *val = data[key];
+ if ([val isKindOfClass:[NSString class]]) {
+ NSString *strVal = (NSString *)val;
+ [self addParamWithKey:key value:strVal toStanza:stanza];
+ size += [key length] + [strVal length];
+ } else if ([val isKindOfClass:[NSNumber class]]) {
+ NSString *strVal = [(NSNumber *)val stringValue];
+ [self addParamWithKey:key value:strVal toStanza:stanza];
+ size += [key length] + [strVal length];
+ } else if ([kFIRMessagingRawDataKey isEqualToString:key] &&
+ [val isKindOfClass:[NSData class]]) {
+ NSData *rawData = (NSData *)val;
+ [stanza setRawData:[rawData copy]];
+ size += [rawData length];
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager009, @"Ignoring key: %@",
+ key);
+ }
+ }
+ return size;
+}
+
+/**
+ * Notify the messenger that send data message completed with success. This is called for
+ * TTL=0, after the message has been sent, or when message is saved, to unlock the send()
+ * method.
+ */
+- (void)willSendDataMessageSuccess:(GtalkDataMessageStanza *)stanza
+ withMessageId:(NSString *)messageId {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager010,
+ @"send message success: %@", messageId);
+ [self.delegate willSendDataMessageWithID:messageId error:nil];
+}
+
+/**
+ * We send 'send failures' from server as normal FIRMessaging messages, with a 'message_type'
+ * extra - same as 'message deleted'.
+ *
+ * For TTL=0 or errors that can be detected during send ( too many messages, invalid, etc)
+ * we throw IOExceptions
+ */
+- (void)willSendDataMessageFail:(GtalkDataMessageStanza *)stanza
+ withMessageId:(NSString *)messageId
+ error:(FIRMessagingInternalErrorCode)errorCode {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager011,
+ @"Send message fail: %@ error: %lu", messageId, (unsigned long)errorCode);
+
+ NSError *error = [NSError errorWithFCMErrorCode:errorCode];
+ if ([self.delegate respondsToSelector:@selector(willSendDataMessageWithID:error:)]) {
+ [self.delegate willSendDataMessageWithID:messageId error:error];
+ }
+}
+
+- (void)resendMessagesWithConnection:(FIRMessagingConnection *)connection {
+ NSMutableString *rmqIdsResent = [NSMutableString string];
+ NSMutableArray *toRemoveRmqIds = [NSMutableArray array];
+ FIRMessaging_WEAKIFY(self);
+ FIRMessaging_WEAKIFY(connection);
+ FIRMessagingRmqMessageHandler messageHandler = ^(int64_t rmqId, int8_t tag, NSData *data) {
+ FIRMessaging_STRONGIFY(self);
+ FIRMessaging_STRONGIFY(connection);
+ GPBMessage *proto =
+ [FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data error:NULL];
+ if ([proto isKindOfClass:GtalkDataMessageStanza.class]) {
+ GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto;
+
+ if (![self handleExpirationForDataMessage:stanza]) {
+ // time expired let's delete from RMQ
+ [toRemoveRmqIds addObject:stanza.persistentId];
+ return;
+ }
+ [rmqIdsResent appendString:[NSString stringWithFormat:@"%@,", stanza.id_p]];
+ }
+
+ [connection sendProto:proto];
+ };
+ [self.rmq2Manager scanWithRmqMessageHandler:messageHandler
+ dataMessageHandler:nil];
+
+ if ([rmqIdsResent length]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager012, @"Resent: %@",
+ rmqIdsResent);
+ }
+
+ if ([toRemoveRmqIds count]) {
+ [self.rmq2Manager removeRmqMessagesWithRmqIds:toRemoveRmqIds];
+ }
+}
+
+/**
+ * Check the TTL and generate an error if needed.
+ *
+ * @return false if the message needs to be deleted
+ */
+- (BOOL)handleExpirationForDataMessage:(GtalkDataMessageStanza *)message {
+ if (message.ttl == 0) {
+ return NO;
+ }
+
+ int64_t now = FIRMessagingCurrentTimestampInSeconds();
+ if (now > message.sent + message.ttl) {
+ [self willSendDataMessageFail:message
+ withMessageId:message.id_p
+ error:kFIRMessagingErrorServiceNotAvailable];
+ return NO;
+ }
+ return YES;
+}
+
+#pragma mark - Private
+
+- (int)delayForMessage:(NSMutableDictionary *)message {
+ int delay = 0; // default
+ if (message[kFIRMessagingSendDelay]) {
+ delay = [message[kFIRMessagingSendDelay] intValue];
+ [message removeObjectForKey:kFIRMessagingSendDelay];
+ if (delay < kMinDelaySeconds) {
+ delay = 0;
+ } else if (delay > kMaxDelaySeconds) {
+ delay = kMaxDelaySeconds;
+ }
+ }
+ return delay;
+}
+
+// return True if successfully delayed else False
+- (BOOL)delayMessage:(GtalkDataMessageStanza *)message {
+ return [self.delayedMessagesQueue queueMessage:message];
+}
+
+- (BOOL)tryToSendDataMessageStanza:(GtalkDataMessageStanza *)stanza {
+ if (self.client.isConnectionActive) {
+ [self.client sendMessage:stanza];
+ return YES;
+ }
+
+ // if we only reconnect for TTL = 0 messages check if we ttl = 0 or
+ // if we reconnect for all messages try to reconnect
+ if ((self.upstreamForceReconnect == kUpstreamForceReconnectTTL0 && stanza.ttl == 0) ||
+ self.upstreamForceReconnect == kUpstreamForceReconnectAll) {
+ BOOL isNetworkAvailable = [[FIRMessaging messaging] isNetworkAvailable];
+ if (isNetworkAvailable) {
+ if (stanza.ttl == 0) {
+ // Add TTL = 0 messages to be sent on next connect. TTL != 0 messages are
+ // persisted, and will be sent from the RMQ.
+ [self.client sendOnConnectOrDrop:stanza];
+ }
+
+ [self.client retryConnectionImmediately:YES];
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (NSString *)categoryForUpstreamMessages {
+ return FIRMessagingAppIdentifier();
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingDefines.h b/Firebase/Messaging/FIRMessagingDefines.h
new file mode 100644
index 0000000..36448ed
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDefines.h
@@ -0,0 +1,96 @@
+/*
+ * 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 FIRMessaging_xcodeproj_FIRMessagingDefines_h
+#define FIRMessaging_xcodeproj_FIRMessagingDefines_h
+
+#define _FIRMessaging_VERBOSE_LOGGING 1
+
+// Verbose Logging
+#if (_FIRMessaging_VERBOSE_LOGGING)
+#define FIRMessaging_DEV_VERBOSE_LOG(...) NSLog(__VA_ARGS__)
+#else
+#define FIRMessaging_DEV_VERBOSE_LOG(...) do { } while (0)
+#endif // FIRMessaging_VERBOSE_LOGGING
+
+
+// FIRMessaging_FAIL
+#ifdef DEBUG
+#define FIRMessaging_FAIL(format, ...) \
+do { \
+ NSLog(format, ##__VA_ARGS__); \
+ __builtin_trap(); \
+} while (false)
+#else
+#define FIRMessaging_FAIL(...) do { } while (0)
+#endif
+
+
+// WEAKIFY & STRONGIFY
+// Helper macro.
+#define _FIRMessaging_WEAKNAME(VAR) VAR ## _weak_
+
+#define FIRMessaging_WEAKIFY(VAR) __weak __typeof__(VAR) _FIRMessaging_WEAKNAME(VAR) = (VAR);
+
+#define FIRMessaging_STRONGIFY(VAR) \
+_Pragma("clang diagnostic push") \
+_Pragma("clang diagnostic ignored \"-Wshadow\"") \
+__strong __typeof__(VAR) VAR = _FIRMessaging_WEAKNAME(VAR); \
+_Pragma("clang diagnostic pop")
+
+
+// Type Conversions (used for NSInteger etc)
+#ifndef _FIRMessaging_L
+#define _FIRMessaging_L(v) (long)(v)
+#endif
+
+#ifndef _FIRMessaging_UL
+#define _FIRMessaging_UL(v) (unsigned long)(v)
+#endif
+
+#endif
+
+// Debug Assert
+#ifndef _FIRMessagingDevAssert
+// 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 _FIRMessagingDevAssert(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 _FIRMessagingDevAssert(condition, ...) do { } while (0)
+#endif // !defined(NS_BLOCK_ASSERTIONS)
+
+#endif // _FIRMessagingDevAssert
+
+// Invalidates the initializer from which it's called.
+#ifndef FIRMessagingInvalidateInitializer
+#define FIRMessagingInvalidateInitializer() \
+ do { \
+ [self class]; /* Avoid warning of dead store to |self|. */ \
+ _FIRMessagingDevAssert(NO, @"Invalid initializer."); \
+ return nil; \
+ } while (0)
+#endif
diff --git a/Firebase/Messaging/FIRMessagingDelayedMessageQueue.h b/Firebase/Messaging/FIRMessagingDelayedMessageQueue.h
new file mode 100644
index 0000000..d20ec91
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDelayedMessageQueue.h
@@ -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 <Foundation/Foundation.h>
+
+@class GtalkDataMessageStanza;
+@class FIRMessagingRmqManager;
+
+@protocol FIRMessagingRmqScanner;
+
+typedef void(^FIRMessagingSendDelayedMessagesHandler)(NSArray *messages);
+
+@interface FIRMessagingDelayedMessageQueue : NSObject
+
+- (instancetype)initWithRmqScanner:(id<FIRMessagingRmqScanner>)rmqScanner
+ sendDelayedMessagesHandler:(FIRMessagingSendDelayedMessagesHandler)sendDelayedMessagesHandler;
+
+- (BOOL)queueMessage:(GtalkDataMessageStanza *)message;
+
+- (NSArray *)removeDelayedMessages;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingDelayedMessageQueue.m b/Firebase/Messaging/FIRMessagingDelayedMessageQueue.m
new file mode 100644
index 0000000..0371c02
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDelayedMessageQueue.m
@@ -0,0 +1,146 @@
+/*
+ * 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 "FIRMessagingDelayedMessageQueue.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingUtilities.h"
+
+static const int kMaxQueuedMessageCount = 10;
+
+@interface FIRMessagingDelayedMessageQueue ()
+
+@property(nonatomic, readonly, weak) id<FIRMessagingRmqScanner> rmqScanner;
+@property(nonatomic, readonly, copy) FIRMessagingSendDelayedMessagesHandler sendDelayedMessagesHandler;
+
+@property(nonatomic, readwrite, assign) int persistedMessageCount;
+// the scheduled timeout or -1 if not set
+@property(nonatomic, readwrite, assign) int64_t scheduledTimeoutMilliseconds;
+// The time of the last scan of the message DB,
+// used to avoid retrieving messages more than once.
+@property(nonatomic, readwrite, assign) int64_t lastDBScanTimestampSeconds;
+
+@property(nonatomic, readwrite, strong) NSMutableArray *messages;
+@property(nonatomic, readwrite, strong) NSTimer *sendTimer;
+
+@end
+
+@implementation FIRMessagingDelayedMessageQueue
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithRmqScanner:(id<FIRMessagingRmqScanner>)rmqScanner
+ sendDelayedMessagesHandler:(FIRMessagingSendDelayedMessagesHandler)sendDelayedMessagesHandler {
+ _FIRMessagingDevAssert(sendDelayedMessagesHandler, @"Invalid nil callback for delayed messages");
+ self = [super init];
+ if (self) {
+ _rmqScanner = rmqScanner;
+ _sendDelayedMessagesHandler = sendDelayedMessagesHandler;
+ _messages = [NSMutableArray arrayWithCapacity:10];
+ _scheduledTimeoutMilliseconds = -1;
+ }
+ return self;
+}
+
+- (BOOL)queueMessage:(GtalkDataMessageStanza *)message {
+ if (self.messages.count >= kMaxQueuedMessageCount) {
+ return NO;
+ }
+ if (message.ttl == 0) {
+ // ttl=0 messages aren't persisted, add it to memory
+ [self.messages addObject:message];
+ } else {
+ self.persistedMessageCount++;
+ }
+ int64_t timeoutMillis = [self calculateTimeoutInMillisWithDelayInSeconds:message.maxDelay];
+ if (![self isTimeoutScheduled] || timeoutMillis < self.scheduledTimeoutMilliseconds) {
+ [self scheduleTimeoutInMillis:timeoutMillis];
+ }
+ return YES;
+}
+
+- (NSArray *)removeDelayedMessages {
+ [self cancelTimeout];
+ if ([self messageCount] == 0) {
+ return @[];
+ }
+
+ NSMutableArray *delayedMessages = [NSMutableArray array];
+ // add the ttl=0 messages
+ if (self.messages.count) {
+ [delayedMessages addObjectsFromArray:delayedMessages];
+ [self.messages removeAllObjects];
+ }
+
+ // add persistent messages
+ if (self.persistedMessageCount > 0) {
+ FIRMessaging_WEAKIFY(self);
+ [self.rmqScanner scanWithRmqMessageHandler:nil
+ dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+ FIRMessaging_STRONGIFY(self);
+ if ([stanza hasMaxDelay] &&
+ [stanza sent] >= self.lastDBScanTimestampSeconds) {
+ [delayedMessages addObject:stanza];
+ }
+ }];
+ self.lastDBScanTimestampSeconds = FIRMessagingCurrentTimestampInSeconds();
+ self.persistedMessageCount = 0;
+ }
+ return delayedMessages;
+}
+
+- (void)sendMessages {
+ if (self.sendDelayedMessagesHandler) {
+ self.sendDelayedMessagesHandler([self removeDelayedMessages]);
+ }
+}
+
+#pragma mark - Private
+
+- (NSInteger)messageCount {
+ return self.messages.count + self.persistedMessageCount;
+}
+
+- (BOOL)isTimeoutScheduled {
+ return self.scheduledTimeoutMilliseconds > 0;
+}
+
+- (int64_t)calculateTimeoutInMillisWithDelayInSeconds:(int)delay {
+ return FIRMessagingCurrentTimestampInMilliseconds() + delay * 1000.0;
+}
+
+- (void)scheduleTimeoutInMillis:(int64_t)time {
+ [self cancelTimeout];
+ self.scheduledTimeoutMilliseconds = time;
+ double delay = (time - FIRMessagingCurrentTimestampInMilliseconds()) / 1000.0;
+ [self performSelector:@selector(sendMessages) withObject:self afterDelay:delay];
+}
+
+- (void)cancelTimeout {
+ if ([self isTimeoutScheduled]) {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(sendMessages)
+ object:nil];
+ self.scheduledTimeoutMilliseconds = -1;
+ }
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingFileLogger.h b/Firebase/Messaging/FIRMessagingFileLogger.h
new file mode 100644
index 0000000..ec11369
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingFileLogger.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 "FIRMessagingLogger.h"
+
+#if FIRMessaging_PROBER
+@interface FIRMessagingFileLogFilter : NSObject <FIRMessagingLogFilter>
+
+@end
+
+@interface FIRMessagingFileLogFormatter : NSObject <FIRMessagingLogFormatter>
+
+@end
+
+@interface FIRMessagingFileLogWriter : NSObject <FIRMessagingLogWriter>
+
+@end
+#endif
diff --git a/Firebase/Messaging/FIRMessagingFileLogger.m b/Firebase/Messaging/FIRMessagingFileLogger.m
new file mode 100644
index 0000000..7570a79
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingFileLogger.m
@@ -0,0 +1,108 @@
+/*
+ * 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 "FIRMessagingFileLogger.h"
+
+#if FIRMessaging_PROBER
+
+#import "DDFileLogger.h"
+#import "DDLog.h"
+
+@interface FIRMessagingFileLogFilter ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingLogLevel level;
+@end
+
+@implementation FIRMessagingFileLogFilter
+
+#pragma mark - GTMLogFilter protocol
+
+- (BOOL)filterAllowsMessage:(NSString *)msg level:(FIRMessagingLogLevel)level {
+ // allow everything
+ return YES;
+}
+
+@end
+
+@interface FIRMessagingFileLogFormatter ()
+
+@property(nonatomic, readwrite, strong) NSDateFormatter *dateFormatter;
+
+@end
+
+@implementation FIRMessagingFileLogFormatter
+
+static NSString *const kFIRMessagingLogPrefix = @"FIRMessaging";
+
+- (id)init {
+ if ((self = [super init])) {
+ _dateFormatter = [[NSDateFormatter alloc] init];
+ [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
+ [_dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"];
+ }
+ return self;
+}
+
+#pragma mark - GTMLogFormatter protocol
+
+static DDLogMessage *currentMessage;
+- (NSString *)stringForFunc:(NSString *)func
+ withFormat:(NSString *)fmt
+ valist:(va_list)args
+ level:(FIRMessagingLogLevel)level {
+ NSString *logMessage = [[NSString alloc] initWithFormat:fmt arguments:args];
+ currentMessage = [[DDLogMessage alloc] initWithMessage:logMessage
+ level:0
+ flag:0
+ context:0
+ file:NULL
+ function:NULL
+ line:0
+ tag:0
+ options:0
+ timestamp:[NSDate date]];
+ return logMessage;
+}
+
+@end
+
+@interface FIRMessagingFileLogWriter ()
+
+@property(nonatomic, readwrite, strong) DDFileLogger *fileLogger;
+
+@end
+
+@implementation FIRMessagingFileLogWriter
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _fileLogger = [[DDFileLogger alloc] init];
+ }
+ return self;
+}
+
+#pragma mark - GTMLogWriter protocol
+
+- (void)logMessage:(NSString *)msg level:(FIRMessagingLogLevel)level {
+ // log to stdout
+ NSLog(@"%@", msg);
+ [self.fileLogger logMessage:currentMessage];
+}
+
+@end
+
+#endif
diff --git a/Firebase/Messaging/FIRMessagingInstanceIDProxy.h b/Firebase/Messaging/FIRMessagingInstanceIDProxy.h
new file mode 100644
index 0000000..b7ebd4b
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingInstanceIDProxy.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 <Foundation/Foundation.h>
+
+typedef void(^FIRMessagingInstanceIDProxyTokenHandler)(NSString * __nullable token,
+ NSError * __nullable error);
+
+typedef void(^FIRMessagingInstanceIDProxyDeleteTokenHandler)(NSError * __nullable error);
+
+typedef NS_ENUM(NSInteger, FIRMessagingInstanceIDProxyAPNSTokenType) {
+ /// Unknown token type.
+ FIRMessagingInstanceIDProxyAPNSTokenTypeUnknown,
+ /// Sandbox token type.
+ FIRMessagingInstanceIDProxyAPNSTokenTypeSandbox,
+ /// Production token type.
+ FIRMessagingInstanceIDProxyAPNSTokenTypeProd,
+};
+
+/**
+ * FIRMessaging cannot always depend on FIRInstanceID directly, due to how FIRMessaging is
+ * packaged. To make it easier to make calls to FIRInstanceID, this proxy class, will provide
+ * method names duplicated from FIRInstanceID, while using reflection-based called to proxy
+ * the requests.
+ */
+@interface FIRMessagingInstanceIDProxy : NSObject
+
+- (void)setAPNSToken:(nonnull NSData *)token type:(FIRMessagingInstanceIDProxyAPNSTokenType)type;
+
+#pragma mark - Tokens
+
+- (nullable NSString *)token;
+
+- (void)tokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity
+ scope:(nonnull NSString *)scope
+ options:(nullable NSDictionary *)options
+ handler:(nonnull FIRMessagingInstanceIDProxyTokenHandler)handler;
+
+- (void)deleteTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity
+ scope:(nonnull NSString *)scope
+ handler:
+ (nonnull FIRMessagingInstanceIDProxyDeleteTokenHandler)handler;
+@end
diff --git a/Firebase/Messaging/FIRMessagingInstanceIDProxy.m b/Firebase/Messaging/FIRMessagingInstanceIDProxy.m
new file mode 100644
index 0000000..01b4e73
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingInstanceIDProxy.m
@@ -0,0 +1,123 @@
+/*
+ * 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 "FIRMessagingInstanceIDProxy.h"
+
+@implementation FIRMessagingInstanceIDProxy
+
++ (nonnull instancetype)instanceIDProxy {
+ static id proxyInstanceID = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ Class instanceIDClass = NSClassFromString(@"FIRInstanceID");
+ if (!instanceIDClass) {
+ proxyInstanceID = nil;
+ return;
+ }
+ SEL instanceIDSelector = NSSelectorFromString(@"instanceID");
+ if (![instanceIDClass respondsToSelector:instanceIDSelector]) {
+ proxyInstanceID = nil;
+ return;
+ }
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ proxyInstanceID = [instanceIDClass performSelector:instanceIDSelector];
+#pragma clang diagnostic pop
+ });
+ return (FIRMessagingInstanceIDProxy *)proxyInstanceID;
+
+}
+
+- (void)setAPNSToken:(nonnull NSData *)token
+ type:(FIRMessagingInstanceIDProxyAPNSTokenType)type {
+ id proxy = [[self class] instanceIDProxy];
+
+ SEL setAPNSTokenSelector = NSSelectorFromString(@"setAPNSToken:type:");
+ if (![proxy respondsToSelector:setAPNSTokenSelector]) {
+ return;
+ }
+ // Since setAPNSToken takes a scalar value, use NSInvocation
+ NSMethodSignature *methodSignature =
+ [[proxy class] instanceMethodSignatureForSelector:setAPNSTokenSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
+ invocation.selector = setAPNSTokenSelector;
+ invocation.target = proxy;
+ [invocation setArgument:&token atIndex:2];
+ [invocation setArgument:&type atIndex:3];
+ [invocation invoke];
+}
+
+#pragma mark - Tokens
+
+- (nullable NSString *)token {
+ id proxy = [[self class] instanceIDProxy];
+ SEL getTokenSelector = NSSelectorFromString(@"token");
+ if (![proxy respondsToSelector:getTokenSelector]) {
+ return nil;
+ }
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ return [proxy performSelector:getTokenSelector];
+#pragma clang diagnostic pop
+}
+
+
+- (void)tokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity
+ scope:(nonnull NSString *)scope
+ options:(nullable NSDictionary *)options
+ handler:(nonnull FIRMessagingInstanceIDProxyTokenHandler)handler {
+
+ id proxy = [[self class] instanceIDProxy];
+ SEL getTokenSelector = NSSelectorFromString(@"tokenWithAuthorizedEntity:scope:options:handler:");
+ if (![proxy respondsToSelector:getTokenSelector]) {
+ return;
+ }
+ // Since there are >2 arguments, use NSInvocation
+ NSMethodSignature *methodSignature =
+ [[proxy class] instanceMethodSignatureForSelector:getTokenSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
+ invocation.selector = getTokenSelector;
+ invocation.target = proxy;
+ [invocation setArgument:&authorizedEntity atIndex:2];
+ [invocation setArgument:&scope atIndex:3];
+ [invocation setArgument:&options atIndex:4];
+ [invocation setArgument:&handler atIndex:5];
+ [invocation invoke];
+}
+
+- (void)deleteTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity
+ scope:(nonnull NSString *)scope
+ handler:
+ (nonnull FIRMessagingInstanceIDProxyDeleteTokenHandler)handler {
+
+ id proxy = [[self class] instanceIDProxy];
+ SEL deleteTokenSelector = NSSelectorFromString(@"deleteTokenWithAuthorizedEntity:scope:handler:");
+ if (![proxy respondsToSelector:deleteTokenSelector]) {
+ return;
+ }
+ // Since there are >2 arguments, use NSInvocation
+ NSMethodSignature *methodSignature =
+ [[proxy class] instanceMethodSignatureForSelector:deleteTokenSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
+ invocation.selector = deleteTokenSelector;
+ invocation.target = proxy;
+ [invocation setArgument:&authorizedEntity atIndex:2];
+ [invocation setArgument:&scope atIndex:3];
+ [invocation setArgument:&handler atIndex:4];
+ [invocation invoke];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingLogger.h b/Firebase/Messaging/FIRMessagingLogger.h
new file mode 100644
index 0000000..cd3c29a
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingLogger.h
@@ -0,0 +1,97 @@
+/*
+ * 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 "FIRMessagingConfig.h"
+#import "FIRMMessageCode.h"
+
+// The convenience macros are only defined if they haven't already been defined.
+#ifndef FIRMessagingLoggerInfo
+
+// Convenience macros that log to the shared FIRMessagingLogger instance. These macros
+// are how users should typically log to FIRMessagingLogger.
+#define FIRMessagingLoggerDebug(code, ...) \
+ [FIRMessagingSharedLogger() logFuncDebug:__func__ messageCode:code msg:__VA_ARGS__]
+#define FIRMessagingLoggerInfo(code, ...) \
+ [FIRMessagingSharedLogger() logFuncInfo:__func__ messageCode:code msg:__VA_ARGS__]
+#define FIRMessagingLoggerNotice(code, ...) \
+ [FIRMessagingSharedLogger() logFuncNotice:__func__ messageCode:code msg:__VA_ARGS__]
+#define FIRMessagingLoggerWarn(code, ...) \
+ [FIRMessagingSharedLogger() logFuncWarning:__func__ messageCode:code msg:__VA_ARGS__]
+#define FIRMessagingLoggerError(code, ...) \
+ [FIRMessagingSharedLogger() logFuncError:__func__ messageCode:code msg:__VA_ARGS__]
+
+#endif // !defined(FIRMessagingLoggerInfo)
+
+/// Protocols
+@protocol FIRMessagingLogFormatter <NSObject>
+- (NSString *)stringForFunc:(NSString *)func
+ withFormat:(NSString *)fmt
+ valist:(va_list)args
+ level:(FIRMessagingLogLevel)level NS_FORMAT_FUNCTION(2, 0);
+@end
+
+/// FIRMessagingLogWriter
+@protocol FIRMessagingLogWriter <NSObject>
+// Writes the given log message to where the log writer is configured to write.
+- (void)logMessage:(NSString *)msg level:(FIRMessagingLogLevel)level;
+@end
+
+/// FIRMessagingLogFilter
+@protocol FIRMessagingLogFilter <NSObject>
+// Returns YES if |msg| at |level| should be logged; NO otherwise.
+- (BOOL)filterAllowsMessage:(NSString *)msg level:(FIRMessagingLogLevel)level;
+@end
+
+@interface FIRMessagingLogLevelFilter : NSObject <FIRMessagingLogFilter>
+- (instancetype)initWithLevel:(FIRMessagingLogLevel)level;
+@end
+
+
+@interface FIRMessagingLogger : NSObject
+
+@property(nonatomic, readwrite, strong) id<FIRMessagingLogFilter> filter;
+@property(nonatomic, readwrite, strong) id<FIRMessagingLogWriter> writer;
+@property(nonatomic, readwrite, strong) id<FIRMessagingLogFormatter> formatter;
+
+- (void)logFuncDebug:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
+
+- (void)logFuncInfo:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
+
+- (void)logFuncNotice:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
+
+- (void)logFuncWarning:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
+
+- (void)logFuncError:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
+
+@end
+
+/**
+ * Instantiates and/or returns a shared FIRMessagingLogger used exclusively
+ * for FIRMessaging log messages.
+ *
+ * @return the shared FIRMessagingLogger instance
+ */
+FIRMessagingLogger *FIRMessagingSharedLogger();
diff --git a/Firebase/Messaging/FIRMessagingLogger.m b/Firebase/Messaging/FIRMessagingLogger.m
new file mode 100644
index 0000000..0ded97c
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingLogger.m
@@ -0,0 +1,305 @@
+/*
+ * 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 "FIRMessagingLogger.h"
+
+#import "FIRLogger.h"
+#import "FIRMessagingFileLogger.h"
+
+/**
+ * A log formatter that prefixes log messages with "FIRMessaging".
+ */
+@interface FIRMessagingLogStandardFormatter : NSObject<FIRMessagingLogFormatter>
+
+@property(nonatomic, readwrite, strong) NSDateFormatter *dateFormatter;
+
+@end
+
+@implementation FIRMessagingLogStandardFormatter
+
+static NSString *const kFIRMessagingLogPrefix = @"FIRMessaging";
+
+- (id)init {
+ if ((self = [super init])) {
+ _dateFormatter = [[NSDateFormatter alloc] init];
+ [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
+ [_dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"];
+ }
+ return self;
+}
+/**
+ * Returns a formatted string prefixed with "FIRMessaging" to allow
+ * FIRMessaging output to be easily differentiated in logs.
+ *
+ * @param func the name of the function calling the logger
+ * @param fmt the format string
+ * @param args the list of arguments for the format string
+ * @param level the logging level (eg. debug, info)
+ * @return the formatted string prefixed with "FIRMessaging".
+ */
+- (NSString *)stringForFunc:(NSString *)func
+ withFormat:(NSString *)fmt
+ valist:(va_list)args
+ level:(FIRMessagingLogLevel)level NS_FORMAT_FUNCTION(2, 0) {
+ if (!(fmt && args)) {
+ return nil;
+ }
+
+ NSString *logMessage = [[NSString alloc] initWithFormat:fmt arguments:args];
+ NSString *logLevelString = [self stringForLogLevel:level];
+ NSString *dateString = [self.dateFormatter stringFromDate:[NSDate date]];
+ return [NSString stringWithFormat:@"%@: <%@/%@> %@",
+ dateString, kFIRMessagingLogPrefix, logLevelString, logMessage];
+}
+
+- (NSString *)stringForLogLevel:(FIRMessagingLogLevel)level {
+ switch (level) {
+ case kFIRMessagingLogLevelDebug:
+ return @"DEBUG";
+
+ case kFIRMessagingLogLevelInfo:
+ return @"INFO";
+
+ case kFIRMessagingLogLevelError:
+ return @"WARNING";
+
+ case kFIRMessagingLogLevelAssert:
+ return @"ERROR";
+
+ default:
+ return @"INFO";
+ }
+}
+
+@end
+
+@interface FIRMessagingLogLevelFilter ()
+
+@property(nonatomic, readwrite, assign) FIRMessagingLogLevel level;
+
+@end
+
+@implementation FIRMessagingLogLevelFilter
+
+- (instancetype)initWithLevel:(FIRMessagingLogLevel)level {
+ self = [super init];
+ if (self) {
+ _level = level;
+ }
+ return self;
+}
+
+- (BOOL)filterAllowsMessage:(NSString *)msg level:(FIRMessagingLogLevel)level {
+#if defined(DEBUG) && DEBUG
+ return YES;
+#endif
+
+ BOOL allow = YES;
+
+ switch (level) {
+ case kFIRMessagingLogLevelDebug:
+ allow = NO;
+ break;
+ case kFIRMessagingLogLevelInfo:
+ case kFIRMessagingLogLevelError:
+ case kFIRMessagingLogLevelAssert:
+ allow = (level >= self.level);
+ break;
+ default:
+ allow = NO;
+ break;
+ }
+
+ return allow;
+}
+
+@end
+
+
+// Copied from FIRMessagingLogger. Standard implementation to write logs to console.
+@interface NSFileHandle (FIRMessagingFileHandleLogWriter) <FIRMessagingLogWriter>
+@end
+
+@implementation NSFileHandle (FIRMessagingFileHandleLogWriter)
+- (void)logMessage:(NSString *)msg level:(FIRMessagingLogLevel)level {
+ @synchronized(self) {
+ // Closed pipes should not generate exceptions in our caller. Catch here
+ // as well [FIRMessagingLogger logInternalFunc:...] so that an exception in this
+ // writer does not prevent other writers from having a chance.
+ @try {
+ NSString *line = [NSString stringWithFormat:@"%@\n", msg];
+ [self writeData:[line dataUsingEncoding:NSUTF8StringEncoding]];
+ }
+ @catch (id e) {
+ // Ignored
+ }
+ }
+}
+@end
+
+@interface FIRMessagingLogger ()
+
+@end
+
+@implementation FIRMessagingLogger
+
++ (instancetype)standardLogger {
+
+ id<FIRMessagingLogWriter> writer;
+ id<FIRMessagingLogFormatter> formatter;
+ id<FIRMessagingLogFilter> filter;
+
+#if FIRMessaging_PROBER
+ writer = [[FIRMessagingFileLogWriter alloc] init];
+ formatter = [[FIRMessagingFileLogFormatter alloc] init];
+ filter = [[FIRMessagingFileLogFilter alloc] init];
+#else
+ writer = [NSFileHandle fileHandleWithStandardOutput];
+ formatter = [[FIRMessagingLogStandardFormatter alloc] init];
+ filter = [[FIRMessagingLogLevelFilter alloc] init];
+#endif
+
+ return [[FIRMessagingLogger alloc] initWithFilter:filter formatter:formatter writer:writer];
+}
+
+- (instancetype)initWithFilter:(id<FIRMessagingLogFilter>)filter
+ formatter:(id<FIRMessagingLogFormatter>)formatter
+ writer:(id<FIRMessagingLogWriter>)writer {
+ self = [super init];
+ if (self) {
+ _filter = filter;
+ _formatter = formatter;
+ _writer = writer;
+ }
+ return self;
+}
+
+#pragma mark - Log Helpers
+
++ (NSString *)formatMessageCode:(FIRMessagingMessageCode)messageCode {
+ return [NSString stringWithFormat:@"I-FCM%06ld", (long)messageCode];
+}
+
+- (void)logFuncDebug:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... {
+ va_list args;
+ va_start(args, fmt);
+ FIRLogBasic(FIRLoggerLevelDebug, kFIRLoggerMessaging,
+ [FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
+ va_end(args);
+#if FIRMessaging_PROBER
+ va_start(args, fmt);
+ [self logInternalFunc:func format:fmt valist:args level:kFIRMessagingLogLevelDebug];
+ va_end(args);
+#endif
+}
+
+- (void)logFuncInfo:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... {
+ va_list args;
+ va_start(args, fmt);
+ FIRLogBasic(FIRLoggerLevelInfo, kFIRLoggerMessaging,
+ [FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
+ va_end(args);
+#if FIRMessaging_PROBER
+ va_start(args, fmt);
+ [self logInternalFunc:func format:fmt valist:args level:kFIRMessagingLogLevelInfo];
+ va_end(args);
+#endif
+}
+
+- (void)logFuncNotice:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... {
+ va_list args;
+ va_start(args, fmt);
+ FIRLogBasic(FIRLoggerLevelNotice, kFIRLoggerMessaging,
+ [FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
+ va_end(args);
+#if FIRMessaging_PROBER
+ va_start(args, fmt);
+ // Treat FIRLoggerLevelNotice as "info" locally, since we don't have an equivalent
+ [self logInternalFunc:func format:fmt valist:args level:kFIRMessagingLogLevelInfo];
+ va_end(args);
+#endif
+}
+
+- (void)logFuncWarning:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... {
+ va_list args;
+ va_start(args, fmt);
+ FIRLogBasic(FIRLoggerLevelWarning, kFIRLoggerMessaging,
+ [FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
+ va_end(args);
+#if FIRMessaging_PROBER
+ va_start(args, fmt);
+ // Treat FIRLoggerLevelWarning as "error" locally, since we don't have an equivalent
+ [self logInternalFunc:func format:fmt valist:args level:kFIRMessagingLogLevelError];
+ va_end(args);
+#endif
+}
+
+- (void)logFuncError:(const char *)func
+ messageCode:(FIRMessagingMessageCode)messageCode
+ msg:(NSString *)fmt, ... {
+ va_list args;
+ va_start(args, fmt);
+ FIRLogBasic(FIRLoggerLevelError, kFIRLoggerMessaging,
+ [FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
+ va_end(args);
+#if FIRMessaging_PROBER
+ va_start(args, fmt);
+ [self logInternalFunc:func format:fmt valist:args level:kFIRMessagingLogLevelError];
+ va_end(args);
+#endif
+}
+
+#pragma mark - Internal Helpers
+
+- (void)logInternalFunc:(const char *)func
+ format:(NSString *)fmt
+ valist:(va_list)args
+ level:(FIRMessagingLogLevel)level {
+ // Primary point where logging happens, logging should never throw, catch
+ // everything.
+ @try {
+ NSString *fname = func ? [NSString stringWithUTF8String:func] : nil;
+ NSString *msg = [self.formatter stringForFunc:fname
+ withFormat:fmt
+ valist:args
+ level:level];
+ if (msg && [self.filter filterAllowsMessage:msg level:level])
+ [self.writer logMessage:msg level:level];
+ }
+ @catch (id e) {
+ // Ignored
+ }
+}
+
+@end
+
+FIRMessagingLogger *FIRMessagingSharedLogger() {
+ static dispatch_once_t onceToken;
+ static FIRMessagingLogger *logger;
+ dispatch_once(&onceToken, ^{
+ logger = [FIRMessagingLogger standardLogger];
+ });
+
+ return logger;
+}
diff --git a/Firebase/Messaging/FIRMessagingPacketQueue.h b/Firebase/Messaging/FIRMessagingPacketQueue.h
new file mode 100644
index 0000000..1f528ab
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPacketQueue.h
@@ -0,0 +1,43 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@interface FIRMessagingPacket : NSObject
+
++ (FIRMessagingPacket *)packetWithTag:(int8_t)tag rmqId:(NSString *)rmqId data:(NSData *)data;
+
+@property(nonatomic, readonly, strong) NSData *data;
+@property(nonatomic, readonly, assign) int8_t tag;
+// not sent over the wire required for bookkeeping
+@property(nonatomic, readonly, assign) NSString *rmqId;
+
+@end
+
+
+/**
+ * A queue of the packets(protos) that need to be send over the wire.
+ */
+@interface FIRMessagingPacketQueue : NSObject
+
+@property(nonatomic, readonly, assign) NSUInteger count;
+@property(nonatomic, readonly, assign) BOOL isEmpty;
+
+- (void)push:(FIRMessagingPacket *)packet;
+- (void)pushHead:(FIRMessagingPacket *)packet;
+- (FIRMessagingPacket *)pop;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPacketQueue.m b/Firebase/Messaging/FIRMessagingPacketQueue.m
new file mode 100644
index 0000000..2b3410a
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPacketQueue.m
@@ -0,0 +1,103 @@
+/*
+ * 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 "FIRMessagingPacketQueue.h"
+
+#import "FIRMessagingDefines.h"
+
+@interface FIRMessagingPacket ()
+
+@property(nonatomic, readwrite, strong) NSData *data;
+@property(nonatomic, readwrite, assign) int8_t tag;
+@property(nonatomic, readwrite, assign) NSString *rmqId;
+
+@end
+
+@implementation FIRMessagingPacket
+
++ (FIRMessagingPacket *)packetWithTag:(int8_t)tag rmqId:(NSString *)rmqId data:(NSData *)data {
+ return [[self alloc] initWithTag:tag rmqId:rmqId data:data];
+}
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithTag:(int8_t)tag rmqId:(NSString *)rmqId data:(NSData *)data {
+ self = [super init];
+ if (self != nil) {
+ _data = data;
+ _tag = tag;
+ _rmqId = rmqId;
+ }
+ return self;
+}
+
+- (NSString *)description {
+ if ([self.rmqId length]) {
+ return [NSString stringWithFormat:@"<Packet: Tag - %d, Length - %lu>, RmqId - %@",
+ self.tag, _FIRMessaging_UL(self.data.length), self.rmqId];
+ } else {
+ return [NSString stringWithFormat:@"<Packet: Tag - %d, Length - %lu>",
+ self.tag, _FIRMessaging_UL(self.data.length)];
+ }
+}
+
+@end
+
+@interface FIRMessagingPacketQueue ()
+
+@property(nonatomic, readwrite, strong) NSMutableArray *packetsContainer;
+
+@end
+
+
+@implementation FIRMessagingPacketQueue;
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ _packetsContainer = [[NSMutableArray alloc] init];
+ }
+ return self;
+}
+
+- (BOOL)isEmpty {
+ return self.packetsContainer.count == 0;
+}
+
+- (NSUInteger)count {
+ return self.packetsContainer.count;
+}
+
+- (void)push:(FIRMessagingPacket *)packet {
+ [self.packetsContainer addObject:packet];
+}
+
+- (void)pushHead:(FIRMessagingPacket *)packet {
+ [self.packetsContainer insertObject:packet atIndex:0];
+}
+
+- (FIRMessagingPacket *)pop {
+ if (!self.isEmpty) {
+ FIRMessagingPacket *packet = self.packetsContainer[0];
+ [self.packetsContainer removeObjectAtIndex:0];
+ return packet;
+ }
+ return nil;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPendingTopicsList.h b/Firebase/Messaging/FIRMessagingPendingTopicsList.h
new file mode 100644
index 0000000..c5a306a
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPendingTopicsList.h
@@ -0,0 +1,118 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRMessagingTopicsCommon.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Represents a single batch of topics, with the same action.
+ *
+ * Topic operations which have the same action (subscribe or unsubscribe) can be executed
+ * simultaneously, as the order of operations do not matter with the same action. The set of
+ * topics is unique, as it doesn't make sense to apply the same action to the same topic
+ * repeatedly; the result would be the same as the first time.
+ */
+@interface FIRMessagingTopicBatch : NSObject <NSCoding>
+
+@property(nonatomic, readonly, assign) FIRMessagingTopicAction action;
+@property(nonatomic, readonly, copy) NSMutableSet <NSString *> *topics;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithAction:(FIRMessagingTopicAction)action NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@class FIRMessagingPendingTopicsList;
+/**
+ * This delegate must be supplied to the instance of FIRMessagingPendingTopicsList, via the
+ * @cdelegate property. It lets the
+ * pending topics list know whether or not it can begin making requests via
+ * @c-pendingTopicsListCanRequestTopicUpdates:, and handles the request to actually
+ * perform the topic operation. The delegate also handles when the pending topics list is updated,
+ * so that it can be archived or persisted.
+ *
+ * @see FIRMessagingPendingTopicsList
+ */
+@protocol FIRMessagingPendingTopicsListDelegate <NSObject>
+
+- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
+ requestedUpdateForTopic:(NSString *)topic
+ action:(FIRMessagingTopicAction)action
+ completion:(FIRMessagingTopicOperationCompletion)completion;
+- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list;
+- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list;
+
+@end
+
+/**
+ * FIRMessagingPendingTopicsList manages a list of topic subscription updates, batched by the same
+ * action (subscribe or unsubscribe). The list roughly maintains the order of the topic operations,
+ * batched together whenever the topic action (subscribe or unsubscribe) changes.
+ *
+ * Topics operations are batched by action because it is safe to perform the same topic action
+ * (subscribe or unsubscribe) on many topics simultaneously. After each batch is successfully
+ * completed, the next batch operations can begin.
+ *
+ * When asked to resume its operations, FIRMessagingPendingTopicsList will begin performing updates
+ * of its current batch of topics. For example, it may begin subscription operations for topics
+ * [A, B, C] simultaneously.
+ *
+ * When the current batch is completed, the next batch of operations will be started. For example
+ * the list may begin unsubscribe operations for [D, A, E]. Note that because A is in both batches,
+ * A will be correctly subscribed in the first batch, then unsubscribed as part of the second batch
+ * of operations. Without batching, it would be ambiguous whether A's subscription operation or the
+ * unsubscription operation would be completed first.
+ *
+ * An app can subscribe and unsubscribe from many topics, and this class helps persist the pending
+ * topics and perform the operation safely and correctly.
+ *
+ * When a topic fails to subscribe or unsubscribe due to a network error, it is considered a
+ * recoverable error, and so it remains in the current batch until it is succesfully completed.
+ * Topic updates are completed when they either (a) succeed, (b) are cancelled, or (c) result in an
+ * unrecoverable error. Any error outside of `NSURLErrorDomain` is considered an unrecoverable
+ * error.
+ *
+ * In addition to maintaining the list of pending topic updates, FIRMessagingPendingTopicsList also
+ * can track completion handlers for topic operations.
+ *
+ * @discussion Completion handlers for topic updates are not maintained if it was restored from a
+ * keyed archive. They are only called if the topic operation finished within the same app session.
+ *
+ * You must supply an object conforming to FIRMessagingPendingTopicsListDelegate in order for the
+ * topic operations to execute.
+ *
+ * @see FIRMessagingPendingTopicsListDelegate
+ */
+@interface FIRMessagingPendingTopicsList : NSObject <NSCoding>
+
+@property(nonatomic, weak) NSObject <FIRMessagingPendingTopicsListDelegate> *delegate;
+
+@property(nonatomic, readonly, strong, nullable) NSDate *archiveDate;
+@property(nonatomic, readonly) NSUInteger numberOfBatches;
+
+
+- (instancetype)init NS_DESIGNATED_INITIALIZER;
+- (void)addOperationForTopic:(NSString *)topic
+ withAction:(FIRMessagingTopicAction)action
+ completion:(nullable FIRMessagingTopicOperationCompletion)completion;
+- (void)resumeOperationsIfNeeded;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Messaging/FIRMessagingPendingTopicsList.m b/Firebase/Messaging/FIRMessagingPendingTopicsList.m
new file mode 100644
index 0000000..792090e
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPendingTopicsList.m
@@ -0,0 +1,261 @@
+/*
+ * 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 "FIRMessagingPendingTopicsList.h"
+
+#import "FIRMessaging_Private.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPubSub.h"
+
+#import "FIRMessagingDefines.h"
+
+NSString *const kPendingTopicBatchActionKey = @"action";
+NSString *const kPendingTopicBatchTopicsKey = @"topics";
+
+NSString *const kPendingBatchesEncodingKey = @"batches";
+NSString *const kPendingTopicsTimestampEncodingKey = @"ts";
+
+#pragma mark - FIRMessagingTopicBatch
+
+@interface FIRMessagingTopicBatch ()
+
+@property(nonatomic, strong, nonnull) NSMutableDictionary
+ <NSString *, NSMutableArray <FIRMessagingTopicOperationCompletion> *> *topicHandlers;
+
+@end
+
+@implementation FIRMessagingTopicBatch
+
+- (instancetype)initWithAction:(FIRMessagingTopicAction)action {
+ if (self = [super init]) {
+ _action = action;
+ _topics = [NSMutableSet set];
+ _topicHandlers = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+#pragma mark NSCoding
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeInteger:self.action forKey:kPendingTopicBatchActionKey];
+ [aCoder encodeObject:self.topics forKey:kPendingTopicBatchTopicsKey];
+}
+
+- (instancetype)initWithCoder:(NSCoder *)aDecoder {
+
+ // Ensure that our integer -> enum casting is safe
+ NSInteger actionRawValue = [aDecoder decodeIntegerForKey:kPendingTopicBatchActionKey];
+ FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe;
+ if (actionRawValue == FIRMessagingTopicActionUnsubscribe) {
+ action = FIRMessagingTopicActionUnsubscribe;
+ }
+
+ if (self = [self initWithAction:action]) {
+ NSSet *topics = [aDecoder decodeObjectForKey:kPendingTopicBatchTopicsKey];
+ if ([topics isKindOfClass:[NSSet class]]) {
+ _topics = [topics mutableCopy];
+ }
+ _topicHandlers = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+@end
+
+#pragma mark - FIRMessagingPendingTopicsList
+
+@interface FIRMessagingPendingTopicsList ()
+
+@property(nonatomic, readwrite, strong) NSDate *archiveDate;
+@property(nonatomic, strong) NSMutableArray <FIRMessagingTopicBatch *> *topicBatches;
+
+@property(nonatomic, strong) FIRMessagingTopicBatch *currentBatch;
+@property(nonatomic, strong) NSMutableSet <NSString *> *topicsInFlight;
+
+@end
+
+@implementation FIRMessagingPendingTopicsList
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _topicBatches = [NSMutableArray array];
+ _topicsInFlight = [NSMutableSet set];
+ }
+ return self;
+}
+
++ (void)pruneTopicBatches:(NSMutableArray <FIRMessagingTopicBatch *> *)topicBatches {
+ // For now, just remove empty batches. In the future we can use this to make the subscriptions
+ // more efficient, by actually pruning topic actions that cancel each other out, for example.
+ for (NSInteger i = topicBatches.count-1; i >= 0; i--) {
+ FIRMessagingTopicBatch *batch = topicBatches[i];
+ if (batch.topics.count == 0) {
+ [topicBatches removeObjectAtIndex:i];
+ }
+ }
+}
+
+#pragma mark NSCoding
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[NSDate date] forKey:kPendingTopicsTimestampEncodingKey];
+ [aCoder encodeObject:self.topicBatches forKey:kPendingBatchesEncodingKey];
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+
+ if (self = [self init]) {
+ _archiveDate = [aDecoder decodeObjectForKey:kPendingTopicsTimestampEncodingKey];
+ NSArray *archivedBatches = [aDecoder decodeObjectForKey:kPendingBatchesEncodingKey];
+ if (archivedBatches) {
+ _topicBatches = [archivedBatches mutableCopy];
+ [FIRMessagingPendingTopicsList pruneTopicBatches:_topicBatches];
+ }
+ _topicsInFlight = [NSMutableSet set];
+ }
+ return self;
+}
+
+#pragma mark Getters
+
+- (NSUInteger)numberOfBatches {
+ return self.topicBatches.count;
+}
+
+#pragma mark Adding/Removing topics
+
+- (void)addOperationForTopic:(NSString *)topic
+ withAction:(FIRMessagingTopicAction)action
+ completion:(nullable FIRMessagingTopicOperationCompletion)completion {
+
+ FIRMessagingTopicBatch *lastBatch = nil;
+ @synchronized (self) {
+ lastBatch = self.topicBatches.lastObject;
+ if (!lastBatch || lastBatch.action != action) {
+ // There either was no last batch, or our last batch's action was not the same, so we have to
+ // create a new batch
+ lastBatch = [[FIRMessagingTopicBatch alloc] initWithAction:action];
+ [self.topicBatches addObject:lastBatch];
+ }
+ BOOL topicExistedBefore = ([lastBatch.topics member:topic] != nil);
+ if (!topicExistedBefore) {
+ [lastBatch.topics addObject:topic];
+ [self.delegate pendingTopicsListDidUpdate:self];
+ }
+ // Add the completion handler to the batch
+ if (completion) {
+ NSMutableArray *handlers = lastBatch.topicHandlers[topic];
+ if (!handlers) {
+ handlers = [NSMutableArray arrayWithCapacity:1];
+ }
+ [handlers addObject:completion];
+ }
+ if (!self.currentBatch) {
+ self.currentBatch = lastBatch;
+ }
+ // This may have been the first topic added, or was added to an ongoing batch
+ if (self.currentBatch == lastBatch && !topicExistedBefore) {
+ // Add this topic to our ongoing operations
+ FIRMessaging_WEAKIFY(self);
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+ FIRMessaging_STRONGIFY(self);
+ [self resumeOperationsIfNeeded];
+ });
+ }
+ }
+}
+
+- (void)resumeOperationsIfNeeded {
+ @synchronized (self) {
+ // If current batch is not set, set it now
+ if (!self.currentBatch) {
+ self.currentBatch = self.topicBatches.firstObject;
+ }
+ if (self.currentBatch.topics.count == 0) {
+ return;
+ }
+ if (!self.delegate) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodePendingTopicsList000,
+ @"Attempted to update pending topics without a delegate");
+ return;
+ }
+ if (![self.delegate pendingTopicsListCanRequestTopicUpdates:self]) {
+ return;
+ }
+ for (NSString *topic in self.currentBatch.topics) {
+ if ([self.topicsInFlight member:topic]) {
+ // This topic is already active, so skip
+ continue;
+ }
+ [self beginUpdateForCurrentBatchTopic:topic];
+ }
+ }
+}
+
+- (BOOL)subscriptionErrorIsRecoverable:(NSError *)error {
+ return [error.domain isEqualToString:NSURLErrorDomain];
+}
+
+- (void)beginUpdateForCurrentBatchTopic:(NSString *)topic {
+
+ @synchronized (self) {
+ [self.topicsInFlight addObject:topic];
+ }
+ FIRMessaging_WEAKIFY(self);
+ [self.delegate pendingTopicsList:self
+ requestedUpdateForTopic:topic
+ action:self.currentBatch.action
+ completion:^(FIRMessagingTopicOperationResult result, NSError * error) {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+ FIRMessaging_STRONGIFY(self);
+ @synchronized (self) {
+ [self.topicsInFlight removeObject:topic];
+
+ BOOL recoverableError = [self subscriptionErrorIsRecoverable:error];
+ if (result == FIRMessagingTopicOperationResultSucceeded ||
+ result == FIRMessagingTopicOperationResultCancelled ||
+ !recoverableError) {
+ // Notify our handlers and remove the topic from our batch
+ NSMutableArray *handlers = self.currentBatch.topicHandlers[topic];
+ if (handlers.count) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ for (FIRMessagingTopicOperationCompletion handler in handlers) {
+ handler(result, error);
+ }
+ [handlers removeAllObjects];
+ });
+ }
+ [self.currentBatch.topics removeObject:topic];
+ [self.currentBatch.topicHandlers removeObjectForKey:topic];
+ if (self.currentBatch.topics.count == 0) {
+ // All topic updates successfully finished in this batch, move on to the next batch
+ [self.topicBatches removeObject:self.currentBatch];
+ self.currentBatch = nil;
+ }
+ [self.delegate pendingTopicsListDidUpdate:self];
+ FIRMessaging_WEAKIFY(self)
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+ FIRMessaging_STRONGIFY(self)
+ [self resumeOperationsIfNeeded];
+ });
+ }
+ }
+ });
+ }];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPersistentSyncMessage.h b/Firebase/Messaging/FIRMessagingPersistentSyncMessage.h
new file mode 100644
index 0000000..5a48e99
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPersistentSyncMessage.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 <Foundation/Foundation.h>
+
+@interface FIRMessagingPersistentSyncMessage : NSObject
+
+@property(nonatomic, readonly, strong) NSString *rmqID;
+@property(nonatomic, readwrite, assign) BOOL apnsReceived;
+@property(nonatomic, readwrite, assign) BOOL mcsReceived;
+@property(nonatomic, readonly, assign) int64_t expirationTime;
+
+- (instancetype)initWithRMQID:(NSString *)rmqID expirationTime:(int64_t)expirationTime;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPersistentSyncMessage.m b/Firebase/Messaging/FIRMessagingPersistentSyncMessage.m
new file mode 100644
index 0000000..bf7d05b
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPersistentSyncMessage.m
@@ -0,0 +1,54 @@
+/*
+ * 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 "FIRMessagingPersistentSyncMessage.h"
+
+#import "FIRMessagingDefines.h"
+
+@interface FIRMessagingPersistentSyncMessage ()
+
+@property(nonatomic, readwrite, strong) NSString *rmqID;
+@property(nonatomic, readwrite, assign) int64_t expirationTime;
+
+@end
+
+@implementation FIRMessagingPersistentSyncMessage
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithRMQID:(NSString *)rmqID expirationTime:(int64_t)expirationTime {
+ self = [super init];
+ if (self) {
+ _rmqID = [rmqID copy];
+ _expirationTime = expirationTime;
+ }
+ return self;
+}
+
+- (NSString *)description {
+ NSString *classDescription = NSStringFromClass([self class]);
+ NSDate *date = [NSDate dateWithTimeIntervalSince1970:self.expirationTime];
+ return [NSString stringWithFormat:@"%@: (rmqID: %@, apns: %d, mcs: %d, expiry: %@",
+ classDescription, self.rmqID, self.mcsReceived, self.apnsReceived, date];
+}
+
+- (NSString *)debugDescription {
+ return [self description];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPubSub.h b/Firebase/Messaging/FIRMessagingPubSub.h
new file mode 100644
index 0000000..3a03494
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPubSub.h
@@ -0,0 +1,148 @@
+/*
+ * 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 "FIRMessagingTopicsCommon.h"
+
+@class FIRMessagingClient;
+@class FIRMessagingPubSubCache;
+
+/**
+ * FIRMessagingPubSub provides a publish-subscribe model for sending FIRMessaging topic messages.
+ *
+ * An app can subscribe to different topics defined by the
+ * developer. The app server can then send messages to the subscribed devices
+ * without having to maintain topic-subscribers mapping. Topics do not
+ * need to be explicitly created before subscribing or publishing&mdash;they
+ * are automatically created when publishing or subscribing.
+ *
+ * Messages published to the topic will be received as regular FIRMessaging messages
+ * with `"from"` set to `"/topics/myTopic"`.
+ *
+ * Only topic names that match the pattern `"/topics/[a-zA-Z0-9-_.~%]{1,900}"`
+ * are allowed for subscribing and publishing.
+ */
+@interface FIRMessagingPubSub : NSObject
+
+@property(nonatomic, readonly, strong) FIRMessagingPubSubCache *cache;
+@property(nonatomic, readonly, strong) FIRMessagingClient *client;
+
+/**
+ * Initializes an instance of FIRMessagingPubSub.
+ *
+ * @return An instance of FIRMessagingPubSub.
+ */
+- (instancetype)initWithClient:(FIRMessagingClient *)client NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Subscribes an app instance to a topic, enabling it to receive messages
+ * sent to that topic.
+ *
+ * This is an asynchronous call. If subscription fails, FIRMessaging
+ * invokes the completion callback with the appropriate error.
+ *
+ * @see FIRMessagingPubSub unsubscribeWithToken:topic:handler:
+ *
+ * @param token The registration token as received from the InstanceID
+ * library for a given `authorizedEntity` and "gcm" scope.
+ * @param topic The topic to subscribe to. Should be of the form
+ * `"/topics/<topic-name>"`.
+ * @param handler The callback handler invoked when the subscribe call
+ * ends. In case of success, a nil error is returned. Otherwise,
+ * an appropriate error object is returned.
+ * @discussion This method is thread-safe. However, it is not guaranteed to
+ * return on the main thread.
+ */
+- (void)subscribeWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ handler:(FIRMessagingTopicOperationCompletion)handler;
+
+
+/**
+ * Unsubscribes an app instance from a topic, stopping it from receiving
+ * any further messages sent to that topic.
+ *
+ * This is an asynchronous call. If the attempt to unsubscribe fails,
+ * we invoke the `completion` callback passed in with an appropriate error.
+ *
+ * @param token The token used to subscribe to this topic.
+ * @param topic The topic to unsubscribe from. Should be of the form
+ * `"/topics/<topic-name>"`.
+ * @param handler The handler that is invoked once the unsubscribe call ends.
+ * In case of success, nil error is returned. Otherwise, an
+ * appropriate error object is returned.
+ * @discussion This method is thread-safe. However, it is not guaranteed to
+ * return on the main thread.
+ */
+- (void)unsubscribeWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ handler:(FIRMessagingTopicOperationCompletion)handler;
+
+/**
+ * Asynchronously subscribe to the topic. Adds to the pending list of topic operations.
+ * Retry in case of failures. This makes a repeated attempt to subscribe to the topic
+ * as compared to the `subscribe` method above which tries once.
+ *
+ * @param topic The topic name to subscribe to. Should be of the form `"/topics/<topic-name>"`.
+ */
+- (void)subscribeToTopic:(NSString *)topic;
+
+/**
+ * Asynchronously unsubscribe from the topic. Adds to the pending list of topic operations.
+ * Retry in case of failures. This makes a repeated attempt to unsubscribe from the topic
+ * as compared to the `unsubscribe` method above which tries once.
+ *
+ * @param topic The topic name to unsubscribe from. Should be of the form `"/topics/<topic-name>"`.
+ */
+- (void)unsubscribeFromTopic:(NSString *)topic;
+
+/**
+ * Schedule subscriptions sync.
+ *
+ * @param immediately YES if the sync should be scheduled immediately else NO if we can delay
+ * the sync.
+ */
+- (void)scheduleSync:(BOOL)immediately;
+
+/**
+ * Adds the "/topics/" prefix to the topic.
+ *
+ * @param topic The topic to add the prefix to.
+ *
+ * @return The new topic name with the "/topics/" prefix added.
+ */
++ (NSString *)addPrefixToTopic:(NSString *)topic;
+
+/**
+ * Check if the topic name has "/topics/" prefix.
+ *
+ * @param topic The topic name to verify.
+ *
+ * @return YES if the topic name has "/topics/" prefix else NO.
+ */
++ (BOOL)hasTopicsPrefix:(NSString *)topic;
+
+/**
+ * Check if it's a valid topic name. This includes "/topics/" prefix in the topic name.
+ *
+ * @param topic The topic name to verify.
+ *
+ * @return YES if the topic name satisfies the regex "/topics/[a-zA-Z0-9-_.~%]{1,900}".
+ */
++ (BOOL)isValidTopicWithPrefix:(NSString *)topic;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPubSub.m b/Firebase/Messaging/FIRMessagingPubSub.m
new file mode 100644
index 0000000..c8293e0
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPubSub.m
@@ -0,0 +1,278 @@
+/*
+ * 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 "FIRMessagingPubSub.h"
+
+#import "FIRMessaging.h"
+#import "FIRMessagingClient.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPendingTopicsList.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRMessaging_Private.h"
+#import "NSDictionary+FIRMessaging.h"
+#import "NSError+FIRMessaging.h"
+
+static NSString *const kPendingSubscriptionsListKey =
+ @"com.firebase.messaging.pending-subscriptions";
+
+@interface FIRMessagingPubSub () <FIRMessagingPendingTopicsListDelegate>
+
+@property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates;
+@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
+
+@end
+
+@implementation FIRMessagingPubSub
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+ // Need this to disable an Xcode warning.
+ return [self initWithClient:nil];
+}
+
+- (instancetype)initWithClient:(FIRMessagingClient *)client {
+ self = [super init];
+ if (self) {
+ _client = client;
+ [self restorePendingTopicsList];
+ }
+ return self;
+}
+
+- (void)subscribeWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ handler:(FIRMessagingTopicOperationCompletion)handler {
+ _FIRMessagingDevAssert([token length], @"FIRMessaging error no token specified");
+ _FIRMessagingDevAssert([topic length], @"FIRMessaging error Invalid empty topic specified");
+ if (!self.client) {
+ handler(FIRMessagingTopicOperationResultError,
+ [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
+ return;
+ }
+
+ token = [token copy];
+ topic = [topic copy];
+
+ if (![options count]) {
+ options = @{};
+ }
+
+ if (![[self class] isValidTopicWithPrefix:topic]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000,
+ @"Invalid FIRMessaging Pubsub topic %@", topic);
+ handler(FIRMessagingTopicOperationResultError,
+ [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
+ return;
+ }
+
+ if (![self verifyPubSubOptions:options]) {
+ // we do not want to quit even if options have some invalid values.
+ FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub001,
+ @"Invalid options passed to FIRMessagingPubSub with non-string keys or "
+ "values.");
+ }
+ // copy the dictionary would trim non-string keys or values if any.
+ options = [options fcm_trimNonStringValues];
+
+ [self.client updateSubscriptionWithToken:token
+ topic:topic
+ options:options
+ shouldDelete:NO
+ handler:
+ ^void(FIRMessagingTopicOperationResult result, NSError * error) {
+
+ handler(result, error);
+ }];
+}
+
+- (void)unsubscribeWithToken:(NSString *)token
+ topic:(NSString *)topic
+ options:(NSDictionary *)options
+ handler:(FIRMessagingTopicOperationCompletion)handler {
+ _FIRMessagingDevAssert([token length], @"FIRMessaging error no token specified");
+ _FIRMessagingDevAssert([topic length], @"FIRMessaging error Invalid empty topic specified");
+
+ if (!self.client) {
+ handler(FIRMessagingTopicOperationResultError,
+ [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
+ return;
+ }
+
+ token = [token copy];
+ topic = [topic copy];
+ if (![options count]) {
+ options = @{};
+ }
+
+ if (![[self class] isValidTopicWithPrefix:topic]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002,
+ @"Invalid FIRMessaging Pubsub topic %@", topic);
+ handler(FIRMessagingTopicOperationResultError,
+ [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
+ return;
+ }
+ if (![self verifyPubSubOptions:options]) {
+ // we do not want to quit even if options have some invalid values.
+ FIRMessagingLoggerError(
+ kFIRMessagingMessageCodePubSub003,
+ @"Invalid options passed to FIRMessagingPubSub with non-string keys or values.");
+ }
+ // copy the dictionary would trim non-string keys or values if any.
+ options = [options fcm_trimNonStringValues];
+
+ [self.client updateSubscriptionWithToken:token
+ topic:topic
+ options:options
+ shouldDelete:YES
+ handler:
+ ^void(FIRMessagingTopicOperationResult result, NSError * error) {
+
+ handler(result, error);
+ }];
+}
+
+- (void)subscribeToTopic:(NSString *)topic {
+ [self.pendingTopicUpdates addOperationForTopic:topic
+ withAction:FIRMessagingTopicActionSubscribe
+ completion:nil];
+}
+
+- (void)unsubscribeFromTopic:(NSString *)topic {
+ [self.pendingTopicUpdates addOperationForTopic:topic
+ withAction:FIRMessagingTopicActionUnsubscribe
+ completion:nil];
+}
+
+- (void)scheduleSync:(BOOL)immediately {
+ NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
+ if (fcmToken.length) {
+ [self.pendingTopicUpdates resumeOperationsIfNeeded];
+ }
+}
+
+#pragma mark - FIRMessagingPendingTopicsListDelegate
+
+- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
+ requestedUpdateForTopic:(NSString *)topic
+ action:(FIRMessagingTopicAction)action
+ completion:(FIRMessagingTopicOperationCompletion)completion {
+
+ NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
+ if (action == FIRMessagingTopicActionSubscribe) {
+ [self subscribeWithToken:fcmToken topic:topic options:nil handler:completion];
+ } else {
+ [self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion];
+ }
+}
+
+- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
+ [self archivePendingTopicsList:list];
+}
+
+- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
+ NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
+ return (fcmToken.length > 0);
+}
+
+#pragma mark - Storing Pending Topics
+
+- (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSData *pendingData = [NSKeyedArchiver archivedDataWithRootObject:topicsList];
+ [defaults setObject:pendingData forKey:kPendingSubscriptionsListKey];
+ [defaults synchronize];
+}
+
+- (void)restorePendingTopicsList {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey];
+ FIRMessagingPendingTopicsList *subscriptions;
+ @try {
+ if (pendingData) {
+ subscriptions = [NSKeyedUnarchiver unarchiveObjectWithData:pendingData];
+ }
+ } @catch (NSException *exception) {
+ // Nothing we can do, just continue as if we don't have pending subscriptions
+ } @finally {
+ if (subscriptions) {
+ self.pendingTopicUpdates = subscriptions;
+ } else {
+ self.pendingTopicUpdates = [[FIRMessagingPendingTopicsList alloc] init];
+ }
+ self.pendingTopicUpdates.delegate = self;
+ }
+}
+
+#pragma mark - Private Helpers
+
+- (BOOL)verifyPubSubOptions:(NSDictionary *)options {
+ return ![options fcm_hasNonStringKeysOrValues];
+}
+
+#pragma mark - Topic Name Helpers
+
+static NSString *const kTopicsPrefix = @"/topics/";
+static NSString *const kTopicRegexPattern = @"/topics/([a-zA-Z0-9-_.~%]+)";
+
++ (NSString *)addPrefixToTopic:(NSString *)topic {
+ if (![self hasTopicsPrefix:topic]) {
+ return [NSString stringWithFormat:@"%@%@", kTopicsPrefix, topic];
+ } else {
+ return [topic copy];
+ }
+}
+
++ (BOOL)hasTopicsPrefix:(NSString *)topic {
+ return [topic hasPrefix:kTopicsPrefix];
+}
+
+/**
+ * Returns a regular expression for matching a topic sender.
+ *
+ * @return The topic matching regular expression
+ */
++ (NSRegularExpression *)topicRegex {
+ // Since this is a static regex pattern, we only only need to declare it once.
+ static NSRegularExpression *topicRegex;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSError *error;
+ topicRegex =
+ [NSRegularExpression regularExpressionWithPattern:kTopicRegexPattern
+ options:NSRegularExpressionAnchorsMatchLines
+ error:&error];
+ });
+ return topicRegex;
+}
+
+/**
+ * Gets the class describing occurences of topic names and sender IDs in the sender.
+ *
+ * @param expression The topic expression used to generate a pubsub topic
+ *
+ * @return Representation of captured subexpressions in topic regular expression
+ */
++ (BOOL)isValidTopicWithPrefix:(NSString *)topic {
+ NSRange topicRange = NSMakeRange(0, topic.length);
+ NSRange regexMatchRange = [[self topicRegex] rangeOfFirstMatchInString:topic
+ options:NSMatchingAnchored
+ range:topicRange];
+ return NSEqualRanges(topicRange, regexMatchRange);
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPubSubRegistrar.h b/Firebase/Messaging/FIRMessagingPubSubRegistrar.h
new file mode 100644
index 0000000..b51813f
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPubSubRegistrar.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 "FIRMessagingTopicOperation.h"
+
+@class FIRMessagingCheckinService;
+
+@interface FIRMessagingPubSubRegistrar : NSObject
+
+/**
+ * Designated Initializer.
+ *
+ * @param checkinService The checkin service used to register with Checkin
+ * server.
+ *
+ * @return A new FIRMessagingPubSubRegistrar instance used to subscribe/unsubscribe.
+ */
+- (instancetype)initWithCheckinService:(FIRMessagingCheckinService *)checkinService;
+
+/**
+ * Stops all the subscription requests going on in parallel. This would
+ * invalidate all the handlers associated with the subscription requests.
+ */
+- (void)stopAllSubscriptionRequests;
+
+/**
+ * Update subscription status for a given topic with FIRMessaging's backend.
+ *
+ * @param topic The topic to subscribe to.
+ * @param token The registration token to be used.
+ * @param options The options to be passed in during subscription request.
+ * @param shouldDelete NO if the subscription is being added else YES if being
+ * removed.
+ * @param handler The handler invoked once the update subscription request
+ * finishes.
+ */
+- (void)updateSubscriptionToTopic:(NSString *)topic
+ withToken:(NSString *)token
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingPubSubRegistrar.m b/Firebase/Messaging/FIRMessagingPubSubRegistrar.m
new file mode 100644
index 0000000..6268302
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingPubSubRegistrar.m
@@ -0,0 +1,78 @@
+/*
+ * 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 "FIRMessagingPubSubRegistrar.h"
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingPubSubRegistrar.h"
+#import "FIRMessagingTopicsCommon.h"
+#import "NSError+FIRMessaging.h"
+
+@interface FIRMessagingPubSubRegistrar ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
+
+@property(nonatomic, readonly, strong) NSOperationQueue *topicOperations;
+// Common errors, instantiated, to avoid generating multiple copies
+@property(nonatomic, readwrite, strong) NSError *operationInProgressError;
+
+@end
+
+@implementation FIRMessagingPubSubRegistrar
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithCheckinService:(FIRMessagingCheckinService *)checkinService {
+ self = [super init];
+ if (self) {
+ _checkinService = checkinService;
+ _topicOperations = [[NSOperationQueue alloc] init];
+ // Do 10 topic operations at a time; it's enough to keep the TCP connection to the host alive,
+ // saving hundreds of milliseconds on each request (compared to a serial queue).
+ _topicOperations.maxConcurrentOperationCount = 10;
+ }
+ return self;
+}
+
+- (void)stopAllSubscriptionRequests {
+ [self.topicOperations cancelAllOperations];
+}
+
+- (void)updateSubscriptionToTopic:(NSString *)topic
+ withToken:(NSString *)token
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler {
+
+ FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe;
+ if (shouldDelete) {
+ action = FIRMessagingTopicActionUnsubscribe;
+ }
+ FIRMessagingTopicOperation *operation =
+ [[FIRMessagingTopicOperation alloc] initWithTopic:topic
+ action:action
+ token:token
+ options:options
+ checkinService:self.checkinService
+ completion:handler];
+ [self.topicOperations addOperation:operation];
+
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingReceiver.h b/Firebase/Messaging/FIRMessagingReceiver.h
new file mode 100644
index 0000000..6e4a693
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingReceiver.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 "FIRMessagingDataMessageManager.h"
+#import "FIRMessaging.h"
+
+@class FIRMessagingReceiver;
+@protocol FIRMessagingReceiverDelegate <NSObject>
+
+- (void)receiver:(nonnull FIRMessagingReceiver *)receiver
+ receivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage;
+
+@end
+
+
+@interface FIRMessagingReceiver : NSObject <FIRMessagingDataMessageManagerDelegate>
+@property(nonatomic, weak, nullable) id<FIRMessagingReceiverDelegate> delegate;
+@end
diff --git a/Firebase/Messaging/FIRMessagingReceiver.m b/Firebase/Messaging/FIRMessagingReceiver.m
new file mode 100644
index 0000000..7a99c92
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingReceiver.m
@@ -0,0 +1,141 @@
+/*
+ * 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 "FIRMessagingReceiver.h"
+
+#import <UIKit/UIKit.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessaging_Private.h"
+#import "FIRMessagingLogger.h"
+
+static NSString *const kUpstreamMessageIDUserInfoKey = @"messageID";
+static NSString *const kUpstreamErrorUserInfoKey = @"error";
+
+// Copied from Apple's header in case it is missing in some cases.
+#ifndef NSFoundationVersionNumber_iOS_9_x_Max
+#define NSFoundationVersionNumber_iOS_9_x_Max 1299
+#endif
+
+static int downstreamMessageID = 0;
+
+@implementation FIRMessagingReceiver
+
+#pragma mark - FIRMessagingDataMessageManager protocol
+
+- (void)didReceiveMessage:(NSDictionary *)message withIdentifier:(nullable NSString *)messageID {
+ if (![messageID length]) {
+ messageID = [[self class] nextMessageID];
+ }
+
+ if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max) {
+ // Use delegate method for iOS 10
+ [self scheduleIos10NotificationForMessage:message withIdentifier:messageID];
+ } else {
+ // Post notification directly to AppDelegate handlers. This is valid pre-iOS 10.
+ [self scheduleNotificationForMessage:message];
+ }
+}
+
+- (void)willSendDataMessageWithID:(NSString *)messageID error:(NSError *)error {
+ NSNotification *notification;
+ if (error) {
+ NSDictionary *userInfo = @{
+ kUpstreamMessageIDUserInfoKey : [messageID copy],
+ kUpstreamErrorUserInfoKey : error
+ };
+ notification = [NSNotification notificationWithName:FIRMessagingSendErrorNotification
+ object:nil
+ userInfo:userInfo];
+ [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver000,
+ @"Fail to send upstream message: %@ error: %@", messageID, error);
+ } else {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver001, @"Will send upstream message: %@",
+ messageID);
+ }
+}
+
+- (void)didSendDataMessageWithID:(NSString *)messageID {
+ // invoke the callbacks asynchronously
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver002, @"Did send upstream message: %@",
+ messageID);
+ NSNotification * notification =
+ [NSNotification notificationWithName:FIRMessagingSendSuccessNotification
+ object:nil
+ userInfo:@{ kUpstreamMessageIDUserInfoKey : [messageID copy] }];
+
+ [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
+}
+
+- (void)didDeleteMessagesOnServer {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver003,
+ @"Will send deleted messages notification");
+ NSNotification * notification =
+ [NSNotification notificationWithName:FIRMessagingMessagesDeletedNotification
+ object:nil];
+
+ [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
+}
+
+#pragma mark - Private Helpers
+// As the new UserNotifications framework in iOS 10 doesn't support constructor/mutation for
+// UNNotification object, FCM can't inject the message to the app with UserNotifications framework.
+// Define our own protocol, which means app developers need to implement two interfaces to receive
+// display notifications and data messages respectively for devices running iOS 10 or above. Devices
+// running iOS 9 or below are not affected.
+- (void)scheduleIos10NotificationForMessage:(NSDictionary *)message
+ withIdentifier:(NSString *)messageID {
+ FIRMessagingRemoteMessage *wrappedMessage = [[FIRMessagingRemoteMessage alloc] init];
+ // TODO: wrap title, body, badge and other fields
+ wrappedMessage.appData = [message copy];
+ [self.delegate receiver:self receivedRemoteMessage:wrappedMessage];
+}
+
+- (void)scheduleNotificationForMessage:(NSDictionary *)message {
+ SEL newNotificationSelector =
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
+ SEL oldNotificationSelector = @selector(application:didReceiveRemoteNotification:);
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ id<UIApplicationDelegate> appDelegate = [[UIApplication sharedApplication] delegate];
+ if ([appDelegate respondsToSelector:newNotificationSelector]) {
+ // Try the new remote notification callback
+ [appDelegate application:[UIApplication sharedApplication]
+ didReceiveRemoteNotification:message
+ fetchCompletionHandler:^(UIBackgroundFetchResult result) {}];
+
+ } else if ([appDelegate respondsToSelector:oldNotificationSelector]) {
+ // Try the old remote notification callback
+ [appDelegate application:
+ [UIApplication sharedApplication] didReceiveRemoteNotification:message];
+
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeReceiver005,
+ @"None of the remote notification callbacks implemented by "
+ @"UIApplicationDelegate");
+ }
+ });
+}
+
++ (NSString *)nextMessageID {
+ @synchronized (self) {
+ ++downstreamMessageID;
+ return [NSString stringWithFormat:@"gcm-%d", downstreamMessageID];
+ }
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRegistrar.h b/Firebase/Messaging/FIRMessagingRegistrar.h
new file mode 100644
index 0000000..5b60437
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRegistrar.h
@@ -0,0 +1,87 @@
+/*
+ * 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 "FIRMessagingCheckinService.h"
+#import "FIRMessagingTopicsCommon.h"
+
+@class FIRMessagingCheckinStore;
+@class FIRMessagingPubSubRegistrar;
+
+/**
+ * Handle the registration process for the client. Fetch checkin information from the Checkin
+ * service if not cached on the device and then try to register the client with FIRMessaging backend.
+ */
+@interface FIRMessagingRegistrar : NSObject
+
+@property(nonatomic, readonly, strong) FIRMessagingPubSubRegistrar *pubsubRegistrar;
+@property(nonatomic, readonly, strong) NSString *deviceAuthID;
+@property(nonatomic, readonly, strong) NSString *secretToken;
+
+/**
+ * Initialize a FIRMessaging Registrar.
+ *
+ * @return A FIRMessaging Registrar object.
+ */
+- (instancetype)init NS_DESIGNATED_INITIALIZER;
+
+#pragma mark - Checkin
+
+/**
+ * Try to load checkin info from the disk if not currently loaded into memory.
+ *
+ * @return YES if successfully loaded valid checkin info to memory else NO.
+ */
+- (BOOL)tryToLoadValidCheckinInfo;
+
+/**
+ * Check if we have a valid checkin info in memory.
+ *
+ * @return YES if we have valid checkin info in memory else NO.
+ */
+- (BOOL)hasValidCheckinInfo;
+
+#pragma mark - Subscribe/Unsubscribe
+
+/**
+ * Update the subscription for a given topic for the client.
+ *
+ * @param topic The topic for which the subscription should be updated.
+ * @param token The registration token to be used by the client.
+ * @param options The extra options if any being passed as part of
+ * subscription request.
+ * @param shouldDelete YES if we want to delete an existing subscription else NO
+ * if we want to create a new subscription.
+ * @param handler The handler to invoke once the subscription request is
+ * complete.
+ */
+- (void)updateSubscriptionToTopic:(NSString *)topic
+ withToken:(NSString *)token
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler;
+
+/**
+ * Cancel all subscription requests as well as any requests to checkin. Note if
+ * there are subscription requests waiting on checkin to complete those requests
+ * would be marked as stale and be NO-OP's if they happen in the future.
+ *
+ * Also note this is a one time operation, you should only call this if you want
+ * to immediately stop all requests and deallocate the registrar. After calling
+ * this once you would no longer be able to use this registrar object.
+ */
+- (void)cancelAllRequests;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRegistrar.m b/Firebase/Messaging/FIRMessagingRegistrar.m
new file mode 100644
index 0000000..ab57b9e
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRegistrar.m
@@ -0,0 +1,112 @@
+/*
+ * 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 "FIRMessagingRegistrar.h"
+
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPubSubRegistrar.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+@interface FIRMessagingRegistrar ()
+
+@property(nonatomic, readwrite, assign) BOOL stopAllSubscriptions;
+
+@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
+@property(nonatomic, readwrite, strong) FIRMessagingPubSubRegistrar *pubsubRegistrar;
+
+@end
+
+@implementation FIRMessagingRegistrar
+
+- (NSString *)deviceAuthID {
+ return self.checkinService.deviceAuthID;
+}
+
+- (NSString *)secretToken {
+ return self.checkinService.secretToken;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _checkinService = [[FIRMessagingCheckinService alloc] init];
+ _pubsubRegistrar = [[FIRMessagingPubSubRegistrar alloc] initWithCheckinService:_checkinService];
+ }
+ return self;
+}
+
+#pragma mark - Checkin
+
+- (BOOL)tryToLoadValidCheckinInfo {
+ if (![self.checkinService hasValidCheckinInfo]) {
+ [self.checkinService tryToLoadPrefetchedCheckinPreferences];
+ }
+ return [self.checkinService hasValidCheckinInfo];
+}
+
+- (BOOL)hasValidCheckinInfo {
+ return [self.checkinService hasValidCheckinInfo];
+}
+
+#pragma mark - Subscribe/Unsubscribe
+
+- (void)updateSubscriptionToTopic:(NSString *)topic
+ withToken:(NSString *)token
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ handler:(FIRMessagingTopicOperationCompletion)handler {
+ _FIRMessagingDevAssert(handler, @"Invalid nil handler");
+
+ if ([self tryToLoadValidCheckinInfo]) {
+ [self doUpdateSubscriptionForTopic:topic
+ token:token
+ options:options
+ shouldDelete:shouldDelete
+ completion:handler];
+
+ } else {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRegistrar000,
+ @"Device check in error, no auth credentials found");
+ NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID];
+ handler(FIRMessagingTopicOperationResultError, error);
+ }
+}
+
+- (void)cancelAllRequests {
+ self.stopAllSubscriptions = YES;
+ [self.pubsubRegistrar stopAllSubscriptionRequests];
+}
+
+#pragma mark - Private
+
+- (void)doUpdateSubscriptionForTopic:(NSString *)topic
+ token:(NSString *)token
+ options:(NSDictionary *)options
+ shouldDelete:(BOOL)shouldDelete
+ completion:(FIRMessagingTopicOperationCompletion)completion {
+ _FIRMessagingDevAssert([self.checkinService hasValidCheckinInfo],
+ @"No valid checkin info found before subscribe");
+
+ [self.pubsubRegistrar updateSubscriptionToTopic:topic
+ withToken:token
+ options:options
+ shouldDelete:shouldDelete
+ handler:completion];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.h b/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.h
new file mode 100644
index 0000000..59c3c15
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+/**
+ * Swizzle remote-notification callbacks to invoke FIRMessaging methods
+ * before calling original implementations.
+ */
+@interface FIRMessagingRemoteNotificationsProxy : NSObject
+
+/**
+ * Checks the `FirebaseAppDelegateProxyEnabled` key in the App's Info.plist. If the key is
+ * missing or incorrectly formatted, returns `YES`.
+ *
+ * @return YES if the Application Delegate and User Notification Center methods can be swizzled.
+ * Otherwise, returns NO.
+ */
++ (BOOL)canSwizzleMethods;
+
+/**
+ * Swizzles Application Delegate's remote-notification callbacks and User Notification Center
+ * delegate callback, and invokes the original selectors once done.
+ */
++ (void)swizzleMethods;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m b/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m
new file mode 100644
index 0000000..5432c79
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m
@@ -0,0 +1,613 @@
+/*
+ * 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 "FIRMessagingRemoteNotificationsProxy.h"
+
+#import <objc/runtime.h>
+#import <UIKit/UIKit.h>
+
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessaging_Private.h"
+
+static const BOOL kDefaultAutoRegisterEnabledValue = YES;
+static void * UserNotificationObserverContext = &UserNotificationObserverContext;
+
+static NSString *kUserNotificationWillPresentSelectorString =
+ @"userNotificationCenter:willPresentNotification:withCompletionHandler:";
+
+@interface FIRMessagingRemoteNotificationsProxy ()
+
+@property(strong, nonatomic) NSMutableDictionary<NSString *, NSValue *> *originalAppDelegateImps;
+@property(strong, nonatomic) NSMutableDictionary<NSString *, NSArray *> *swizzledSelectorsByClass;
+
+@property(nonatomic) BOOL didSwizzleMethods;
+@property(nonatomic) BOOL didSwizzleAppDelegateMethods;
+
+@property(nonatomic) BOOL hasSwizzledUserNotificationDelegate;
+@property(nonatomic) BOOL isObservingUserNotificationDelegateChanges;
+
+@property(strong, nonatomic) id userNotificationCenter;
+@property(strong, nonatomic) id currentUserNotificationCenterDelegate;
+
+@end
+
+@implementation FIRMessagingRemoteNotificationsProxy
+
++ (BOOL)canSwizzleMethods {
+ id canSwizzleValue =
+ [[NSBundle mainBundle]
+ objectForInfoDictionaryKey: kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey];
+ if (canSwizzleValue && [canSwizzleValue isKindOfClass:[NSNumber class]]) {
+ NSNumber *canSwizzleNumberValue = (NSNumber *)canSwizzleValue;
+ return canSwizzleNumberValue.boolValue;
+ } else {
+ return kDefaultAutoRegisterEnabledValue;
+ }
+}
+
++ (void)swizzleMethods {
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] swizzleMethodsIfPossible];
+}
+
++ (instancetype)sharedProxy {
+ static FIRMessagingRemoteNotificationsProxy *proxy;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ proxy = [[FIRMessagingRemoteNotificationsProxy alloc] init];
+ });
+ return proxy;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _originalAppDelegateImps = [[NSMutableDictionary alloc] init];
+ _swizzledSelectorsByClass = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self unswizzleAllMethods];
+ self.swizzledSelectorsByClass = nil;
+ [self.originalAppDelegateImps removeAllObjects];
+ self.originalAppDelegateImps = nil;
+ [self removeUserNotificationCenterDelegateObserver];
+}
+
+- (void)swizzleMethodsIfPossible {
+ // Already swizzled.
+ if (self.didSwizzleMethods) {
+ return;
+ }
+
+ NSObject<UIApplicationDelegate> *appDelegate = [[UIApplication sharedApplication] delegate];
+ [self swizzleAppDelegateMethods:appDelegate];
+
+ // Add KVO listener on [UNUserNotificationCenter currentNotificationCenter]'s delegate property
+ Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
+ if (notificationCenterClass) {
+ // We are linked against iOS 10 SDK or above
+ id notificationCenter = getNamedPropertyFromObject(notificationCenterClass,
+ @"currentNotificationCenter",
+ notificationCenterClass);
+ if (notificationCenter) {
+ [self listenForDelegateChangesInUserNotificationCenter:notificationCenter];
+ }
+ }
+
+ self.didSwizzleMethods = YES;
+}
+
+- (void)unswizzleAllMethods {
+ for (NSString *className in self.swizzledSelectorsByClass) {
+ Class klass = NSClassFromString(className);
+ NSArray *selectorStrings = self.swizzledSelectorsByClass[className];
+ for (NSString *selectorString in selectorStrings) {
+ SEL selector = NSSelectorFromString(selectorString);
+ [self unswizzleSelector:selector inClass:klass];
+ }
+ }
+ [self.swizzledSelectorsByClass removeAllObjects];
+}
+
+- (void)swizzleAppDelegateMethods:(id<UIApplicationDelegate>)appDelegate {
+ if (![appDelegate conformsToProtocol:@protocol(UIApplicationDelegate)]) {
+ return;
+ }
+ Class appDelegateClass = [appDelegate class];
+
+ BOOL didSwizzleAppDelegate = NO;
+ // Message receiving handler for iOS 9, 8, 7 devices (both display notification and data message).
+ SEL remoteNotificationSelector =
+ @selector(application:didReceiveRemoteNotification:);
+
+ SEL remoteNotificationWithFetchHandlerSelector =
+ @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
+
+ // For data message from MCS.
+ SEL receiveDataMessageSelector = NSSelectorFromString(@"applicationReceivedRemoteMessage:");
+
+ // For recording when APNS tokens are registered (or fail to register)
+ SEL registerForAPNSFailSelector =
+ @selector(application:didFailToRegisterForRemoteNotificationsWithError:);
+
+ SEL registerForAPNSSuccessSelector =
+ @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
+
+
+ // Receive Remote Notifications.
+ BOOL selectorWithFetchHandlerImplemented = NO;
+ if ([appDelegate respondsToSelector:remoteNotificationWithFetchHandlerSelector]) {
+ selectorWithFetchHandlerImplemented = YES;
+ [self swizzleSelector:remoteNotificationWithFetchHandlerSelector
+ inClass:appDelegateClass
+ withImplementation:(IMP)FCM_swizzle_appDidReceiveRemoteNotificationWithHandler
+ inProtocol:@protocol(UIApplicationDelegate)];
+ didSwizzleAppDelegate = YES;
+ }
+
+ if ([appDelegate respondsToSelector:remoteNotificationSelector] ||
+ !selectorWithFetchHandlerImplemented) {
+ [self swizzleSelector:remoteNotificationSelector
+ inClass:appDelegateClass
+ withImplementation:(IMP)FCM_swizzle_appDidReceiveRemoteNotification
+ inProtocol:@protocol(UIApplicationDelegate)];
+ didSwizzleAppDelegate = YES;
+ }
+
+ if ([appDelegate respondsToSelector:receiveDataMessageSelector]) {
+ [self swizzleSelector:receiveDataMessageSelector
+ inClass:appDelegateClass
+ withImplementation:(IMP)FCM_swizzle_applicationReceivedRemoteMessage
+ inProtocol:@protocol(UIApplicationDelegate)];
+ didSwizzleAppDelegate = YES;
+ }
+
+ // Receive APNS token
+ [self swizzleSelector:registerForAPNSSuccessSelector
+ inClass:appDelegateClass
+ withImplementation:(IMP)FCM_swizzle_appDidRegisterForRemoteNotifications
+ inProtocol:@protocol(UIApplicationDelegate)];
+
+ [self swizzleSelector:registerForAPNSFailSelector
+ inClass:appDelegateClass
+ withImplementation:(IMP)FCM_swizzle_appDidFailToRegisterForRemoteNotifications
+ inProtocol:@protocol(UIApplicationDelegate)];
+
+ self.didSwizzleAppDelegateMethods = didSwizzleAppDelegate;
+}
+
+- (void)listenForDelegateChangesInUserNotificationCenter:(id)notificationCenter {
+ Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
+ if (![notificationCenter isKindOfClass:notificationCenterClass]) {
+ return;
+ }
+ id delegate = getNamedPropertyFromObject(notificationCenter, @"delegate", nil);
+ Protocol *delegateProtocol = NSProtocolFromString(@"UNUserNotificationCenterDelegate");
+ if ([delegate conformsToProtocol:delegateProtocol]) {
+ // Swizzle this object now, if available
+ [self swizzleUserNotificationCenterDelegate:delegate];
+ }
+ // Add KVO observer for "delegate" keyPath for future changes
+ [self addDelegateObserverToUserNotificationCenter:notificationCenter];
+}
+
+#pragma mark - UNNotificationCenter Swizzling
+
+- (void)swizzleUserNotificationCenterDelegate:(id)delegate {
+ if (self.currentUserNotificationCenterDelegate == delegate) {
+ // Via pointer-check, compare if we have already swizzled this item.
+ return;
+ }
+ Protocol *userNotificationCenterProtocol =
+ NSProtocolFromString(@"UNUserNotificationCenterDelegate");
+ if ([delegate conformsToProtocol:userNotificationCenterProtocol]) {
+ SEL willPresentNotificationSelector =
+ NSSelectorFromString(kUserNotificationWillPresentSelectorString);
+ [self swizzleSelector:willPresentNotificationSelector
+ inClass:[delegate class]
+ withImplementation:(IMP)FCM_swizzle_willPresentNotificationWithHandler
+ inProtocol:userNotificationCenterProtocol];
+ self.currentUserNotificationCenterDelegate = delegate;
+ self.hasSwizzledUserNotificationDelegate = YES;
+ }
+}
+
+- (void)unswizzleUserNotificationCenterDelegate:(id)delegate {
+ if (self.currentUserNotificationCenterDelegate != delegate) {
+ // We aren't swizzling this delegate, so don't do anything.
+ return;
+ }
+ SEL willPresentNotificationSelector =
+ NSSelectorFromString(kUserNotificationWillPresentSelectorString);
+ [self unswizzleSelector:willPresentNotificationSelector
+ inClass:[self.currentUserNotificationCenterDelegate class]];
+ self.currentUserNotificationCenterDelegate = nil;
+ self.hasSwizzledUserNotificationDelegate = NO;
+}
+
+#pragma mark - KVO for UNUserNotificationCenter
+
+- (void)addDelegateObserverToUserNotificationCenter:(id)userNotificationCenter {
+ [self removeUserNotificationCenterDelegateObserver];
+ @try {
+ [userNotificationCenter addObserver:self
+ forKeyPath:NSStringFromSelector(@selector(delegate))
+ options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
+ context:UserNotificationObserverContext];
+ self.userNotificationCenter = userNotificationCenter;
+ self.isObservingUserNotificationDelegateChanges = YES;
+ } @catch (NSException *exception) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxy000,
+ @"Encountered exception trying to add a KVO observer for "
+ @"UNUserNotificationCenter's 'delegate' property: %@",
+ exception);
+ } @finally {
+
+ }
+}
+
+- (void)removeUserNotificationCenterDelegateObserver {
+ if (!self.userNotificationCenter) {
+ return;
+ }
+ @try {
+ [self.userNotificationCenter removeObserver:self
+ forKeyPath:NSStringFromSelector(@selector(delegate))
+ context:UserNotificationObserverContext];
+ self.userNotificationCenter = nil;
+ self.isObservingUserNotificationDelegateChanges = NO;
+ } @catch (NSException *exception) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxy001,
+ @"Encountered exception trying to remove a KVO observer for "
+ @"UNUserNotificationCenter's 'delegate' property: %@",
+ exception);
+ } @finally {
+
+ }
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath
+ ofObject:(id)object
+ change:(NSDictionary<NSKeyValueChangeKey, id> *)change
+ context:(void *)context {
+ if (context == UserNotificationObserverContext) {
+ if ([keyPath isEqualToString:NSStringFromSelector(@selector(delegate))]) {
+ id oldDelegate = change[NSKeyValueChangeOldKey];
+ if (oldDelegate && oldDelegate != [NSNull null]) {
+ [self unswizzleUserNotificationCenterDelegate:oldDelegate];
+ }
+ id newDelegate = change[NSKeyValueChangeNewKey];
+ if (newDelegate && newDelegate != [NSNull null]) {
+ [self swizzleUserNotificationCenterDelegate:newDelegate];
+ }
+ }
+ } else {
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+ }
+}
+
+#pragma mark - NSProxy methods
+
+- (void)saveOriginalImplementation:(IMP)imp forSelector:(SEL)selector {
+ if (imp && selector) {
+ NSValue *IMPValue = [NSValue valueWithPointer:imp];
+ NSString *selectorString = NSStringFromSelector(selector);
+ self.originalAppDelegateImps[selectorString] = IMPValue;
+ }
+}
+
+- (IMP)originalImplementationForSelector:(SEL)selector {
+ NSString *selectorString = NSStringFromSelector(selector);
+ NSValue *implementation_value = self.originalAppDelegateImps[selectorString];
+ if (!implementation_value) {
+ return nil;
+ }
+
+ IMP imp;
+ [implementation_value getValue:&imp];
+ return imp;
+}
+
+- (void)trackSwizzledSelector:(SEL)selector ofClass:(Class)klass {
+ NSString *className = NSStringFromClass(klass);
+ NSString *selectorString = NSStringFromSelector(selector);
+ NSArray *selectors = self.swizzledSelectorsByClass[selectorString];
+ if (selectors) {
+ selectors = [selectors arrayByAddingObject:selectorString];
+ } else {
+ selectors = @[selectorString];
+ }
+ self.swizzledSelectorsByClass[className] = selectors;
+}
+
+- (void)removeImplementationForSelector:(SEL)selector {
+ NSString *selectorString = NSStringFromSelector(selector);
+ [self.originalAppDelegateImps removeObjectForKey:selectorString];
+}
+
+- (void)swizzleSelector:(SEL)originalSelector
+ inClass:(Class)klass
+ withImplementation:(IMP)swizzledImplementation
+ inProtocol:(Protocol *)protocol {
+ Method originalMethod = class_getInstanceMethod(klass, originalSelector);
+
+ if (originalMethod) {
+ // This class implements this method, so replace the original implementation
+ // with our new implementation and save the old implementation.
+
+ IMP __original_method_implementation =
+ method_setImplementation(originalMethod, swizzledImplementation);
+
+ IMP __nonexistant_method_implementation = [self nonExistantMethodImplementationForClass:klass];
+
+ if (__original_method_implementation &&
+ __original_method_implementation != __nonexistant_method_implementation &&
+ __original_method_implementation != swizzledImplementation) {
+ [self saveOriginalImplementation:__original_method_implementation
+ forSelector:originalSelector];
+ }
+ } else {
+ // The class doesn't have this method, so add our swizzled implementation as the
+ // original implementation of the original method.
+ struct objc_method_description method_description =
+ protocol_getMethodDescription(protocol, originalSelector, NO, YES);
+
+ class_addMethod(klass,
+ originalSelector,
+ swizzledImplementation,
+ method_description.types);
+ }
+ [self trackSwizzledSelector:originalSelector ofClass:klass];
+}
+
+- (void)unswizzleSelector:(SEL)selector inClass:(Class)klass {
+
+ Method swizzledMethod = class_getInstanceMethod(klass, selector);
+ if (!swizzledMethod) {
+ // This class doesn't seem to have this selector as an instance method? Bail out.
+ return;
+ }
+
+ IMP original_imp = [self originalImplementationForSelector:selector];
+ if (original_imp) {
+ // Restore the original implementation as the current implementation
+ method_setImplementation(swizzledMethod, original_imp);
+ [self removeImplementationForSelector:selector];
+ } else {
+ // This class originally did not have an implementation for this selector.
+
+ // We can't actually remove methods in Objective C 2.0, but we could set
+ // its method to something non-existent. This should give us the same
+ // behavior as if the method was not implemented.
+ // See: http://stackoverflow.com/a/8276527/9849
+
+ IMP nonExistantMethodImplementation = [self nonExistantMethodImplementationForClass:klass];
+ method_setImplementation(swizzledMethod, nonExistantMethodImplementation);
+ }
+}
+
+#pragma mark - Reflection Helpers
+
+// This is useful to generate from a stable, "known missing" selector, as the IMP can be compared
+// in case we are setting an implementation for a class that was previously "unswizzled" into a
+// non-existant implementation.
+- (IMP)nonExistantMethodImplementationForClass:(Class)klass {
+ SEL nonExistantSelector = NSSelectorFromString(@"aNonExistantMethod");
+ IMP nonExistantMethodImplementation = class_getMethodImplementation(klass, nonExistantSelector);
+ return nonExistantMethodImplementation;
+}
+
+// A safe, non-leaky way return a property object by its name
+id getNamedPropertyFromObject(id object, NSString *propertyName, Class klass) {
+ SEL selector = NSSelectorFromString(propertyName);
+ if (![object respondsToSelector:selector]) {
+ return nil;
+ }
+ if (!klass) {
+ klass = [NSObject class];
+ }
+ // Suppress clang warning about leaks in performSelector
+ // The alternative way to perform this is to invoke
+ // the method as a block (see http://stackoverflow.com/a/20058585),
+ // but this approach sometimes returns incomplete objects.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ id property = [object performSelector:selector];
+#pragma clang diagnostic pop
+ if (![property isKindOfClass:klass]) {
+ return nil;
+ }
+ return property;
+}
+
+#pragma mark - Swizzled Methods
+
+void FCM_swizzle_appDidReceiveRemoteNotification(id self,
+ SEL _cmd,
+ UIApplication *app,
+ NSDictionary *userInfo) {
+ [[FIRMessaging messaging] appDidReceiveMessage:userInfo];
+
+ IMP original_imp =
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd];
+ if (original_imp) {
+ ((void (*)(id, SEL, UIApplication *, NSDictionary *))original_imp)(self,
+ _cmd,
+ app,
+ userInfo);
+ }
+}
+
+void FCM_swizzle_appDidReceiveRemoteNotificationWithHandler(
+ id self, SEL _cmd, UIApplication *app, NSDictionary *userInfo,
+ void (^handler)(UIBackgroundFetchResult)) {
+
+ [[FIRMessaging messaging] appDidReceiveMessage:userInfo];
+
+ IMP original_imp =
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd];
+ if (original_imp) {
+ ((void (*)(id, SEL, UIApplication *, NSDictionary *,
+ void (^)(UIBackgroundFetchResult)))original_imp)(
+ self, _cmd, app, userInfo, handler);
+ }
+}
+
+/**
+ * Swizzle the notification handler for iOS 10+ devices.
+ * Signature of original handler is as below:
+ * - (void)userNotificationCenter:(UNUserNotificationCenter *)center
+ * willPresentNotification:(UNNotification *)notification
+ * withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
+ * In order to make FCM SDK compile and compatible with iOS SDKs before iOS 10, hide the
+ * parameter types from the swizzling implementation.
+ */
+void FCM_swizzle_willPresentNotificationWithHandler(
+ id self, SEL _cmd, id center, id notification, void (^handler)(NSUInteger)) {
+
+ FIRMessagingRemoteNotificationsProxy *proxy = [FIRMessagingRemoteNotificationsProxy sharedProxy];
+ IMP original_imp = [proxy originalImplementationForSelector:_cmd];
+
+ void (^callOriginalMethodIfAvailable)() = ^{
+ if (original_imp) {
+ ((void (*)(id, SEL, id, id, void (^)(NSUInteger)))original_imp)(
+ self, _cmd, center, notification, handler);
+ }
+ return;
+ };
+
+ Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
+ Class notificationClass = NSClassFromString(@"UNNotification");
+ if (!notificationCenterClass || !notificationClass) {
+ // Can't find UserNotifications framework. Do not swizzle, just execute the original method.
+ callOriginalMethodIfAvailable();
+ }
+
+ if (!center || ![center isKindOfClass:[notificationCenterClass class]]) {
+ // Invalid parameter type from the original method.
+ // Do not swizzle, just execute the original method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+
+ if (!notification || ![notification isKindOfClass:[notificationClass class]]) {
+ // Invalid parameter type from the original method.
+ // Do not swizzle, just execute the original method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+
+ if (!handler) {
+ // Invalid parameter type from the original method.
+ // Do not swizzle, just execute the original method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+
+ // Valid original method signature, go ahead to swizzle.
+ // Select the userInfo field from UNNotification.request.content.userInfo.
+ SEL requestSelector = NSSelectorFromString(@"request");
+ if (![notification respondsToSelector:requestSelector]) {
+ // This is not the expected notification handler. Do not swizzle, just execute the original
+ // method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+ Class requestClass = NSClassFromString(@"UNNotificationRequest");
+ id notificationRequest = getNamedPropertyFromObject(notification, @"request", requestClass);
+
+ SEL notificationContentSelector = NSSelectorFromString(@"content");
+ if (!notificationRequest
+ || ![notificationRequest respondsToSelector:notificationContentSelector]) {
+ // This is not the expected notification handler. Do not swizzle, just execute the original
+ // method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+ Class contentClass = NSClassFromString(@"UNNotificationContent");
+ id notificationContent = getNamedPropertyFromObject(notificationRequest,
+ @"content",
+ contentClass);
+
+ SEL notificationUserInfoSelector = NSSelectorFromString(@"userInfo");
+ if (!notificationContent
+ || ![notificationContent respondsToSelector:notificationUserInfoSelector]) {
+ // This is not the expected notification handler. Do not swizzle, just execute the original
+ // method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+ id notificationUserInfo = getNamedPropertyFromObject(notificationContent,
+ @"userInfo",
+ [NSDictionary class]);
+
+ if (!notificationUserInfo) {
+ // This is not the expected notification handler. Do not swizzle, just execute the original
+ // method.
+ callOriginalMethodIfAvailable();
+ return;
+ }
+
+ [[FIRMessaging messaging] appDidReceiveMessage:notificationUserInfo];
+ // Execute the original implementation.
+ callOriginalMethodIfAvailable();
+}
+
+void FCM_swizzle_applicationReceivedRemoteMessage(
+ id self, SEL _cmd, FIRMessagingRemoteMessage *remoteMessage) {
+ [[FIRMessaging messaging] appDidReceiveMessage:remoteMessage.appData];
+
+ IMP original_imp =
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd];
+ if (original_imp) {
+ ((void (*)(id, SEL, FIRMessagingRemoteMessage *))original_imp)(self, _cmd, remoteMessage);
+ }
+}
+
+void FCM_swizzle_appDidFailToRegisterForRemoteNotifications(id self,
+ SEL _cmd,
+ UIApplication *app,
+ NSError *error) {
+ // Log the fact that we failed to register for remote notifications
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxyAPNSFailed,
+ @"Error in "
+ @"application:didFailToRegisterForRemoteNotificationsWithError: %@",
+ error.localizedDescription);
+ IMP original_imp =
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd];
+ if (original_imp) {
+ ((void (*)(id, SEL, UIApplication *, NSError *))original_imp)(self, _cmd, app, error);
+ }
+}
+
+void FCM_swizzle_appDidRegisterForRemoteNotifications(id self,
+ SEL _cmd,
+ UIApplication *app,
+ NSData *deviceToken) {
+ // Pass the APNSToken along to FIRMessaging (and auto-detect the token type)
+ [FIRMessaging messaging].APNSToken = deviceToken;
+
+ IMP original_imp =
+ [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd];
+ if (original_imp) {
+ ((void (*)(id, SEL, UIApplication *, NSData *))original_imp)(self, _cmd, app, deviceToken);
+ }
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRmq2PersistentStore.h b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.h
new file mode 100644
index 0000000..09f1d44
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.h
@@ -0,0 +1,201 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FIRMessagingPersistentSyncMessage;
+
+// table data handlers
+/**
+ * Handle message stored in the outgoing RMQ messages table.
+ *
+ * @param rmqId The rmqID of the message.
+ * @param tag The message tag.
+ * @param data The data stored in the message.
+ */
+typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);
+
+/// Outgoing messages RMQ table
+extern NSString *const kTableOutgoingRmqMessages;
+/// Server to device RMQ table
+extern NSString *const kTableS2DRmqIds;
+
+@interface FIRMessagingRmq2PersistentStore : NSObject
+
+/**
+ * Initialize and open the RMQ database on the client.
+ *
+ * @param databaseName The name for RMQ database.
+ *
+ * @return A store used to persist messages on the client.
+ */
+- (instancetype)initWithDatabaseName:(NSString *)databaseName;
+
+/**
+ * Save outgoing message in RMQ.
+ *
+ * @param rmqId The rmqID for the message.
+ * @param tag The tag of the message proto.
+ * @param data The data being sent in the message.
+ * @param error The error if any while saving the message to the persistent store.
+ *
+ * @return YES if the message was successfully saved to the persistent store else NO.
+ */
+- (BOOL)saveMessageWithRmqId:(int64_t)rmqId
+ tag:(int8_t)tag
+ data:(NSData *)data
+ error:(NSError **)error;
+
+/**
+ * Add unacked server to device message with a given rmqID to the persistent store.
+ *
+ * @param rmqId The rmqID of the message that was not acked by the cient.
+ *
+ * @return YES if the save was successful else NO.
+ */
+- (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId;
+
+/**
+ * Update the last RMQ ID that was sent by the client.
+ *
+ * @param rmqID The latest rmqID sent by the device.
+ *
+ * @return YES if the last rmqID was successfully saved else NO.
+ */
+- (BOOL)updateLastOutgoingRmqId:(int64_t)rmqID;
+
+#pragma mark - Query
+
+/**
+ * Query the highest rmqID saved in the Outgoing messages table.
+ *
+ * @return The highest rmqID amongst all the messages in the Outgoing RMQ table. If no message
+ * was ever persisted return 0.
+ */
+- (int64_t)queryHighestRmqId;
+
+/**
+ * The last rmqID that was saved on the client.
+ *
+ * @return The last rmqID that was saved. If no rmqID was ever persisted return 0.
+ */
+- (int64_t)queryLastRmqId;
+
+/**
+ * Get a list of all unacked server to device messages stored on the client.
+ *
+ * @return List of all unacked s2d messages in the persistent store.
+ */
+- (NSArray *)unackedS2dRmqIds;
+
+/**
+ * Iterate over all outgoing messages in the RMQ table.
+ *
+ * @param handler The handler invoked with each message in the outgoing RMQ table.
+ */
+- (void)scanOutgoingRmqMessagesWithHandler:(FCMOutgoingRmqMessagesTableHandler)handler;
+
+#pragma mark - Delete
+
+/**
+ * Delete messages with given rmqID's from a table.
+ *
+ * @param tableName The table name from which to delete the rmq messages.
+ * @param rmqIds The rmqID's of the messages to be deleted.
+ *
+ * @return The number of messages that were successfully deleted.
+ */
+- (int)deleteMessagesFromTable:(NSString *)tableName
+ withRmqIds:(NSArray *)rmqIds;
+
+/**
+ * Remove database from the device.
+ *
+ * @param dbName The database name to be deleted.
+ */
++ (void)removeDatabase:(NSString *)dbName;
+
+#pragma mark - Sync Messages
+
+/**
+ * Save sync message to persistent store to check for duplicates.
+ *
+ * @param rmqID The rmqID of the message to save.
+ * @param expirationTime The expiration time of the message to save.
+ * @param apnsReceived YES if the message was received via APNS else NO.
+ * @param mcsReceived YES if the message was received via MCS else NO.
+ * @param error The error if any while saving the message to store.
+ *
+ * @return YES if the message was saved successfully else NO.
+ */
+- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
+ expirationTime:(int64_t)expirationTime
+ apnsReceived:(BOOL)apnsReceived
+ mcsReceived:(BOOL)mcsReceived
+ error:(NSError **)error;
+
+/**
+ * Update sync message received via APNS.
+ *
+ * @param rmqID The rmqID of the sync message.
+ * @param error The error if any while updating the sync message in persistence.
+ *
+ * @return YES if the update was successful else NO.
+ */
+- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
+ error:(NSError **)error;
+
+/**
+ * Update sync message received via MCS.
+ *
+ * @param rmqID The rmqID of the sync message.
+ * @param error The error if any while updating the sync message in persistence.
+ *
+ * @return YES if the update was successful else NO.
+ */
+- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
+ error:(NSError **)error;
+
+/**
+ * Query sync message table for a given rmqID.
+ *
+ * @param rmqID The rmqID to search for in SYNC_RMQ.
+ *
+ * @return The sync message that was persisted with `rmqID`. If no such message was persisted
+ * return nil.
+ */
+- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID;
+
+/**
+ * Delete sync message with rmqID.
+ *
+ * @param rmqID The rmqID of the message to delete.
+ *
+ * @return YES if a sync message with rmqID was found and deleted successfully else NO.
+ */
+- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID;
+
+/**
+ * Delete the expired sync messages from persisten store. Also deletes messages that have been
+ * delivered both via APNS and MCS.
+ *
+ * @param error The error if any while deleting the messages.
+ *
+ * @return The total number of messages that were deleted from the persistent store.
+ */
+- (int)deleteExpiredOrFinishedSyncMessages:(NSError **)error;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m
new file mode 100644
index 0000000..9edac40
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m
@@ -0,0 +1,770 @@
+/*
+ * 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 "FIRMessagingRmq2PersistentStore.h"
+
+#import "sqlite3.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPersistentSyncMessage.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+#ifndef _FIRMessagingRmqLogAndExit
+#define _FIRMessagingRmqLogAndExit(stmt, return_value) \
+do { \
+[self logErrorAndFinalizeStatement:stmt]; \
+return return_value; \
+} while(0)
+#endif
+
+typedef enum : NSUInteger {
+ FIRMessagingRmqDirectoryUnknown,
+ FIRMessagingRmqDirectoryDocuments,
+ FIRMessagingRmqDirectoryApplicationSupport,
+} FIRMessagingRmqDirectory;
+
+static NSString *const kFCMRmqStoreTag = @"FIRMessagingRmqStore:";
+
+// table names
+NSString *const kTableOutgoingRmqMessages = @"outgoingRmqMessages";
+NSString *const kTableLastRmqId = @"lastrmqid";
+NSString *const kOldTableS2DRmqIds = @"s2dRmqIds";
+NSString *const kTableS2DRmqIds = @"s2dRmqIds_1";
+
+// Used to prevent de-duping of sync messages received both via APNS and MCS.
+NSString *const kTableSyncMessages = @"incomingSyncMessages";
+
+static NSString *const kTablePrefix = @"";
+
+// create tables
+static NSString *const kCreateTableOutgoingRmqMessages =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id INTEGER, "
+ @"type INTEGER, "
+ @"ts INTEGER, "
+ @"data BLOB)";
+
+static NSString *const kCreateTableLastRmqId =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id INTEGER)";
+
+static NSString *const kCreateTableS2DRmqIds =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id TEXT)";
+
+static NSString *const kCreateTableSyncMessages =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id TEXT, "
+ @"expiration_ts INTEGER, "
+ @"apns_recv INTEGER, "
+ @"mcs_recv INTEGER)";
+
+static NSString *const kDropTableCommand =
+ @"drop TABLE if exists %@%@";
+
+// table infos
+static NSString *const kRmqIdColumn = @"rmq_id";
+static NSString *const kDataColumn = @"data";
+static NSString *const kProtobufTagColumn = @"type";
+static NSString *const kIdColumn = @"_id";
+
+static NSString *const kOutgoingRmqMessagesColumns = @"rmq_id, type, data";
+
+// Sync message columns
+static NSString *const kSyncMessagesColumns = @"rmq_id, expiration_ts, apns_recv, mcs_recv";
+// Message time expiration in seconds since 1970
+static NSString *const kSyncMessageExpirationTimestampColumn = @"expiration_ts";
+static NSString *const kSyncMessageAPNSReceivedColumn = @"apns_recv";
+static NSString *const kSyncMessageMCSReceivedColumn = @"mcs_recv";
+
+// table data handlers
+typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);
+
+@interface FIRMessagingRmq2PersistentStore () {
+ sqlite3 *_database;
+}
+
+@property(nonatomic, readwrite, strong) NSString *databaseName;
+@property(nonatomic, readwrite, assign) FIRMessagingRmqDirectory currentDirectory;
+
+@end
+
+@implementation FIRMessagingRmq2PersistentStore
+
+- (instancetype)initWithDatabaseName:(NSString *)databaseName {
+ self = [super init];
+ if (self) {
+ _databaseName = [databaseName copy];
+ BOOL didMoveToApplicationSupport =
+ [self moveToApplicationSupportSubDirectory:kFIRMessagingApplicationSupportSubDirectory];
+
+ _currentDirectory = didMoveToApplicationSupport
+ ? FIRMessagingRmqDirectoryApplicationSupport
+ : FIRMessagingRmqDirectoryDocuments;
+
+ [self openDatabase:_databaseName];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ sqlite3_close(_database);
+}
+
+- (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName {
+ NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+ NSUserDomainMask, YES);
+ 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:FIRMessagingRmqDirectoryDocuments]) {
+ NSString *oldPlistPath = [[self class] pathForDatabase:self.databaseName
+ inDirectory:FIRMessagingRmqDirectoryDocuments];
+ NSString *newPlistPath = [[self class]
+ pathForDatabase:self.databaseName
+ inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
+
+ if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryApplicationSupport]) {
+ // File exists in both Documents and ApplicationSupport, delete the one in Documents
+ NSError *deleteError;
+ if (![[NSFileManager defaultManager] removeItemAtPath:oldPlistPath error:&deleteError]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore000,
+ @"Failed to delete old copy of %@.sqlite in Documents %@",
+ self.databaseName, deleteError);
+ }
+ return NO;
+ }
+ NSError *moveError;
+ if (![[NSFileManager defaultManager] moveItemAtPath:oldPlistPath
+ toPath:newPlistPath
+ error:&moveError]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore001,
+ @"Failed to move file %@ from %@ to %@. Error: %@", self.databaseName,
+ oldPlistPath, newPlistPath, moveError);
+ return NO;
+ }
+ }
+ // We moved the file if it existed, otherwise we didn't need to do anything
+ return YES;
+}
+
+- (BOOL)doesFileExistInDirectory:(FIRMessagingRmqDirectory)directory {
+ NSString *path = [[self class] pathForDatabase:self.databaseName inDirectory:directory];
+ return [[NSFileManager defaultManager] fileExistsAtPath:path];
+}
+
++ (NSString *)pathForDatabase:(NSString *)dbName inDirectory:(FIRMessagingRmqDirectory)directory {
+ NSArray *paths;
+ NSArray *components;
+ NSString *dbNameWithExtension = [NSString stringWithFormat:@"%@.sqlite", dbName];
+
+ switch (directory) {
+ case FIRMessagingRmqDirectoryDocuments:
+ paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ components = @[paths.lastObject, dbNameWithExtension];
+ break;
+
+ case FIRMessagingRmqDirectoryApplicationSupport:
+ paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+ NSUserDomainMask,
+ YES);
+ components = @[
+ paths.lastObject,
+ kFIRMessagingApplicationSupportSubDirectory,
+ dbNameWithExtension
+ ];
+ break;
+
+ default:
+ FIRMessaging_FAIL(@"Invalid directory type %zd", directory);
+ break;
+ }
+
+ return [NSString pathWithComponents:components];
+}
+
+- (void)createTableWithName:(NSString *)tableName command:(NSString *)command {
+ char *error;
+ NSString *createDatabase = [NSString stringWithFormat:command, kTablePrefix, tableName];
+ if (sqlite3_exec(_database, [createDatabase UTF8String], NULL, NULL, &error) != SQLITE_OK) {
+ // remove db before failing
+ [self removeDatabase];
+ FIRMessaging_FAIL(@"Couldn't create table: %@ %@",
+ kCreateTableOutgoingRmqMessages,
+ [NSString stringWithCString:error encoding:NSUTF8StringEncoding]);
+ }
+}
+
+- (void)dropTableWithName:(NSString *)tableName {
+ char *error;
+ NSString *dropTableSQL = [NSString stringWithFormat:kDropTableCommand, kTablePrefix, tableName];
+ if (sqlite3_exec(_database, [dropTableSQL UTF8String], NULL, NULL, &error) != SQLITE_OK) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore002,
+ @"Failed to remove table %@", tableName);
+ }
+}
+
+- (void)removeDatabase {
+ NSString *path = [[self class] pathForDatabase:self.databaseName
+ inDirectory:self.currentDirectory];
+ [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+}
+
++ (void)removeDatabase:(NSString *)dbName {
+ NSString *documentsDirPath = [self pathForDatabase:dbName
+ inDirectory:FIRMessagingRmqDirectoryDocuments];
+ NSString *applicationSupportDirPath =
+ [self pathForDatabase:dbName inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
+ [[NSFileManager defaultManager] removeItemAtPath:documentsDirPath error:nil];
+ [[NSFileManager defaultManager] removeItemAtPath:applicationSupportDirPath error:nil];
+}
+
+- (void)openDatabase:(NSString *)dbName {
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSString *path = [[self class] pathForDatabase:dbName inDirectory:self.currentDirectory];
+
+ BOOL didOpenDatabase = YES;
+ if (![fileManager fileExistsAtPath:path]) {
+ // We've to separate between different versions here because of backwards compatbility issues.
+ if (sqlite3_open([path UTF8String], &_database) != SQLITE_OK) {
+ FIRMessaging_FAIL(@"%@ Could not open rmq database: %@", kFCMRmqStoreTag, path);
+ didOpenDatabase = NO;
+ return;
+ }
+ [self createTableWithName:kTableOutgoingRmqMessages
+ command:kCreateTableOutgoingRmqMessages];
+
+ [self createTableWithName:kTableLastRmqId command:kCreateTableLastRmqId];
+ [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
+ } else {
+ if (sqlite3_open([path UTF8String], &_database) != SQLITE_OK) {
+ FIRMessaging_FAIL(@"%@ Could not open rmq database: %@", kFCMRmqStoreTag, path);
+ didOpenDatabase = NO;
+ } else {
+ [self updateDbWithStringRmqID];
+ }
+ }
+
+ if (didOpenDatabase) {
+ [self createTableWithName:kTableSyncMessages command:kCreateTableSyncMessages];
+ }
+}
+
+- (void)updateDbWithStringRmqID {
+ [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
+ [self dropTableWithName:kOldTableS2DRmqIds];
+}
+
+#pragma mark - Insert
+
+- (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId {
+ NSString *insertFormat = @"INSERT INTO %@ (%@) VALUES (?)";
+ NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+ kTableS2DRmqIds,
+ kRmqIdColumn];
+ sqlite3_stmt *insert_statement;
+ if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
+ != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_text(insert_statement,
+ 1,
+ [rmqId UTF8String],
+ (int)[rmqId length],
+ SQLITE_STATIC) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_step(insert_statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ sqlite3_finalize(insert_statement);
+ return YES;
+}
+
+- (BOOL)saveMessageWithRmqId:(int64_t)rmqId
+ tag:(int8_t)tag
+ data:(NSData *)data
+ error:(NSError **)error {
+ NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@) VALUES (?, ?, ?)";
+ NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+ kTableOutgoingRmqMessages, // table
+ kRmqIdColumn, kProtobufTagColumn, kDataColumn /* columns */];
+ sqlite3_stmt *insert_statement;
+ if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
+ != SQLITE_OK) {
+ if (error) {
+ *error = [NSError errorWithDomain:[NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]
+ code:sqlite3_errcode(_database)
+ userInfo:nil];
+ }
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_int64(insert_statement, 1, rmqId) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_int(insert_statement, 2, tag) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_blob(insert_statement, 3, [data bytes], (int)[data length], NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_step(insert_statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+
+ sqlite3_finalize(insert_statement);
+ return YES;
+}
+
+- (int)deleteMessagesFromTable:(NSString *)tableName
+ withRmqIds:(NSArray *)rmqIds {
+ _FIRMessagingDevAssert([tableName isEqualToString:kTableOutgoingRmqMessages] ||
+ [tableName isEqualToString:kTableLastRmqId] ||
+ [tableName isEqualToString:kTableS2DRmqIds] ||
+ [tableName isEqualToString:kTableSyncMessages],
+ @"%@: Invalid Table Name %@", kFCMRmqStoreTag, tableName);
+
+ BOOL isRmqIDString = NO;
+ // RmqID is a string only for outgoing messages
+ if ([tableName isEqualToString:kTableS2DRmqIds] ||
+ [tableName isEqualToString:kTableSyncMessages]) {
+ isRmqIDString = YES;
+ }
+
+ NSMutableString *delete = [NSMutableString stringWithFormat:@"DELETE FROM %@ WHERE ", tableName];
+
+ NSString *toDeleteArgument = [NSString stringWithFormat:@"%@ = ? OR ", kRmqIdColumn];
+
+ int toDelete = (int)[rmqIds count];
+ if (toDelete == 0) {
+ return 0;
+ }
+ int maxBatchSize = 100;
+ int start = 0;
+ int deleteCount = 0;
+ while (start < toDelete) {
+
+ // construct the WHERE argument
+ int end = MIN(start + maxBatchSize, toDelete);
+ NSMutableString *whereArgument = [NSMutableString string];
+ for (int i = start; i < end; i++) {
+ [whereArgument appendString:toDeleteArgument];
+ }
+ // remove the last * OR * from argument
+ NSRange range = NSMakeRange([whereArgument length] -4, 4);
+ [whereArgument deleteCharactersInRange:range];
+ NSString *deleteQuery = [NSString stringWithFormat:@"%@ %@", delete, whereArgument];
+
+
+ // sqlite update
+ sqlite3_stmt *delete_statement;
+ if (sqlite3_prepare_v2(_database, [deleteQuery UTF8String],
+ -1, &delete_statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(delete_statement, 0);
+ }
+
+ // bind values
+ int rmqIndex = 0;
+ int placeholderIndex = 1; // placeholders in sqlite3 start with 1
+ for (NSString *rmqId in rmqIds) { // objectAtIndex: is O(n) -- would make it slow
+ if (rmqIndex < start) {
+ rmqIndex++;
+ continue;
+ } else if (rmqIndex >= end) {
+ break;
+ } else {
+ if (isRmqIDString) {
+ if (sqlite3_bind_text(delete_statement,
+ placeholderIndex,
+ [rmqId UTF8String],
+ (int)[rmqId length],
+ SQLITE_STATIC) != SQLITE_OK) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore003,
+ @"Failed to bind rmqID %@", rmqId);
+ continue;
+ }
+ } else {
+ int64_t rmqIdValue = [rmqId longLongValue];
+ sqlite3_bind_int64(delete_statement, placeholderIndex, rmqIdValue);
+ }
+ placeholderIndex++;
+ }
+ rmqIndex++;
+ }
+ if (sqlite3_step(delete_statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(delete_statement, deleteCount);
+ }
+ sqlite3_finalize(delete_statement);
+ deleteCount += sqlite3_changes(_database);
+ start = end;
+ }
+
+ // if we are here all of our sqlite queries should have succeeded
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore004,
+ @"%@ Trying to delete %d s2D ID's, successfully deleted %d",
+ kFCMRmqStoreTag, toDelete, deleteCount);
+ return deleteCount;
+}
+
+#pragma mark - Query
+
+- (int64_t)queryHighestRmqId {
+ NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kRmqIdColumn, // column
+ kTableOutgoingRmqMessages, // table
+ kRmqIdColumn, // order by column
+ 1]; // limit
+
+ sqlite3_stmt *statement;
+ int64_t highestRmqId = 0;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, highestRmqId);
+ }
+ if (sqlite3_step(statement) == SQLITE_ROW) {
+ highestRmqId = sqlite3_column_int64(statement, 0);
+ }
+ sqlite3_finalize(statement);
+ return highestRmqId;
+}
+
+- (int64_t)queryLastRmqId {
+ NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kRmqIdColumn, // column
+ kTableLastRmqId, // table
+ kRmqIdColumn, // order by column
+ 1]; // limit
+
+ sqlite3_stmt *statement;
+ int64_t lastRmqId = 0;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, lastRmqId);
+ }
+ if (sqlite3_step(statement) == SQLITE_ROW) {
+ lastRmqId = sqlite3_column_int64(statement, 0);
+ }
+ sqlite3_finalize(statement);
+ return lastRmqId;
+}
+
+- (BOOL)updateLastOutgoingRmqId:(int64_t)rmqID {
+ NSString *queryFormat = @"INSERT OR REPLACE INTO %@ (%@, %@) VALUES (?, ?)";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kTableLastRmqId, // table
+ kIdColumn, kRmqIdColumn]; // columns
+ sqlite3_stmt *statement;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ if (sqlite3_bind_int(statement, 1, 1) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ if (sqlite3_bind_int64(statement, 2, rmqID) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ if (sqlite3_step(statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ sqlite3_finalize(statement);
+ return YES;
+}
+
+- (NSArray *)unackedS2dRmqIds {
+ NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ ASC";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kRmqIdColumn,
+ kTableS2DRmqIds,
+ kRmqIdColumn];
+ sqlite3_stmt *statement;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore005,
+ @"%@: Could not find s2d ids", kFCMRmqStoreTag);
+ _FIRMessagingRmqLogAndExit(statement, @[]);
+ }
+ NSMutableArray *rmqIDArray = [NSMutableArray array];
+ while (sqlite3_step(statement) == SQLITE_ROW) {
+ const char *rmqID = (char *)sqlite3_column_text(statement, 0);
+ [rmqIDArray addObject:[NSString stringWithUTF8String:rmqID]];
+ }
+ sqlite3_finalize(statement);
+ return rmqIDArray;
+}
+
+#pragma mark - Scan
+
+- (void)scanOutgoingRmqMessagesWithHandler:(FCMOutgoingRmqMessagesTableHandler)handler {
+ static NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ != 0 ORDER BY %@ ASC";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kOutgoingRmqMessagesColumns, // select (rmq_id, type, data)
+ kTableOutgoingRmqMessages, // from table
+ kRmqIdColumn, // where
+ kRmqIdColumn]; // order by
+ sqlite3_stmt *statement;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ [self logError];
+ sqlite3_finalize(statement);
+ return;
+ }
+ // can query sqlite3 for this but this is fine
+ const int rmqIdColumnNumber = 0;
+ const int typeColumnNumber = 1;
+ const int dataColumnNumber = 2;
+ while (sqlite3_step(statement) == SQLITE_ROW) {
+ int64_t rmqId = sqlite3_column_int64(statement, rmqIdColumnNumber);
+ int8_t type = sqlite3_column_int(statement, typeColumnNumber);
+ const void *bytes = sqlite3_column_blob(statement, dataColumnNumber);
+ int length = sqlite3_column_bytes(statement, dataColumnNumber);
+ _FIRMessagingDevAssert(bytes != NULL,
+ @"%@ Message with no data being stored in Rmq",
+ kFCMRmqStoreTag);
+ NSData *data = [NSData dataWithBytes:bytes length:length];
+ handler(rmqId, type, data);
+ }
+ sqlite3_finalize(statement);
+}
+
+#pragma mark - Sync Messages
+
+- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
+ _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to search in SYNC_RMQ", rmqID);
+
+ NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ = '%@'";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kSyncMessagesColumns, // SELECT (rmq_id, expiration_ts, apns_recv, mcs_recv)
+ kTableSyncMessages, // FROM sync_rmq
+ kRmqIdColumn, // WHERE rmq_id
+ rmqID];
+
+ sqlite3_stmt *stmt;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ [self logError];
+ sqlite3_finalize(stmt);
+ return nil;
+ }
+
+ const int rmqIDColumn = 0;
+ const int expirationTimestampColumn = 1;
+ const int apnsReceivedColumn = 2;
+ const int mcsReceivedColumn = 3;
+
+ int count = 0;
+ FIRMessagingPersistentSyncMessage *persistentMessage;
+
+ while (sqlite3_step(stmt) == SQLITE_ROW) {
+ NSString *rmqID =
+ [NSString stringWithUTF8String:(char *)sqlite3_column_text(stmt, rmqIDColumn)];
+ int64_t expirationTimestamp = sqlite3_column_int64(stmt, expirationTimestampColumn);
+ BOOL apnsReceived = sqlite3_column_int(stmt, apnsReceivedColumn);
+ BOOL mcsReceived = sqlite3_column_int(stmt, mcsReceivedColumn);
+
+ // create a new persistent message
+ persistentMessage =
+ [[FIRMessagingPersistentSyncMessage alloc] initWithRMQID:rmqID expirationTime:expirationTimestamp];
+ persistentMessage.apnsReceived = apnsReceived;
+ persistentMessage.mcsReceived = mcsReceived;
+
+ count++;
+ }
+ sqlite3_finalize(stmt);
+
+ _FIRMessagingDevAssert(count <= 1, @"Found multiple messages in %@ with same RMQ ID", kTableSyncMessages);
+ return persistentMessage;
+}
+
+- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID {
+ _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to delete in SYNC_RMQ", rmqID);
+ return [self deleteMessagesFromTable:kTableSyncMessages withRmqIds:@[rmqID]] > 0;
+}
+
+- (int)deleteExpiredOrFinishedSyncMessages:(NSError *__autoreleasing *)error {
+ int64_t now = FIRMessagingCurrentTimestampInSeconds();
+ NSString *deleteSQL = @"DELETE FROM %@ "
+ @"WHERE %@ < %lld OR " // expirationTime < now
+ @"(%@ = 1 AND %@ = 1)"; // apns_received = 1 AND mcs_received = 1
+ NSString *query = [NSString stringWithFormat:deleteSQL,
+ kTableSyncMessages,
+ kSyncMessageExpirationTimestampColumn,
+ now,
+ kSyncMessageAPNSReceivedColumn,
+ kSyncMessageMCSReceivedColumn];
+
+ NSString *errorReason = @"Failed to save delete expired sync messages from store.";
+
+ sqlite3_stmt *stmt;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : errorReason }];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, 0);
+ }
+
+ if (sqlite3_step(stmt) != SQLITE_DONE) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : errorReason }];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, 0);
+ }
+
+ sqlite3_finalize(stmt);
+ int deleteCount = sqlite3_changes(_database);
+ return deleteCount;
+}
+
+- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
+ expirationTime:(int64_t)expirationTime
+ apnsReceived:(BOOL)apnsReceived
+ mcsReceived:(BOOL)mcsReceived
+ error:(NSError **)error {
+ _FIRMessagingDevAssert([rmqID length], @"Invalid nil message to persist to SYNC_RMQ");
+
+ NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@, %@) VALUES (?, ?, ?, ?)";
+ NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+ kTableSyncMessages, // Table name
+ kRmqIdColumn, // rmq_id
+ kSyncMessageExpirationTimestampColumn, // expiration_ts
+ kSyncMessageAPNSReceivedColumn, // apns_recv
+ kSyncMessageMCSReceivedColumn /* mcs_recv */];
+
+ sqlite3_stmt *stmt;
+
+ if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : @"Failed to save sync message to store." }];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_int64(stmt, 2, expirationTime) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_int(stmt, 3, apnsReceived ? 1 : 0) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_int(stmt, 4, mcsReceived ? 1 : 0) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_step(stmt) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ sqlite3_finalize(stmt);
+ return YES;
+}
+
+- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
+ error:(NSError **)error {
+ return [self updateSyncMessageWithRmqID:rmqID
+ column:kSyncMessageAPNSReceivedColumn
+ value:YES
+ error:error];
+}
+
+- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
+ error:(NSError *__autoreleasing *)error {
+ return [self updateSyncMessageWithRmqID:rmqID
+ column:kSyncMessageMCSReceivedColumn
+ value:YES
+ error:error];
+}
+
+- (BOOL)updateSyncMessageWithRmqID:(NSString *)rmqID
+ column:(NSString *)column
+ value:(BOOL)value
+ error:(NSError **)error {
+ _FIRMessagingDevAssert([column isEqualToString:kSyncMessageAPNSReceivedColumn] ||
+ [column isEqualToString:kSyncMessageMCSReceivedColumn],
+ @"Invalid column name %@ for SYNC_RMQ", column);
+ NSString *queryFormat = @"UPDATE %@ " // Table name
+ @"SET %@ = %d " // column=value
+ @"WHERE %@ = ?"; // condition
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kTableSyncMessages,
+ column,
+ value ? 1 : 0,
+ kRmqIdColumn];
+ sqlite3_stmt *stmt;
+
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : @"Failed to update sync message"}];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_step(stmt) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ sqlite3_finalize(stmt);
+ return YES;
+
+}
+
+#pragma mark - Private
+
+- (NSString *)lastErrorMessage {
+ return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
+}
+
+- (int)lastErrorCode {
+ return sqlite3_errcode(_database);
+}
+
+- (void)logError {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore006,
+ @"%@ error: code (%d) message: %@", kFCMRmqStoreTag, [self lastErrorCode],
+ [self lastErrorMessage]);
+}
+
+- (void)logErrorAndFinalizeStatement:(sqlite3_stmt *)stmt {
+ [self logError];
+ sqlite3_finalize(stmt);
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRmqManager.h b/Firebase/Messaging/FIRMessagingRmqManager.h
new file mode 100644
index 0000000..ba48b98
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRmqManager.h
@@ -0,0 +1,190 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class GtalkDataMessageStanza;
+@class GPBMessage;
+
+@class FIRMessagingPersistentSyncMessage;
+
+/**
+ * Called on each raw message.
+ */
+typedef void(^FIRMessagingRmqMessageHandler)(int64_t rmqId, int8_t tag, NSData *data);
+
+/**
+ * Called on each DataMessageStanza.
+ */
+typedef void(^FIRMessagingDataMessageHandler)(int64_t rmqId, GtalkDataMessageStanza *stanza);
+
+/**
+ * Used to scan through the rmq and perform actions on messages as required.
+ */
+@protocol FIRMessagingRmqScanner <NSObject>
+
+/**
+ * Scan the RMQ for outgoing messages and process them as required.
+ */
+- (void)scanWithRmqMessageHandler:(FIRMessagingRmqMessageHandler)rmqMessageHandler
+ dataMessageHandler:(FIRMessagingDataMessageHandler)dataMessageHandler;
+
+@end
+
+/**
+ * This manages the RMQ persistent store.
+ *
+ * The store is used to store all the S2D id's that were received by the client and were ACK'ed
+ * by us but the server hasn't confirmed the ACK. We don't delete these id's until the server
+ * ACK's us that they have received them.
+ *
+ * We also store the upstream messages(d2s) that were sent by the client.
+ *
+ * Also store the lastRMQId that was sent by us so that for a new connection being setup we don't
+ * duplicate RMQ Id's for the new messages.
+ */
+@interface FIRMessagingRmqManager : NSObject <FIRMessagingRmqScanner>
+
+// designated initializer
+- (instancetype)initWithDatabaseName:(NSString *)databaseName;
+
+- (void)loadRmqId;
+
+/**
+ * Save an upstream message to RMQ. If the message send fails for some reason we would not
+ * lose the message since it would be saved in the RMQ.
+ *
+ * @param message The upstream message to be saved.
+ * @param error The error if any while saving the message else nil.
+ *
+ * @return YES if the message was successfully saved to RMQ else NO.
+ */
+- (BOOL)saveRmqMessage:(GPBMessage *)message error:(NSError **)error;
+
+/**
+ * Save Server to device message with the given RMQ-ID.
+ *
+ * @param rmqID The rmqID of the s2d message to save.
+ *
+ * @return YES if the save was successfull else NO.
+ */
+- (BOOL)saveS2dMessageWithRmqId:(NSString *)rmqID;
+
+/**
+ * A list of all unacked Server to device RMQ IDs.
+ *
+ * @return A list of unacked Server to Device RMQ ID's. All values are Strings.
+ */
+- (NSArray *)unackedS2dRmqIds;
+
+/**
+ * Removes the outgoing message from RMQ store.
+ *
+ * @param rmqId The rmqID to remove from the store.
+ *
+ * @return The number of messages deleted successfully.
+ */
+- (int)removeRmqMessagesWithRmqId:(NSString *)rmqId;
+
+/**
+ * Removes the messages with the given rmqIDs from RMQ store.
+ *
+ * @param rmqIds The lsit of rmqID's to remove from the store.
+ *
+ * @return The number of messages deleted successfully.
+ */
+- (int)removeRmqMessagesWithRmqIds:(NSArray *)rmqIds;
+
+/**
+ * Removes a list of downstream messages from the RMQ.
+ *
+ * @param s2dIds The list of messages ACK'ed by the server that we should remove
+ * from the RMQ store.
+ */
+- (void)removeS2dIds:(NSArray *)s2dIds;
+
+#pragma mark - Sync Messages
+
+/**
+ * Get persisted sync message with rmqID.
+ *
+ * @param rmqID The rmqID of the persisted sync message.
+ *
+ * @return A valid persistent sync message with the given rmqID if found in the RMQ else nil.
+ */
+- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID;
+
+/**
+ * Delete sync message with rmqID.
+ *
+ * @param rmqID The rmqID of the persisted sync message.
+ *
+ * @return YES if the message was successfully deleted else NO.
+ */
+- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID;
+
+/**
+ * Delete the expired sync messages from persisten store. Also deletes messages that have been
+ * delivered both via APNS and MCS.
+ *
+ * @param error The error if any while deleting the messages.
+ *
+ * @return The total number of messages that were deleted from the persistent store.
+ */
+- (int)deleteExpiredOrFinishedSyncMessages:(NSError **)error;
+
+/**
+ * Save sync message received by the device.
+ *
+ * @param rmqID The rmqID of the message received.
+ * @param expirationTime The expiration time of the sync message received.
+ * @param apnsReceived YES if the message was received via APNS else NO.
+ * @param mcsReceived YES if the message was received via MCS else NO.
+ * @param error The error if any while saving the sync message to persistent store.
+ *
+ * @return YES if the message save was successful else NO.
+ */
+- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
+ expirationTime:(int64_t)expirationTime
+ apnsReceived:(BOOL)apnsReceived
+ mcsReceived:(BOOL)mcsReceived
+ error:(NSError **)error;
+
+/**
+ * Update sync message received via APNS.
+ *
+ * @param rmqID The rmqID of the received message.
+ * @param error The error if any while updating the sync message.
+ *
+ * @return YES if the persistent sync message was successfully updated else NO.
+ */
+- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID error:(NSError **)error;
+
+/**
+ * Update sync message received via MCS.
+ *
+ * @param rmqID The rmqID of the received message.
+ * @param error The error if any while updating the sync message.
+ *
+ * @return YES if the persistent sync message was successfully updated else NO.
+ */
+- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID error:(NSError **)error;
+
+#pragma mark - Testing
+
++ (void)removeDatabaseWithName:(NSString *)dbName;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingRmqManager.m b/Firebase/Messaging/FIRMessagingRmqManager.m
new file mode 100644
index 0000000..de63a73
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRmqManager.m
@@ -0,0 +1,264 @@
+/*
+ * 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 "FIRMessagingRmqManager.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+#import "sqlite3.h"
+
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingRmq2PersistentStore.h"
+#import "FIRMessagingUtilities.h"
+
+#ifndef _FIRMessagingRmqLogAndExit
+#define _FIRMessagingRmqLogAndExit(stmt, return_value) \
+do { \
+ [self logErrorAndFinalizeStatement:stmt]; \
+ return return_value; \
+} while(0)
+#endif
+
+static NSString *const kFCMRmqTag = @"FIRMessagingRmq:";
+
+@interface FIRMessagingRmqManager ()
+
+@property(nonatomic, readwrite, strong) FIRMessagingRmq2PersistentStore *rmq2Store;
+// map the category of an outgoing message with the number of messages for that category
+// should always have two keys -- the app, gcm
+@property(nonatomic, readwrite, strong) NSMutableDictionary *outstandingMessages;
+
+// Outgoing RMQ persistent id
+@property(nonatomic, readwrite, assign) int64_t rmqId;
+
+@end
+
+@implementation FIRMessagingRmqManager
+
+- (instancetype)initWithDatabaseName:(NSString *)databaseName {
+ self = [super init];
+ if (self) {
+ _FIRMessagingDevAssert([databaseName length] > 0, @"RMQ: Invalid rmq db name");
+ _rmq2Store = [[FIRMessagingRmq2PersistentStore alloc] initWithDatabaseName:databaseName];
+ _outstandingMessages = [NSMutableDictionary dictionaryWithCapacity:2];
+ _rmqId = -1;
+ }
+ return self;
+}
+
+- (void)loadRmqId {
+ if (self.rmqId >= 0) {
+ return; // already done
+ }
+
+ [self loadInitialOutgoingPersistentId];
+ if (self.outstandingMessages.count) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmqManager000,
+ @"%@: outstanding categories %ld", kFCMRmqTag,
+ _FIRMessaging_UL(self.outstandingMessages.count));
+ }
+}
+
+/**
+ * Initialize the 'initial RMQ':
+ * - max ID of any message in the queue
+ * - if the queue is empty, stored value in separate DB.
+ *
+ * Stream acks will remove from RMQ, when we remove the highest message we keep track
+ * of its ID.
+ */
+- (void)loadInitialOutgoingPersistentId {
+
+ // we shouldn't always trust the lastRmqId stored in the LastRmqId table, because
+ // we only save to the LastRmqId table once in a while (after getting the lastRmqId sent
+ // by the server after reconnect, and after getting a rmq ack from the server). The
+ // rmq message with the highest rmq id tells the real story, so check against that first.
+
+ int64_t rmqId = [self queryHighestRmqId];
+ if (rmqId == 0) {
+ rmqId = [self querylastRmqId];
+ }
+ self.rmqId = rmqId + 1;
+}
+
+#pragma mark - Save
+
+/**
+ * Save a message to RMQ2. Will populate the rmq2 persistent ID.
+ */
+- (BOOL)saveRmqMessage:(GPBMessage *)message
+ error:(NSError **)error {
+ // send using rmq2manager
+ // the wire format of rmq2 id is a string. However, we keep it as a long internally
+ // in the database. So only convert the id to string when preparing for sending over
+ // the wire.
+ NSString *rmq2Id = FIRMessagingGetRmq2Id(message);
+ if (![rmq2Id length]) {
+ int64_t rmqId = [self nextRmqId];
+ rmq2Id = [NSString stringWithFormat:@"%lld", rmqId];
+ FIRMessagingSetRmq2Id(message, rmq2Id);
+ }
+ FIRMessagingProtoTag tag = FIRMessagingGetTagForProto(message);
+ return [self saveMessage:message withRmqId:[rmq2Id integerValue] tag:tag error:error];
+}
+
+- (BOOL)saveMessage:(GPBMessage *)message
+ withRmqId:(int64_t)rmqId
+ tag:(int8_t)tag
+ error:(NSError **)error {
+ NSData *data = [message data];
+ return [self.rmq2Store saveMessageWithRmqId:rmqId tag:tag data:data error:error];
+}
+
+/**
+ * This is called when we delete the largest outgoing message from queue.
+ */
+- (void)saveLastOutgoingRmqId:(int64_t)rmqID {
+ [self.rmq2Store updateLastOutgoingRmqId:rmqID];
+}
+
+- (BOOL)saveS2dMessageWithRmqId:(NSString *)rmqID {
+ return [self.rmq2Store saveUnackedS2dMessageWithRmqId:rmqID];
+}
+
+#pragma mark - Query
+
+- (int64_t)queryHighestRmqId {
+ return [self.rmq2Store queryHighestRmqId];
+}
+
+- (int64_t)querylastRmqId {
+ return [self.rmq2Store queryLastRmqId];
+}
+
+- (NSArray *)unackedS2dRmqIds {
+ return [self.rmq2Store unackedS2dRmqIds];
+}
+
+#pragma mark - FIRMessagingRMQScanner protocol
+
+/**
+ * We don't have a 'getMessages' method - it would require loading in memory
+ * the entire content body of all messages.
+ *
+ * Instead we iterate and call 'resend' for each message.
+ *
+ * This is called:
+ * - on connect MCS, to resend any outstanding messages
+ * - init
+ */
+- (void)scanWithRmqMessageHandler:(FIRMessagingRmqMessageHandler)rmqMessageHandler
+ dataMessageHandler:(FIRMessagingDataMessageHandler)dataMessageHandler {
+ // no need to scan database with no callbacks
+ if (rmqMessageHandler || dataMessageHandler) {
+ [self.rmq2Store scanOutgoingRmqMessagesWithHandler:^(int64_t rmqId, int8_t tag, NSData *data) {
+ if (rmqMessageHandler != nil) {
+ rmqMessageHandler(rmqId, tag, data);
+ }
+ if (dataMessageHandler != nil && kFIRMessagingProtoTagDataMessageStanza == tag) {
+ GPBMessage *proto =
+ [FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data error:NULL];
+ GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto;
+ dataMessageHandler(rmqId, stanza);
+ }
+ }];
+ }
+}
+
+#pragma mark - Remove
+
+- (void)ackReceivedForRmqId:(NSString *)rmqId {
+ // TODO: Optional book-keeping
+}
+
+- (int)removeRmqMessagesWithRmqId:(NSString *)rmqId {
+ return [self removeRmqMessagesWithRmqIds:@[rmqId]];
+}
+
+- (int)removeRmqMessagesWithRmqIds:(NSArray *)rmqIds {
+ if (![rmqIds count]) {
+ return 0;
+ }
+ for (NSString *rmqId in rmqIds) {
+ [self ackReceivedForRmqId:rmqId];
+ }
+ int64_t maxRmqId = -1;
+ for (NSString *rmqId in rmqIds) {
+ int64_t rmqIdValue = [rmqId longLongValue];
+ if (rmqIdValue > maxRmqId) {
+ maxRmqId = rmqIdValue;
+ }
+ }
+ maxRmqId++;
+ if (maxRmqId >= self.rmqId) {
+ [self saveLastOutgoingRmqId:maxRmqId];
+ }
+ return [self.rmq2Store deleteMessagesFromTable:kTableOutgoingRmqMessages withRmqIds:rmqIds];
+}
+
+- (void)removeS2dIds:(NSArray *)s2dIds {
+ [self.rmq2Store deleteMessagesFromTable:kTableS2DRmqIds withRmqIds:s2dIds];
+}
+
+#pragma mark - Sync Messages
+
+// TODO: RMQManager should also have a cache for all the sync messages
+// so we don't hit the DB each time.
+- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
+ return [self.rmq2Store querySyncMessageWithRmqID:rmqID];
+}
+
+- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID {
+ return [self.rmq2Store deleteSyncMessageWithRmqID:rmqID];
+}
+
+- (int)deleteExpiredOrFinishedSyncMessages:(NSError **)error {
+ return [self.rmq2Store deleteExpiredOrFinishedSyncMessages:error];
+}
+
+- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
+ expirationTime:(int64_t)expirationTime
+ apnsReceived:(BOOL)apnsReceived
+ mcsReceived:(BOOL)mcsReceived
+ error:(NSError *__autoreleasing *)error {
+ return [self.rmq2Store saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:apnsReceived
+ mcsReceived:mcsReceived
+ error:error];
+}
+
+- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID error:(NSError **)error {
+ return [self.rmq2Store updateSyncMessageViaAPNSWithRmqID:rmqID error:error];
+}
+
+- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID error:(NSError **)error {
+ return [self.rmq2Store updateSyncMessageViaMCSWithRmqID:rmqID error:error];
+}
+
+#pragma mark - Testing
+
++ (void)removeDatabaseWithName:(NSString *)dbName {
+ [FIRMessagingRmq2PersistentStore removeDatabase:dbName];
+}
+
+#pragma mark - Private
+
+- (int64_t)nextRmqId {
+ return ++self.rmqId;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingSecureSocket.h b/Firebase/Messaging/FIRMessagingSecureSocket.h
new file mode 100644
index 0000000..169f60e
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingSecureSocket.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 <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSUInteger, FIRMessagingSecureSocketState){
+ kFIRMessagingSecureSocketNotOpen = 0,
+ kFIRMessagingSecureSocketOpening,
+ kFIRMessagingSecureSocketOpen,
+ kFIRMessagingSecureSocketClosing,
+ kFIRMessagingSecureSocketClosed,
+ kFIRMessagingSecureSocketError
+};
+
+@class FIRMessagingSecureSocket;
+
+@protocol FIRMessagingSecureSocketDelegate<NSObject>
+
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didReceiveData:(NSData *)data
+ withTag:(int8_t)tag;
+- (void)secureSocket:(FIRMessagingSecureSocket *)socket
+ didSendProtoWithTag:(int8_t)tag
+ rmqId:(NSString *)rmqId;
+- (void)secureSocketDidConnect:(FIRMessagingSecureSocket *)socket;
+- (void)didDisconnectWithSecureSocket:(FIRMessagingSecureSocket *)socket;
+
+@end
+
+/**
+ * This manages the input/output streams connected to the MCS server. Used to receive data from
+ * the server and send to it over the wire.
+ */
+@interface FIRMessagingSecureSocket : NSObject
+
+@property(nonatomic, readwrite, weak) id<FIRMessagingSecureSocketDelegate> delegate;
+@property(nonatomic, readonly, assign) FIRMessagingSecureSocketState state;
+
+- (void)connectToHost:(NSString *)host port:(NSUInteger)port onRunLoop:(NSRunLoop *)runLoop;
+- (void)disconnect;
+- (void)sendData:(NSData *)data withTag:(int8_t)tag rmqId:(NSString *)rmqId;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingSecureSocket.m b/Firebase/Messaging/FIRMessagingSecureSocket.m
new file mode 100644
index 0000000..b7e8133
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingSecureSocket.m
@@ -0,0 +1,448 @@
+/*
+ * 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 "FIRMessagingSecureSocket.h"
+
+#import "GPBMessage.h"
+#import "GPBCodedOutputStream.h"
+#import "GPBUtilities.h"
+
+#import "FIRMessagingCodedInputStream.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPacketQueue.h"
+
+static const NSUInteger kMaxBufferLength = 1024 * 1024; // 1M
+static const NSUInteger kBufferLengthIncrement = 16 * 1024; // 16k
+static const uint8_t kVersion = 40;
+static const uint8_t kInvalidTag = -1;
+
+typedef NS_ENUM(NSUInteger, FIRMessagingSecureSocketReadResult) {
+ kFIRMessagingSecureSocketReadResultNone,
+ kFIRMessagingSecureSocketReadResultIncomplete,
+ kFIRMessagingSecureSocketReadResultCorrupt,
+ kFIRMessagingSecureSocketReadResultSuccess
+};
+
+static int32_t LogicalRightShift32(int32_t value, int32_t spaces) {
+ return (int32_t)((uint32_t)(value) >> spaces);
+}
+
+static NSUInteger SerializedSize(int32_t value) {
+ NSUInteger bytes = 0;
+ while (YES) {
+ if ((value & ~0x7F) == 0) {
+ bytes += sizeof(uint8_t);
+ return bytes;
+ } else {
+ bytes += sizeof(uint8_t);
+ value = LogicalRightShift32(value, 7);
+ }
+ }
+}
+
+@interface FIRMessagingSecureSocket() <NSStreamDelegate>
+
+@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
+@property(nonatomic, readwrite, strong) NSInputStream *inStream;
+@property(nonatomic, readwrite, strong) NSOutputStream *outStream;
+
+@property(nonatomic, readwrite, strong) NSMutableData *inputBuffer;
+@property(nonatomic, readwrite, assign) NSUInteger inputBufferLength;
+@property(nonatomic, readwrite, strong) NSMutableData *outputBuffer;
+@property(nonatomic, readwrite, assign) NSUInteger outputBufferLength;
+
+@property(nonatomic, readwrite, strong) FIRMessagingPacketQueue *packetQueue;
+@property(nonatomic, readwrite, assign) BOOL isVersionSent;
+@property(nonatomic, readwrite, assign) BOOL isVersionReceived;
+@property(nonatomic, readwrite, assign) BOOL isInStreamOpen;
+@property(nonatomic, readwrite, assign) BOOL isOutStreamOpen;
+
+@property(nonatomic, readwrite, strong) NSRunLoop *runLoop;
+@property(nonatomic, readwrite, strong) NSString *currentRmqIdBeingSent;
+@property(nonatomic, readwrite, assign) int8_t currentProtoTypeBeingSent;
+
+@end
+
+@implementation FIRMessagingSecureSocket
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _state = kFIRMessagingSecureSocketNotOpen;
+ _inputBuffer = [NSMutableData dataWithLength:kBufferLengthIncrement];
+ _packetQueue = [[FIRMessagingPacketQueue alloc] init];
+ _currentProtoTypeBeingSent = kInvalidTag;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self disconnect];
+}
+
+- (void)connectToHost:(NSString *)host
+ port:(NSUInteger)port
+ onRunLoop:(NSRunLoop *)runLoop {
+ _FIRMessagingDevAssert(host != nil, @"Invalid host");
+ _FIRMessagingDevAssert(runLoop != nil, @"Invalid runloop");
+ _FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketNotOpen, @"Socket is already connected");
+
+ if (!host || self.state != kFIRMessagingSecureSocketNotOpen) {
+ return;
+ }
+
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket000,
+ @"Opening secure socket to FIRMessaging service");
+ self.state = kFIRMessagingSecureSocketOpening;
+ self.runLoop = runLoop;
+ CFReadStreamRef inputStreamRef;
+ CFWriteStreamRef outputStreamRef;
+ CFStreamCreatePairWithSocketToHost(NULL,
+ (__bridge CFStringRef)host,
+ (int)port,
+ &inputStreamRef,
+ &outputStreamRef);
+ self.inStream = CFBridgingRelease(inputStreamRef);
+ self.outStream = CFBridgingRelease(outputStreamRef);
+ if (!self.inStream || !self.outStream) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket001,
+ @"Failed to initialize socket.");
+ return;
+ }
+
+ self.isInStreamOpen = NO;
+ self.isOutStreamOpen = NO;
+
+ BOOL isVOIPSocket = NO;
+
+#if FIRMessaging_PROBER
+ isVOIPSocket = YES;
+#endif
+
+ [self openStream:self.outStream isVOIPStream:isVOIPSocket];
+ [self openStream:self.inStream isVOIPStream:isVOIPSocket];
+}
+
+- (void)disconnect {
+ if (self.state == kFIRMessagingSecureSocketClosing) {
+ return;
+ }
+ if (!self.inStream && !self.outStream) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket002,
+ @"The socket is not open or already closed.");
+ _FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketClosed || self.state == kFIRMessagingSecureSocketNotOpen,
+ @"Socket is already disconnected.");
+ return;
+ }
+
+ self.state = kFIRMessagingSecureSocketClosing;
+ if (self.inStream) {
+ [self closeStream:self.inStream];
+ self.inStream = nil;
+ }
+ if (self.outStream) {
+ [self closeStream:self.outStream];
+ self.outStream = nil;
+ }
+ self.state = kFIRMessagingSecureSocketClosed;
+ [self.delegate didDisconnectWithSecureSocket:self];
+}
+
+- (void)sendData:(NSData *)data withTag:(int8_t)tag rmqId:(NSString *)rmqId {
+ [self.packetQueue push:[FIRMessagingPacket packetWithTag:tag rmqId:rmqId data:data]];
+ if ([self.outStream hasSpaceAvailable]) {
+ [self performWrite];
+ }
+}
+
+#pragma mark - NSStreamDelegate
+
+- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
+ switch (eventCode) {
+ case NSStreamEventHasBytesAvailable:
+ if (self.state != kFIRMessagingSecureSocketOpen) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket003,
+ @"Try to read from socket that is not opened");
+ return;
+ }
+ _FIRMessagingDevAssert(stream == self.inStream, @"Incorrect stream");
+ if (![self performRead]) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket004,
+ @"Error occured when reading incoming stream");
+ [self disconnect];
+ }
+ break;
+ case NSStreamEventEndEncountered:
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeSecureSocket005, @"%@ end encountered",
+ stream == self.inStream
+ ? @"Input stream"
+ : (stream == self.outStream ? @"Output stream" : @"Unknown stream"));
+ [self disconnect];
+ break;
+ case NSStreamEventOpenCompleted:
+ if (stream == self.inStream) {
+ self.isInStreamOpen = YES;
+ } else if (stream == self.outStream) {
+ self.isOutStreamOpen = YES;
+ }
+ if (self.isInStreamOpen && self.isOutStreamOpen) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket006,
+ @"Secure socket to FIRMessaging service opened");
+ self.state = kFIRMessagingSecureSocketOpen;
+ [self.delegate secureSocketDidConnect:self];
+ }
+ break;
+ case NSStreamEventErrorOccurred: {
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeSecureSocket007, @"%@ error occurred",
+ stream == self.inStream
+ ? @"Input stream"
+ : (stream == self.outStream ? @"Output stream" : @"Unknown stream"));
+ [self disconnect];
+ break;
+ }
+ case NSStreamEventHasSpaceAvailable:
+ if (self.state != kFIRMessagingSecureSocketOpen) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket008,
+ @"Try to write to socket that is not opened");
+ return;
+ }
+ _FIRMessagingDevAssert(stream == self.outStream, @"Incorrect stream");
+ [self performWrite];
+ break;
+ default:
+ break;
+ }
+}
+
+#pragma mark - Private
+
+- (void)openStream:(NSStream *)stream isVOIPStream:(BOOL)isVOIPStream {
+ _FIRMessagingDevAssert(stream != nil, @"Invalid stream");
+ _FIRMessagingDevAssert(self.runLoop != nil, @"Invalid runloop");
+
+ if (stream) {
+ _FIRMessagingDevAssert([stream streamStatus] == NSStreamStatusNotOpen, @"Stream already open");
+ if ([stream streamStatus] != NSStreamStatusNotOpen) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket009,
+ @"stream should not be open.");
+ return;
+ }
+ [stream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
+ forKey:NSStreamSocketSecurityLevelKey];
+ if (isVOIPStream) {
+ [stream setProperty:NSStreamNetworkServiceTypeVoIP
+ forKey:NSStreamNetworkServiceType];
+ }
+ stream.delegate = self;
+ [stream scheduleInRunLoop:self.runLoop forMode:NSDefaultRunLoopMode];
+ [stream open];
+ }
+}
+
+- (void)closeStream:(NSStream *)stream {
+ _FIRMessagingDevAssert(stream != nil, @"Invalid stream");
+ _FIRMessagingDevAssert(self.runLoop != nil, @"Invalid runloop");
+
+ if (stream) {
+ [stream close];
+ [stream removeFromRunLoop:self.runLoop forMode:NSDefaultRunLoopMode];
+ stream.delegate = nil;
+ }
+}
+
+- (BOOL)performRead {
+ _FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketOpen, @"Socket should be open");
+
+ if (!self.isVersionReceived) {
+ self.isVersionReceived = YES;
+ uint8_t versionByte = 0;
+ NSInteger bytesRead = [self.inStream read:&versionByte maxLength:sizeof(uint8_t)];
+ if (bytesRead != sizeof(uint8_t) || kVersion != versionByte) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket010,
+ @"Version do not match. Received %d, Expecting %d", versionByte,
+ kVersion);
+ return NO;
+ }
+ }
+
+ while (YES) {
+ BOOL isInputBufferValid = [self.inputBuffer length] > 0;
+ _FIRMessagingDevAssert(isInputBufferValid,
+ @"Invalid input buffer size %lu. Used bytes length %lu, buffer content: %@",
+ _FIRMessaging_UL([self.inputBuffer length]),
+ _FIRMessaging_UL(self.inputBufferLength),
+ self.inputBuffer);
+ if (!isInputBufferValid) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket011,
+ @"Input buffer is not valid.");
+ return NO;
+ }
+
+ if (![self.inStream hasBytesAvailable]) {
+ break;
+ }
+
+ // try to read more data
+ uint8_t *unusedBufferPtr = (uint8_t *)self.inputBuffer.mutableBytes + self.inputBufferLength;
+ NSUInteger unusedBufferLength = [self.inputBuffer length] - self.inputBufferLength;
+ NSInteger bytesRead = [self.inStream read:unusedBufferPtr maxLength:unusedBufferLength];
+ if (bytesRead <= 0) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket012,
+ @"Failed to read input stream. Bytes read %ld, Used buffer size %lu, "
+ @"Unused buffer size %lu",
+ _FIRMessaging_UL(bytesRead), _FIRMessaging_UL(self.inputBufferLength),
+ _FIRMessaging_UL(unusedBufferLength));
+ break;
+ }
+ // did successfully read some more data
+ self.inputBufferLength += (NSUInteger)bytesRead;
+
+ if ([self.inputBuffer length] <= self.inputBufferLength) {
+ // shouldn't be reading more than 1MB of data in one go
+ if ([self.inputBuffer length] + kBufferLengthIncrement > kMaxBufferLength) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket013,
+ @"Input buffer exceed 1M, disconnect socket");
+ return NO;
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket014,
+ @"Input buffer limit exceeded. Used input buffer size %lu, "
+ @"Total input buffer size %lu. No unused buffer left. "
+ @"Increase buffer size.",
+ _FIRMessaging_UL(self.inputBufferLength),
+ _FIRMessaging_UL([self.inputBuffer length]));
+ [self.inputBuffer increaseLengthBy:kBufferLengthIncrement];
+ _FIRMessagingDevAssert([self.inputBuffer length] > self.inputBufferLength, @"Invalid buffer size");
+ }
+
+ while (self.inputBufferLength > 0 && [self.inputBuffer length] > 0) {
+ _FIRMessagingDevAssert([self.inputBuffer length] >= self.inputBufferLength,
+ @"Buffer longer than length");
+ NSRange inputRange = NSMakeRange(0, self.inputBufferLength);
+ size_t protoBytes = 0;
+ // read the actual proto data coming in
+ FIRMessagingSecureSocketReadResult readResult =
+ [self processCurrentInputBuffer:[self.inputBuffer subdataWithRange:inputRange]
+ outOffset:&protoBytes];
+ // Corrupt data encountered, stop processing.
+ if (readResult == kFIRMessagingSecureSocketReadResultCorrupt) {
+ return NO;
+ // Incomplete data, keep trying to read by loading more from the stream.
+ } else if (readResult == kFIRMessagingSecureSocketReadResultIncomplete) {
+ break;
+ }
+ _FIRMessagingDevAssert(self.inputBufferLength >= protoBytes, @"More bytes than buffer can handle");
+ // we have read (0, protoBytes) of data in the inputBuffer
+ if (protoBytes == self.inputBufferLength) {
+ // did completely read the buffer data can be reset for further processing
+ self.inputBufferLength = 0;
+ } else {
+ // delete processed bytes while maintaining the buffer size.
+ NSUInteger prevLength __unused = [self.inputBuffer length];
+ // delete the processed bytes
+ [self.inputBuffer replaceBytesInRange:NSMakeRange(0, protoBytes) withBytes:NULL length:0];
+ // reallocate more data
+ [self.inputBuffer increaseLengthBy:protoBytes];
+ _FIRMessagingDevAssert([self.inputBuffer length] == prevLength,
+ @"Invalid input buffer size %lu. Used bytes length %lu, "
+ @"buffer content: %@",
+ _FIRMessaging_UL([self.inputBuffer length]),
+ _FIRMessaging_UL(self.inputBufferLength),
+ self.inputBuffer);
+ self.inputBufferLength -= protoBytes;
+ }
+ }
+ }
+ return YES;
+}
+
+- (FIRMessagingSecureSocketReadResult)processCurrentInputBuffer:(NSData *)readData
+ outOffset:(size_t *)outOffset {
+ *outOffset = 0;
+
+ FIRMessagingCodedInputStream *input = [[FIRMessagingCodedInputStream alloc] initWithData:readData];
+ int8_t rawTag;
+ if (![input readTag:&rawTag]) {
+ return kFIRMessagingSecureSocketReadResultIncomplete;
+ }
+ int32_t length;
+ if (![input readLength:&length]) {
+ return kFIRMessagingSecureSocketReadResultIncomplete;
+ }
+ // NOTE tag can be zero for |HeartbeatPing|, and length can be zero for |Close| proto
+ _FIRMessagingDevAssert(rawTag >= 0 && length >= 0, @"Invalid tag or length");
+ if (rawTag < 0 || length < 0) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket015, @"Buffer data corrupted.");
+ return kFIRMessagingSecureSocketReadResultCorrupt;
+ }
+ NSData *data = [input readDataWithLength:(uint32_t)length];
+ if (data == nil) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket016,
+ @"Incomplete data, buffered data length %ld, expected length %d",
+ _FIRMessaging_UL(self.inputBufferLength), length);
+ return kFIRMessagingSecureSocketReadResultIncomplete;
+ }
+ [self.delegate secureSocket:self didReceiveData:data withTag:rawTag];
+ *outOffset = input.offset;
+ return kFIRMessagingSecureSocketReadResultSuccess;
+}
+
+- (void)performWrite {
+ _FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketOpen, @"Invalid socket state");
+
+ if (!self.isVersionSent) {
+ self.isVersionSent = YES;
+ uint8_t versionByte = kVersion;
+ [self.outStream write:&versionByte maxLength:sizeof(uint8_t)];
+ }
+
+ while (!self.packetQueue.isEmpty && self.outStream.hasSpaceAvailable) {
+ if (self.outputBuffer.length == 0) {
+ // serialize new packets only when the output buffer is flushed.
+ FIRMessagingPacket *packet = [self.packetQueue pop];
+ self.currentRmqIdBeingSent = packet.rmqId;
+ self.currentProtoTypeBeingSent = packet.tag;
+ NSUInteger length = SerializedSize(packet.tag) +
+ SerializedSize((int)packet.data.length) + packet.data.length;
+ self.outputBuffer = [NSMutableData dataWithLength:length];
+ GPBCodedOutputStream *output = [GPBCodedOutputStream streamWithData:self.outputBuffer];
+ [output writeRawVarint32:packet.tag];
+ [output writeBytesNoTag:packet.data];
+ self.outputBufferLength = 0;
+ }
+
+ // flush the output buffer.
+ NSInteger written = [self.outStream write:self.outputBuffer.bytes + self.outputBufferLength
+ maxLength:self.outputBuffer.length - self.outputBufferLength];
+ if (written <= 0) {
+ continue;
+ }
+ self.outputBufferLength += (NSUInteger)written;
+ if (self.outputBufferLength >= self.outputBuffer.length) {
+ self.outputBufferLength = 0;
+ self.outputBuffer = nil;
+ [self.delegate secureSocket:self
+ didSendProtoWithTag:self.currentProtoTypeBeingSent
+ rmqId:self.currentRmqIdBeingSent];
+ self.currentRmqIdBeingSent = nil;
+ self.currentProtoTypeBeingSent = kInvalidTag;
+ }
+ }
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingSyncMessageManager.h b/Firebase/Messaging/FIRMessagingSyncMessageManager.h
new file mode 100644
index 0000000..3d30bdb
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingSyncMessageManager.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 <Foundation/Foundation.h>
+
+@class FIRMessagingRmqManager;
+
+/**
+ * Handle sync messages being received both via MCS and APNS.
+ */
+@interface FIRMessagingSyncMessageManager : NSObject
+
+/**
+ * Initialize sync message manager.
+ *
+ * @param rmqManager The RMQ manager on the client.
+ *
+ * @return Sync message manager.
+ */
+- (instancetype)initWithRmqManager:(FIRMessagingRmqManager *)rmqManager;
+
+/**
+ * Remove expired sync message from persistent store. Also removes messages that have
+ * been received both via APNS and MCS.
+ */
+- (void)removeExpiredSyncMessages;
+
+/**
+ * App did recive a sync message via APNS.
+ *
+ * @param message The sync message received.
+ *
+ * @return YES if the message is a duplicate of an already received sync message else NO.
+ */
+- (BOOL)didReceiveAPNSSyncMessage:(NSDictionary *)message;
+
+/**
+ * App did receive a sync message via MCS.
+ *
+ * @param message The sync message received.
+ *
+ * @return YES if the message is a duplicate of an already received sync message else NO.
+ */
+- (BOOL)didReceiveMCSSyncMessage:(NSDictionary *)message;
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingSyncMessageManager.m b/Firebase/Messaging/FIRMessagingSyncMessageManager.m
new file mode 100644
index 0000000..1257b02
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingSyncMessageManager.m
@@ -0,0 +1,147 @@
+/*
+ * 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 "FIRMessagingSyncMessageManager.h"
+
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPersistentSyncMessage.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingUtilities.h"
+
+static const int64_t kDefaultSyncMessageTTL = 4 * 7 * 24 * 60 * 60; // 4 weeks
+// 4 MB of free space is required to persist Sync messages
+static const uint64_t kMinFreeDiskSpaceInMB = 1;
+
+@interface FIRMessagingSyncMessageManager()
+
+@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmqManager;
+
+@end
+
+@implementation FIRMessagingSyncMessageManager
+
+- (instancetype)init {
+ FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithRmqManager:(FIRMessagingRmqManager *)rmqManager {
+ _FIRMessagingDevAssert(rmqManager, @"Invalid nil rmq manager while initalizing sync message manager");
+ self = [super init];
+ if (self) {
+ _rmqManager = rmqManager;
+ }
+ return self;
+}
+
+- (void)removeExpiredSyncMessages {
+ NSError *error;
+ int deleteCount = [self.rmqManager deleteExpiredOrFinishedSyncMessages:&error];
+ if (error) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager000,
+ @"Error while deleting expired sync messages %@", error);
+ } else if (deleteCount > 0) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSyncMessageManager001,
+ @"Successfully deleted %d sync messages from store", deleteCount);
+ }
+}
+
+- (BOOL)didReceiveAPNSSyncMessage:(NSDictionary *)message {
+ return [self didReceiveSyncMessage:message viaAPNS:YES viaMCS:NO];
+}
+
+- (BOOL)didReceiveMCSSyncMessage:(NSDictionary *)message {
+ return [self didReceiveSyncMessage:message viaAPNS:NO viaMCS:YES];
+}
+
+- (BOOL)didReceiveSyncMessage:(NSDictionary *)message
+ viaAPNS:(BOOL)viaAPNS
+ viaMCS:(BOOL)viaMCS {
+ NSString *rmqID = message[kFIRMessagingMessageIDKey];
+ _FIRMessagingDevAssert([rmqID length], @"Invalid nil rmqID for message");
+ if (![rmqID length]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager002,
+ @"Invalid nil rmqID for sync message.");
+ return NO;
+ }
+
+ FIRMessagingPersistentSyncMessage *persistentMessage =
+ [self.rmqManager querySyncMessageWithRmqID:rmqID];
+
+ NSError *error;
+ if (!persistentMessage) {
+
+ // Do not persist the new message if we don't have enough disk space
+ uint64_t freeDiskSpace = FIRMessagingGetFreeDiskSpaceInMB();
+ if (freeDiskSpace < kMinFreeDiskSpaceInMB) {
+ return NO;
+ }
+
+ int64_t expirationTime = [[self class] expirationTimeForSyncMessage:message];
+ if (![self.rmqManager saveSyncMessageWithRmqID:rmqID
+ expirationTime:expirationTime
+ apnsReceived:viaAPNS
+ mcsReceived:viaMCS
+ error:&error]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager003,
+ @"Failed to save sync message with rmqID %@", rmqID);
+ } else {
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeSyncMessageManager004,
+ @"Added sync message to cache: %@", rmqID);
+ }
+ return NO;
+ }
+
+ if (viaAPNS && !persistentMessage.apnsReceived) {
+ persistentMessage.apnsReceived = YES;
+ if (![self.rmqManager updateSyncMessageViaAPNSWithRmqID:rmqID error:&error]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager005,
+ @"Failed to update APNS state for sync message %@", rmqID);
+ }
+ } else if (viaMCS && !persistentMessage.mcsReceived) {
+ persistentMessage.mcsReceived = YES;
+ if (![self.rmqManager updateSyncMessageViaMCSWithRmqID:rmqID error:&error]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager006,
+ @"Failed to update MCS state for sync message %@", rmqID);
+ }
+ }
+
+ // Received message via both ways we can safely delete it.
+ if (persistentMessage.apnsReceived && persistentMessage.mcsReceived) {
+ if (![self.rmqManager deleteSyncMessageWithRmqID:rmqID]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager007,
+ @"Failed to delete sync message %@", rmqID);
+ } else {
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeSyncMessageManager008,
+ @"Successfully deleted sync message from cache %@", rmqID);
+ }
+ }
+
+ // Already received this message either via MCS or APNS.
+ return YES;
+}
+
++ (int64_t)expirationTimeForSyncMessage:(NSDictionary *)message {
+ int64_t ttl = kDefaultSyncMessageTTL;
+ if (message[kFIRMessagingMessageSyncMessageTTLKey]) {
+ ttl = [message[kFIRMessagingMessageSyncMessageTTLKey] longLongValue];
+ }
+ int64_t currentTime = FIRMessagingCurrentTimestampInSeconds();
+ return currentTime + ttl;
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingTopicOperation.h b/Firebase/Messaging/FIRMessagingTopicOperation.h
new file mode 100644
index 0000000..e4bbde8
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingTopicOperation.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingTopicsCommon.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An asynchronous NSOperation subclass which performs a single network request for a topic
+ * subscription operation. Once completed, it calls its provided completion handler.
+ */
+@interface FIRMessagingTopicOperation : NSOperation
+
+@property(nonatomic, readonly, copy) NSString *topic;
+@property(nonatomic, readonly, assign) FIRMessagingTopicAction action;
+@property(nonatomic, readonly, copy) NSString *token;
+@property(nonatomic, readonly, copy, nullable) NSDictionary *options;
+@property(nonatomic, readonly, strong) FIRMessagingCheckinService *checkinService;
+
+- (instancetype)initWithTopic:(NSString *)topic
+ action:(FIRMessagingTopicAction)action
+ token:(NSString *)token
+ options:(nullable NSDictionary *)options
+ checkinService:(FIRMessagingCheckinService *)checkinService
+ completion:(FIRMessagingTopicOperationCompletion)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Messaging/FIRMessagingTopicOperation.m b/Firebase/Messaging/FIRMessagingTopicOperation.m
new file mode 100644
index 0000000..955c4a6
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingTopicOperation.m
@@ -0,0 +1,246 @@
+/*
+ * 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 "FIRMessagingTopicOperation.h"
+
+#import "FIRMessagingCheckinService.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+#define DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS 0
+
+static NSString *const kFIRMessagingSubscribeServerHost =
+ @"https://iid.googleapis.com/iid/register";
+
+NSString *FIRMessagingSubscriptionsServer() {
+ static NSString *serverHost = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSDictionary *environment = [[NSProcessInfo processInfo] environment];
+ NSString *customServerHost = environment[@"FCM_SERVER_ENDPOINT"];
+ if (customServerHost.length) {
+ serverHost = customServerHost;
+ } else {
+ serverHost = kFIRMessagingSubscribeServerHost;
+ }
+ });
+ return serverHost;
+}
+
+@interface FIRMessagingTopicOperation () {
+ BOOL _isFinished;
+ BOOL _isExecuting;
+}
+
+@property(nonatomic, readwrite, copy) NSString *topic;
+@property(nonatomic, readwrite, assign) FIRMessagingTopicAction action;
+@property(nonatomic, readwrite, copy) NSString *token;
+@property(nonatomic, readwrite, copy) NSDictionary *options;
+@property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
+@property(nonatomic, readwrite, copy) FIRMessagingTopicOperationCompletion completion;
+
+@property(atomic, strong) NSURLSessionDataTask *dataTask;
+
+@end
+
+@implementation FIRMessagingTopicOperation
+
++ (NSURLSession *)sharedSession {
+ static NSURLSession *subscriptionOperationSharedSession;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
+ config.timeoutIntervalForResource = 60.0f; // 1 minute
+ subscriptionOperationSharedSession = [NSURLSession sessionWithConfiguration:config];
+ subscriptionOperationSharedSession.sessionDescription = @"com.google.fcm.topics.session";
+ });
+ return subscriptionOperationSharedSession;
+}
+
+- (instancetype)initWithTopic:(NSString *)topic
+ action:(FIRMessagingTopicAction)action
+ token:(NSString *)token
+ options:(NSDictionary *)options
+ checkinService:(FIRMessagingCheckinService *)checkinService
+ completion:(FIRMessagingTopicOperationCompletion)completion {
+ if (self = [super init]) {
+ _topic = topic;
+ _action = action;
+ _token = token;
+ _checkinService = checkinService;
+ _completion = completion;
+
+ _isExecuting = NO;
+ _isFinished = NO;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ _topic = nil;
+ _token = nil;
+ _checkinService = nil;
+ _completion = nil;
+}
+
+- (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:FIRMessagingTopicOperationResultCancelled error:nil];
+ return;
+ }
+
+ [self setExecuting:YES];
+
+ [self performSubscriptionChange];
+}
+
+- (void)finishWithResult:(FIRMessagingTopicOperationResult)result error:(NSError *)error {
+ // Add a check to prevent this finish from being called more than once.
+ if (self.isFinished) {
+ return;
+ }
+ self.dataTask = nil;
+ if (self.completion) {
+ self.completion(result, error);
+ }
+
+ [self setExecuting:NO];
+ [self setFinished:YES];
+}
+
+- (void)cancel {
+ [super cancel];
+ [self.dataTask cancel];
+ [self finishWithResult:FIRMessagingTopicOperationResultCancelled error:nil];
+}
+
+- (void)performSubscriptionChange {
+
+ NSURL *url = [NSURL URLWithString:FIRMessagingSubscriptionsServer()];
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
+ NSString *appIdentifier = FIRMessagingAppIdentifier();
+ NSString *deviceAuthID = self.checkinService.deviceAuthID;
+ NSString *secretToken = self.checkinService.secretToken;
+ NSString *authString = [NSString stringWithFormat:@"AidLogin %@:%@", deviceAuthID, secretToken];
+ [request setValue:authString forHTTPHeaderField:@"Authorization"];
+ [request setValue:appIdentifier forHTTPHeaderField:@"app"];
+ [request setValue:self.checkinService.versionInfo forHTTPHeaderField:@"info"];
+
+ NSMutableString *content = [NSMutableString stringWithFormat:
+ @"sender=%@&app=%@&device=%@&"
+ @"app_ver=%@&X-gcm.topic=%@&X-scope=%@",
+ self.token,
+ appIdentifier,
+ deviceAuthID,
+ FIRMessagingCurrentAppVersion(),
+ self.topic,
+ self.topic];
+
+ if (self.action == FIRMessagingTopicActionUnsubscribe) {
+ [content appendString:@"&delete=true"];
+ }
+
+ FIRMessagingLoggerInfo(kFIRMessagingMessageCodeTopicOption000, @"Topic subscription request: %@",
+ content);
+
+ request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding];
+ [request setHTTPMethod:@"POST"];
+
+#if DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS
+ NSDate *start = [NSDate date];
+#endif
+
+ FIRMessaging_WEAKIFY(self)
+ void(^requestHandler)(NSData *, NSURLResponse *, NSError *) =
+ ^(NSData *data, NSURLResponse *URLResponse, NSError *error) {
+ FIRMessaging_STRONGIFY(self)
+ if (error) {
+ // Our operation could have been cancelled, which would result in our data task's error being
+ // NSURLErrorCancelled
+ if (error.code == NSURLErrorCancelled) {
+ // We would only have been cancelled in the -cancel method, which will call finish for us
+ // so just return and do nothing.
+ return;
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption001,
+ @"Device registration HTTP fetch error. Error Code: %ld",
+ _FIRMessaging_L(error.code));
+ [self finishWithResult:FIRMessagingTopicOperationResultError error:error];
+ return;
+ }
+ NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ if (response.length == 0) {
+ [self finishWithResult:FIRMessagingTopicOperationResultError
+ error:[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeUnknown]];
+ return;
+ }
+ NSArray *parts = [response componentsSeparatedByString:@"="];
+ _FIRMessagingDevAssert(parts.count, @"Invalid registration response");
+ if (![parts[0] isEqualToString:@"token"] || parts.count <= 1) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption002,
+ @"Invalid registration request, response");
+ [self finishWithResult:FIRMessagingTopicOperationResultError
+ error:[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeUnknown]];
+ return;
+ }
+#if DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS
+ NSTimeInterval duration = -[start timeIntervalSinceNow];
+ FIRMessagingLoggerDebug(@"%@ change took %.2fs", self.topic, duration);
+#endif
+ [self finishWithResult:FIRMessagingTopicOperationResultSucceeded error:nil];
+
+ };
+
+ NSURLSession *urlSession = [FIRMessagingTopicOperation sharedSession];
+
+ self.dataTask = [urlSession dataTaskWithRequest:request completionHandler:requestHandler];
+ NSString *description;
+ if (_action == FIRMessagingTopicActionSubscribe) {
+ description = [NSString stringWithFormat:@"com.google.fcm.topics.subscribe: %@", _topic];
+ } else {
+ description = [NSString stringWithFormat:@"com.google.fcm.topics.unsubscribe: %@", _topic];
+ }
+ self.dataTask.taskDescription = description;
+ [self.dataTask resume];
+}
+
+@end
diff --git a/Firebase/Messaging/FIRMessagingTopicsCommon.h b/Firebase/Messaging/FIRMessagingTopicsCommon.h
new file mode 100644
index 0000000..50d8906
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingTopicsCommon.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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Represents the action taken on a subscription topic.
+ */
+typedef NS_ENUM(NSInteger, FIRMessagingTopicAction) {
+ FIRMessagingTopicActionSubscribe,
+ FIRMessagingTopicActionUnsubscribe
+};
+
+/**
+ * Represents the possible results of a topic operation.
+ */
+typedef NS_ENUM(NSInteger, FIRMessagingTopicOperationResult) {
+ FIRMessagingTopicOperationResultSucceeded,
+ FIRMessagingTopicOperationResultError,
+ FIRMessagingTopicOperationResultCancelled,
+};
+
+/**
+ * Callback to invoke once the HTTP call to FIRMessaging backend for updating
+ * subscription finishes.
+ *
+ * @param result The result of the operation. If the result is
+ * FIRMessagingTopicOperationResultError, the error parameter will be
+ * non-nil.
+ * @param error The error which occurred while updating the subscription topic
+ * on the FIRMessaging server. This will be nil in case the operation
+ * was successful, or if the operation was cancelled.
+ */
+typedef void(^FIRMessagingTopicOperationCompletion)
+ (FIRMessagingTopicOperationResult result, NSError * _Nullable error);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Messaging/FIRMessagingUtilities.h b/Firebase/Messaging/FIRMessagingUtilities.h
new file mode 100644
index 0000000..ed9dc83
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingUtilities.h
@@ -0,0 +1,54 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+typedef NS_ENUM(int8_t, FIRMessagingProtoTag) {
+ kFIRMessagingProtoTagInvalid = -1,
+ kFIRMessagingProtoTagHeartbeatPing = 0,
+ kFIRMessagingProtoTagHeartbeatAck = 1,
+ kFIRMessagingProtoTagLoginRequest = 2,
+ kFIRMessagingProtoTagLoginResponse = 3,
+ kFIRMessagingProtoTagClose = 4,
+ kFIRMessagingProtoTagIqStanza = 7,
+ kFIRMessagingProtoTagDataMessageStanza = 8,
+};
+
+@class GPBMessage;
+
+#pragma mark - Protocol Buffers
+
+FOUNDATION_EXPORT FIRMessagingProtoTag FIRMessagingGetTagForProto(GPBMessage *protoClass);
+FOUNDATION_EXPORT Class FIRMessagingGetClassForTag(FIRMessagingProtoTag tag);
+
+#pragma mark - MCS
+
+FOUNDATION_EXPORT NSString *FIRMessagingGetRmq2Id(GPBMessage *proto);
+FOUNDATION_EXPORT void FIRMessagingSetRmq2Id(GPBMessage *proto, NSString *pID);
+FOUNDATION_EXPORT int FIRMessagingGetLastStreamId(GPBMessage *proto);
+FOUNDATION_EXPORT void FIRMessagingSetLastStreamId(GPBMessage *proto, int sid);
+
+#pragma mark - Time
+
+FOUNDATION_EXPORT int64_t FIRMessagingCurrentTimestampInSeconds();
+FOUNDATION_EXPORT int64_t FIRMessagingCurrentTimestampInMilliseconds();
+
+#pragma mark - App Info
+
+FOUNDATION_EXPORT NSString *FIRMessagingCurrentAppVersion();
+FOUNDATION_EXPORT NSString *FIRMessagingAppIdentifier();
+
+FOUNDATION_EXPORT uint64_t FIRMessagingGetFreeDiskSpaceInMB();
diff --git a/Firebase/Messaging/FIRMessagingUtilities.m b/Firebase/Messaging/FIRMessagingUtilities.m
new file mode 100644
index 0000000..13c7a05
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingUtilities.m
@@ -0,0 +1,173 @@
+/*
+ * 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 "FIRMessagingUtilities.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingLogger.h"
+
+// Convert the macro to a string
+#define STR_EXPAND(x) #x
+#define STR(x) STR_EXPAND(x)
+
+static const uint64_t kBytesToMegabytesDivisor = 1024 * 1024LL;
+
+#pragma mark - Protocol Buffers
+
+FIRMessagingProtoTag FIRMessagingGetTagForProto(GPBMessage *proto) {
+ if ([proto isKindOfClass:[GtalkHeartbeatPing class]]) {
+ return kFIRMessagingProtoTagHeartbeatPing;
+ } else if ([proto isKindOfClass:[GtalkHeartbeatAck class]]) {
+ return kFIRMessagingProtoTagHeartbeatAck;
+ } else if ([proto isKindOfClass:[GtalkLoginRequest class]]) {
+ return kFIRMessagingProtoTagLoginRequest;
+ } else if ([proto isKindOfClass:[GtalkLoginResponse class]]) {
+ return kFIRMessagingProtoTagLoginResponse;
+ } else if ([proto isKindOfClass:[GtalkClose class]]) {
+ return kFIRMessagingProtoTagClose;
+ } else if ([proto isKindOfClass:[GtalkIqStanza class]]) {
+ return kFIRMessagingProtoTagIqStanza;
+ } else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
+ return kFIRMessagingProtoTagDataMessageStanza;
+ }
+ return kFIRMessagingProtoTagInvalid;
+}
+
+Class FIRMessagingGetClassForTag(FIRMessagingProtoTag tag) {
+ switch (tag) {
+ case kFIRMessagingProtoTagHeartbeatPing:
+ return GtalkHeartbeatPing.class;
+ case kFIRMessagingProtoTagHeartbeatAck:
+ return GtalkHeartbeatAck.class;
+ case kFIRMessagingProtoTagLoginRequest:
+ return GtalkLoginRequest.class;
+ case kFIRMessagingProtoTagLoginResponse:
+ return GtalkLoginResponse.class;
+ case kFIRMessagingProtoTagClose:
+ return GtalkClose.class;
+ case kFIRMessagingProtoTagIqStanza:
+ return GtalkIqStanza.class;
+ case kFIRMessagingProtoTagDataMessageStanza:
+ return GtalkDataMessageStanza.class;
+ case kFIRMessagingProtoTagInvalid:
+ return NSNull.class;
+ }
+ return NSNull.class;
+}
+
+#pragma mark - MCS
+
+NSString *FIRMessagingGetRmq2Id(GPBMessage *proto) {
+ if ([proto isKindOfClass:[GtalkIqStanza class]]) {
+ if (((GtalkIqStanza *)proto).hasPersistentId) {
+ return ((GtalkIqStanza *)proto).persistentId;
+ }
+ } else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
+ if (((GtalkDataMessageStanza *)proto).hasPersistentId) {
+ return ((GtalkDataMessageStanza *)proto).persistentId;
+ }
+ }
+ return nil;
+}
+
+void FIRMessagingSetRmq2Id(GPBMessage *proto, NSString *pID) {
+ if ([proto isKindOfClass:[GtalkIqStanza class]]) {
+ ((GtalkIqStanza *)proto).persistentId = pID;
+ } else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
+ ((GtalkDataMessageStanza *)proto).persistentId = pID;
+ }
+}
+
+int FIRMessagingGetLastStreamId(GPBMessage *proto) {
+ if ([proto isKindOfClass:[GtalkIqStanza class]]) {
+ if (((GtalkIqStanza *)proto).hasLastStreamIdReceived) {
+ return ((GtalkIqStanza *)proto).lastStreamIdReceived;
+ }
+ } else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
+ if (((GtalkDataMessageStanza *)proto).hasLastStreamIdReceived) {
+ return ((GtalkDataMessageStanza *)proto).lastStreamIdReceived;
+ }
+ } else if ([proto isKindOfClass:[GtalkHeartbeatPing class]]) {
+ if (((GtalkHeartbeatPing *)proto).hasLastStreamIdReceived) {
+ return ((GtalkHeartbeatPing *)proto).lastStreamIdReceived;
+ }
+ } else if ([proto isKindOfClass:[GtalkHeartbeatAck class]]) {
+ if (((GtalkHeartbeatAck *)proto).hasLastStreamIdReceived) {
+ return ((GtalkHeartbeatAck *)proto).lastStreamIdReceived;
+ }
+ }
+ return -1;
+}
+
+void FIRMessagingSetLastStreamId(GPBMessage *proto, int sid) {
+ if ([proto isKindOfClass:[GtalkIqStanza class]]) {
+ ((GtalkIqStanza *)proto).lastStreamIdReceived = sid;
+ } else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
+ ((GtalkDataMessageStanza *)proto).lastStreamIdReceived = sid;
+ } else if ([proto isKindOfClass:[GtalkHeartbeatPing class]]) {
+ ((GtalkHeartbeatPing *)proto).lastStreamIdReceived = sid;
+ } else if ([proto isKindOfClass:[GtalkHeartbeatAck class]]) {
+ ((GtalkHeartbeatAck *)proto).lastStreamIdReceived = sid;
+ }
+}
+
+#pragma mark - Time
+
+int64_t FIRMessagingCurrentTimestampInSeconds() {
+ return (int64_t)[[NSDate date] timeIntervalSince1970];
+}
+
+int64_t FIRMessagingCurrentTimestampInMilliseconds() {
+ return (int64_t)(FIRMessagingCurrentTimestampInSeconds() * 1000.0);
+}
+
+#pragma mark - App Info
+
+NSString *FIRMessagingCurrentAppVersion() {
+ NSString *version = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"];
+ if (![version length]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeUtilities000,
+ @"Could not find current app version");
+ return @"";
+ }
+ return version;
+}
+
+NSString *FIRMessagingAppIdentifier() {
+ return [[NSBundle mainBundle] bundleIdentifier];
+}
+
+uint64_t FIRMessagingGetFreeDiskSpaceInMB() {
+ NSError *error;
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+
+ NSDictionary *attributesMap =
+ [[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject]
+ error:&error];
+ if (attributesMap) {
+ uint64_t totalSizeInBytes __unused = [attributesMap[NSFileSystemSize] longLongValue];
+ uint64_t freeSizeInBytes = [attributesMap[NSFileSystemFreeSize] longLongValue];
+ FIRMessagingLoggerDebug(
+ kFIRMessagingMessageCodeUtilities001, @"Device has capacity %llu MB with %llu MB free.",
+ totalSizeInBytes / kBytesToMegabytesDivisor, freeSizeInBytes / kBytesToMegabytesDivisor);
+ return ((double)freeSizeInBytes) / kBytesToMegabytesDivisor;
+ } else {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeUtilities002,
+ @"Error in retreiving device's free memory %@", error);
+ return 0;
+ }
+}
diff --git a/Firebase/Messaging/FIRMessagingVersionUtilities.h b/Firebase/Messaging/FIRMessagingVersionUtilities.h
new file mode 100644
index 0000000..df7cebe
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingVersionUtilities.h
@@ -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 <Foundation/Foundation.h>
+
+/**
+ * Parsing utility for FIRMessaging Library versions. FIRMessaging Library 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 *FIRMessagingCurrentLibraryVersion();
+/// Returns the current Major version of FIRMessaging library.
+FOUNDATION_EXPORT int FIRMessagingCurrentLibraryVersionMajor();
+/// Returns the current Minor version of FIRMessaging library.
+FOUNDATION_EXPORT int FIRMessagingCurrentLibraryVersionMinor();
+/// Returns the current Patch version of FIRMessaging library.
+FOUNDATION_EXPORT int FIRMessagingCurrentLibraryVersionPatch();
+/// Returns YES if current library version is `beta` else NO.
+FOUNDATION_EXPORT BOOL FIRMessagingCurrentLibraryVersionIsBeta();
diff --git a/Firebase/Messaging/FIRMessagingVersionUtilities.m b/Firebase/Messaging/FIRMessagingVersionUtilities.m
new file mode 100644
index 0000000..e0f922c
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingVersionUtilities.m
@@ -0,0 +1,87 @@
+/*
+ * 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 "FIRMessagingVersionUtilities.h"
+
+#import "FIRMessagingDefines.h"
+
+// Convert the macro to a string
+#define STR_EXPAND(x) #x
+#define STR(x) STR_EXPAND(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 FIRMessagingParseCurrentLibraryVersion() {
+ static NSArray *allVersions;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSMutableString *daylightVersion = [NSMutableString stringWithUTF8String:STR(FIRMessaging_LIB_VERSION)];
+ // Parse versions
+ // major, minor, patch[-beta#]
+ allVersions = [daylightVersion componentsSeparatedByString:kSemanticVersioningSeparator];
+ _FIRMessagingDevAssert(allVersions.count == 3, @"Invalid versioning of FIRMessaging library");
+ if (allVersions.count == 3) {
+ majorVersion = [allVersions[0] intValue];
+ minorVersion = [allVersions[1] intValue];
+
+ // Parse patch and beta versions
+ NSArray *patchAndBetaVersion =
+ [allVersions[2] componentsSeparatedByString:kBetaVersionPrefix];
+ _FIRMessagingDevAssert(patchAndBetaVersion.count <= 2, @"Invalid versioning of FIRMessaging 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 *FIRMessagingCurrentLibraryVersion() {
+ FIRMessagingParseCurrentLibraryVersion();
+ return libraryVersion;
+}
+
+int FIRMessagingCurrentLibraryVersionMajor() {
+ FIRMessagingParseCurrentLibraryVersion();
+ return majorVersion;
+}
+
+int FIRMessagingCurrentLibraryVersionMinor() {
+ FIRMessagingParseCurrentLibraryVersion();
+ return minorVersion;
+}
+
+int FIRMessagingCurrentLibraryVersionPatch() {
+ FIRMessagingParseCurrentLibraryVersion();
+ return patchVersion;
+}
+
+BOOL FIRMessagingCurrentLibraryVersionIsBeta() {
+ FIRMessagingParseCurrentLibraryVersion();
+ return betaVersion > 0;
+}
diff --git a/Firebase/Messaging/FIRMessaging_Private.h b/Firebase/Messaging/FIRMessaging_Private.h
new file mode 100644
index 0000000..0c35179
--- /dev/null
+++ b/Firebase/Messaging/FIRMessaging_Private.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 "FIRMessaging.h"
+
+@class FIRMessagingClient;
+@class FIRMessagingPubSub;
+
+typedef NS_ENUM(int8_t, FIRMessagingNetworkStatus) {
+ kFIRMessagingReachabilityNotReachable = 0,
+ kFIRMessagingReachabilityReachableViaWiFi,
+ kFIRMessagingReachabilityReachableViaWWAN,
+};
+
+@interface FIRMessagingRemoteMessage ()
+
+@property(nonatomic, strong) NSDictionary *appData;
+
+@end
+
+@interface FIRMessaging ()
+
+#pragma mark - Private API
+
+- (NSString *)defaultFcmToken;
+- (FIRMessagingClient *)client;
+- (FIRMessagingPubSub *)pubsub;
+
+// Create a sample message to be sent over the wire using FIRMessaging. Look at
+// FIRMessagingService.h to see what each param signifies.
++ (NSMutableDictionary *)createFIRMessagingMessageWithMessage:(NSDictionary *)message
+ to:(NSString *)to
+ withID:(NSString *)msgID
+ timeToLive:(int64_t)ttl
+ delay:(int)delay;
+
+- (BOOL)isNetworkAvailable;
+- (FIRMessagingNetworkStatus)networkType;
+
+// Set the APNS token for FCM.
+- (void)setAPNSToken:(NSData *)apnsToken error:(NSError *)error;
+
+@end
diff --git a/Firebase/Messaging/FirebaseMessaging.h b/Firebase/Messaging/FirebaseMessaging.h
new file mode 100644
index 0000000..ef081c9
--- /dev/null
+++ b/Firebase/Messaging/FirebaseMessaging.h
@@ -0,0 +1,17 @@
+/*
+ * 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 "FIRMessaging.h"
diff --git a/Firebase/Messaging/FirebaseMessaging.podspec b/Firebase/Messaging/FirebaseMessaging.podspec
new file mode 100644
index 0000000..4bcbebb
--- /dev/null
+++ b/Firebase/Messaging/FirebaseMessaging.podspec
@@ -0,0 +1,41 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseMessaging'
+ s.version = '2.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]'
+ s.requires_arc = '*.m'
+ s.public_header_files =
+ 'Public/FirebaseMessaging.h',
+ 'Public/FIRMessaging.h'
+
+ s.library = 'sqlite3'
+ s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' +
+ 'GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1 ' +
+ 'FIRMessaging_LIB_VERSION=' + String(s.version)
+ }
+ s.framework = 'AddressBook'
+ s.framework = 'SystemConfiguration'
+# s.dependency 'FirebaseDev/Core'
+ s.dependency 'GoogleToolboxForMac/Logger', '~> 2.1'
+ s.dependency 'Protobuf', '~> 3.1'
+end
diff --git a/Firebase/Messaging/InternalHeaders/FIRMessagingInternalUtilities.h b/Firebase/Messaging/InternalHeaders/FIRMessagingInternalUtilities.h
new file mode 100644
index 0000000..d6a1639
--- /dev/null
+++ b/Firebase/Messaging/InternalHeaders/FIRMessagingInternalUtilities.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.
+ */
+
+/// @file FIRMessagingInternalUtilities.h
+///
+/// Internal Class Names and Methods that other libs can query at runtime.
+
+/// FIRMessaging Class that responds to the FIRMessaging SDK version selector.
+/// Verify at runtime if the class exists and implements the
+/// required method.
+static NSString *const kFIRMessagingSDKClassString = @"FIRMessaging";
+
+/// FIRMessaging selector that returns the current FIRMessaging library version.
+static NSString *const kFIRMessagingSDKVersionSelectorString = @"FIRMessagingSDKVersion";
+
+/// FIRMessaging selector that returns the current device locale.
+static NSString *const kFIRMessagingSDKLocaleSelectorString = @"FIRMessagingSDKCurrentLocale";
diff --git a/Firebase/Messaging/NSDictionary+FIRMessaging.h b/Firebase/Messaging/NSDictionary+FIRMessaging.h
new file mode 100644
index 0000000..fe14451
--- /dev/null
+++ b/Firebase/Messaging/NSDictionary+FIRMessaging.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@interface NSDictionary (FIRMessaging)
+
+/**
+ * Returns a string representation for the given dictionary. Assumes that all
+ * keys and values are strings.
+ *
+ * @return A string representation of all keys and values in the dictionary.
+ * The returned string is not pretty-printed.
+ */
+- (NSString *)fcm_string;
+
+/**
+ * Check if the dictionary has any non-string keys or values.
+ *
+ * @return YES if the dictionary has any non-string keys or values else NO.
+ */
+- (BOOL)fcm_hasNonStringKeysOrValues;
+
+/**
+ * Trims all (key, value) pair in a dictionary that are not strings.
+ *
+ * @return A new copied dictionary with all the non-string keys or values
+ * removed from the original dictionary.
+ */
+- (NSDictionary *)fcm_trimNonStringValues;
+
+@end
diff --git a/Firebase/Messaging/NSDictionary+FIRMessaging.m b/Firebase/Messaging/NSDictionary+FIRMessaging.m
new file mode 100644
index 0000000..8df22ab
--- /dev/null
+++ b/Firebase/Messaging/NSDictionary+FIRMessaging.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 "NSDictionary+FIRMessaging.h"
+
+@implementation NSDictionary (FIRMessaging)
+
+- (NSString *)fcm_string {
+ NSMutableString *dictAsString = [NSMutableString string];
+ NSString *separator = @"|";
+ for (id key in self) {
+ id value = self[key];
+ if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) {
+ [dictAsString appendFormat:@"%@:%@%@", key, value, separator];
+ }
+ }
+ // remove the last separator
+ if ([dictAsString length]) {
+ [dictAsString deleteCharactersInRange:NSMakeRange(dictAsString.length - 1, 1)];
+ }
+ return [dictAsString copy];
+}
+
+- (BOOL)fcm_hasNonStringKeysOrValues {
+ for (id key in self) {
+ id value = self[key];
+ if (![key isKindOfClass:[NSString class]] || ![value isKindOfClass:[NSString class]]) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (NSDictionary *)fcm_trimNonStringValues {
+ NSMutableDictionary *trimDictionary =
+ [NSMutableDictionary dictionaryWithCapacity:self.count];
+ for (id key in self) {
+ id value = self[key];
+ if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) {
+ trimDictionary[(NSString *)key] = value;
+ }
+ }
+ return trimDictionary;
+}
+
+@end
diff --git a/Firebase/Messaging/NSError+FIRMessaging.h b/Firebase/Messaging/NSError+FIRMessaging.h
new file mode 100644
index 0000000..9b1e214
--- /dev/null
+++ b/Firebase/Messaging/NSError+FIRMessaging.h
@@ -0,0 +1,68 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+FOUNDATION_EXPORT NSString *const kFIRMessagingDomain;
+
+typedef NS_ENUM(NSUInteger, FIRMessagingInternalErrorCode) {
+ // Unknown error.
+ kFIRMessagingErrorCodeUnknown = 0,
+
+ // HTTP related errors.
+ kFIRMessagingErrorCodeAuthentication = 1,
+ kFIRMessagingErrorCodeNoAccess = 2,
+ kFIRMessagingErrorCodeTimeout = 3,
+ kFIRMessagingErrorCodeNetwork = 4,
+
+ // Another operation is in progress.
+ kFIRMessagingErrorCodeOperationInProgress = 5,
+
+ // Failed to perform device check in.
+ kFIRMessagingErrorCodeRegistrarFailedToCheckIn = 6,
+
+ kFIRMessagingErrorCodeInvalidRequest = 7,
+
+ // FIRMessaging generic errors
+ kFIRMessagingErrorCodeMissingDeviceID = 501,
+
+ // upstream send errors
+ kFIRMessagingErrorServiceNotAvailable = 1001,
+ kFIRMessagingErrorInvalidParameters = 1002,
+ kFIRMessagingErrorMissingTo = 1003,
+ kFIRMessagingErrorSave = 1004,
+ kFIRMessagingErrorSizeExceeded = 1005,
+ // Future Send Errors
+
+ // MCS errors
+ // Already connected with MCS
+ kFIRMessagingErrorCodeAlreadyConnected = 2001,
+
+ // PubSub errors
+ kFIRMessagingErrorCodePubSubAlreadySubscribed = 3001,
+ kFIRMessagingErrorCodePubSubAlreadyUnsubscribed = 3002,
+ kFIRMessagingErrorCodePubSubInvalidTopic = 3003,
+ kFIRMessagingErrorCodePubSubFIRMessagingNotSetup = 3004,
+};
+
+@interface NSError (FIRMessaging)
+
+@property(nonatomic, readonly) FIRMessagingInternalErrorCode fcmErrorCode;
+
++ (NSError *)errorWithFCMErrorCode:(FIRMessagingInternalErrorCode)fcmErrorCode;
++ (NSError *)fcm_errorWithCode:(NSInteger)code userInfo:(NSDictionary *)userInfo;
+
+@end
diff --git a/Firebase/Messaging/NSError+FIRMessaging.m b/Firebase/Messaging/NSError+FIRMessaging.m
new file mode 100644
index 0000000..e4b8736
--- /dev/null
+++ b/Firebase/Messaging/NSError+FIRMessaging.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 "NSError+FIRMessaging.h"
+
+NSString *const kFIRMessagingDomain = @"com.google.fcm";
+
+@implementation NSError (FIRMessaging)
+
+- (FIRMessagingInternalErrorCode)fcmErrorCode {
+ return (FIRMessagingInternalErrorCode)self.code;
+}
+
++ (NSError *)errorWithFCMErrorCode:(FIRMessagingInternalErrorCode)fcmErrorCode {
+ return [NSError errorWithDomain:kFIRMessagingDomain code:fcmErrorCode userInfo:nil];
+}
+
++ (NSError *)fcm_errorWithCode:(NSInteger)code userInfo:(NSDictionary *)userInfo {
+ return [NSError errorWithDomain:kFIRMessagingDomain code:code userInfo:userInfo];
+}
+
+@end
diff --git a/Firebase/Messaging/Protos/GtalkCore.pbobjc.h b/Firebase/Messaging/Protos/GtalkCore.pbobjc.h
new file mode 100644
index 0000000..d4c8c8c
--- /dev/null
+++ b/Firebase/Messaging/Protos/GtalkCore.pbobjc.h
@@ -0,0 +1,1344 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: buzz/mobile/proto/gtalk_core.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GtalkAppData;
+@class GtalkCellTower;
+@class GtalkClientEvent;
+@class GtalkErrorInfo;
+@class GtalkExtension;
+@class GtalkHeartbeatConfig;
+@class GtalkHeartbeatStat;
+@class GtalkPresenceStanza;
+@class GtalkSetting;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - Enum GtalkLoginRequest_AuthService
+
+typedef GPB_ENUM(GtalkLoginRequest_AuthService) {
+ GtalkLoginRequest_AuthService_Mail = 0,
+ GtalkLoginRequest_AuthService_AndroidCloudToDeviceMessage = 1,
+ GtalkLoginRequest_AuthService_AndroidId = 2,
+};
+
+GPBEnumDescriptor *GtalkLoginRequest_AuthService_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkLoginRequest_AuthService_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkMessageStanza_MessageType
+
+typedef GPB_ENUM(GtalkMessageStanza_MessageType) {
+ GtalkMessageStanza_MessageType_Normal = 0,
+ GtalkMessageStanza_MessageType_Chat = 1,
+ GtalkMessageStanza_MessageType_Groupchat = 2,
+ GtalkMessageStanza_MessageType_Headline = 3,
+ GtalkMessageStanza_MessageType_Error = 4,
+};
+
+GPBEnumDescriptor *GtalkMessageStanza_MessageType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkMessageStanza_MessageType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkPresenceStanza_PresenceType
+
+typedef GPB_ENUM(GtalkPresenceStanza_PresenceType) {
+ GtalkPresenceStanza_PresenceType_Unavailable = 0,
+ GtalkPresenceStanza_PresenceType_Subscribe = 1,
+ GtalkPresenceStanza_PresenceType_Subscribed = 2,
+ GtalkPresenceStanza_PresenceType_Unsubscribe = 3,
+ GtalkPresenceStanza_PresenceType_Unsubscribed = 4,
+ GtalkPresenceStanza_PresenceType_Probe = 5,
+ GtalkPresenceStanza_PresenceType_Error = 6,
+};
+
+GPBEnumDescriptor *GtalkPresenceStanza_PresenceType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkPresenceStanza_PresenceType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkPresenceStanza_ShowType
+
+typedef GPB_ENUM(GtalkPresenceStanza_ShowType) {
+ GtalkPresenceStanza_ShowType_Away = 0,
+ GtalkPresenceStanza_ShowType_Chat = 1,
+ GtalkPresenceStanza_ShowType_Dnd = 2,
+ GtalkPresenceStanza_ShowType_Xa = 3,
+};
+
+GPBEnumDescriptor *GtalkPresenceStanza_ShowType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkPresenceStanza_ShowType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkPresenceStanza_ClientType
+
+typedef GPB_ENUM(GtalkPresenceStanza_ClientType) {
+ GtalkPresenceStanza_ClientType_Mobile = 0,
+ GtalkPresenceStanza_ClientType_Android = 1,
+};
+
+GPBEnumDescriptor *GtalkPresenceStanza_ClientType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkPresenceStanza_ClientType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkPresenceStanza_CapabilitiesFlags
+
+typedef GPB_ENUM(GtalkPresenceStanza_CapabilitiesFlags) {
+ GtalkPresenceStanza_CapabilitiesFlags_HasVoiceV1 = 1,
+ GtalkPresenceStanza_CapabilitiesFlags_HasVideoV1 = 2,
+ GtalkPresenceStanza_CapabilitiesFlags_HasCameraV1 = 4,
+ GtalkPresenceStanza_CapabilitiesFlags_HasPmucV1 = 8,
+};
+
+GPBEnumDescriptor *GtalkPresenceStanza_CapabilitiesFlags_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkPresenceStanza_CapabilitiesFlags_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkBatchPresenceStanza_Type
+
+typedef GPB_ENUM(GtalkBatchPresenceStanza_Type) {
+ GtalkBatchPresenceStanza_Type_Get = 0,
+ GtalkBatchPresenceStanza_Type_Set = 1,
+};
+
+GPBEnumDescriptor *GtalkBatchPresenceStanza_Type_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkBatchPresenceStanza_Type_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkIqStanza_IqType
+
+typedef GPB_ENUM(GtalkIqStanza_IqType) {
+ GtalkIqStanza_IqType_Get = 0,
+ GtalkIqStanza_IqType_Set = 1,
+ GtalkIqStanza_IqType_Result = 2,
+ GtalkIqStanza_IqType_Error = 3,
+};
+
+GPBEnumDescriptor *GtalkIqStanza_IqType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkIqStanza_IqType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkClientEvent_Type
+
+typedef GPB_ENUM(GtalkClientEvent_Type) {
+ GtalkClientEvent_Type_Unknown = 0,
+ GtalkClientEvent_Type_DiscardedEvents = 1,
+ GtalkClientEvent_Type_FailedConnection = 2,
+ GtalkClientEvent_Type_SuccessfulConnection = 3,
+};
+
+GPBEnumDescriptor *GtalkClientEvent_Type_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkClientEvent_Type_IsValidValue(int32_t value);
+
+#pragma mark - GtalkGtalkCoreRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GtalkGtalkCoreRoot : GPBRootObject
+@end
+
+#pragma mark - GtalkHeartbeatPing
+
+typedef GPB_ENUM(GtalkHeartbeatPing_FieldNumber) {
+ GtalkHeartbeatPing_FieldNumber_StreamId = 1,
+ GtalkHeartbeatPing_FieldNumber_LastStreamIdReceived = 2,
+ GtalkHeartbeatPing_FieldNumber_Status = 3,
+ GtalkHeartbeatPing_FieldNumber_CellTower = 4,
+ GtalkHeartbeatPing_FieldNumber_IntervalMs = 5,
+};
+
+@interface GtalkHeartbeatPing : GPBMessage
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int64_t status;
+
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkCellTower *cellTower;
+/** Test to see if @c cellTower has been set. */
+@property(nonatomic, readwrite) BOOL hasCellTower;
+
+
+@property(nonatomic, readwrite) int32_t intervalMs;
+
+@property(nonatomic, readwrite) BOOL hasIntervalMs;
+@end
+
+#pragma mark - GtalkHeartbeatAck
+
+typedef GPB_ENUM(GtalkHeartbeatAck_FieldNumber) {
+ GtalkHeartbeatAck_FieldNumber_StreamId = 1,
+ GtalkHeartbeatAck_FieldNumber_LastStreamIdReceived = 2,
+ GtalkHeartbeatAck_FieldNumber_Status = 3,
+ GtalkHeartbeatAck_FieldNumber_CellTower = 4,
+ GtalkHeartbeatAck_FieldNumber_IntervalMs = 5,
+};
+
+@interface GtalkHeartbeatAck : GPBMessage
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int64_t status;
+
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkCellTower *cellTower;
+/** Test to see if @c cellTower has been set. */
+@property(nonatomic, readwrite) BOOL hasCellTower;
+
+
+@property(nonatomic, readwrite) int32_t intervalMs;
+
+@property(nonatomic, readwrite) BOOL hasIntervalMs;
+@end
+
+#pragma mark - GtalkErrorInfo
+
+typedef GPB_ENUM(GtalkErrorInfo_FieldNumber) {
+ GtalkErrorInfo_FieldNumber_Code = 1,
+ GtalkErrorInfo_FieldNumber_Message = 2,
+ GtalkErrorInfo_FieldNumber_Type = 3,
+ GtalkErrorInfo_FieldNumber_Extension = 4,
+};
+
+@interface GtalkErrorInfo : GPBMessage
+
+
+@property(nonatomic, readwrite) int32_t code;
+
+@property(nonatomic, readwrite) BOOL hasCode;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *message;
+/** Test to see if @c message has been set. */
+@property(nonatomic, readwrite) BOOL hasMessage;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *type;
+/** Test to see if @c type has been set. */
+@property(nonatomic, readwrite) BOOL hasType;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkExtension *extension;
+/** Test to see if @c extension has been set. */
+@property(nonatomic, readwrite) BOOL hasExtension;
+
+@end
+
+#pragma mark - GtalkSetting
+
+typedef GPB_ENUM(GtalkSetting_FieldNumber) {
+ GtalkSetting_FieldNumber_Name = 1,
+ GtalkSetting_FieldNumber_Value = 2,
+};
+
+@interface GtalkSetting : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *name;
+/** Test to see if @c name has been set. */
+@property(nonatomic, readwrite) BOOL hasName;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *value;
+/** Test to see if @c value has been set. */
+@property(nonatomic, readwrite) BOOL hasValue;
+
+@end
+
+#pragma mark - GtalkHeartbeatStat
+
+typedef GPB_ENUM(GtalkHeartbeatStat_FieldNumber) {
+ GtalkHeartbeatStat_FieldNumber_Ip = 1,
+ GtalkHeartbeatStat_FieldNumber_Timeout = 2,
+ GtalkHeartbeatStat_FieldNumber_IntervalMs = 3,
+};
+
+@interface GtalkHeartbeatStat : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *ip;
+/** Test to see if @c ip has been set. */
+@property(nonatomic, readwrite) BOOL hasIp;
+
+
+@property(nonatomic, readwrite) BOOL timeout;
+
+@property(nonatomic, readwrite) BOOL hasTimeout;
+
+@property(nonatomic, readwrite) int32_t intervalMs;
+
+@property(nonatomic, readwrite) BOOL hasIntervalMs;
+@end
+
+#pragma mark - GtalkHeartbeatConfig
+
+typedef GPB_ENUM(GtalkHeartbeatConfig_FieldNumber) {
+ GtalkHeartbeatConfig_FieldNumber_UploadStat = 1,
+ GtalkHeartbeatConfig_FieldNumber_Ip = 2,
+ GtalkHeartbeatConfig_FieldNumber_IntervalMs = 3,
+};
+
+@interface GtalkHeartbeatConfig : GPBMessage
+
+
+@property(nonatomic, readwrite) BOOL uploadStat;
+
+@property(nonatomic, readwrite) BOOL hasUploadStat;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *ip;
+/** Test to see if @c ip has been set. */
+@property(nonatomic, readwrite) BOOL hasIp;
+
+
+@property(nonatomic, readwrite) int32_t intervalMs;
+
+@property(nonatomic, readwrite) BOOL hasIntervalMs;
+@end
+
+#pragma mark - GtalkLoginRequest
+
+typedef GPB_ENUM(GtalkLoginRequest_FieldNumber) {
+ GtalkLoginRequest_FieldNumber_Id_p = 1,
+ GtalkLoginRequest_FieldNumber_Domain = 2,
+ GtalkLoginRequest_FieldNumber_User = 3,
+ GtalkLoginRequest_FieldNumber_Resource = 4,
+ GtalkLoginRequest_FieldNumber_AuthToken = 5,
+ GtalkLoginRequest_FieldNumber_DeviceId = 6,
+ GtalkLoginRequest_FieldNumber_LastRmqId = 7,
+ GtalkLoginRequest_FieldNumber_SettingArray = 8,
+ GtalkLoginRequest_FieldNumber_Compress = 9,
+ GtalkLoginRequest_FieldNumber_ReceivedPersistentIdArray = 10,
+ GtalkLoginRequest_FieldNumber_IncludeStreamIds = 11,
+ GtalkLoginRequest_FieldNumber_HeartbeatStat = 13,
+ GtalkLoginRequest_FieldNumber_UseRmq2 = 14,
+ GtalkLoginRequest_FieldNumber_AccountId = 15,
+ GtalkLoginRequest_FieldNumber_AuthService = 16,
+ GtalkLoginRequest_FieldNumber_NetworkType = 17,
+ GtalkLoginRequest_FieldNumber_Status = 18,
+ GtalkLoginRequest_FieldNumber_TokenVersionInfo = 19,
+ GtalkLoginRequest_FieldNumber_CellTower = 20,
+ GtalkLoginRequest_FieldNumber_GcmStartTimeMs = 21,
+ GtalkLoginRequest_FieldNumber_ClientEventArray = 22,
+ GtalkLoginRequest_FieldNumber_OnFallback = 23,
+};
+
+@interface GtalkLoginRequest : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *domain;
+/** Test to see if @c domain has been set. */
+@property(nonatomic, readwrite) BOOL hasDomain;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *user;
+/** Test to see if @c user has been set. */
+@property(nonatomic, readwrite) BOOL hasUser;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *resource;
+/** Test to see if @c resource has been set. */
+@property(nonatomic, readwrite) BOOL hasResource;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *authToken;
+/** Test to see if @c authToken has been set. */
+@property(nonatomic, readwrite) BOOL hasAuthToken;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *deviceId;
+/** Test to see if @c deviceId has been set. */
+@property(nonatomic, readwrite) BOOL hasDeviceId;
+
+
+@property(nonatomic, readwrite) int64_t lastRmqId;
+
+@property(nonatomic, readwrite) BOOL hasLastRmqId;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkSetting*> *settingArray;
+/** The number of items in @c settingArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger settingArray_Count;
+
+
+@property(nonatomic, readwrite) int32_t compress;
+
+@property(nonatomic, readwrite) BOOL hasCompress;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *receivedPersistentIdArray;
+/** The number of items in @c receivedPersistentIdArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger receivedPersistentIdArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL includeStreamIds;
+
+@property(nonatomic, readwrite) BOOL hasIncludeStreamIds;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkHeartbeatStat *heartbeatStat;
+/** Test to see if @c heartbeatStat has been set. */
+@property(nonatomic, readwrite) BOOL hasHeartbeatStat;
+
+
+@property(nonatomic, readwrite) BOOL useRmq2;
+
+@property(nonatomic, readwrite) BOOL hasUseRmq2;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+
+@property(nonatomic, readwrite) GtalkLoginRequest_AuthService authService;
+
+@property(nonatomic, readwrite) BOOL hasAuthService;
+
+@property(nonatomic, readwrite) int32_t networkType;
+
+@property(nonatomic, readwrite) BOOL hasNetworkType;
+
+@property(nonatomic, readwrite) int64_t status;
+
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *tokenVersionInfo;
+/** Test to see if @c tokenVersionInfo has been set. */
+@property(nonatomic, readwrite) BOOL hasTokenVersionInfo;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkCellTower *cellTower;
+/** Test to see if @c cellTower has been set. */
+@property(nonatomic, readwrite) BOOL hasCellTower;
+
+
+@property(nonatomic, readwrite) uint64_t gcmStartTimeMs;
+
+@property(nonatomic, readwrite) BOOL hasGcmStartTimeMs;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkClientEvent*> *clientEventArray;
+/** The number of items in @c clientEventArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger clientEventArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL onFallback;
+
+@property(nonatomic, readwrite) BOOL hasOnFallback;
+@end
+
+#pragma mark - GtalkLoginResponse
+
+typedef GPB_ENUM(GtalkLoginResponse_FieldNumber) {
+ GtalkLoginResponse_FieldNumber_Id_p = 1,
+ GtalkLoginResponse_FieldNumber_Jid = 2,
+ GtalkLoginResponse_FieldNumber_Error = 3,
+ GtalkLoginResponse_FieldNumber_SettingArray = 4,
+ GtalkLoginResponse_FieldNumber_StreamId = 5,
+ GtalkLoginResponse_FieldNumber_LastStreamIdReceived = 6,
+ GtalkLoginResponse_FieldNumber_HeartbeatConfig = 7,
+ GtalkLoginResponse_FieldNumber_ServerTimestamp = 8,
+};
+
+@interface GtalkLoginResponse : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *jid;
+/** Test to see if @c jid has been set. */
+@property(nonatomic, readwrite) BOOL hasJid;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkSetting*> *settingArray;
+/** The number of items in @c settingArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger settingArray_Count;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkHeartbeatConfig *heartbeatConfig;
+/** Test to see if @c heartbeatConfig has been set. */
+@property(nonatomic, readwrite) BOOL hasHeartbeatConfig;
+
+
+@property(nonatomic, readwrite) int64_t serverTimestamp;
+
+@property(nonatomic, readwrite) BOOL hasServerTimestamp;
+@end
+
+#pragma mark - GtalkBindAccountRequest
+
+typedef GPB_ENUM(GtalkBindAccountRequest_FieldNumber) {
+ GtalkBindAccountRequest_FieldNumber_Id_p = 1,
+ GtalkBindAccountRequest_FieldNumber_Domain = 2,
+ GtalkBindAccountRequest_FieldNumber_User = 3,
+ GtalkBindAccountRequest_FieldNumber_Resource = 4,
+ GtalkBindAccountRequest_FieldNumber_AuthToken = 5,
+ GtalkBindAccountRequest_FieldNumber_PersistentId = 6,
+ GtalkBindAccountRequest_FieldNumber_StreamId = 7,
+ GtalkBindAccountRequest_FieldNumber_LastStreamIdReceived = 8,
+ GtalkBindAccountRequest_FieldNumber_AccountId = 9,
+};
+
+@interface GtalkBindAccountRequest : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *domain;
+/** Test to see if @c domain has been set. */
+@property(nonatomic, readwrite) BOOL hasDomain;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *user;
+/** Test to see if @c user has been set. */
+@property(nonatomic, readwrite) BOOL hasUser;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *resource;
+/** Test to see if @c resource has been set. */
+@property(nonatomic, readwrite) BOOL hasResource;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *authToken;
+/** Test to see if @c authToken has been set. */
+@property(nonatomic, readwrite) BOOL hasAuthToken;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+@end
+
+#pragma mark - GtalkBindAccountResponse
+
+typedef GPB_ENUM(GtalkBindAccountResponse_FieldNumber) {
+ GtalkBindAccountResponse_FieldNumber_Id_p = 1,
+ GtalkBindAccountResponse_FieldNumber_Jid = 2,
+ GtalkBindAccountResponse_FieldNumber_Error = 3,
+ GtalkBindAccountResponse_FieldNumber_StreamId = 4,
+ GtalkBindAccountResponse_FieldNumber_LastStreamIdReceived = 5,
+};
+
+@interface GtalkBindAccountResponse : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *jid;
+/** Test to see if @c jid has been set. */
+@property(nonatomic, readwrite) BOOL hasJid;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+@end
+
+#pragma mark - GtalkStreamErrorStanza
+
+typedef GPB_ENUM(GtalkStreamErrorStanza_FieldNumber) {
+ GtalkStreamErrorStanza_FieldNumber_Type = 1,
+ GtalkStreamErrorStanza_FieldNumber_Text = 2,
+};
+
+@interface GtalkStreamErrorStanza : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *type;
+/** Test to see if @c type has been set. */
+@property(nonatomic, readwrite) BOOL hasType;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *text;
+/** Test to see if @c text has been set. */
+@property(nonatomic, readwrite) BOOL hasText;
+
+@end
+
+#pragma mark - GtalkClose
+
+@interface GtalkClose : GPBMessage
+
+@end
+
+#pragma mark - GtalkExtension
+
+typedef GPB_ENUM(GtalkExtension_FieldNumber) {
+ GtalkExtension_FieldNumber_Id_p = 1,
+ GtalkExtension_FieldNumber_Data_p = 2,
+};
+
+@interface GtalkExtension : GPBMessage
+
+
+@property(nonatomic, readwrite) int32_t id_p;
+
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *data_p;
+/** Test to see if @c data_p has been set. */
+@property(nonatomic, readwrite) BOOL hasData_p;
+
+@end
+
+#pragma mark - GtalkMessageStanza
+
+typedef GPB_ENUM(GtalkMessageStanza_FieldNumber) {
+ GtalkMessageStanza_FieldNumber_RmqId = 1,
+ GtalkMessageStanza_FieldNumber_Type = 2,
+ GtalkMessageStanza_FieldNumber_Id_p = 3,
+ GtalkMessageStanza_FieldNumber_From = 4,
+ GtalkMessageStanza_FieldNumber_To = 5,
+ GtalkMessageStanza_FieldNumber_Subject = 6,
+ GtalkMessageStanza_FieldNumber_Body = 7,
+ GtalkMessageStanza_FieldNumber_Thread = 8,
+ GtalkMessageStanza_FieldNumber_Error = 9,
+ GtalkMessageStanza_FieldNumber_ExtensionArray = 10,
+ GtalkMessageStanza_FieldNumber_Nosave = 11,
+ GtalkMessageStanza_FieldNumber_Timestamp = 12,
+ GtalkMessageStanza_FieldNumber_PersistentId = 13,
+ GtalkMessageStanza_FieldNumber_StreamId = 14,
+ GtalkMessageStanza_FieldNumber_LastStreamIdReceived = 15,
+ GtalkMessageStanza_FieldNumber_Read = 16,
+ GtalkMessageStanza_FieldNumber_AccountId = 17,
+};
+
+@interface GtalkMessageStanza : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t rmqId;
+
+@property(nonatomic, readwrite) BOOL hasRmqId;
+
+@property(nonatomic, readwrite) GtalkMessageStanza_MessageType type;
+
+@property(nonatomic, readwrite) BOOL hasType;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *from;
+/** Test to see if @c from has been set. */
+@property(nonatomic, readwrite) BOOL hasFrom;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *to;
+/** Test to see if @c to has been set. */
+@property(nonatomic, readwrite) BOOL hasTo;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *subject;
+/** Test to see if @c subject has been set. */
+@property(nonatomic, readwrite) BOOL hasSubject;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *body;
+/** Test to see if @c body has been set. */
+@property(nonatomic, readwrite) BOOL hasBody;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *thread;
+/** Test to see if @c thread has been set. */
+@property(nonatomic, readwrite) BOOL hasThread;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkExtension*> *extensionArray;
+/** The number of items in @c extensionArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger extensionArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL nosave;
+
+@property(nonatomic, readwrite) BOOL hasNosave;
+
+@property(nonatomic, readwrite) int64_t timestamp;
+
+@property(nonatomic, readwrite) BOOL hasTimestamp;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL read;
+
+@property(nonatomic, readwrite) BOOL hasRead;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+@end
+
+#pragma mark - GtalkPresenceStanza
+
+typedef GPB_ENUM(GtalkPresenceStanza_FieldNumber) {
+ GtalkPresenceStanza_FieldNumber_RmqId = 1,
+ GtalkPresenceStanza_FieldNumber_Type = 2,
+ GtalkPresenceStanza_FieldNumber_Id_p = 3,
+ GtalkPresenceStanza_FieldNumber_From = 4,
+ GtalkPresenceStanza_FieldNumber_To = 5,
+ GtalkPresenceStanza_FieldNumber_Show = 6,
+ GtalkPresenceStanza_FieldNumber_Status = 7,
+ GtalkPresenceStanza_FieldNumber_Priority = 8,
+ GtalkPresenceStanza_FieldNumber_Error = 9,
+ GtalkPresenceStanza_FieldNumber_ExtensionArray = 10,
+ GtalkPresenceStanza_FieldNumber_Client = 11,
+ GtalkPresenceStanza_FieldNumber_AvatarHash = 12,
+ GtalkPresenceStanza_FieldNumber_PersistentId = 13,
+ GtalkPresenceStanza_FieldNumber_StreamId = 14,
+ GtalkPresenceStanza_FieldNumber_LastStreamIdReceived = 15,
+ GtalkPresenceStanza_FieldNumber_CapabilitiesFlags = 16,
+ GtalkPresenceStanza_FieldNumber_AccountId = 17,
+};
+
+@interface GtalkPresenceStanza : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t rmqId;
+
+@property(nonatomic, readwrite) BOOL hasRmqId;
+
+@property(nonatomic, readwrite) GtalkPresenceStanza_PresenceType type;
+
+@property(nonatomic, readwrite) BOOL hasType;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *from;
+/** Test to see if @c from has been set. */
+@property(nonatomic, readwrite) BOOL hasFrom;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *to;
+/** Test to see if @c to has been set. */
+@property(nonatomic, readwrite) BOOL hasTo;
+
+
+@property(nonatomic, readwrite) GtalkPresenceStanza_ShowType show;
+
+@property(nonatomic, readwrite) BOOL hasShow;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *status;
+/** Test to see if @c status has been set. */
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+
+@property(nonatomic, readwrite) int32_t priority;
+
+@property(nonatomic, readwrite) BOOL hasPriority;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkExtension*> *extensionArray;
+/** The number of items in @c extensionArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger extensionArray_Count;
+
+
+@property(nonatomic, readwrite) GtalkPresenceStanza_ClientType client;
+
+@property(nonatomic, readwrite) BOOL hasClient;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *avatarHash;
+/** Test to see if @c avatarHash has been set. */
+@property(nonatomic, readwrite) BOOL hasAvatarHash;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int32_t capabilitiesFlags;
+
+@property(nonatomic, readwrite) BOOL hasCapabilitiesFlags;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+@end
+
+#pragma mark - GtalkBatchPresenceStanza
+
+typedef GPB_ENUM(GtalkBatchPresenceStanza_FieldNumber) {
+ GtalkBatchPresenceStanza_FieldNumber_Id_p = 1,
+ GtalkBatchPresenceStanza_FieldNumber_To = 2,
+ GtalkBatchPresenceStanza_FieldNumber_PresenceArray = 3,
+ GtalkBatchPresenceStanza_FieldNumber_PersistentId = 4,
+ GtalkBatchPresenceStanza_FieldNumber_StreamId = 5,
+ GtalkBatchPresenceStanza_FieldNumber_LastStreamIdReceived = 6,
+ GtalkBatchPresenceStanza_FieldNumber_AccountId = 7,
+ GtalkBatchPresenceStanza_FieldNumber_Type = 8,
+ GtalkBatchPresenceStanza_FieldNumber_Error = 9,
+};
+
+@interface GtalkBatchPresenceStanza : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *to;
+/** Test to see if @c to has been set. */
+@property(nonatomic, readwrite) BOOL hasTo;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkPresenceStanza*> *presenceArray;
+/** The number of items in @c presenceArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger presenceArray_Count;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+
+@property(nonatomic, readwrite) GtalkBatchPresenceStanza_Type type;
+
+@property(nonatomic, readwrite) BOOL hasType;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+@end
+
+#pragma mark - GtalkIqStanza
+
+typedef GPB_ENUM(GtalkIqStanza_FieldNumber) {
+ GtalkIqStanza_FieldNumber_RmqId = 1,
+ GtalkIqStanza_FieldNumber_Type = 2,
+ GtalkIqStanza_FieldNumber_Id_p = 3,
+ GtalkIqStanza_FieldNumber_From = 4,
+ GtalkIqStanza_FieldNumber_To = 5,
+ GtalkIqStanza_FieldNumber_Error = 6,
+ GtalkIqStanza_FieldNumber_Extension = 7,
+ GtalkIqStanza_FieldNumber_PersistentId = 8,
+ GtalkIqStanza_FieldNumber_StreamId = 9,
+ GtalkIqStanza_FieldNumber_LastStreamIdReceived = 10,
+ GtalkIqStanza_FieldNumber_AccountId = 11,
+ GtalkIqStanza_FieldNumber_Status = 12,
+};
+
+@interface GtalkIqStanza : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t rmqId;
+
+@property(nonatomic, readwrite) BOOL hasRmqId;
+
+@property(nonatomic, readwrite) GtalkIqStanza_IqType type;
+
+@property(nonatomic, readwrite) BOOL hasType;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *from;
+/** Test to see if @c from has been set. */
+@property(nonatomic, readwrite) BOOL hasFrom;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *to;
+/** Test to see if @c to has been set. */
+@property(nonatomic, readwrite) BOOL hasTo;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkErrorInfo *error;
+/** Test to see if @c error has been set. */
+@property(nonatomic, readwrite) BOOL hasError;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkExtension *extension;
+/** Test to see if @c extension has been set. */
+@property(nonatomic, readwrite) BOOL hasExtension;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite) int64_t accountId;
+
+@property(nonatomic, readwrite) BOOL hasAccountId;
+
+@property(nonatomic, readwrite) int64_t status;
+
+@property(nonatomic, readwrite) BOOL hasStatus;
+@end
+
+#pragma mark - GtalkAppData
+
+typedef GPB_ENUM(GtalkAppData_FieldNumber) {
+ GtalkAppData_FieldNumber_Key = 1,
+ GtalkAppData_FieldNumber_Value = 2,
+};
+
+@interface GtalkAppData : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *key;
+/** Test to see if @c key has been set. */
+@property(nonatomic, readwrite) BOOL hasKey;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *value;
+/** Test to see if @c value has been set. */
+@property(nonatomic, readwrite) BOOL hasValue;
+
+@end
+
+#pragma mark - GtalkDataMessageStanza
+
+typedef GPB_ENUM(GtalkDataMessageStanza_FieldNumber) {
+ GtalkDataMessageStanza_FieldNumber_RmqId = 1,
+ GtalkDataMessageStanza_FieldNumber_Id_p = 2,
+ GtalkDataMessageStanza_FieldNumber_From = 3,
+ GtalkDataMessageStanza_FieldNumber_To = 4,
+ GtalkDataMessageStanza_FieldNumber_Category = 5,
+ GtalkDataMessageStanza_FieldNumber_Token = 6,
+ GtalkDataMessageStanza_FieldNumber_AppDataArray = 7,
+ GtalkDataMessageStanza_FieldNumber_FromTrustedServer = 8,
+ GtalkDataMessageStanza_FieldNumber_PersistentId = 9,
+ GtalkDataMessageStanza_FieldNumber_StreamId = 10,
+ GtalkDataMessageStanza_FieldNumber_LastStreamIdReceived = 11,
+ GtalkDataMessageStanza_FieldNumber_Permission = 12,
+ GtalkDataMessageStanza_FieldNumber_RegId = 13,
+ GtalkDataMessageStanza_FieldNumber_PkgSignature = 14,
+ GtalkDataMessageStanza_FieldNumber_ClientId = 15,
+ GtalkDataMessageStanza_FieldNumber_DeviceUserId = 16,
+ GtalkDataMessageStanza_FieldNumber_Ttl = 17,
+ GtalkDataMessageStanza_FieldNumber_Sent = 18,
+ GtalkDataMessageStanza_FieldNumber_Queued = 19,
+ GtalkDataMessageStanza_FieldNumber_Status = 20,
+ GtalkDataMessageStanza_FieldNumber_RawData = 21,
+ GtalkDataMessageStanza_FieldNumber_MaxDelay = 22,
+ GtalkDataMessageStanza_FieldNumber_ActualDelay = 23,
+ GtalkDataMessageStanza_FieldNumber_ImmediateAck = 24,
+ GtalkDataMessageStanza_FieldNumber_DeliveryReceiptRequested = 25,
+ GtalkDataMessageStanza_FieldNumber_ExternalMessageId = 26,
+ GtalkDataMessageStanza_FieldNumber_Flags = 27,
+ GtalkDataMessageStanza_FieldNumber_CellTower = 28,
+ GtalkDataMessageStanza_FieldNumber_Priority = 29,
+};
+
+@interface GtalkDataMessageStanza : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t rmqId;
+
+@property(nonatomic, readwrite) BOOL hasRmqId;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *from;
+/** Test to see if @c from has been set. */
+@property(nonatomic, readwrite) BOOL hasFrom;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *to;
+/** Test to see if @c to has been set. */
+@property(nonatomic, readwrite) BOOL hasTo;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *category;
+/** Test to see if @c category has been set. */
+@property(nonatomic, readwrite) BOOL hasCategory;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *token;
+/** Test to see if @c token has been set. */
+@property(nonatomic, readwrite) BOOL hasToken;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkAppData*> *appDataArray;
+/** The number of items in @c appDataArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger appDataArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL fromTrustedServer;
+
+@property(nonatomic, readwrite) BOOL hasFromTrustedServer;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *persistentId;
+/** Test to see if @c persistentId has been set. */
+@property(nonatomic, readwrite) BOOL hasPersistentId;
+
+
+@property(nonatomic, readwrite) int32_t streamId;
+
+@property(nonatomic, readwrite) BOOL hasStreamId;
+
+@property(nonatomic, readwrite) int32_t lastStreamIdReceived;
+
+@property(nonatomic, readwrite) BOOL hasLastStreamIdReceived;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *permission;
+/** Test to see if @c permission has been set. */
+@property(nonatomic, readwrite) BOOL hasPermission;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *regId;
+/** Test to see if @c regId has been set. */
+@property(nonatomic, readwrite) BOOL hasRegId;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *pkgSignature;
+/** Test to see if @c pkgSignature has been set. */
+@property(nonatomic, readwrite) BOOL hasPkgSignature;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *clientId;
+/** Test to see if @c clientId has been set. */
+@property(nonatomic, readwrite) BOOL hasClientId;
+
+
+@property(nonatomic, readwrite) int64_t deviceUserId;
+
+@property(nonatomic, readwrite) BOOL hasDeviceUserId;
+
+@property(nonatomic, readwrite) int32_t ttl;
+
+@property(nonatomic, readwrite) BOOL hasTtl;
+
+@property(nonatomic, readwrite) int64_t sent;
+
+@property(nonatomic, readwrite) BOOL hasSent;
+
+@property(nonatomic, readwrite) int32_t queued;
+
+@property(nonatomic, readwrite) BOOL hasQueued;
+
+@property(nonatomic, readwrite) int64_t status;
+
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSData *rawData;
+/** Test to see if @c rawData has been set. */
+@property(nonatomic, readwrite) BOOL hasRawData;
+
+
+@property(nonatomic, readwrite) int32_t maxDelay;
+
+@property(nonatomic, readwrite) BOOL hasMaxDelay;
+
+@property(nonatomic, readwrite) int32_t actualDelay;
+
+@property(nonatomic, readwrite) BOOL hasActualDelay;
+
+@property(nonatomic, readwrite) BOOL immediateAck;
+
+@property(nonatomic, readwrite) BOOL hasImmediateAck;
+
+@property(nonatomic, readwrite) BOOL deliveryReceiptRequested;
+
+@property(nonatomic, readwrite) BOOL hasDeliveryReceiptRequested;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *externalMessageId;
+/** Test to see if @c externalMessageId has been set. */
+@property(nonatomic, readwrite) BOOL hasExternalMessageId;
+
+
+@property(nonatomic, readwrite) int64_t flags;
+
+@property(nonatomic, readwrite) BOOL hasFlags;
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkCellTower *cellTower;
+/** Test to see if @c cellTower has been set. */
+@property(nonatomic, readwrite) BOOL hasCellTower;
+
+
+@property(nonatomic, readwrite) int32_t priority;
+
+@property(nonatomic, readwrite) BOOL hasPriority;
+@end
+
+#pragma mark - GtalkTalkMetadata
+
+typedef GPB_ENUM(GtalkTalkMetadata_FieldNumber) {
+ GtalkTalkMetadata_FieldNumber_Foreground = 1,
+};
+
+@interface GtalkTalkMetadata : GPBMessage
+
+
+@property(nonatomic, readwrite) BOOL foreground;
+
+@property(nonatomic, readwrite) BOOL hasForeground;
+@end
+
+#pragma mark - GtalkCellTower
+
+typedef GPB_ENUM(GtalkCellTower_FieldNumber) {
+ GtalkCellTower_FieldNumber_Id_p = 1,
+ GtalkCellTower_FieldNumber_KnownCongestionStatus = 2,
+};
+
+@interface GtalkCellTower : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *id_p;
+/** Test to see if @c id_p has been set. */
+@property(nonatomic, readwrite) BOOL hasId_p;
+
+
+@property(nonatomic, readwrite) int32_t knownCongestionStatus;
+
+@property(nonatomic, readwrite) BOOL hasKnownCongestionStatus;
+@end
+
+#pragma mark - GtalkClientEvent
+
+typedef GPB_ENUM(GtalkClientEvent_FieldNumber) {
+ GtalkClientEvent_FieldNumber_Type = 1,
+ GtalkClientEvent_FieldNumber_NumberDiscardedEvents = 100,
+ GtalkClientEvent_FieldNumber_NetworkType = 200,
+ GtalkClientEvent_FieldNumber_NetworkPort = 201,
+ GtalkClientEvent_FieldNumber_TimeConnectionStartedMs = 202,
+ GtalkClientEvent_FieldNumber_TimeConnectionEndedMs = 203,
+ GtalkClientEvent_FieldNumber_ErrorCode = 204,
+ GtalkClientEvent_FieldNumber_TimeConnectionEstablishedMs = 300,
+};
+
+@interface GtalkClientEvent : GPBMessage
+
+
+@property(nonatomic, readwrite) GtalkClientEvent_Type type;
+
+@property(nonatomic, readwrite) BOOL hasType;
+
+@property(nonatomic, readwrite) uint32_t numberDiscardedEvents;
+
+@property(nonatomic, readwrite) BOOL hasNumberDiscardedEvents;
+
+@property(nonatomic, readwrite) int32_t networkType;
+
+@property(nonatomic, readwrite) BOOL hasNetworkType;
+
+@property(nonatomic, readwrite) int32_t networkPort;
+
+@property(nonatomic, readwrite) BOOL hasNetworkPort;
+
+@property(nonatomic, readwrite) uint64_t timeConnectionStartedMs;
+
+@property(nonatomic, readwrite) BOOL hasTimeConnectionStartedMs;
+
+@property(nonatomic, readwrite) uint64_t timeConnectionEndedMs;
+
+@property(nonatomic, readwrite) BOOL hasTimeConnectionEndedMs;
+
+@property(nonatomic, readwrite) int32_t errorCode;
+
+@property(nonatomic, readwrite) BOOL hasErrorCode;
+
+@property(nonatomic, readwrite) uint64_t timeConnectionEstablishedMs;
+
+@property(nonatomic, readwrite) BOOL hasTimeConnectionEstablishedMs;
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firebase/Messaging/Protos/GtalkCore.pbobjc.m b/Firebase/Messaging/Protos/GtalkCore.pbobjc.m
new file mode 100644
index 0000000..f4efe22
--- /dev/null
+++ b/Firebase/Messaging/Protos/GtalkCore.pbobjc.m
@@ -0,0 +1,2947 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: buzz/mobile/proto/gtalk_core.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+ #import "GtalkCore.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+#pragma mark - GtalkGtalkCoreRoot
+
+@implementation GtalkGtalkCoreRoot
+
+// No extensions in the file and no imports, so no need to generate
+// +extensionRegistry.
+
+@end
+
+#pragma mark - GtalkGtalkCoreRoot_FileDescriptor
+
+static GPBFileDescriptor *GtalkGtalkCoreRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"mobilegtalk"
+ objcPrefix:@"Gtalk"
+ syntax:GPBFileSyntaxProto2];
+ }
+ return descriptor;
+}
+
+#pragma mark - GtalkHeartbeatPing
+
+@implementation GtalkHeartbeatPing
+
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasStatus, status;
+@dynamic hasCellTower, cellTower;
+@dynamic hasIntervalMs, intervalMs;
+
+typedef struct GtalkHeartbeatPing__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ int32_t intervalMs;
+ GtalkCellTower *cellTower;
+ int64_t status;
+} GtalkHeartbeatPing__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatPing_FieldNumber_StreamId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatPing__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatPing_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatPing__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatPing_FieldNumber_Status,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatPing__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "cellTower",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkCellTower),
+ .number = GtalkHeartbeatPing_FieldNumber_CellTower,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatPing__storage_, cellTower),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "intervalMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatPing_FieldNumber_IntervalMs,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatPing__storage_, intervalMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkHeartbeatPing class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkHeartbeatPing__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkHeartbeatAck
+
+@implementation GtalkHeartbeatAck
+
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasStatus, status;
+@dynamic hasCellTower, cellTower;
+@dynamic hasIntervalMs, intervalMs;
+
+typedef struct GtalkHeartbeatAck__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ int32_t intervalMs;
+ GtalkCellTower *cellTower;
+ int64_t status;
+} GtalkHeartbeatAck__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatAck_FieldNumber_StreamId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatAck__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatAck_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatAck__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatAck_FieldNumber_Status,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatAck__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "cellTower",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkCellTower),
+ .number = GtalkHeartbeatAck_FieldNumber_CellTower,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatAck__storage_, cellTower),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "intervalMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatAck_FieldNumber_IntervalMs,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatAck__storage_, intervalMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkHeartbeatAck class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkHeartbeatAck__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkErrorInfo
+
+@implementation GtalkErrorInfo
+
+@dynamic hasCode, code;
+@dynamic hasMessage, message;
+@dynamic hasType, type;
+@dynamic hasExtension, extension;
+
+typedef struct GtalkErrorInfo__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t code;
+ NSString *message;
+ NSString *type;
+ GtalkExtension *extension;
+} GtalkErrorInfo__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "code",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkErrorInfo_FieldNumber_Code,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkErrorInfo__storage_, code),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "message",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkErrorInfo_FieldNumber_Message,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkErrorInfo__storage_, message),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "type",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkErrorInfo_FieldNumber_Type,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkErrorInfo__storage_, type),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "extension",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkExtension),
+ .number = GtalkErrorInfo_FieldNumber_Extension,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkErrorInfo__storage_, extension),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkErrorInfo class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkErrorInfo__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkSetting
+
+@implementation GtalkSetting
+
+@dynamic hasName, name;
+@dynamic hasValue, value;
+
+typedef struct GtalkSetting__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *name;
+ NSString *value;
+} GtalkSetting__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "name",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSetting_FieldNumber_Name,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkSetting__storage_, name),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "value",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSetting_FieldNumber_Value,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkSetting__storage_, value),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkSetting class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkSetting__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkHeartbeatStat
+
+@implementation GtalkHeartbeatStat
+
+@dynamic hasIp, ip;
+@dynamic hasTimeout, timeout;
+@dynamic hasIntervalMs, intervalMs;
+
+typedef struct GtalkHeartbeatStat__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t intervalMs;
+ NSString *ip;
+} GtalkHeartbeatStat__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "ip",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatStat_FieldNumber_Ip,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatStat__storage_, ip),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "timeout",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatStat_FieldNumber_Timeout,
+ .hasIndex = 1,
+ .offset = 2, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "intervalMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatStat_FieldNumber_IntervalMs,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatStat__storage_, intervalMs),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkHeartbeatStat class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkHeartbeatStat__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkHeartbeatConfig
+
+@implementation GtalkHeartbeatConfig
+
+@dynamic hasUploadStat, uploadStat;
+@dynamic hasIp, ip;
+@dynamic hasIntervalMs, intervalMs;
+
+typedef struct GtalkHeartbeatConfig__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t intervalMs;
+ NSString *ip;
+} GtalkHeartbeatConfig__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "uploadStat",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatConfig_FieldNumber_UploadStat,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "ip",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatConfig_FieldNumber_Ip,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatConfig__storage_, ip),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "intervalMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkHeartbeatConfig_FieldNumber_IntervalMs,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkHeartbeatConfig__storage_, intervalMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkHeartbeatConfig class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkHeartbeatConfig__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkLoginRequest
+
+@implementation GtalkLoginRequest
+
+@dynamic hasId_p, id_p;
+@dynamic hasDomain, domain;
+@dynamic hasUser, user;
+@dynamic hasResource, resource;
+@dynamic hasAuthToken, authToken;
+@dynamic hasDeviceId, deviceId;
+@dynamic hasLastRmqId, lastRmqId;
+@dynamic settingArray, settingArray_Count;
+@dynamic hasCompress, compress;
+@dynamic receivedPersistentIdArray, receivedPersistentIdArray_Count;
+@dynamic hasIncludeStreamIds, includeStreamIds;
+@dynamic hasHeartbeatStat, heartbeatStat;
+@dynamic hasUseRmq2, useRmq2;
+@dynamic hasAccountId, accountId;
+@dynamic hasAuthService, authService;
+@dynamic hasNetworkType, networkType;
+@dynamic hasStatus, status;
+@dynamic hasTokenVersionInfo, tokenVersionInfo;
+@dynamic hasCellTower, cellTower;
+@dynamic hasGcmStartTimeMs, gcmStartTimeMs;
+@dynamic clientEventArray, clientEventArray_Count;
+@dynamic hasOnFallback, onFallback;
+
+typedef struct GtalkLoginRequest__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t compress;
+ GtalkLoginRequest_AuthService authService;
+ int32_t networkType;
+ NSString *id_p;
+ NSString *domain;
+ NSString *user;
+ NSString *resource;
+ NSString *authToken;
+ NSString *deviceId;
+ NSMutableArray *settingArray;
+ NSMutableArray *receivedPersistentIdArray;
+ GtalkHeartbeatStat *heartbeatStat;
+ NSString *tokenVersionInfo;
+ GtalkCellTower *cellTower;
+ NSMutableArray *clientEventArray;
+ int64_t lastRmqId;
+ int64_t accountId;
+ int64_t status;
+ uint64_t gcmStartTimeMs;
+} GtalkLoginRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "domain",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_Domain,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, domain),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "user",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_User,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, user),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "resource",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_Resource,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, resource),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "authToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_AuthToken,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, authToken),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "deviceId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_DeviceId,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, deviceId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "lastRmqId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_LastRmqId,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, lastRmqId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "settingArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkSetting),
+ .number = GtalkLoginRequest_FieldNumber_SettingArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, settingArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "compress",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_Compress,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, compress),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "receivedPersistentIdArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_ReceivedPersistentIdArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, receivedPersistentIdArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "includeStreamIds",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_IncludeStreamIds,
+ .hasIndex = 8,
+ .offset = 9, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "heartbeatStat",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkHeartbeatStat),
+ .number = GtalkLoginRequest_FieldNumber_HeartbeatStat,
+ .hasIndex = 10,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, heartbeatStat),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "useRmq2",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_UseRmq2,
+ .hasIndex = 11,
+ .offset = 12, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_AccountId,
+ .hasIndex = 13,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "authService",
+ .dataTypeSpecific.enumDescFunc = GtalkLoginRequest_AuthService_EnumDescriptor,
+ .number = GtalkLoginRequest_FieldNumber_AuthService,
+ .hasIndex = 14,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, authService),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "networkType",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_NetworkType,
+ .hasIndex = 15,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, networkType),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_Status,
+ .hasIndex = 16,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "tokenVersionInfo",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_TokenVersionInfo,
+ .hasIndex = 17,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, tokenVersionInfo),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "cellTower",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkCellTower),
+ .number = GtalkLoginRequest_FieldNumber_CellTower,
+ .hasIndex = 18,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, cellTower),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "gcmStartTimeMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_GcmStartTimeMs,
+ .hasIndex = 19,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, gcmStartTimeMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeUInt64,
+ },
+ {
+ .name = "clientEventArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkClientEvent),
+ .number = GtalkLoginRequest_FieldNumber_ClientEventArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkLoginRequest__storage_, clientEventArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "onFallback",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginRequest_FieldNumber_OnFallback,
+ .hasIndex = 20,
+ .offset = 21, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkLoginRequest class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkLoginRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkLoginRequest_AuthService
+
+GPBEnumDescriptor *GtalkLoginRequest_AuthService_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Mail\000AndroidCloudToDeviceMessage\000Android"
+ "Id\000";
+ static const int32_t values[] = {
+ GtalkLoginRequest_AuthService_Mail,
+ GtalkLoginRequest_AuthService_AndroidCloudToDeviceMessage,
+ GtalkLoginRequest_AuthService_AndroidId,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkLoginRequest_AuthService)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkLoginRequest_AuthService_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkLoginRequest_AuthService_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkLoginRequest_AuthService_Mail:
+ case GtalkLoginRequest_AuthService_AndroidCloudToDeviceMessage:
+ case GtalkLoginRequest_AuthService_AndroidId:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkLoginResponse
+
+@implementation GtalkLoginResponse
+
+@dynamic hasId_p, id_p;
+@dynamic hasJid, jid;
+@dynamic hasError, error;
+@dynamic settingArray, settingArray_Count;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasHeartbeatConfig, heartbeatConfig;
+@dynamic hasServerTimestamp, serverTimestamp;
+
+typedef struct GtalkLoginResponse__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ NSString *id_p;
+ NSString *jid;
+ GtalkErrorInfo *error;
+ NSMutableArray *settingArray;
+ GtalkHeartbeatConfig *heartbeatConfig;
+ int64_t serverTimestamp;
+} GtalkLoginResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginResponse_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "jid",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginResponse_FieldNumber_Jid,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, jid),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkLoginResponse_FieldNumber_Error,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "settingArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkSetting),
+ .number = GtalkLoginResponse_FieldNumber_SettingArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, settingArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginResponse_FieldNumber_StreamId,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginResponse_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "heartbeatConfig",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkHeartbeatConfig),
+ .number = GtalkLoginResponse_FieldNumber_HeartbeatConfig,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, heartbeatConfig),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "serverTimestamp",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkLoginResponse_FieldNumber_ServerTimestamp,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkLoginResponse__storage_, serverTimestamp),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkLoginResponse class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkLoginResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkBindAccountRequest
+
+@implementation GtalkBindAccountRequest
+
+@dynamic hasId_p, id_p;
+@dynamic hasDomain, domain;
+@dynamic hasUser, user;
+@dynamic hasResource, resource;
+@dynamic hasAuthToken, authToken;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasAccountId, accountId;
+
+typedef struct GtalkBindAccountRequest__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ NSString *id_p;
+ NSString *domain;
+ NSString *user;
+ NSString *resource;
+ NSString *authToken;
+ NSString *persistentId;
+ int64_t accountId;
+} GtalkBindAccountRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "domain",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_Domain,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, domain),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "user",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_User,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, user),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "resource",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_Resource,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, resource),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "authToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_AuthToken,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, authToken),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_PersistentId,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_StreamId,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountRequest_FieldNumber_AccountId,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkBindAccountRequest__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkBindAccountRequest class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkBindAccountRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkBindAccountResponse
+
+@implementation GtalkBindAccountResponse
+
+@dynamic hasId_p, id_p;
+@dynamic hasJid, jid;
+@dynamic hasError, error;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+
+typedef struct GtalkBindAccountResponse__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ NSString *id_p;
+ NSString *jid;
+ GtalkErrorInfo *error;
+} GtalkBindAccountResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountResponse_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkBindAccountResponse__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "jid",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountResponse_FieldNumber_Jid,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkBindAccountResponse__storage_, jid),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkBindAccountResponse_FieldNumber_Error,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkBindAccountResponse__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountResponse_FieldNumber_StreamId,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkBindAccountResponse__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBindAccountResponse_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkBindAccountResponse__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkBindAccountResponse class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkBindAccountResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkStreamErrorStanza
+
+@implementation GtalkStreamErrorStanza
+
+@dynamic hasType, type;
+@dynamic hasText, text;
+
+typedef struct GtalkStreamErrorStanza__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *type;
+ NSString *text;
+} GtalkStreamErrorStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "type",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkStreamErrorStanza_FieldNumber_Type,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkStreamErrorStanza__storage_, type),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "text",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkStreamErrorStanza_FieldNumber_Text,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkStreamErrorStanza__storage_, text),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkStreamErrorStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkStreamErrorStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkClose
+
+@implementation GtalkClose
+
+
+typedef struct GtalkClose__storage_ {
+ uint32_t _has_storage_[1];
+} GtalkClose__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkClose class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:NULL
+ fieldCount:0
+ storageSize:sizeof(GtalkClose__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkExtension
+
+@implementation GtalkExtension
+
+@dynamic hasId_p, id_p;
+@dynamic hasData_p, data_p;
+
+typedef struct GtalkExtension__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t id_p;
+ NSString *data_p;
+} GtalkExtension__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkExtension_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkExtension__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "data_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkExtension_FieldNumber_Data_p,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkExtension__storage_, data_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkExtension class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkExtension__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkMessageStanza
+
+@implementation GtalkMessageStanza
+
+@dynamic hasRmqId, rmqId;
+@dynamic hasType, type;
+@dynamic hasId_p, id_p;
+@dynamic hasFrom, from;
+@dynamic hasTo, to;
+@dynamic hasSubject, subject;
+@dynamic hasBody, body;
+@dynamic hasThread, thread;
+@dynamic hasError, error;
+@dynamic extensionArray, extensionArray_Count;
+@dynamic hasNosave, nosave;
+@dynamic hasTimestamp, timestamp;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasRead, read;
+@dynamic hasAccountId, accountId;
+
+typedef struct GtalkMessageStanza__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkMessageStanza_MessageType type;
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ NSString *id_p;
+ NSString *from;
+ NSString *to;
+ NSString *subject;
+ NSString *body;
+ NSString *thread;
+ GtalkErrorInfo *error;
+ NSMutableArray *extensionArray;
+ NSString *persistentId;
+ int64_t rmqId;
+ int64_t timestamp;
+ int64_t accountId;
+} GtalkMessageStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "rmqId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_RmqId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, rmqId),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldTextFormatNameCustom),
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "type",
+ .dataTypeSpecific.enumDescFunc = GtalkMessageStanza_MessageType_EnumDescriptor,
+ .number = GtalkMessageStanza_FieldNumber_Type,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, type),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Id_p,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, id_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "from",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_From,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, from),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "to",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_To,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, to),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "subject",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Subject,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, subject),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "body",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Body,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, body),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "thread",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Thread,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, thread),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkMessageStanza_FieldNumber_Error,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "extensionArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkExtension),
+ .number = GtalkMessageStanza_FieldNumber_ExtensionArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, extensionArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "nosave",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Nosave,
+ .hasIndex = 9,
+ .offset = 10, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "timestamp",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Timestamp,
+ .hasIndex = 11,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, timestamp),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_PersistentId,
+ .hasIndex = 12,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_StreamId,
+ .hasIndex = 13,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 14,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "read",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_Read,
+ .hasIndex = 15,
+ .offset = 16, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkMessageStanza_FieldNumber_AccountId,
+ .hasIndex = 17,
+ .offset = (uint32_t)offsetof(GtalkMessageStanza__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkMessageStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkMessageStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+#if !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ static const char *extraTextFormatInfo =
+ "\001\001\005\000";
+ [localDescriptor setupExtraTextInfo:extraTextFormatInfo];
+#endif // !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkMessageStanza_MessageType
+
+GPBEnumDescriptor *GtalkMessageStanza_MessageType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Normal\000Chat\000Groupchat\000Headline\000Error\000";
+ static const int32_t values[] = {
+ GtalkMessageStanza_MessageType_Normal,
+ GtalkMessageStanza_MessageType_Chat,
+ GtalkMessageStanza_MessageType_Groupchat,
+ GtalkMessageStanza_MessageType_Headline,
+ GtalkMessageStanza_MessageType_Error,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkMessageStanza_MessageType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkMessageStanza_MessageType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkMessageStanza_MessageType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkMessageStanza_MessageType_Normal:
+ case GtalkMessageStanza_MessageType_Chat:
+ case GtalkMessageStanza_MessageType_Groupchat:
+ case GtalkMessageStanza_MessageType_Headline:
+ case GtalkMessageStanza_MessageType_Error:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkPresenceStanza
+
+@implementation GtalkPresenceStanza
+
+@dynamic hasRmqId, rmqId;
+@dynamic hasType, type;
+@dynamic hasId_p, id_p;
+@dynamic hasFrom, from;
+@dynamic hasTo, to;
+@dynamic hasShow, show;
+@dynamic hasStatus, status;
+@dynamic hasPriority, priority;
+@dynamic hasError, error;
+@dynamic extensionArray, extensionArray_Count;
+@dynamic hasClient, client;
+@dynamic hasAvatarHash, avatarHash;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasCapabilitiesFlags, capabilitiesFlags;
+@dynamic hasAccountId, accountId;
+
+typedef struct GtalkPresenceStanza__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkPresenceStanza_PresenceType type;
+ GtalkPresenceStanza_ShowType show;
+ int32_t priority;
+ GtalkPresenceStanza_ClientType client;
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ int32_t capabilitiesFlags;
+ NSString *id_p;
+ NSString *from;
+ NSString *to;
+ NSString *status;
+ GtalkErrorInfo *error;
+ NSMutableArray *extensionArray;
+ NSString *avatarHash;
+ NSString *persistentId;
+ int64_t rmqId;
+ int64_t accountId;
+} GtalkPresenceStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "rmqId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_RmqId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, rmqId),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldTextFormatNameCustom),
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "type",
+ .dataTypeSpecific.enumDescFunc = GtalkPresenceStanza_PresenceType_EnumDescriptor,
+ .number = GtalkPresenceStanza_FieldNumber_Type,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, type),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_Id_p,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, id_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "from",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_From,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, from),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "to",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_To,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, to),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "show",
+ .dataTypeSpecific.enumDescFunc = GtalkPresenceStanza_ShowType_EnumDescriptor,
+ .number = GtalkPresenceStanza_FieldNumber_Show,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, show),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_Status,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "priority",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_Priority,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, priority),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkPresenceStanza_FieldNumber_Error,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "extensionArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkExtension),
+ .number = GtalkPresenceStanza_FieldNumber_ExtensionArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, extensionArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "client",
+ .dataTypeSpecific.enumDescFunc = GtalkPresenceStanza_ClientType_EnumDescriptor,
+ .number = GtalkPresenceStanza_FieldNumber_Client,
+ .hasIndex = 9,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, client),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "avatarHash",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_AvatarHash,
+ .hasIndex = 10,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, avatarHash),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_PersistentId,
+ .hasIndex = 11,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_StreamId,
+ .hasIndex = 12,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 13,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "capabilitiesFlags",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_CapabilitiesFlags,
+ .hasIndex = 14,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, capabilitiesFlags),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPresenceStanza_FieldNumber_AccountId,
+ .hasIndex = 15,
+ .offset = (uint32_t)offsetof(GtalkPresenceStanza__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkPresenceStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkPresenceStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+#if !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ static const char *extraTextFormatInfo =
+ "\001\001\005\000";
+ [localDescriptor setupExtraTextInfo:extraTextFormatInfo];
+#endif // !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkPresenceStanza_PresenceType
+
+GPBEnumDescriptor *GtalkPresenceStanza_PresenceType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Unavailable\000Subscribe\000Subscribed\000Unsubsc"
+ "ribe\000Unsubscribed\000Probe\000Error\000";
+ static const int32_t values[] = {
+ GtalkPresenceStanza_PresenceType_Unavailable,
+ GtalkPresenceStanza_PresenceType_Subscribe,
+ GtalkPresenceStanza_PresenceType_Subscribed,
+ GtalkPresenceStanza_PresenceType_Unsubscribe,
+ GtalkPresenceStanza_PresenceType_Unsubscribed,
+ GtalkPresenceStanza_PresenceType_Probe,
+ GtalkPresenceStanza_PresenceType_Error,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkPresenceStanza_PresenceType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkPresenceStanza_PresenceType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkPresenceStanza_PresenceType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkPresenceStanza_PresenceType_Unavailable:
+ case GtalkPresenceStanza_PresenceType_Subscribe:
+ case GtalkPresenceStanza_PresenceType_Subscribed:
+ case GtalkPresenceStanza_PresenceType_Unsubscribe:
+ case GtalkPresenceStanza_PresenceType_Unsubscribed:
+ case GtalkPresenceStanza_PresenceType_Probe:
+ case GtalkPresenceStanza_PresenceType_Error:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - Enum GtalkPresenceStanza_ShowType
+
+GPBEnumDescriptor *GtalkPresenceStanza_ShowType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Away\000Chat\000Dnd\000Xa\000";
+ static const int32_t values[] = {
+ GtalkPresenceStanza_ShowType_Away,
+ GtalkPresenceStanza_ShowType_Chat,
+ GtalkPresenceStanza_ShowType_Dnd,
+ GtalkPresenceStanza_ShowType_Xa,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkPresenceStanza_ShowType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkPresenceStanza_ShowType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkPresenceStanza_ShowType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkPresenceStanza_ShowType_Away:
+ case GtalkPresenceStanza_ShowType_Chat:
+ case GtalkPresenceStanza_ShowType_Dnd:
+ case GtalkPresenceStanza_ShowType_Xa:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - Enum GtalkPresenceStanza_ClientType
+
+GPBEnumDescriptor *GtalkPresenceStanza_ClientType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Mobile\000Android\000";
+ static const int32_t values[] = {
+ GtalkPresenceStanza_ClientType_Mobile,
+ GtalkPresenceStanza_ClientType_Android,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkPresenceStanza_ClientType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkPresenceStanza_ClientType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkPresenceStanza_ClientType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkPresenceStanza_ClientType_Mobile:
+ case GtalkPresenceStanza_ClientType_Android:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - Enum GtalkPresenceStanza_CapabilitiesFlags
+
+GPBEnumDescriptor *GtalkPresenceStanza_CapabilitiesFlags_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "HasVoiceV1\000HasVideoV1\000HasCameraV1\000HasPmu"
+ "cV1\000";
+ static const int32_t values[] = {
+ GtalkPresenceStanza_CapabilitiesFlags_HasVoiceV1,
+ GtalkPresenceStanza_CapabilitiesFlags_HasVideoV1,
+ GtalkPresenceStanza_CapabilitiesFlags_HasCameraV1,
+ GtalkPresenceStanza_CapabilitiesFlags_HasPmucV1,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkPresenceStanza_CapabilitiesFlags)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkPresenceStanza_CapabilitiesFlags_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkPresenceStanza_CapabilitiesFlags_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkPresenceStanza_CapabilitiesFlags_HasVoiceV1:
+ case GtalkPresenceStanza_CapabilitiesFlags_HasVideoV1:
+ case GtalkPresenceStanza_CapabilitiesFlags_HasCameraV1:
+ case GtalkPresenceStanza_CapabilitiesFlags_HasPmucV1:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkBatchPresenceStanza
+
+@implementation GtalkBatchPresenceStanza
+
+@dynamic hasId_p, id_p;
+@dynamic hasTo, to;
+@dynamic presenceArray, presenceArray_Count;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasAccountId, accountId;
+@dynamic hasType, type;
+@dynamic hasError, error;
+
+typedef struct GtalkBatchPresenceStanza__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ GtalkBatchPresenceStanza_Type type;
+ NSString *id_p;
+ NSString *to;
+ NSMutableArray *presenceArray;
+ NSString *persistentId;
+ GtalkErrorInfo *error;
+ int64_t accountId;
+} GtalkBatchPresenceStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, id_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "to",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_To,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, to),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "presenceArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkPresenceStanza),
+ .number = GtalkBatchPresenceStanza_FieldNumber_PresenceArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, presenceArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_PersistentId,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_StreamId,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkBatchPresenceStanza_FieldNumber_AccountId,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "type",
+ .dataTypeSpecific.enumDescFunc = GtalkBatchPresenceStanza_Type_EnumDescriptor,
+ .number = GtalkBatchPresenceStanza_FieldNumber_Type,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, type),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkBatchPresenceStanza_FieldNumber_Error,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkBatchPresenceStanza__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkBatchPresenceStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkBatchPresenceStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkBatchPresenceStanza_Type
+
+GPBEnumDescriptor *GtalkBatchPresenceStanza_Type_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Get\000Set\000";
+ static const int32_t values[] = {
+ GtalkBatchPresenceStanza_Type_Get,
+ GtalkBatchPresenceStanza_Type_Set,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkBatchPresenceStanza_Type)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkBatchPresenceStanza_Type_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkBatchPresenceStanza_Type_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkBatchPresenceStanza_Type_Get:
+ case GtalkBatchPresenceStanza_Type_Set:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkIqStanza
+
+@implementation GtalkIqStanza
+
+@dynamic hasRmqId, rmqId;
+@dynamic hasType, type;
+@dynamic hasId_p, id_p;
+@dynamic hasFrom, from;
+@dynamic hasTo, to;
+@dynamic hasError, error;
+@dynamic hasExtension, extension;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasAccountId, accountId;
+@dynamic hasStatus, status;
+
+typedef struct GtalkIqStanza__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkIqStanza_IqType type;
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ NSString *id_p;
+ NSString *from;
+ NSString *to;
+ GtalkErrorInfo *error;
+ GtalkExtension *extension;
+ NSString *persistentId;
+ int64_t rmqId;
+ int64_t accountId;
+ int64_t status;
+} GtalkIqStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "rmqId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_RmqId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, rmqId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "type",
+ .dataTypeSpecific.enumDescFunc = GtalkIqStanza_IqType_EnumDescriptor,
+ .number = GtalkIqStanza_FieldNumber_Type,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, type),
+ .flags = (GPBFieldFlags)(GPBFieldRequired | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_Id_p,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "from",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_From,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, from),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "to",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_To,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, to),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "error",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkErrorInfo),
+ .number = GtalkIqStanza_FieldNumber_Error,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, error),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "extension",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkExtension),
+ .number = GtalkIqStanza_FieldNumber_Extension,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, extension),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_PersistentId,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_StreamId,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 9,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "accountId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_AccountId,
+ .hasIndex = 10,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, accountId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIqStanza_FieldNumber_Status,
+ .hasIndex = 11,
+ .offset = (uint32_t)offsetof(GtalkIqStanza__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkIqStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkIqStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkIqStanza_IqType
+
+GPBEnumDescriptor *GtalkIqStanza_IqType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Get\000Set\000Result\000Error\000";
+ static const int32_t values[] = {
+ GtalkIqStanza_IqType_Get,
+ GtalkIqStanza_IqType_Set,
+ GtalkIqStanza_IqType_Result,
+ GtalkIqStanza_IqType_Error,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkIqStanza_IqType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkIqStanza_IqType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkIqStanza_IqType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkIqStanza_IqType_Get:
+ case GtalkIqStanza_IqType_Set:
+ case GtalkIqStanza_IqType_Result:
+ case GtalkIqStanza_IqType_Error:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkAppData
+
+@implementation GtalkAppData
+
+@dynamic hasKey, key;
+@dynamic hasValue, value;
+
+typedef struct GtalkAppData__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *key;
+ NSString *value;
+} GtalkAppData__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "key",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkAppData_FieldNumber_Key,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkAppData__storage_, key),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "value",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkAppData_FieldNumber_Value,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkAppData__storage_, value),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkAppData class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkAppData__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkDataMessageStanza
+
+@implementation GtalkDataMessageStanza
+
+@dynamic hasRmqId, rmqId;
+@dynamic hasId_p, id_p;
+@dynamic hasFrom, from;
+@dynamic hasTo, to;
+@dynamic hasCategory, category;
+@dynamic hasToken, token;
+@dynamic appDataArray, appDataArray_Count;
+@dynamic hasFromTrustedServer, fromTrustedServer;
+@dynamic hasPersistentId, persistentId;
+@dynamic hasStreamId, streamId;
+@dynamic hasLastStreamIdReceived, lastStreamIdReceived;
+@dynamic hasPermission, permission;
+@dynamic hasRegId, regId;
+@dynamic hasPkgSignature, pkgSignature;
+@dynamic hasClientId, clientId;
+@dynamic hasDeviceUserId, deviceUserId;
+@dynamic hasTtl, ttl;
+@dynamic hasSent, sent;
+@dynamic hasQueued, queued;
+@dynamic hasStatus, status;
+@dynamic hasRawData, rawData;
+@dynamic hasMaxDelay, maxDelay;
+@dynamic hasActualDelay, actualDelay;
+@dynamic hasImmediateAck, immediateAck;
+@dynamic hasDeliveryReceiptRequested, deliveryReceiptRequested;
+@dynamic hasExternalMessageId, externalMessageId;
+@dynamic hasFlags, flags;
+@dynamic hasCellTower, cellTower;
+@dynamic hasPriority, priority;
+
+typedef struct GtalkDataMessageStanza__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t streamId;
+ int32_t lastStreamIdReceived;
+ int32_t ttl;
+ int32_t queued;
+ int32_t maxDelay;
+ int32_t actualDelay;
+ int32_t priority;
+ NSString *id_p;
+ NSString *from;
+ NSString *to;
+ NSString *category;
+ NSString *token;
+ NSMutableArray *appDataArray;
+ NSString *persistentId;
+ NSString *permission;
+ NSString *regId;
+ NSString *pkgSignature;
+ NSString *clientId;
+ NSData *rawData;
+ NSString *externalMessageId;
+ GtalkCellTower *cellTower;
+ int64_t rmqId;
+ int64_t deviceUserId;
+ int64_t sent;
+ int64_t status;
+ int64_t flags;
+} GtalkDataMessageStanza__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "rmqId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_RmqId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, rmqId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Id_p,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, id_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "from",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_From,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, from),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "to",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_To,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, to),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "category",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Category,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, category),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "token",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Token,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, token),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "appDataArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkAppData),
+ .number = GtalkDataMessageStanza_FieldNumber_AppDataArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, appDataArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "fromTrustedServer",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_FromTrustedServer,
+ .hasIndex = 6,
+ .offset = 7, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "persistentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_PersistentId,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, persistentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_StreamId,
+ .hasIndex = 9,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamIdReceived",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_LastStreamIdReceived,
+ .hasIndex = 10,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, lastStreamIdReceived),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "permission",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Permission,
+ .hasIndex = 11,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, permission),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "regId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_RegId,
+ .hasIndex = 12,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, regId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "pkgSignature",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_PkgSignature,
+ .hasIndex = 13,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, pkgSignature),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "clientId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_ClientId,
+ .hasIndex = 14,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, clientId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "deviceUserId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_DeviceUserId,
+ .hasIndex = 15,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, deviceUserId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "ttl",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Ttl,
+ .hasIndex = 16,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, ttl),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "sent",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Sent,
+ .hasIndex = 17,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, sent),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "queued",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Queued,
+ .hasIndex = 18,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, queued),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Status,
+ .hasIndex = 19,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "rawData",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_RawData,
+ .hasIndex = 20,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, rawData),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "maxDelay",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_MaxDelay,
+ .hasIndex = 21,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, maxDelay),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "actualDelay",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_ActualDelay,
+ .hasIndex = 22,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, actualDelay),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "immediateAck",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_ImmediateAck,
+ .hasIndex = 23,
+ .offset = 24, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "deliveryReceiptRequested",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_DeliveryReceiptRequested,
+ .hasIndex = 25,
+ .offset = 26, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "externalMessageId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_ExternalMessageId,
+ .hasIndex = 27,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, externalMessageId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "flags",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Flags,
+ .hasIndex = 28,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, flags),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "cellTower",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkCellTower),
+ .number = GtalkDataMessageStanza_FieldNumber_CellTower,
+ .hasIndex = 29,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, cellTower),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "priority",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkDataMessageStanza_FieldNumber_Priority,
+ .hasIndex = 30,
+ .offset = (uint32_t)offsetof(GtalkDataMessageStanza__storage_, priority),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkDataMessageStanza class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkDataMessageStanza__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkTalkMetadata
+
+@implementation GtalkTalkMetadata
+
+@dynamic hasForeground, foreground;
+
+typedef struct GtalkTalkMetadata__storage_ {
+ uint32_t _has_storage_[1];
+} GtalkTalkMetadata__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "foreground",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkTalkMetadata_FieldNumber_Foreground,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkTalkMetadata class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkTalkMetadata__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkCellTower
+
+@implementation GtalkCellTower
+
+@dynamic hasId_p, id_p;
+@dynamic hasKnownCongestionStatus, knownCongestionStatus;
+
+typedef struct GtalkCellTower__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t knownCongestionStatus;
+ NSString *id_p;
+} GtalkCellTower__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCellTower_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkCellTower__storage_, id_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "knownCongestionStatus",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCellTower_FieldNumber_KnownCongestionStatus,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkCellTower__storage_, knownCongestionStatus),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkCellTower class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkCellTower__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkClientEvent
+
+@implementation GtalkClientEvent
+
+@dynamic hasType, type;
+@dynamic hasNumberDiscardedEvents, numberDiscardedEvents;
+@dynamic hasNetworkType, networkType;
+@dynamic hasNetworkPort, networkPort;
+@dynamic hasTimeConnectionStartedMs, timeConnectionStartedMs;
+@dynamic hasTimeConnectionEndedMs, timeConnectionEndedMs;
+@dynamic hasErrorCode, errorCode;
+@dynamic hasTimeConnectionEstablishedMs, timeConnectionEstablishedMs;
+
+typedef struct GtalkClientEvent__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkClientEvent_Type type;
+ uint32_t numberDiscardedEvents;
+ int32_t networkType;
+ int32_t networkPort;
+ int32_t errorCode;
+ uint64_t timeConnectionStartedMs;
+ uint64_t timeConnectionEndedMs;
+ uint64_t timeConnectionEstablishedMs;
+} GtalkClientEvent__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "type",
+ .dataTypeSpecific.enumDescFunc = GtalkClientEvent_Type_EnumDescriptor,
+ .number = GtalkClientEvent_FieldNumber_Type,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, type),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "numberDiscardedEvents",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_NumberDiscardedEvents,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, numberDiscardedEvents),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeUInt32,
+ },
+ {
+ .name = "networkType",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_NetworkType,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, networkType),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "networkPort",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_NetworkPort,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, networkPort),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "timeConnectionStartedMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_TimeConnectionStartedMs,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, timeConnectionStartedMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeUInt64,
+ },
+ {
+ .name = "timeConnectionEndedMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_TimeConnectionEndedMs,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, timeConnectionEndedMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeUInt64,
+ },
+ {
+ .name = "errorCode",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_ErrorCode,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, errorCode),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "timeConnectionEstablishedMs",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkClientEvent_FieldNumber_TimeConnectionEstablishedMs,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkClientEvent__storage_, timeConnectionEstablishedMs),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeUInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkClientEvent class]
+ rootClass:[GtalkGtalkCoreRoot class]
+ file:GtalkGtalkCoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkClientEvent__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkClientEvent_Type
+
+GPBEnumDescriptor *GtalkClientEvent_Type_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Unknown\000DiscardedEvents\000FailedConnection"
+ "\000SuccessfulConnection\000";
+ static const int32_t values[] = {
+ GtalkClientEvent_Type_Unknown,
+ GtalkClientEvent_Type_DiscardedEvents,
+ GtalkClientEvent_Type_FailedConnection,
+ GtalkClientEvent_Type_SuccessfulConnection,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkClientEvent_Type)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkClientEvent_Type_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkClientEvent_Type_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkClientEvent_Type_Unknown:
+ case GtalkClientEvent_Type_DiscardedEvents:
+ case GtalkClientEvent_Type_FailedConnection:
+ case GtalkClientEvent_Type_SuccessfulConnection:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.h b/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.h
new file mode 100644
index 0000000..f461884
--- /dev/null
+++ b/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.h
@@ -0,0 +1,617 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: buzz/mobile/proto/gtalk_extensions.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GtalkOtrItem;
+@class GtalkPhoto;
+@class GtalkRosterItem;
+@class GtalkSharedStatus_StatusList;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - Enum GtalkRosterItem_SubscriptionType
+
+typedef GPB_ENUM(GtalkRosterItem_SubscriptionType) {
+ GtalkRosterItem_SubscriptionType_None = 0,
+ GtalkRosterItem_SubscriptionType_To = 1,
+ GtalkRosterItem_SubscriptionType_From = 2,
+ GtalkRosterItem_SubscriptionType_Both = 3,
+ GtalkRosterItem_SubscriptionType_Remove = 4,
+};
+
+GPBEnumDescriptor *GtalkRosterItem_SubscriptionType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkRosterItem_SubscriptionType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkRosterItem_AskType
+
+typedef GPB_ENUM(GtalkRosterItem_AskType) {
+ GtalkRosterItem_AskType_Subscribe = 0,
+};
+
+GPBEnumDescriptor *GtalkRosterItem_AskType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkRosterItem_AskType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkRosterItem_DisplayType
+
+typedef GPB_ENUM(GtalkRosterItem_DisplayType) {
+ GtalkRosterItem_DisplayType_Blocked = 0,
+ GtalkRosterItem_DisplayType_Hidden = 1,
+ GtalkRosterItem_DisplayType_Pinned = 2,
+};
+
+GPBEnumDescriptor *GtalkRosterItem_DisplayType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkRosterItem_DisplayType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkSharedStatus_ShowType
+
+typedef GPB_ENUM(GtalkSharedStatus_ShowType) {
+ GtalkSharedStatus_ShowType_Default = 0,
+ GtalkSharedStatus_ShowType_Dnd = 1,
+};
+
+GPBEnumDescriptor *GtalkSharedStatus_ShowType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkSharedStatus_ShowType_IsValidValue(int32_t value);
+
+#pragma mark - Enum GtalkPostAuthBatchQuery_CapabilitiesExtFlags
+
+typedef GPB_ENUM(GtalkPostAuthBatchQuery_CapabilitiesExtFlags) {
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVoiceV1 = 1,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVideoV1 = 2,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasCameraV1 = 4,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasPmucV1 = 8,
+};
+
+GPBEnumDescriptor *GtalkPostAuthBatchQuery_CapabilitiesExtFlags_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GtalkPostAuthBatchQuery_CapabilitiesExtFlags_IsValidValue(int32_t value);
+
+#pragma mark - GtalkGtalkExtensionsRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GtalkGtalkExtensionsRoot : GPBRootObject
+@end
+
+#pragma mark - GtalkRosterQuery
+
+typedef GPB_ENUM(GtalkRosterQuery_FieldNumber) {
+ GtalkRosterQuery_FieldNumber_Etag = 1,
+ GtalkRosterQuery_FieldNumber_NotModified = 2,
+ GtalkRosterQuery_FieldNumber_ItemArray = 3,
+ GtalkRosterQuery_FieldNumber_AvatarWidth = 4,
+ GtalkRosterQuery_FieldNumber_AvatarHeight = 5,
+};
+
+@interface GtalkRosterQuery : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *etag;
+/** Test to see if @c etag has been set. */
+@property(nonatomic, readwrite) BOOL hasEtag;
+
+
+@property(nonatomic, readwrite) BOOL notModified;
+
+@property(nonatomic, readwrite) BOOL hasNotModified;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkRosterItem*> *itemArray;
+/** The number of items in @c itemArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger itemArray_Count;
+
+
+@property(nonatomic, readwrite) int32_t avatarWidth;
+
+@property(nonatomic, readwrite) BOOL hasAvatarWidth;
+
+@property(nonatomic, readwrite) int32_t avatarHeight;
+
+@property(nonatomic, readwrite) BOOL hasAvatarHeight;
+@end
+
+#pragma mark - GtalkRosterItem
+
+typedef GPB_ENUM(GtalkRosterItem_FieldNumber) {
+ GtalkRosterItem_FieldNumber_Jid = 1,
+ GtalkRosterItem_FieldNumber_Name = 2,
+ GtalkRosterItem_FieldNumber_Subscription = 3,
+ GtalkRosterItem_FieldNumber_Ask = 4,
+ GtalkRosterItem_FieldNumber_GroupArray = 5,
+ GtalkRosterItem_FieldNumber_QuickContact = 6,
+ GtalkRosterItem_FieldNumber_Display = 7,
+ GtalkRosterItem_FieldNumber_Rejected = 8,
+};
+
+@interface GtalkRosterItem : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *jid;
+/** Test to see if @c jid has been set. */
+@property(nonatomic, readwrite) BOOL hasJid;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *name;
+/** Test to see if @c name has been set. */
+@property(nonatomic, readwrite) BOOL hasName;
+
+
+@property(nonatomic, readwrite) GtalkRosterItem_SubscriptionType subscription;
+
+@property(nonatomic, readwrite) BOOL hasSubscription;
+
+@property(nonatomic, readwrite) GtalkRosterItem_AskType ask;
+
+@property(nonatomic, readwrite) BOOL hasAsk;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *groupArray;
+/** The number of items in @c groupArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger groupArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL quickContact;
+
+@property(nonatomic, readwrite) BOOL hasQuickContact;
+
+@property(nonatomic, readwrite) GtalkRosterItem_DisplayType display;
+
+@property(nonatomic, readwrite) BOOL hasDisplay;
+
+@property(nonatomic, readwrite) BOOL rejected;
+
+@property(nonatomic, readwrite) BOOL hasRejected;
+@end
+
+#pragma mark - GtalkRmqLastId
+
+typedef GPB_ENUM(GtalkRmqLastId_FieldNumber) {
+ GtalkRmqLastId_FieldNumber_Id_p = 1,
+};
+
+@interface GtalkRmqLastId : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t id_p;
+
+@property(nonatomic, readwrite) BOOL hasId_p;
+@end
+
+#pragma mark - GtalkRmqAck
+
+typedef GPB_ENUM(GtalkRmqAck_FieldNumber) {
+ GtalkRmqAck_FieldNumber_Id_p = 1,
+};
+
+@interface GtalkRmqAck : GPBMessage
+
+
+@property(nonatomic, readwrite) int64_t id_p;
+
+@property(nonatomic, readwrite) BOOL hasId_p;
+@end
+
+#pragma mark - GtalkVCard
+
+typedef GPB_ENUM(GtalkVCard_FieldNumber) {
+ GtalkVCard_FieldNumber_Version = 1,
+ GtalkVCard_FieldNumber_FullName = 2,
+ GtalkVCard_FieldNumber_Photo = 3,
+ GtalkVCard_FieldNumber_AvatarHash = 4,
+ GtalkVCard_FieldNumber_Modified = 5,
+};
+
+@interface GtalkVCard : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *version;
+/** Test to see if @c version has been set. */
+@property(nonatomic, readwrite) BOOL hasVersion;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *fullName;
+/** Test to see if @c fullName has been set. */
+@property(nonatomic, readwrite) BOOL hasFullName;
+
+
+@property(nonatomic, readwrite, strong, null_resettable) GtalkPhoto *photo;
+/** Test to see if @c photo has been set. */
+@property(nonatomic, readwrite) BOOL hasPhoto;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *avatarHash;
+/** Test to see if @c avatarHash has been set. */
+@property(nonatomic, readwrite) BOOL hasAvatarHash;
+
+
+@property(nonatomic, readwrite) BOOL modified;
+
+@property(nonatomic, readwrite) BOOL hasModified;
+@end
+
+#pragma mark - GtalkPhoto
+
+typedef GPB_ENUM(GtalkPhoto_FieldNumber) {
+ GtalkPhoto_FieldNumber_Type = 1,
+ GtalkPhoto_FieldNumber_Data_p = 2,
+};
+
+@interface GtalkPhoto : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *type;
+/** Test to see if @c type has been set. */
+@property(nonatomic, readwrite) BOOL hasType;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *data_p;
+/** Test to see if @c data_p has been set. */
+@property(nonatomic, readwrite) BOOL hasData_p;
+
+@end
+
+#pragma mark - GtalkChatRead
+
+typedef GPB_ENUM(GtalkChatRead_FieldNumber) {
+ GtalkChatRead_FieldNumber_User = 1,
+};
+
+@interface GtalkChatRead : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *user;
+/** Test to see if @c user has been set. */
+@property(nonatomic, readwrite) BOOL hasUser;
+
+@end
+
+#pragma mark - GtalkChatClosed
+
+typedef GPB_ENUM(GtalkChatClosed_FieldNumber) {
+ GtalkChatClosed_FieldNumber_User = 1,
+};
+
+@interface GtalkChatClosed : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *user;
+/** Test to see if @c user has been set. */
+@property(nonatomic, readwrite) BOOL hasUser;
+
+@end
+
+#pragma mark - GtalkCapabilities
+
+typedef GPB_ENUM(GtalkCapabilities_FieldNumber) {
+ GtalkCapabilities_FieldNumber_Node = 1,
+ GtalkCapabilities_FieldNumber_Ver = 2,
+ GtalkCapabilities_FieldNumber_Ext = 3,
+ GtalkCapabilities_FieldNumber_Hash_p = 4,
+};
+
+@interface GtalkCapabilities : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *node;
+/** Test to see if @c node has been set. */
+@property(nonatomic, readwrite) BOOL hasNode;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *ver;
+/** Test to see if @c ver has been set. */
+@property(nonatomic, readwrite) BOOL hasVer;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *ext;
+/** Test to see if @c ext has been set. */
+@property(nonatomic, readwrite) BOOL hasExt;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *hash_p;
+/** Test to see if @c hash_p has been set. */
+@property(nonatomic, readwrite) BOOL hasHash_p;
+
+@end
+
+#pragma mark - GtalkSharedStatus
+
+typedef GPB_ENUM(GtalkSharedStatus_FieldNumber) {
+ GtalkSharedStatus_FieldNumber_StatusMax = 1,
+ GtalkSharedStatus_FieldNumber_StatusListMax = 2,
+ GtalkSharedStatus_FieldNumber_StatusListContentsMax = 3,
+ GtalkSharedStatus_FieldNumber_Status = 4,
+ GtalkSharedStatus_FieldNumber_Show = 5,
+ GtalkSharedStatus_FieldNumber_StatusListArray = 6,
+ GtalkSharedStatus_FieldNumber_Invisible = 9,
+ GtalkSharedStatus_FieldNumber_StatusMinVersion = 10,
+};
+
+@interface GtalkSharedStatus : GPBMessage
+
+
+@property(nonatomic, readwrite) int32_t statusMax;
+
+@property(nonatomic, readwrite) BOOL hasStatusMax;
+
+@property(nonatomic, readwrite) int32_t statusListMax;
+
+@property(nonatomic, readwrite) BOOL hasStatusListMax;
+
+@property(nonatomic, readwrite) int32_t statusListContentsMax;
+
+@property(nonatomic, readwrite) BOOL hasStatusListContentsMax;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *status;
+/** Test to see if @c status has been set. */
+@property(nonatomic, readwrite) BOOL hasStatus;
+
+
+@property(nonatomic, readwrite) GtalkSharedStatus_ShowType show;
+
+@property(nonatomic, readwrite) BOOL hasShow;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkSharedStatus_StatusList*> *statusListArray;
+/** The number of items in @c statusListArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger statusListArray_Count;
+
+
+@property(nonatomic, readwrite) BOOL invisible;
+
+@property(nonatomic, readwrite) BOOL hasInvisible;
+
+@property(nonatomic, readwrite) int32_t statusMinVersion;
+
+@property(nonatomic, readwrite) BOOL hasStatusMinVersion;
+@end
+
+#pragma mark - GtalkSharedStatus_StatusList
+
+typedef GPB_ENUM(GtalkSharedStatus_StatusList_FieldNumber) {
+ GtalkSharedStatus_StatusList_FieldNumber_Show = 7,
+ GtalkSharedStatus_StatusList_FieldNumber_StatusArray = 8,
+};
+
+@interface GtalkSharedStatus_StatusList : GPBMessage
+
+
+@property(nonatomic, readwrite) GtalkSharedStatus_ShowType show;
+
+@property(nonatomic, readwrite) BOOL hasShow;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *statusArray;
+/** The number of items in @c statusArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger statusArray_Count;
+
+@end
+
+#pragma mark - GtalkOtrQuery
+
+typedef GPB_ENUM(GtalkOtrQuery_FieldNumber) {
+ GtalkOtrQuery_FieldNumber_NosaveDefault = 1,
+ GtalkOtrQuery_FieldNumber_ItemArray = 2,
+ GtalkOtrQuery_FieldNumber_Etag = 3,
+ GtalkOtrQuery_FieldNumber_NotModified = 4,
+};
+
+@interface GtalkOtrQuery : GPBMessage
+
+
+@property(nonatomic, readwrite) BOOL nosaveDefault;
+
+@property(nonatomic, readwrite) BOOL hasNosaveDefault;
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkOtrItem*> *itemArray;
+/** The number of items in @c itemArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger itemArray_Count;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *etag;
+/** Test to see if @c etag has been set. */
+@property(nonatomic, readwrite) BOOL hasEtag;
+
+
+@property(nonatomic, readwrite) BOOL notModified;
+
+@property(nonatomic, readwrite) BOOL hasNotModified;
+@end
+
+#pragma mark - GtalkOtrItem
+
+typedef GPB_ENUM(GtalkOtrItem_FieldNumber) {
+ GtalkOtrItem_FieldNumber_Jid = 1,
+ GtalkOtrItem_FieldNumber_Nosave = 2,
+ GtalkOtrItem_FieldNumber_ChangedByBuddy = 3,
+};
+
+@interface GtalkOtrItem : GPBMessage
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *jid;
+/** Test to see if @c jid has been set. */
+@property(nonatomic, readwrite) BOOL hasJid;
+
+
+@property(nonatomic, readwrite) BOOL nosave;
+
+@property(nonatomic, readwrite) BOOL hasNosave;
+
+@property(nonatomic, readwrite) BOOL changedByBuddy;
+
+@property(nonatomic, readwrite) BOOL hasChangedByBuddy;
+@end
+
+#pragma mark - GtalkIdle
+
+typedef GPB_ENUM(GtalkIdle_FieldNumber) {
+ GtalkIdle_FieldNumber_Idle = 1,
+ GtalkIdle_FieldNumber_Away = 2,
+};
+
+@interface GtalkIdle : GPBMessage
+
+
+@property(nonatomic, readwrite) BOOL idle;
+
+@property(nonatomic, readwrite) BOOL hasIdle;
+
+@property(nonatomic, readwrite) BOOL away;
+
+@property(nonatomic, readwrite) BOOL hasAway;
+@end
+
+#pragma mark - GtalkPostAuthBatchQuery
+
+typedef GPB_ENUM(GtalkPostAuthBatchQuery_FieldNumber) {
+ GtalkPostAuthBatchQuery_FieldNumber_Available = 1,
+ GtalkPostAuthBatchQuery_FieldNumber_DeviceIdle = 2,
+ GtalkPostAuthBatchQuery_FieldNumber_MobileIndicator = 3,
+ GtalkPostAuthBatchQuery_FieldNumber_SharedStatusVersion = 4,
+ GtalkPostAuthBatchQuery_FieldNumber_RosterEtag = 5,
+ GtalkPostAuthBatchQuery_FieldNumber_OtrEtag = 6,
+ GtalkPostAuthBatchQuery_FieldNumber_AvatarHash = 7,
+ GtalkPostAuthBatchQuery_FieldNumber_VcardQueryStanzaId = 8,
+ GtalkPostAuthBatchQuery_FieldNumber_CapabilitiesExtFlags = 9,
+};
+
+@interface GtalkPostAuthBatchQuery : GPBMessage
+
+
+@property(nonatomic, readwrite) BOOL available;
+
+@property(nonatomic, readwrite) BOOL hasAvailable;
+
+@property(nonatomic, readwrite) BOOL deviceIdle;
+
+@property(nonatomic, readwrite) BOOL hasDeviceIdle;
+
+@property(nonatomic, readwrite) BOOL mobileIndicator;
+
+@property(nonatomic, readwrite) BOOL hasMobileIndicator;
+
+@property(nonatomic, readwrite) int32_t sharedStatusVersion;
+
+@property(nonatomic, readwrite) BOOL hasSharedStatusVersion;
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *rosterEtag;
+/** Test to see if @c rosterEtag has been set. */
+@property(nonatomic, readwrite) BOOL hasRosterEtag;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *otrEtag;
+/** Test to see if @c otrEtag has been set. */
+@property(nonatomic, readwrite) BOOL hasOtrEtag;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *avatarHash;
+/** Test to see if @c avatarHash has been set. */
+@property(nonatomic, readwrite) BOOL hasAvatarHash;
+
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *vcardQueryStanzaId;
+/** Test to see if @c vcardQueryStanzaId has been set. */
+@property(nonatomic, readwrite) BOOL hasVcardQueryStanzaId;
+
+
+@property(nonatomic, readwrite) int32_t capabilitiesExtFlags;
+
+@property(nonatomic, readwrite) BOOL hasCapabilitiesExtFlags;
+@end
+
+#pragma mark - GtalkStreamAck
+
+@interface GtalkStreamAck : GPBMessage
+
+@end
+
+#pragma mark - GtalkSelectiveAck
+
+typedef GPB_ENUM(GtalkSelectiveAck_FieldNumber) {
+ GtalkSelectiveAck_FieldNumber_IdArray = 1,
+};
+
+@interface GtalkSelectiveAck : GPBMessage
+
+
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *idArray;
+/** The number of items in @c idArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger idArray_Count;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.m b/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.m
new file mode 100644
index 0000000..e41d416
--- /dev/null
+++ b/Firebase/Messaging/Protos/GtalkExtensions.pbobjc.m
@@ -0,0 +1,1407 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: buzz/mobile/proto/gtalk_extensions.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+ #import "GtalkExtensions.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+#pragma mark - GtalkGtalkExtensionsRoot
+
+@implementation GtalkGtalkExtensionsRoot
+
+// No extensions in the file and no imports, so no need to generate
+// +extensionRegistry.
+
+@end
+
+#pragma mark - GtalkGtalkExtensionsRoot_FileDescriptor
+
+static GPBFileDescriptor *GtalkGtalkExtensionsRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"mobilegtalk"
+ objcPrefix:@"Gtalk"
+ syntax:GPBFileSyntaxProto2];
+ }
+ return descriptor;
+}
+
+#pragma mark - GtalkRosterQuery
+
+@implementation GtalkRosterQuery
+
+@dynamic hasEtag, etag;
+@dynamic hasNotModified, notModified;
+@dynamic itemArray, itemArray_Count;
+@dynamic hasAvatarWidth, avatarWidth;
+@dynamic hasAvatarHeight, avatarHeight;
+
+typedef struct GtalkRosterQuery__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t avatarWidth;
+ int32_t avatarHeight;
+ NSString *etag;
+ NSMutableArray *itemArray;
+} GtalkRosterQuery__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "etag",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterQuery_FieldNumber_Etag,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkRosterQuery__storage_, etag),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "notModified",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterQuery_FieldNumber_NotModified,
+ .hasIndex = 1,
+ .offset = 2, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "itemArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkRosterItem),
+ .number = GtalkRosterQuery_FieldNumber_ItemArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkRosterQuery__storage_, itemArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "avatarWidth",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterQuery_FieldNumber_AvatarWidth,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkRosterQuery__storage_, avatarWidth),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldTextFormatNameCustom),
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "avatarHeight",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterQuery_FieldNumber_AvatarHeight,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkRosterQuery__storage_, avatarHeight),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldTextFormatNameCustom),
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkRosterQuery class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkRosterQuery__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+#if !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ static const char *extraTextFormatInfo =
+ "\002\004\013\000\005\014\000";
+ [localDescriptor setupExtraTextInfo:extraTextFormatInfo];
+#endif // !GPBOBJC_SKIP_MESSAGE_TEXTFORMAT_EXTRAS
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkRosterItem
+
+@implementation GtalkRosterItem
+
+@dynamic hasJid, jid;
+@dynamic hasName, name;
+@dynamic hasSubscription, subscription;
+@dynamic hasAsk, ask;
+@dynamic groupArray, groupArray_Count;
+@dynamic hasQuickContact, quickContact;
+@dynamic hasDisplay, display;
+@dynamic hasRejected, rejected;
+
+typedef struct GtalkRosterItem__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkRosterItem_SubscriptionType subscription;
+ GtalkRosterItem_AskType ask;
+ GtalkRosterItem_DisplayType display;
+ NSString *jid;
+ NSString *name;
+ NSMutableArray *groupArray;
+} GtalkRosterItem__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "jid",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterItem_FieldNumber_Jid,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, jid),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "name",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterItem_FieldNumber_Name,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, name),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "subscription",
+ .dataTypeSpecific.enumDescFunc = GtalkRosterItem_SubscriptionType_EnumDescriptor,
+ .number = GtalkRosterItem_FieldNumber_Subscription,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, subscription),
+ .flags = (GPBFieldFlags)(GPBFieldRequired | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "ask",
+ .dataTypeSpecific.enumDescFunc = GtalkRosterItem_AskType_EnumDescriptor,
+ .number = GtalkRosterItem_FieldNumber_Ask,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, ask),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "groupArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterItem_FieldNumber_GroupArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, groupArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "quickContact",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterItem_FieldNumber_QuickContact,
+ .hasIndex = 4,
+ .offset = 5, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "display",
+ .dataTypeSpecific.enumDescFunc = GtalkRosterItem_DisplayType_EnumDescriptor,
+ .number = GtalkRosterItem_FieldNumber_Display,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkRosterItem__storage_, display),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "rejected",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRosterItem_FieldNumber_Rejected,
+ .hasIndex = 7,
+ .offset = 8, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkRosterItem class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkRosterItem__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkRosterItem_SubscriptionType
+
+GPBEnumDescriptor *GtalkRosterItem_SubscriptionType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "None\000To\000From\000Both\000Remove\000";
+ static const int32_t values[] = {
+ GtalkRosterItem_SubscriptionType_None,
+ GtalkRosterItem_SubscriptionType_To,
+ GtalkRosterItem_SubscriptionType_From,
+ GtalkRosterItem_SubscriptionType_Both,
+ GtalkRosterItem_SubscriptionType_Remove,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkRosterItem_SubscriptionType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkRosterItem_SubscriptionType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkRosterItem_SubscriptionType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkRosterItem_SubscriptionType_None:
+ case GtalkRosterItem_SubscriptionType_To:
+ case GtalkRosterItem_SubscriptionType_From:
+ case GtalkRosterItem_SubscriptionType_Both:
+ case GtalkRosterItem_SubscriptionType_Remove:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - Enum GtalkRosterItem_AskType
+
+GPBEnumDescriptor *GtalkRosterItem_AskType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Subscribe\000";
+ static const int32_t values[] = {
+ GtalkRosterItem_AskType_Subscribe,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkRosterItem_AskType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkRosterItem_AskType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkRosterItem_AskType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkRosterItem_AskType_Subscribe:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - Enum GtalkRosterItem_DisplayType
+
+GPBEnumDescriptor *GtalkRosterItem_DisplayType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Blocked\000Hidden\000Pinned\000";
+ static const int32_t values[] = {
+ GtalkRosterItem_DisplayType_Blocked,
+ GtalkRosterItem_DisplayType_Hidden,
+ GtalkRosterItem_DisplayType_Pinned,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkRosterItem_DisplayType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkRosterItem_DisplayType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkRosterItem_DisplayType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkRosterItem_DisplayType_Blocked:
+ case GtalkRosterItem_DisplayType_Hidden:
+ case GtalkRosterItem_DisplayType_Pinned:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkRmqLastId
+
+@implementation GtalkRmqLastId
+
+@dynamic hasId_p, id_p;
+
+typedef struct GtalkRmqLastId__storage_ {
+ uint32_t _has_storage_[1];
+ int64_t id_p;
+} GtalkRmqLastId__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRmqLastId_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkRmqLastId__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkRmqLastId class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkRmqLastId__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkRmqAck
+
+@implementation GtalkRmqAck
+
+@dynamic hasId_p, id_p;
+
+typedef struct GtalkRmqAck__storage_ {
+ uint32_t _has_storage_[1];
+ int64_t id_p;
+} GtalkRmqAck__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "id_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkRmqAck_FieldNumber_Id_p,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkRmqAck__storage_, id_p),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeInt64,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkRmqAck class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkRmqAck__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkVCard
+
+@implementation GtalkVCard
+
+@dynamic hasVersion, version;
+@dynamic hasFullName, fullName;
+@dynamic hasPhoto, photo;
+@dynamic hasAvatarHash, avatarHash;
+@dynamic hasModified, modified;
+
+typedef struct GtalkVCard__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *version;
+ NSString *fullName;
+ GtalkPhoto *photo;
+ NSString *avatarHash;
+} GtalkVCard__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "version",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkVCard_FieldNumber_Version,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkVCard__storage_, version),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "fullName",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkVCard_FieldNumber_FullName,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkVCard__storage_, fullName),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "photo",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkPhoto),
+ .number = GtalkVCard_FieldNumber_Photo,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkVCard__storage_, photo),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "avatarHash",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkVCard_FieldNumber_AvatarHash,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkVCard__storage_, avatarHash),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "modified",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkVCard_FieldNumber_Modified,
+ .hasIndex = 4,
+ .offset = 5, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkVCard class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkVCard__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkPhoto
+
+@implementation GtalkPhoto
+
+@dynamic hasType, type;
+@dynamic hasData_p, data_p;
+
+typedef struct GtalkPhoto__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *type;
+ NSString *data_p;
+} GtalkPhoto__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "type",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPhoto_FieldNumber_Type,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkPhoto__storage_, type),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "data_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPhoto_FieldNumber_Data_p,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkPhoto__storage_, data_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkPhoto class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkPhoto__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkChatRead
+
+@implementation GtalkChatRead
+
+@dynamic hasUser, user;
+
+typedef struct GtalkChatRead__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *user;
+} GtalkChatRead__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "user",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkChatRead_FieldNumber_User,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkChatRead__storage_, user),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkChatRead class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkChatRead__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkChatClosed
+
+@implementation GtalkChatClosed
+
+@dynamic hasUser, user;
+
+typedef struct GtalkChatClosed__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *user;
+} GtalkChatClosed__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "user",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkChatClosed_FieldNumber_User,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkChatClosed__storage_, user),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkChatClosed class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkChatClosed__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkCapabilities
+
+@implementation GtalkCapabilities
+
+@dynamic hasNode, node;
+@dynamic hasVer, ver;
+@dynamic hasExt, ext;
+@dynamic hasHash_p, hash_p;
+
+typedef struct GtalkCapabilities__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *node;
+ NSString *ver;
+ NSString *ext;
+ NSString *hash_p;
+} GtalkCapabilities__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "node",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCapabilities_FieldNumber_Node,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkCapabilities__storage_, node),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "ver",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCapabilities_FieldNumber_Ver,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkCapabilities__storage_, ver),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "ext",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCapabilities_FieldNumber_Ext,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkCapabilities__storage_, ext),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "hash_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkCapabilities_FieldNumber_Hash_p,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkCapabilities__storage_, hash_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkCapabilities class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkCapabilities__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkSharedStatus
+
+@implementation GtalkSharedStatus
+
+@dynamic hasStatusMax, statusMax;
+@dynamic hasStatusListMax, statusListMax;
+@dynamic hasStatusListContentsMax, statusListContentsMax;
+@dynamic hasStatus, status;
+@dynamic hasShow, show;
+@dynamic statusListArray, statusListArray_Count;
+@dynamic hasInvisible, invisible;
+@dynamic hasStatusMinVersion, statusMinVersion;
+
+typedef struct GtalkSharedStatus__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t statusMax;
+ int32_t statusListMax;
+ int32_t statusListContentsMax;
+ GtalkSharedStatus_ShowType show;
+ int32_t statusMinVersion;
+ NSString *status;
+ NSMutableArray *statusListArray;
+} GtalkSharedStatus__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "statusMax",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_StatusMax,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, statusMax),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "statusListMax",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_StatusListMax,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, statusListMax),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "statusListContentsMax",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_StatusListContentsMax,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, statusListContentsMax),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "status",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_Status,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, status),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "show",
+ .dataTypeSpecific.enumDescFunc = GtalkSharedStatus_ShowType_EnumDescriptor,
+ .number = GtalkSharedStatus_FieldNumber_Show,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, show),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "statusListArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkSharedStatus_StatusList),
+ .number = GtalkSharedStatus_FieldNumber_StatusListArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, statusListArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeGroup,
+ },
+ {
+ .name = "invisible",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_Invisible,
+ .hasIndex = 5,
+ .offset = 6, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "statusMinVersion",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_FieldNumber_StatusMinVersion,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus__storage_, statusMinVersion),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkSharedStatus class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkSharedStatus__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkSharedStatus_ShowType
+
+GPBEnumDescriptor *GtalkSharedStatus_ShowType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "Default\000Dnd\000";
+ static const int32_t values[] = {
+ GtalkSharedStatus_ShowType_Default,
+ GtalkSharedStatus_ShowType_Dnd,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkSharedStatus_ShowType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkSharedStatus_ShowType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkSharedStatus_ShowType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkSharedStatus_ShowType_Default:
+ case GtalkSharedStatus_ShowType_Dnd:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkSharedStatus_StatusList
+
+@implementation GtalkSharedStatus_StatusList
+
+@dynamic hasShow, show;
+@dynamic statusArray, statusArray_Count;
+
+typedef struct GtalkSharedStatus_StatusList__storage_ {
+ uint32_t _has_storage_[1];
+ GtalkSharedStatus_ShowType show;
+ NSMutableArray *statusArray;
+} GtalkSharedStatus_StatusList__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "show",
+ .dataTypeSpecific.enumDescFunc = GtalkSharedStatus_ShowType_EnumDescriptor,
+ .number = GtalkSharedStatus_StatusList_FieldNumber_Show,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus_StatusList__storage_, show),
+ .flags = (GPBFieldFlags)(GPBFieldRequired | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "statusArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSharedStatus_StatusList_FieldNumber_StatusArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkSharedStatus_StatusList__storage_, statusArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkSharedStatus_StatusList class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkSharedStatus_StatusList__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GtalkSharedStatus)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkOtrQuery
+
+@implementation GtalkOtrQuery
+
+@dynamic hasNosaveDefault, nosaveDefault;
+@dynamic itemArray, itemArray_Count;
+@dynamic hasEtag, etag;
+@dynamic hasNotModified, notModified;
+
+typedef struct GtalkOtrQuery__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *itemArray;
+ NSString *etag;
+} GtalkOtrQuery__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "nosaveDefault",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrQuery_FieldNumber_NosaveDefault,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "itemArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GtalkOtrItem),
+ .number = GtalkOtrQuery_FieldNumber_ItemArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkOtrQuery__storage_, itemArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "etag",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrQuery_FieldNumber_Etag,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GtalkOtrQuery__storage_, etag),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "notModified",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrQuery_FieldNumber_NotModified,
+ .hasIndex = 3,
+ .offset = 4, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkOtrQuery class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkOtrQuery__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkOtrItem
+
+@implementation GtalkOtrItem
+
+@dynamic hasJid, jid;
+@dynamic hasNosave, nosave;
+@dynamic hasChangedByBuddy, changedByBuddy;
+
+typedef struct GtalkOtrItem__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *jid;
+} GtalkOtrItem__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "jid",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrItem_FieldNumber_Jid,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GtalkOtrItem__storage_, jid),
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "nosave",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrItem_FieldNumber_Nosave,
+ .hasIndex = 1,
+ .offset = 2, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "changedByBuddy",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkOtrItem_FieldNumber_ChangedByBuddy,
+ .hasIndex = 3,
+ .offset = 4, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkOtrItem class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkOtrItem__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkIdle
+
+@implementation GtalkIdle
+
+@dynamic hasIdle, idle;
+@dynamic hasAway, away;
+
+typedef struct GtalkIdle__storage_ {
+ uint32_t _has_storage_[1];
+} GtalkIdle__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "idle",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIdle_FieldNumber_Idle,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "away",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkIdle_FieldNumber_Away,
+ .hasIndex = 2,
+ .offset = 3, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkIdle class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkIdle__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkPostAuthBatchQuery
+
+@implementation GtalkPostAuthBatchQuery
+
+@dynamic hasAvailable, available;
+@dynamic hasDeviceIdle, deviceIdle;
+@dynamic hasMobileIndicator, mobileIndicator;
+@dynamic hasSharedStatusVersion, sharedStatusVersion;
+@dynamic hasRosterEtag, rosterEtag;
+@dynamic hasOtrEtag, otrEtag;
+@dynamic hasAvatarHash, avatarHash;
+@dynamic hasVcardQueryStanzaId, vcardQueryStanzaId;
+@dynamic hasCapabilitiesExtFlags, capabilitiesExtFlags;
+
+typedef struct GtalkPostAuthBatchQuery__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t sharedStatusVersion;
+ int32_t capabilitiesExtFlags;
+ NSString *rosterEtag;
+ NSString *otrEtag;
+ NSString *avatarHash;
+ NSString *vcardQueryStanzaId;
+} GtalkPostAuthBatchQuery__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "available",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_Available,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldRequired,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "deviceIdle",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_DeviceIdle,
+ .hasIndex = 2,
+ .offset = 3, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "mobileIndicator",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_MobileIndicator,
+ .hasIndex = 4,
+ .offset = 5, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "sharedStatusVersion",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_SharedStatusVersion,
+ .hasIndex = 6,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, sharedStatusVersion),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "rosterEtag",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_RosterEtag,
+ .hasIndex = 7,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, rosterEtag),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "otrEtag",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_OtrEtag,
+ .hasIndex = 8,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, otrEtag),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "avatarHash",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_AvatarHash,
+ .hasIndex = 9,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, avatarHash),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "vcardQueryStanzaId",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_VcardQueryStanzaId,
+ .hasIndex = 10,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, vcardQueryStanzaId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "capabilitiesExtFlags",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkPostAuthBatchQuery_FieldNumber_CapabilitiesExtFlags,
+ .hasIndex = 11,
+ .offset = (uint32_t)offsetof(GtalkPostAuthBatchQuery__storage_, capabilitiesExtFlags),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkPostAuthBatchQuery class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkPostAuthBatchQuery__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GtalkPostAuthBatchQuery_CapabilitiesExtFlags
+
+GPBEnumDescriptor *GtalkPostAuthBatchQuery_CapabilitiesExtFlags_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "HasVoiceV1\000HasVideoV1\000HasCameraV1\000HasPmu"
+ "cV1\000";
+ static const int32_t values[] = {
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVoiceV1,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVideoV1,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasCameraV1,
+ GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasPmucV1,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GtalkPostAuthBatchQuery_CapabilitiesExtFlags)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GtalkPostAuthBatchQuery_CapabilitiesExtFlags_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GtalkPostAuthBatchQuery_CapabilitiesExtFlags_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVoiceV1:
+ case GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVideoV1:
+ case GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasCameraV1:
+ case GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasPmucV1:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GtalkStreamAck
+
+@implementation GtalkStreamAck
+
+
+typedef struct GtalkStreamAck__storage_ {
+ uint32_t _has_storage_[1];
+} GtalkStreamAck__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkStreamAck class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:NULL
+ fieldCount:0
+ storageSize:sizeof(GtalkStreamAck__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GtalkSelectiveAck
+
+@implementation GtalkSelectiveAck
+
+@dynamic idArray, idArray_Count;
+
+typedef struct GtalkSelectiveAck__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *idArray;
+} GtalkSelectiveAck__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "idArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GtalkSelectiveAck_FieldNumber_IdArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GtalkSelectiveAck__storage_, idArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GtalkSelectiveAck class]
+ rootClass:[GtalkGtalkExtensionsRoot class]
+ file:GtalkGtalkExtensionsRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GtalkSelectiveAck__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firebase/Messaging/Public/FIRMessaging.h b/Firebase/Messaging/Public/FIRMessaging.h
new file mode 100644
index 0000000..84d2526
--- /dev/null
+++ b/Firebase/Messaging/Public/FIRMessaging.h
@@ -0,0 +1,486 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// Wrap it in our own macro if it's a non-compatible SDK.
+#ifndef FIR_SWIFT_NAME
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+#endif // #ifndef FIR_SWIFT_NAME
+
+/**
+ * @related FIRMessaging
+ *
+ * The completion handler invoked when the registration token returns.
+ * If the call fails we return the appropriate `error code`, described by
+ * `FIRMessagingError`.
+ *
+ * @param FCMToken The valid registration token returned by FCM.
+ * @param error The error describing why a token request failed. The error code
+ * will match a value from the FIRMessagingError enumeration.
+ */
+typedef void(^FIRMessagingFCMTokenFetchCompletion)(NSString * _Nullable FCMToken,
+ NSError * _Nullable error)
+ FIR_SWIFT_NAME(MessagingFCMTokenFetchCompletion);
+
+
+/**
+ * @related FIRMessaging
+ *
+ * The completion handler invoked when the registration token deletion request is
+ * completed. If the call fails we return the appropriate `error code`, described
+ * by `FIRMessagingError`.
+ *
+ * @param error The error describing why a token deletion failed. The error code
+ * will match a value from the FIRMessagingError enumeration.
+ */
+typedef void(^FIRMessagingDeleteFCMTokenCompletion)(NSError * _Nullable error)
+ FIR_SWIFT_NAME(MessagingDeleteFCMTokenCompletion);
+
+/**
+ * The completion handler invoked once the data connection with FIRMessaging is
+ * established. The data connection is used to send a continous stream of
+ * data and all the FIRMessaging data notifications arrive through this connection.
+ * Once the connection is established we invoke the callback with `nil` error.
+ * Correspondingly if we get an error while trying to establish a connection
+ * we invoke the handler with an appropriate error object and do an
+ * exponential backoff to try and connect again unless successful.
+ *
+ * @param error The error object if any describing why the data connection
+ * to FIRMessaging failed.
+ */
+typedef void(^FIRMessagingConnectCompletion)(NSError * __nullable error)
+ FIR_SWIFT_NAME(MessagingConnectCompletion)
+ __deprecated_msg("Please listen for the FIRMessagingConnectionStateChangedNotification "
+ "NSNotification instead.");
+
+#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+/**
+ * Notification sent when the upstream message has been delivered
+ * successfully to the server. The notification object will be the messageID
+ * of the successfully delivered message.
+ */
+FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingSendSuccessNotification
+ FIR_SWIFT_NAME(MessagingSendSuccess);
+
+/**
+ * Notification sent when the upstream message was failed to be sent to the
+ * server. The notification object will be the messageID of the failed
+ * message. The userInfo dictionary will contain the relevant error
+ * information for the failure.
+ */
+FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingSendErrorNotification
+ FIR_SWIFT_NAME(MessagingSendError);
+
+/**
+ * Notification sent when the Firebase messaging server deletes pending
+ * messages due to exceeded storage limits. This may occur, for example, when
+ * the device cannot be reached for an extended period of time.
+ *
+ * It is recommended to retrieve any missing messages directly from the
+ * server.
+ */
+FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingMessagesDeletedNotification
+ FIR_SWIFT_NAME(MessagingMessagesDeleted);
+
+/**
+ * Notification sent when Firebase Messaging establishes or disconnects from
+ * an FCM socket connection. You can query the connection state in this
+ * notification by checking the `isDirectChannelEstablished` property of FIRMessaging.
+ */
+FOUNDATION_EXPORT const NSNotificationName __nonnull FIRMessagingConnectionStateChangedNotification
+ FIR_SWIFT_NAME(MessagingConnectionStateChanged);
+
+/**
+ * Notification sent when the FCM registration token has been refreshed. You can also
+ * receive the FCM token via the FIRMessagingDelegate method
+ * `-messaging:didRefreshRegistrationToken:`
+ */
+FOUNDATION_EXPORT const NSNotificationName __nonnull
+ FIRMessagingRegistrationTokenRefreshedNotification
+ FIR_SWIFT_NAME(MessagingRegistrationTokenRefreshed);
+#else
+/**
+ * Notification sent when the upstream message has been delivered
+ * successfully to the server. The notification object will be the messageID
+ * of the successfully delivered message.
+ */
+FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingSendSuccessNotification
+ FIR_SWIFT_NAME(MessagingSendSuccessNotification);
+
+/**
+ * Notification sent when the upstream message was failed to be sent to the
+ * server. The notification object will be the messageID of the failed
+ * message. The userInfo dictionary will contain the relevant error
+ * information for the failure.
+ */
+FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingSendErrorNotification
+ FIR_SWIFT_NAME(MessagingSendErrorNotification);
+
+/**
+ * Notification sent when the Firebase messaging server deletes pending
+ * messages due to exceeded storage limits. This may occur, for example, when
+ * the device cannot be reached for an extended period of time.
+ *
+ * It is recommended to retrieve any missing messages directly from the
+ * server.
+ */
+FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingMessagesDeletedNotification
+ FIR_SWIFT_NAME(MessagingMessagesDeletedNotification);
+
+/**
+ * Notification sent when Firebase Messaging establishes or disconnects from
+ * an FCM socket connection. You can query the connection state in this
+ * notification by checking the `isDirectChannelEstablished` property of FIRMessaging.
+ */
+FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingConnectionStateChangedNotification
+ FIR_SWIFT_NAME(MessagingConnectionStateChangedNotification);
+
+/**
+ * Notification sent when the FCM registration token has been refreshed. You can also
+ * receive the FCM token via the FIRMessagingDelegate method
+ * `-messaging:didRefreshRegistrationToken:`
+ */
+FOUNDATION_EXPORT NSString * __nonnull const FIRMessagingRegistrationTokenRefreshedNotification
+ FIR_SWIFT_NAME(MessagingRegistrationTokenRefreshedNotification);
+#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
+
+/**
+ * @enum FIRMessagingError
+ */
+typedef NS_ENUM(NSUInteger, FIRMessagingError) {
+ /// Unknown error.
+ FIRMessagingErrorUnknown = 0,
+
+ /// FIRMessaging couldn't validate request from this client.
+ FIRMessagingErrorAuthentication = 1,
+
+ /// InstanceID service cannot be accessed.
+ FIRMessagingErrorNoAccess = 2,
+
+ /// Request to InstanceID backend timed out.
+ FIRMessagingErrorTimeout = 3,
+
+ /// No network available to reach the servers.
+ FIRMessagingErrorNetwork = 4,
+
+ /// Another similar operation in progress, bailing this one.
+ FIRMessagingErrorOperationInProgress = 5,
+
+ /// Some parameters of the request were invalid.
+ FIRMessagingErrorInvalidRequest = 7,
+} FIR_SWIFT_NAME(MessagingError);
+
+/// Status for the downstream message received by the app.
+typedef NS_ENUM(NSInteger, FIRMessagingMessageStatus) {
+ /// Unknown status.
+ FIRMessagingMessageStatusUnknown,
+ /// New downstream message received by the app.
+ FIRMessagingMessageStatusNew,
+} FIR_SWIFT_NAME(MessagingMessageStatus);
+
+/**
+ * The APNS token type for the app. If the token type is set to `UNKNOWN`
+ * Firebase Messaging will implicitly try to figure out what the actual token type
+ * is from the provisioning profile.
+ * Unless you really need to specify the type, you should use the `APNSToken`
+ * property instead.
+ */
+typedef NS_ENUM(NSInteger, FIRMessagingAPNSTokenType) {
+ /// Unknown token type.
+ FIRMessagingAPNSTokenTypeUnknown,
+ /// Sandbox token type.
+ FIRMessagingAPNSTokenTypeSandbox,
+ /// Production token type.
+ FIRMessagingAPNSTokenTypeProd,
+} FIR_SWIFT_NAME(MessagingAPNSTokenType);
+
+/// Information about a downstream message received by the app.
+FIR_SWIFT_NAME(MessagingMessageInfo)
+@interface FIRMessagingMessageInfo : NSObject
+
+/// The status of the downstream message
+@property(nonatomic, readonly, assign) FIRMessagingMessageStatus status;
+
+@end
+
+/**
+ * A remote data message received by the app via FCM (not just the APNs interface).
+ *
+ * This is only for devices running iOS 10 or above. To support devices running iOS 9 or below, use
+ * the local and remote notifications handlers defined in UIApplicationDelegate protocol.
+ */
+FIR_SWIFT_NAME(MessagingRemoteMessage)
+@interface FIRMessagingRemoteMessage : NSObject
+
+/// The downstream message received by the application.
+@property(nonatomic, readonly, strong, nonnull) NSDictionary *appData;
+@end
+
+@class FIRMessaging;
+/**
+ * A protocol to handle events from FCM for devices running iOS 10 or above.
+ *
+ * To support devices running iOS 9 or below, use the local and remote notifications handlers
+ * defined in UIApplicationDelegate protocol.
+ */
+FIR_SWIFT_NAME(MessagingDelegate)
+@protocol FIRMessagingDelegate <NSObject>
+
+/// This method will be called whenever FCM receives a new, default FCM token for your
+/// Firebase project's Sender ID.
+/// You can send this token to your application server to send notifications to this device.
+- (void)messaging:(nonnull FIRMessaging *)messaging
+ didRefreshRegistrationToken:(nonnull NSString *)fcmToken
+ FIR_SWIFT_NAME(messaging(_:didRefreshRegistrationToken:));
+
+@optional
+/// This method is called on iOS 10 devices to handle data messages received via FCM through its
+/// direct channel (not via APNS). For iOS 9 and below, the FCM data message is delivered via the
+/// UIApplicationDelegate's -application:didReceiveRemoteNotification: method.
+- (void)messaging:(nonnull FIRMessaging *)messaging
+ didReceiveMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage
+ FIR_SWIFT_NAME(messaging(_:didReceive:))
+ __IOS_AVAILABLE(10.0);
+
+/// The callback to handle data message received via FCM for devices running iOS 10 or above.
+- (void)applicationReceivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage
+ FIR_SWIFT_NAME(application(received:))
+ __deprecated_msg("Use FIRMessagingDelegate’s -messaging:didReceiveMessage:");
+
+@end
+
+/**
+ * Firebase Messaging lets you reliably deliver messages at no cost.
+ *
+ * To send or receive messages, the app must get a
+ * registration token from FIRInstanceID. This token authorizes an
+ * app server to send messages to an app instance.
+ *
+ * In order to receive FIRMessaging messages, declare `application:didReceiveRemoteNotification:`.
+ */
+FIR_SWIFT_NAME(Messaging)
+@interface FIRMessaging : NSObject
+
+/**
+ * Delegate to handle FCM token refreshes, and remote data messages received via FCM for devices
+ * running iOS 10 or above.
+ */
+@property(nonatomic, weak, nullable) id<FIRMessagingDelegate> delegate;
+
+
+/**
+ * Delegate to handle remote data messages received via FCM for devices running iOS 10 or above.
+ */
+@property(nonatomic, weak, nullable) id<FIRMessagingDelegate> remoteMessageDelegate
+ __deprecated_msg("Use 'delegate' property");
+
+/**
+ * When set to YES, Firebase Messaging will automatically establish a socket-based, direct channel
+ * to the FCM server. You only need to enable this if you are sending upstream messages or
+ * receiving non-APNS, data-only messages in foregrounded apps.
+ * Default is NO.
+ */
+@property(nonatomic) BOOL shouldEstablishDirectChannel;
+
+/**
+ * Returns YES if the direct channel to the FCM server is active, NO otherwise.
+ */
+@property(nonatomic, readonly) BOOL isDirectChannelEstablished;
+
+/**
+ * FIRMessaging
+ *
+ * @return An instance of FIRMessaging.
+ */
++ (nonnull instancetype)messaging FIR_SWIFT_NAME(messaging());
+
+/**
+ * Unavailable. Use +messaging instead.
+ */
+- (nonnull instancetype)init __attribute__((unavailable("Use +messaging instead.")));
+
+#pragma mark - APNS
+
+/**
+ * This property is used to set the APNS Token received by the application delegate.
+ *
+ * FIRMessaging uses method swizzling to ensure the APNS token is set automatically.
+ * However, if you have disabled swizzling by setting `FirebaseAppDelegateProxyEnabled`
+ * to `NO` in your app's Info.plist, you should manually set the APNS token in your
+ * application delegate's -application:didRegisterForRemoteNotificationsWithDeviceToken:
+ * method.
+ *
+ * If you would like to set the type of the APNS token, rather than relying on automatic
+ * detection, see: -setAPNSToken:type:.
+ */
+@property(nonatomic, copy, nullable) NSData *APNSToken FIR_SWIFT_NAME(apnsToken);
+
+/**
+ * Set APNS token for the application. This APNS token will be used to register
+ * with Firebase Messaging using `FCMToken` or
+ * `tokenWithAuthorizedEntity:scope:options:handler`.
+ *
+ * @param apnsToken The APNS token for the application.
+ * @param type The type of APNS token. Debug builds should use
+ * FIRMessagingAPNSTokenTypeSandbox. Alternatively, you can supply
+ * FIRMessagingAPNSTokenTypeUnknown to have the type automatically
+ * detected based on your provisioning profile.
+ */
+- (void)setAPNSToken:(nonnull NSData *)apnsToken type:(FIRMessagingAPNSTokenType)type;
+
+#pragma mark - FCM Tokens
+
+/**
+ * The FCM token is used to identify this device so that FCM can send notifications to it.
+ * It is associated with your APNS token when the APNS token is supplied, so that sending
+ * messages to the FCM token will be delivered over APNS.
+ *
+ * The FCM token is sometimes refreshed automatically. You can be notified of these changes
+ * via the FIRMessagingDelegate method `-message:didRefreshRegistrationToken:`, or by
+ * listening for the `FIRMessagingRegistrationTokenRefreshedNotification` notification.
+ *
+ * Once you have an FCM token, you should send it to your application server, so it can use
+ * the FCM token to send notifications to your device.
+ */
+@property(nonatomic, readonly, nullable) NSString *FCMToken FIR_SWIFT_NAME(fcmToken);
+
+
+/**
+ * Retrieves an FCM registration token for a particular Sender ID. This registration token is
+ * not cached by FIRMessaging. FIRMessaging should have an APNS token set before calling this
+ * to ensure that notifications can be delivered via APNS using this FCM token. You may
+ * re-retrieve the FCM token once you have the APNS token set, to associate it with the FCM
+ * token. The default FCM token is automatically associated with the APNS token, if the APNS
+ * token data is available.
+ *
+ * @param senderID The Sender ID for a particular Firebase project.
+ * @param completion The completion handler to handle the token request.
+ */
+- (void)retrieveFCMTokenForSenderID:(nonnull NSString *)senderID
+ completion:(nonnull FIRMessagingFCMTokenFetchCompletion)completion
+ FIR_SWIFT_NAME(retrieveFCMToken(forSenderID:completion:));
+
+
+/**
+ * Invalidates an FCM token for a particular Sender ID. That Sender ID cannot no longer send
+ * notifications to that FCM token.
+ *
+ * @param senderID The senderID for a particular Firebase project.
+ * @param completion The completion handler to handle the token deletion.
+ */
+- (void)deleteFCMTokenForSenderID:(nonnull NSString *)senderID
+ completion:(nonnull FIRMessagingDeleteFCMTokenCompletion)completion
+ FIR_SWIFT_NAME(deleteFCMToken(forSenderID:completion:));
+
+
+#pragma mark - Connect
+
+/**
+ * Create a FIRMessaging data connection which will be used to send the data notifications
+ * sent by your server. It will also be used to send ACKS and other messages based
+ * on the FIRMessaging ACKS and other messages based on the FIRMessaging protocol.
+ *
+ *
+ * @param handler The handler to be invoked once the connection is established.
+ * If the connection fails we invoke the handler with an
+ * appropriate error code letting you know why it failed. At
+ * the same time, FIRMessaging performs exponential backoff to retry
+ * establishing a connection and invoke the handler when successful.
+ */
+- (void)connectWithCompletion:(nonnull FIRMessagingConnectCompletion)handler
+ FIR_SWIFT_NAME(connect(handler:))
+ __deprecated_msg("Please use the shouldEstablishDirectChannel property instead.");
+
+/**
+ * Disconnect the current FIRMessaging data connection. This stops any attempts to
+ * connect to FIRMessaging. Calling this on an already disconnected client is a no-op.
+ *
+ * Call this before `teardown` when your app is going to the background.
+ * Since the FIRMessaging connection won't be allowed to live when in background it is
+ * prudent to close the connection.
+ */
+- (void)disconnect
+ __deprecated_msg("Please use the shouldEstablishDirectChannel property instead.");
+
+#pragma mark - Topics
+
+/**
+ * Asynchronously subscribes to a topic.
+ *
+ * @param topic The name of the topic, for example, @"sports".
+ */
+- (void)subscribeToTopic:(nonnull NSString *)topic FIR_SWIFT_NAME(subscribe(toTopic:));
+
+/**
+ * Asynchronously unsubscribe from a topic.
+ *
+ * @param topic The name of the topic, for example @"sports".
+ */
+- (void)unsubscribeFromTopic:(nonnull NSString *)topic FIR_SWIFT_NAME(unsubscribe(fromTopic:));
+
+#pragma mark - Upstream
+
+/**
+ * Sends an upstream ("device to cloud") message.
+ *
+ * The message is queued if we don't have an active connection.
+ * You can only use the upstream feature if your FCM implementation
+ * uses the XMPP server protocol.
+ *
+ * @param message Key/Value pairs to be sent. Values must be String, any
+ * other type will be ignored.
+ * @param receiver A string identifying the receiver of the message. For FCM
+ * project IDs the value is `SENDER_ID@gcm.googleapis.com`.
+ * @param messageID The ID of the message. This is generated by the application. It
+ * must be unique for each message generated by this application.
+ * It allows error callbacks and debugging, to uniquely identify
+ * each message.
+ * @param ttl The time to live for the message. In case we aren't able to
+ * send the message before the TTL expires we will send you a
+ * callback. If 0, we'll attempt to send immediately and return
+ * an error if we're not connected. Otherwise, the message will
+ * be queued. As for server-side messages, we don't return an error
+ * if the message has been dropped because of TTL; this can happen
+ * on the server side, and it would require extra communication.
+ */
+- (void)sendMessage:(nonnull NSDictionary *)message
+ to:(nonnull NSString *)receiver
+ withMessageID:(nonnull NSString *)messageID
+ timeToLive:(int64_t)ttl;
+
+#pragma mark - Analytics
+
+/**
+ * Use this to track message delivery and analytics for messages, typically
+ * when you receive a notification in `application:didReceiveRemoteNotification:`.
+ * However, you only need to call this if you set the `FirebaseAppDelegateProxyEnabled`
+ * flag to NO in your Info.plist. If `FirebaseAppDelegateProxyEnabled` is either missing
+ * or set to YES in your Info.plist, the library will call this automatically.
+ *
+ * @param message The downstream message received by the application.
+ *
+ * @return Information about the downstream message.
+ */
+- (nonnull FIRMessagingMessageInfo *)appDidReceiveMessage:(nonnull NSDictionary *)message;
+
+@end
diff --git a/Firebase/Messaging/Public/FirebaseMessaging.h b/Firebase/Messaging/Public/FirebaseMessaging.h
new file mode 100755
index 0000000..ef081c9
--- /dev/null
+++ b/Firebase/Messaging/Public/FirebaseMessaging.h
@@ -0,0 +1,17 @@
+/*
+ * 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 "FIRMessaging.h"
diff --git a/Firebase/Storage/FIRStorage.h b/Firebase/Storage/FIRStorage.h
new file mode 100644
index 0000000..3b37a0e
--- /dev/null
+++ b/Firebase/Storage/FIRStorage.h
@@ -0,0 +1,130 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageSwiftNameSupport.h"
+
+@class FIRApp;
+@class FIRStorageReference;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Project version string for FirebaseStorage. */
+FOUNDATION_EXPORT const unsigned char *const FIRStorageVersionString;
+
+/**
+ * FirebaseStorage is a service that supports uploading and downloading binary objects,
+ * such as images, videos, and other files to Google Cloud Storage.
+ *
+ * If you call [FIRStorage storage], the instance will initialize with the default FIRApp,
+ * [FIRApp defaultApp], and the storage location will come from the provided
+ * GoogleService-Info.plist.
+ *
+ * If you call [FIRStorage storageForApp:] and provide a custom instance of FIRApp,
+ * the storage location will be specified via the FIROptions#storageBucket property.
+ */
+FIR_SWIFT_NAME(Storage)
+@interface FIRStorage : NSObject
+
+/**
+ * Creates an instance of FIRStorage, configured with the default FIRApp.
+ * @return the FIRStorage instance, initialized with the default FIRApp.
+ */
++ (instancetype)storage FIR_SWIFT_NAME(storage());
+
+/**
+ * Creates an instance of FIRStorage, configured with the custom FIRApp @a app.
+ * @param app The custom FIRApp used for initialization.
+ * @return the FIRStorage instance, initialized with the custom FIRApp.
+ */
++ (instancetype)storageForApp:(FIRApp *)app FIR_SWIFT_NAME(storage(app:));
+
+/**
+ * Creates an instance of FIRStorage, configured with a custom storage bucket @a url.
+ * @param url The gs:// url to your Firebase Storage Bucket.
+ * @return the FIRStorage instance, initialized with the custom FIRApp.
+ */
++ (instancetype)storageWithURL:(NSString *)url FIR_SWIFT_NAME(storage(url:));
+
+/**
+ * Creates an instance of FIRStorage, configured with a custom FIRApp @a app and a custom storage
+ * bucket @a url.
+ * @param app The custom FIRApp used for initialization.
+ * @param url The gs:// url to your Firebase Storage Bucket.
+ * @return the FIRStorage instance, initialized with the custom FIRApp.
+ */
++ (instancetype)storageForApp:(FIRApp *)app
+ URL:(NSString *)url FIR_SWIFT_NAME(storage(app:url:));
+
+/**
+ * The Firebase App associated with this Firebase Storage instance.
+ */
+@property(strong, nonatomic, readonly) FIRApp *app;
+
+/**
+ * Maximum time in seconds to retry an upload if a failure occurs.
+ * Defaults to 10 minutes (600 seconds).
+ */
+@property NSTimeInterval maxUploadRetryTime;
+
+/**
+ * Maximum time in seconds to retry a download if a failure occurs.
+ * Defaults to 10 minutes (600 seconds).
+ */
+@property NSTimeInterval maxDownloadRetryTime;
+
+/**
+ * Maximum time in seconds to retry operations other than upload and download if a failure occurs.
+ * Defaults to 2 minutes (120 seconds).
+ */
+@property NSTimeInterval maxOperationRetryTime;
+
+/**
+ * Queue that all developer callbacks are fired on. Defaults to the main queue.
+ */
+@property(strong, nonatomic) dispatch_queue_t callbackQueue;
+
+/**
+ * Creates a FIRStorageReference initialized at the root Firebase Storage location.
+ * @return An instance of FIRStorageReference initialized at the root.
+ */
+- (FIRStorageReference *)reference;
+
+/**
+ * Creates a FIRStorageReference given a gs:// or https:// URL pointing to a Firebase Storage
+ * location. For example, you can pass in an https:// download URL retrieved from
+ * [FIRStorageReference downloadURLWithCompletion] or the gs:// URI from
+ * [FIRStorageReference description].
+ * @param string A gs:// or https:// URL to initialize the reference with.
+ * @return An instance of FIRStorageReference at the given child path.
+ * @throws Throws an exception if passed in URL is not associated with the FIRApp used to initialize
+ * this FIRStorage.
+ */
+- (FIRStorageReference *)referenceForURL:(NSString *)string;
+
+/**
+ * Creates a FIRStorageReference initialized at a child Firebase Storage location.
+ * @param string A relative path from the root to initialize the reference with,
+ * for instance @"path/to/object".
+ * @return An instance of FIRStorageReference at the given child path.
+ */
+- (FIRStorageReference *)referenceWithPath:(NSString *)string;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorage.m b/Firebase/Storage/FIRStorage.m
new file mode 100644
index 0000000..dd11391
--- /dev/null
+++ b/Firebase/Storage/FIRStorage.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 "FIRStorage.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStoragePath.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageReference_Private.h"
+#import "FIRStorageTokenAuthorizer.h"
+#import "FIRStorageUtils.h"
+#import "FIRStorage_Private.h"
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import <GTMSessionFetcher/GTMSessionFetcherLogging.h>
+
+static NSMutableDictionary<
+ NSString * /* app name */,
+ NSMutableDictionary<NSString * /* bucket */, GTMSessionFetcherService *> *> *_fetcherServiceMap;
+static GTMSessionFetcherRetryBlock _retryWhenOffline;
+
+@implementation FIRStorage
+
++ (void)initialize {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ _retryWhenOffline = ^(BOOL suggestedWillRetry,
+ NSError * GTM_NULLABLE_TYPE error,
+ GTMSessionFetcherRetryResponse response) {
+ bool shouldRetry = suggestedWillRetry;
+ // GTMSessionFetcher does not consider being offline a retryable error, but we do, so we
+ // special-case it here.
+ if (!shouldRetry && error) {
+ shouldRetry = error.code == NSURLErrorNotConnectedToInternet;
+ }
+ response(shouldRetry);
+ };
+ _fetcherServiceMap = [[NSMutableDictionary alloc] init];
+ });
+}
+
++ (GTMSessionFetcherService *)fetcherServiceForApp:(FIRApp *)app bucket:(NSString *)bucket {
+ @synchronized(_fetcherServiceMap) {
+ NSMutableDictionary *bucketMap = _fetcherServiceMap[app.name];
+ if (!bucketMap) {
+ bucketMap = [[NSMutableDictionary alloc] init];
+ _fetcherServiceMap[app.name] = bucketMap;
+ }
+
+ GTMSessionFetcherService *fetcherService = bucketMap[bucket];
+ if (!fetcherService) {
+ fetcherService = [[GTMSessionFetcherService alloc] init];
+ [fetcherService setRetryEnabled:YES];
+ [fetcherService setRetryBlock:_retryWhenOffline];
+ FIRStorageTokenAuthorizer *authorizer =
+ [[FIRStorageTokenAuthorizer alloc] initWithApp:app fetcherService:fetcherService];
+ [fetcherService setAuthorizer:authorizer];
+ bucketMap[bucket] = fetcherService;
+ }
+ return fetcherService;
+ }
+}
+
++ (void)setGTMSessionFetcherLoggingEnabled:(BOOL)isLoggingEnabled {
+ [GTMSessionFetcher setLoggingEnabled:isLoggingEnabled];
+}
+
++ (instancetype)storage {
+ return [self storageForApp:[FIRApp defaultApp]];
+}
+
++ (instancetype)storageForApp:(FIRApp *)app {
+ NSString* url;
+
+ if (app.options.storageBucket) {
+ url = [app.options.storageBucket isEqualToString:@""] ? @""
+ : [@"gs://" stringByAppendingString:app.options.storageBucket];
+ }
+
+ return [self storageForApp:app URL:url];
+}
+
++ (instancetype)storageWithURL:(NSString *)url {
+ return [self storageForApp:[FIRApp defaultApp] URL:url];
+}
+
++ (instancetype)storageForApp:(FIRApp *)app URL:(NSString *)url {
+ if (!url) {
+ NSString *const kAppNotConfiguredMessage =
+ @"No default Storage bucket found. Did you configure Firebase Storage properly?";
+ [NSException raise:NSInvalidArgumentException format:kAppNotConfiguredMessage];
+ }
+
+ NSString *bucket;
+ if ([url isEqualToString:@""]) {
+ bucket = @"";
+ } else {
+ FIRStoragePath *path;
+
+ @try {
+ path = [FIRStoragePath pathFromGSURI:url];
+ } @catch (NSException *e) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"URI must be in the form of gs://<bucket>/"];
+ }
+
+ if (path.object != nil && ![path.object isEqualToString:@""]) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"Storage bucket cannot be initialized with a path"];
+ }
+
+ bucket = path.bucket;
+ }
+
+ return [[self alloc] initWithApp:app bucket:bucket];
+}
+
+- (instancetype)initWithApp:(FIRApp *)app bucket:(NSString *)bucket {
+ self = [super init];
+ if (self) {
+ _app = app;
+ _storageBucket = bucket;
+ _fetcherServiceForApp = [FIRStorage fetcherServiceForApp:_app bucket:bucket];
+ _maxDownloadRetryTime = 600.0;
+ _maxOperationRetryTime = 120.0;
+ _maxUploadRetryTime = 600.0;
+ }
+ return self;
+}
+
+#pragma mark - NSObject overrides
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+ FIRStorage *storage = [[[self class] allocWithZone:zone] initWithApp:_app bucket:_storageBucket];
+ storage.callbackQueue = _callbackQueue;
+ return storage;
+}
+
+// Two FIRStorage objects are equal if they use the same app
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FIRStorage class]]) {
+ return NO;
+ }
+
+ BOOL isEqualObject = [self isEqualToFIRStorage:(FIRStorage *)object];
+ return isEqualObject;
+}
+
+- (BOOL)isEqualToFIRStorage:(FIRStorage *)storage {
+ BOOL isEqual = [_app isEqual:storage->_app];
+ return isEqual;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = [_app hash] ^ [_callbackQueue hash];
+ return hash;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %p: %@", [self class], self, _app];
+}
+
+#pragma mark - Public methods
+
+- (FIRStorageReference *)reference {
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:_storageBucket object:nil];
+ return [[FIRStorageReference alloc] initWithStorage:self path:path];
+}
+
+- (FIRStorageReference *)referenceForURL:(NSString *)string {
+ FIRStoragePath *path = [FIRStoragePath pathFromString:string];
+
+ // If no default bucket exists (empty string), accept anything.
+ if ([_storageBucket isEqual:@""]) {
+ FIRStorageReference *reference = [[FIRStorageReference alloc] initWithStorage:self path:path];
+ return reference;
+ }
+
+ // If there exists a default bucket, throw if provided a different bucket.
+ if (![path.bucket isEqual:_storageBucket]) {
+ NSString *const kInvalidBucketFormat =
+ @"Provided bucket: %@ does not match the Storage bucket of the current instance: %@";
+ [NSException raise:NSInvalidArgumentException
+ format:kInvalidBucketFormat, path.bucket, _storageBucket];
+ }
+
+ FIRStorageReference *reference = [[FIRStorageReference alloc] initWithStorage:self path:path];
+ return reference;
+}
+
+- (FIRStorageReference *)referenceWithPath:(NSString *)string {
+ FIRStorageReference *reference = [[self reference] child:string];
+ return reference;
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t)callbackQueue {
+ _fetcherServiceForApp.callbackQueue = callbackQueue;
+}
+
+#pragma mark - Background tasks
+
++ (void)enableBackgroundTasks:(BOOL)isEnabled {
+ [NSException raise:NSGenericException format:@"enableBackgroundTasks not implemented"];
+}
+
+- (NSArray<FIRStorageUploadTask *> *)uploadTasks {
+ [NSException raise:NSGenericException format:@"getUploadTasks not implemented"];
+ return nil;
+}
+
+- (NSArray<FIRStorageDownloadTask *> *)downloadTasks {
+ [NSException raise:NSGenericException format:@"getDownloadTasks not implemented"];
+ return nil;
+}
+@end
diff --git a/Firebase/Storage/FIRStorageConstants.h b/Firebase/Storage/FIRStorageConstants.h
new file mode 100644
index 0000000..cf6c3b8
--- /dev/null
+++ b/Firebase/Storage/FIRStorageConstants.h
@@ -0,0 +1,173 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRStorageSwiftNameSupport.h"
+
+@class FIRStorageDownloadTask;
+@class FIRStorageMetadata;
+@class FIRStorageTaskSnapshot;
+@class FIRStorageUploadTask;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * NSString typedef representing a task listener handle.
+ */
+typedef NSString *FIRStorageHandle FIR_SWIFT_NAME(StorageHandle);
+
+/**
+ * Block typedef typically used when downloading data.
+ * @param data The data returned by the download, or nil if no data available or download failed.
+ * @param error The error describing failure, if one occurred.
+ */
+typedef void (^FIRStorageVoidDataError)(NSData *_Nullable data, NSError *_Nullable error)
+ FIR_SWIFT_NAME(StorageVoidDataError);
+
+/**
+ * Block typedef typically used when performing "binary" async operations such as delete,
+ * where the operation either succeeds without an error or fails with an error.
+ * @param error The error describing failure, if one occurred.
+ */
+typedef void (^FIRStorageVoidError)(NSError *_Nullable error) FIR_SWIFT_NAME(StorageVoidError);
+
+/**
+ * Block typedef typically used when retrieving metadata.
+ * @param metadata The metadata returned by the operation, if metadata exists.
+ */
+typedef void (^FIRStorageVoidMetadata)(FIRStorageMetadata *_Nullable metadata)
+ FIR_SWIFT_NAME(StorageVoidMetadata);
+
+/**
+ * Block typedef typically used when retrieving metadata with the possibility of an error.
+ * @param metadata The metadata returned by the operation, if metadata exists.
+ * @param error The error describing failure, if one occurred.
+ */
+typedef void (^FIRStorageVoidMetadataError)(FIRStorageMetadata *_Nullable metadata,
+ NSError *_Nullable error)
+ FIR_SWIFT_NAME(StorageVoidMetadataError);
+
+/**
+ * Block typedef typically used to asynchronously return a storage task snapshot.
+ * @param snapshot The returned task snapshot.
+ */
+typedef void (^FIRStorageVoidSnapshot)(FIRStorageTaskSnapshot *snapshot)
+ FIR_SWIFT_NAME(StorageVoidSnapshot);
+
+/**
+ * Block typedef typically used when retrieving a download URL.
+ * @param URL The download URL associated with the operation.
+ * @param error The error describing failure, if one occurred.
+ */
+typedef void (^FIRStorageVoidURLError)(NSURL *_Nullable URL, NSError *_Nullable error)
+ FIR_SWIFT_NAME(StorageVoidURLError);
+
+/**
+ * Enum representing the upload and download task status.
+ */
+typedef NS_ENUM(NSInteger, FIRStorageTaskStatus) {
+ /**
+ * Unknown task status.
+ */
+ FIRStorageTaskStatusUnknown,
+
+ /**
+ * Task is being resumed.
+ */
+ FIRStorageTaskStatusResume,
+
+ /**
+ * Task reported a progress event.
+ */
+ FIRStorageTaskStatusProgress,
+
+ /**
+ * Task is paused.
+ */
+ FIRStorageTaskStatusPause,
+
+ /**
+ * Task has completed successfully.
+ */
+ FIRStorageTaskStatusSuccess,
+
+ /**
+ * Task has failed and is unrecoverable.
+ */
+ FIRStorageTaskStatusFailure
+} FIR_SWIFT_NAME(StorageTaskStatus);
+
+/**
+ * Firebase Storage error domain.
+ */
+FOUNDATION_EXPORT NSString *const FIRStorageErrorDomain FIR_SWIFT_NAME(StorageErrorDomain);
+
+/**
+ * Enum representing the errors raised by Firebase Storage.
+ */
+typedef NS_ENUM(NSInteger, FIRStorageErrorCode) {
+ /** An unknown error occurred. */
+ FIRStorageErrorCodeUnknown = -13000,
+
+ /** No object exists at the desired reference. */
+ FIRStorageErrorCodeObjectNotFound = -13010,
+
+ /** No bucket is configured for Firebase Storage. */
+ FIRStorageErrorCodeBucketNotFound = -13011,
+
+ /** No project is configured for Firebase Storage. */
+ FIRStorageErrorCodeProjectNotFound = -13012,
+
+ /**
+ * Quota on your Firebase Storage bucket has been exceeded.
+ * If you're on the free tier, upgrade to a paid plan.
+ * If you're on a paid plan, reach out to Firebase support.
+ */
+ FIRStorageErrorCodeQuotaExceeded = -13013,
+
+ /** User is unauthenticated. Authenticate and try again. */
+ FIRStorageErrorCodeUnauthenticated = -13020,
+
+ /**
+ * User is not authorized to perform the desired action.
+ * Check your rules to ensure they are correct.
+ */
+ FIRStorageErrorCodeUnauthorized = -13021,
+
+ /**
+ * The maximum time limit on an operation (upload, download, delete, etc.) has been exceeded.
+ * Try uploading again.
+ */
+ FIRStorageErrorCodeRetryLimitExceeded = -13030,
+
+ /**
+ * File on the client does not match the checksum of the file received by the server.
+ * Try uploading again.
+ */
+ FIRStorageErrorCodeNonMatchingChecksum = -13031,
+
+ /**
+ * Size of the downloaded file exceeds the amount of memory allocated for the download.
+ * Increase memory cap and try downloading again.
+ */
+ FIRStorageErrorCodeDownloadSizeExceeded = -13032,
+
+ /** User cancelled the operation. */
+ FIRStorageErrorCodeCancelled = -13040
+} FIR_SWIFT_NAME(StorageErrorCode);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageConstants.m b/Firebase/Storage/FIRStorageConstants.m
new file mode 100644
index 0000000..aa3da1b
--- /dev/null
+++ b/Firebase/Storage/FIRStorageConstants.m
@@ -0,0 +1,83 @@
+// 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 "FIRStorageConstants.h"
+
+#import "FIRStorageConstants_Private.h"
+
+NSString *const kGCSScheme = @"https";
+NSString *const kGCSHost = @"www.googleapis.com";
+NSString *const kGCSUploadPath = @"upload";
+NSString *const kGCSStorageVersionPath = @"storage/v1";
+NSString *const kGCSBucketPathFormat = @"b/%@";
+NSString *const kGCSObjectPathFormat = @"o/%@";
+
+NSString *const kFIRStorageScheme = @"https";
+NSString *const kFIRStorageHost = @"firebasestorage.googleapis.com";
+NSString *const kFIRStorageVersionPath = @"v0";
+NSString *const kFIRStorageBucketPathFormat = @"b/%@";
+NSString *const kFIRStorageObjectPathFormat = @"o/%@";
+NSString *const kFIRStorageFullPathFormat = @"/v0/b/%@/o/%@";
+
+NSString *const kFIRStorageAuthTokenFormat = @"Firebase %@";
+NSString *const kFIRStorageDefaultBucketFormat = @"gs://%@";
+
+NSString *const kFIRStorageResponseErrorDomain = @"ResponseErrorDomain";
+NSString *const kFIRStorageResponseErrorCode = @"ResponseErrorCode";
+NSString *const kFIRStorageResponseBody = @"ResponseBody";
+
+NSString *const FIRStorageErrorDomain = @"FIRStorageErrorDomain";
+
+NSString *const kFIRStorageInvalidDataFormat = @"Invalid data returned from the server: %@";
+NSString *const kFIRStorageInvalidObserverStatus = @"Invalid observer status requested, use one "
+ @"of: FIRStorageTaskStatusPause, Resume, Progress, " @"Complete, or Failure";
+
+/**
+ * String constants mapping GCS Object#resource mappings to metadata fields.
+ */
+NSString *const kFIRStorageMetadataBucket = @"bucket";
+NSString *const kFIRStorageMetadataCacheControl = @"cacheControl";
+NSString *const kFIRStorageMetadataContentDisposition = @"contentDisposition";
+NSString *const kFIRStorageMetadataContentEncoding = @"contentEncoding";
+NSString *const kFIRStorageMetadataContentLanguage = @"contentLanguage";
+NSString *const kFIRStorageMetadataContentType = @"contentType";
+NSString *const kFIRStorageMetadataCustomMetadata = @"metadata";
+NSString *const kFIRStorageMetadataSize = @"size";
+NSString *const kFIRStorageMetadataDownloadURLs = @"downloadURLs";
+NSString *const kFIRStorageMetadataGeneration = @"generation";
+NSString *const kFIRStorageMetadataMetageneration = @"metageneration";
+NSString *const kFIRStorageMetadataTimeCreated = @"timeCreated";
+NSString *const kFIRStorageMetadataUpdated = @"updated";
+NSString *const kFIRStorageMetadataName = @"name";
+NSString *const kFIRStorageMetadataDownloadTokens = @"downloadTokens";
+
+// TODO: add notification support
+NSString *const kFIRStorageTaskStatusResumeNotification =
+ @"kFIRStorageTaskStatusResumeNotification";
+NSString *const kFIRStorageTaskStatusPauseNotification = @"kFIRStorageTaskStatusResumeNotification";
+NSString *const kFIRStorageTaskStatusProgressNotification =
+ @"kFIRStorageTaskStatusResumeNotification";
+NSString *const kFIRStorageTaskStatusCompleteNotification =
+ @"kFIRStorageTaskStatusResumeNotification";
+NSString *const kFIRStorageTaskStatusFailureNotification =
+ @"kFIRStorageTaskStatusResumeNotification";
+
+NSString *const kFIRStorageBundleIdentifier = @"com.google.firebase.storage";
+
+// The STR and STR_EXPAND macro allow a numeric version passed to he compiler driver
+// with a -D to be treated as a string instead of an invalid floating point value.
+#define STR(x) STR_EXPAND(x)
+#define STR_EXPAND(x) #x
+const unsigned char *const FIRStorageVersionString =
+ (const unsigned char *const) STR(FIRStorage_VERSION);
diff --git a/Firebase/Storage/FIRStorageDeleteTask.m b/Firebase/Storage/FIRStorageDeleteTask.m
new file mode 100644
index 0000000..4f3f1cc
--- /dev/null
+++ b/Firebase/Storage/FIRStorageDeleteTask.m
@@ -0,0 +1,54 @@
+// 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 "FIRStorageDeleteTask.h"
+
+#import "FIRStorageTask_Private.h"
+
+@implementation FIRStorageDeleteTask {
+ @private
+ FIRStorageVoidError _completion;
+}
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ completion:(FIRStorageVoidError)completion {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _completion = [completion copy];
+ }
+ return self;
+}
+
+- (void)enqueue {
+ NSMutableURLRequest *request = [self.baseRequest mutableCopy];
+ request.HTTPMethod = @"DELETE";
+ request.timeoutInterval = self.reference.storage.maxOperationRetryTime;
+
+ FIRStorageVoidError callback = _completion;
+ _completion = nil;
+
+ GTMSessionFetcher *fetcher = [self.fetcherService fetcherWithRequest:request];
+ fetcher.comment = @"DeleteTask";
+ [fetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
+ if (!self.error) {
+ self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference];
+ }
+ if (callback) {
+ callback(self.error);
+ }
+ }];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageDownloadTask.h b/Firebase/Storage/FIRStorageDownloadTask.h
new file mode 100644
index 0000000..252b910
--- /dev/null
+++ b/Firebase/Storage/FIRStorageDownloadTask.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 <Foundation/Foundation.h>
+
+#import "FIRStorageObservableTask.h"
+#import "FIRStorageSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FIRStorageDownloadTask implements resumable downloads from an object in Firebase Storage.
+ * Downloads can be returned on completion with a completion handler, and can be monitored
+ * by attaching observers, or controlled by calling FIRStorageTask#pause, FIRStorageTask#resume,
+ * or FIRStorageTask#cancel.
+ * Downloads can currently be returned as NSData in memory, or as an NSURL to a file on disk.
+ * Downloads are performed on a background queue, and callbacks are raised on the developer
+ * specified callbackQueue in FIRStorage, or the main queue if left unspecified.
+ * Currently all uploads must be initiated and managed on the main queue.
+ */
+FIR_SWIFT_NAME(StorageDownloadTask)
+@interface FIRStorageDownloadTask : FIRStorageObservableTask<FIRStorageTaskManagement>
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageDownloadTask.m b/Firebase/Storage/FIRStorageDownloadTask.m
new file mode 100644
index 0000000..0d71e52
--- /dev/null
+++ b/Firebase/Storage/FIRStorageDownloadTask.m
@@ -0,0 +1,162 @@
+// 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 "FIRStorageDownloadTask.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageDownloadTask_Private.h"
+#import "FIRStorageObservableTask_Private.h"
+#import "FIRStorageTask_Private.h"
+
+@implementation FIRStorageDownloadTask
+
+@synthesize progress = _progress;
+@synthesize fetcher = _fetcher;
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ file:(nullable NSURL *)fileURL {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _fileURL = [fileURL copy];
+ _progress = [NSProgress progressWithTotalUnitCount:0];
+ }
+ return self;
+}
+
+- (void)enqueue {
+ [self enqueueWithData:nil];
+}
+
+- (void)enqueueWithData:(nullable NSData *)resumeData {
+ NSAssert([NSThread isMainThread], @"Download attempting to execute on non main queue! Please "
+ @"only execute this method on the main queue.");
+ self.state = FIRStorageTaskStateQueueing;
+ NSMutableURLRequest *request = [self.baseRequest mutableCopy];
+ request.HTTPMethod = @"GET";
+ request.timeoutInterval = self.reference.storage.maxDownloadRetryTime;
+ NSURLComponents *components =
+ [NSURLComponents componentsWithURL:request.URL resolvingAgainstBaseURL:NO];
+ [components setQuery:@"alt=media"];
+ request.URL = components.URL;
+
+ GTMSessionFetcher *fetcher;
+ if (resumeData) {
+ fetcher = [GTMSessionFetcher fetcherWithDownloadResumeData:resumeData];
+ fetcher.comment = @"Resuming DownloadTask";
+ } else {
+ fetcher = [self.fetcherService fetcherWithRequest:request];
+ fetcher.comment = @"Starting DownloadTask";
+ }
+
+ [fetcher setResumeDataBlock:^(NSData *data) {
+ if (data) {
+ _downloadData = data;
+ }
+ }];
+
+ fetcher.maxRetryInterval = self.reference.storage.maxDownloadRetryTime;
+
+ if (_fileURL) {
+ // Handle file downloads
+ [fetcher setDestinationFileURL:_fileURL];
+ [fetcher setDownloadProgressBlock:^(int64_t bytesWritten, int64_t totalBytesWritten,
+ int64_t totalBytesExpectedToWrite) {
+ self.state = FIRStorageTaskStateProgress;
+ self.progress.completedUnitCount = totalBytesWritten;
+ self.progress.totalUnitCount = totalBytesExpectedToWrite;
+ FIRStorageTaskSnapshot *snapshot = self.snapshot;
+ [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:snapshot];
+ self.state = FIRStorageTaskStateRunning;
+ }];
+ } else {
+ // Handle data downloads
+ [fetcher setReceivedProgressBlock:^(int64_t bytesWritten, int64_t totalBytesWritten) {
+ self.state = FIRStorageTaskStateProgress;
+ self.progress.completedUnitCount = totalBytesWritten;
+ int64_t totalLength = [[self.fetcher response] expectedContentLength];
+ self.progress.totalUnitCount = totalLength;
+ FIRStorageTaskSnapshot *snapshot = self.snapshot;
+ [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:snapshot];
+ self.state = FIRStorageTaskStateRunning;
+ }];
+ }
+
+ _fetcher = fetcher;
+
+ self.state = FIRStorageTaskStateRunning;
+ [self.fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+ // Fire last progress updates
+ [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:self.snapshot];
+
+ // Handle potential issues with download
+ if (error) {
+ self.state = FIRStorageTaskStateFailed;
+ self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference];
+ [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot];
+ [self removeAllObservers];
+ return;
+ }
+
+ // Download completed successfully, fire completion callbacks
+ self.state = FIRStorageTaskStateSuccess;
+
+ if (data) {
+ _downloadData = data;
+ }
+
+ [self fireHandlersForStatus:FIRStorageTaskStatusSuccess snapshot:self.snapshot];
+ [self removeAllObservers];
+ }];
+}
+
+#pragma mark - Download Management
+
+- (void)cancel {
+ NSError *error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeCancelled];
+ [self cancelWithError:error];
+}
+
+- (void)cancelWithError:(NSError *)error {
+ NSAssert([NSThread isMainThread], @"Cancel attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStateCancelled;
+ [self.fetcher stopFetching];
+ self.error = error;
+ [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot];
+}
+
+- (void)pause {
+ NSAssert([NSThread isMainThread], @"Pause attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStatePausing;
+ [self.fetcher stopFetching];
+ // Give the resume callback a chance to run (if scheduled)
+ [self.fetcher waitForCompletionWithTimeout:0.001];
+ self.state = FIRStorageTaskStatePaused;
+ FIRStorageTaskSnapshot *snapshot = self.snapshot;
+ [self fireHandlersForStatus:FIRStorageTaskStatusPause snapshot:snapshot];
+}
+
+- (void)resume {
+ NSAssert([NSThread isMainThread], @"Resume attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStateResuming;
+ FIRStorageTaskSnapshot *snapshot = self.snapshot;
+ [self fireHandlersForStatus:FIRStorageTaskStatusResume snapshot:snapshot];
+ self.state = FIRStorageTaskStateRunning;
+ [self enqueueWithData:_downloadData];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageErrors.m b/Firebase/Storage/FIRStorageErrors.m
new file mode 100644
index 0000000..49a5ffa
--- /dev/null
+++ b/Firebase/Storage/FIRStorageErrors.m
@@ -0,0 +1,172 @@
+// 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 "FIRStorageErrors.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageReference_Private.h"
+
+@implementation FIRStorageErrors
+
++ (NSError *)errorWithCode:(FIRStorageErrorCode)code {
+ return [FIRStorageErrors errorWithCode:code infoDictionary:nil];
+}
+
++ (NSError *)errorWithCode:(FIRStorageErrorCode)code
+ infoDictionary:(nullable NSDictionary *)dictionary {
+ NSMutableDictionary *errorDictionary;
+ if (dictionary) {
+ errorDictionary = [dictionary mutableCopy];
+ } else {
+ errorDictionary = [[NSMutableDictionary alloc] init];
+ }
+
+ NSString *errorMessage;
+ switch (code) {
+ case FIRStorageErrorCodeObjectNotFound:
+ errorMessage =
+ [NSString stringWithFormat:@"Object %@ does not exist.", errorDictionary[@"object"]];
+ break;
+
+ case FIRStorageErrorCodeBucketNotFound:
+ errorMessage =
+ [NSString stringWithFormat:@"Bucket %@ does not exist.", errorDictionary[@"bucket"]];
+ break;
+
+ case FIRStorageErrorCodeProjectNotFound:
+ errorMessage =
+ [NSString stringWithFormat:@"Project %@ does not exist.", errorDictionary[@"project"]];
+ break;
+
+ case FIRStorageErrorCodeQuotaExceeded: {
+ NSString *const kQuotaExceededFormat =
+ @"Quota for bucket %@ exceeded, please view quota on firebase.google.com.";
+ errorMessage = [NSString stringWithFormat:kQuotaExceededFormat, errorDictionary[@"bucket"]];
+ break;
+ }
+
+ case FIRStorageErrorCodeDownloadSizeExceeded: {
+ int64_t total = [errorDictionary[@"totalSize"] longLongValue];
+ int64_t size = [errorDictionary[@"maxAllowedSize"] longLongValue];
+ NSString *totalString = total ? @(total).stringValue : @"unknown";
+ NSString *sizeString = total ? @(size).stringValue : @"unknown";
+ NSString *const kSizeExceededErrorFormat =
+ @"Attempeted to download object with size of %@ bytes, "
+ @"which exceeds the maximum size of %@ bytes. "
+ @"Consider raising the maximum download size, or using "
+ @"[FIRStorageReference writeToFile:]";
+ errorMessage = [NSString stringWithFormat:kSizeExceededErrorFormat, totalString, sizeString];
+ break;
+ }
+
+ case FIRStorageErrorCodeUnauthenticated:
+ errorMessage = @"User is not authenticated, please authenticate using Firebase "
+ @"Authentication and try again.";
+ break;
+
+ case FIRStorageErrorCodeUnauthorized: {
+ NSString *bucket = errorDictionary[@"bucket"];
+ NSString *object = errorDictionary[@"object"];
+ NSString *const kUnauthorizedFormat = @"User does not have permission to access gs://%@/%@.";
+ errorMessage = [NSString stringWithFormat:kUnauthorizedFormat, bucket, object];
+ break;
+ }
+
+ case FIRStorageErrorCodeRetryLimitExceeded:
+ errorMessage = @"Max retry time for operation exceeded, please try again.";
+ break;
+
+ case FIRStorageErrorCodeNonMatchingChecksum: {
+ // TODO: replace with actual checksum strings when we choose to implement.
+ NSString *const kChecksumFailedErrorFormat =
+ @"Uploaded/downloaded object %@ has checksum: %@ "
+ @"which does not match server checksum: %@. Please retry the upload/download.";
+ errorMessage = [NSString stringWithFormat:kChecksumFailedErrorFormat, @"object",
+ @"client checksum", @"server checksum"];
+ break;
+ }
+
+ case FIRStorageErrorCodeCancelled:
+ errorMessage = @"User cancelled the upload/download.";
+ break;
+
+ case FIRStorageErrorCodeUnknown:
+ /* Fall through to default case for unknown errors */
+
+ default:
+ errorMessage = @"An unknown error occurred, please check the server response.";
+ break;
+ }
+
+ errorDictionary[NSLocalizedDescriptionKey] = errorMessage;
+
+ NSError *err = [NSError errorWithDomain:FIRStorageErrorDomain code:code userInfo:errorDictionary];
+ return err;
+}
+
++ (nullable NSError *)errorWithServerError:(nullable NSError *)error
+ reference:(nullable FIRStorageReference *)reference {
+ if (error == nil) {
+ return nil;
+ }
+
+ FIRStorageErrorCode errorCode;
+ switch (error.code) {
+ case 400:
+ errorCode = FIRStorageErrorCodeUnknown;
+ break;
+
+ case 401:
+ errorCode = FIRStorageErrorCodeUnauthenticated;
+ break;
+
+ case 402:
+ errorCode = FIRStorageErrorCodeQuotaExceeded;
+ break;
+
+ case 403:
+ errorCode = FIRStorageErrorCodeUnauthorized;
+ break;
+
+ case 404:
+ errorCode = FIRStorageErrorCodeObjectNotFound;
+ break;
+
+ default:
+ errorCode = FIRStorageErrorCodeUnknown;
+ break;
+ }
+
+ NSMutableDictionary *errorDictionary =
+ [[[NSDictionary alloc] initWithDictionary:error.userInfo] mutableCopy];
+ errorDictionary[kFIRStorageResponseErrorDomain] = error.domain;
+ errorDictionary[kFIRStorageResponseErrorCode] = @(error.code);
+
+ // Turn raw response into a string
+ NSData *responseData = errorDictionary[@"data"];
+ if (responseData) {
+ NSString *errorString =
+ [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
+ errorDictionary[kFIRStorageResponseBody] = errorString ?: @"No Response from Server.";
+ }
+
+ errorDictionary[@"bucket"] = reference.path.bucket;
+ errorDictionary[@"object"] = reference.path.object;
+
+ NSError *clientError = [FIRStorageErrors errorWithCode:errorCode infoDictionary:errorDictionary];
+ return clientError;
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageGetMetadataTask.m b/Firebase/Storage/FIRStorageGetMetadataTask.m
new file mode 100644
index 0000000..d0e8981
--- /dev/null
+++ b/Firebase/Storage/FIRStorageGetMetadataTask.m
@@ -0,0 +1,84 @@
+// 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 "FIRStorageGetMetadataTask.h"
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageTask_Private.h"
+#import "FIRStorageUtils.h"
+
+#import "FirebaseStorage.h"
+
+@implementation FIRStorageGetMetadataTask {
+ @private
+ FIRStorageVoidMetadataError _completion;
+}
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ completion:(FIRStorageVoidMetadataError)completion {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _completion = [completion copy];
+ }
+ return self;
+}
+
+- (void)enqueue {
+ NSMutableURLRequest *request = [self.baseRequest mutableCopy];
+ request.HTTPMethod = @"GET";
+ request.timeoutInterval = self.reference.storage.maxDownloadRetryTime;
+
+ FIRStorageVoidMetadataError callback = _completion;
+ _completion = nil;
+
+ GTMSessionFetcher *fetcher = [self.fetcherService fetcherWithRequest:request];
+ fetcher.comment = @"GetMetadataTask";
+ [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+ if (error) {
+ if (!self.error) {
+ self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference];
+ }
+ if (callback) {
+ callback(nil, self.error);
+ }
+ return;
+ }
+
+ NSDictionary *responseDictionary = [NSDictionary frs_dictionaryFromJSONData:data];
+ if (responseDictionary != nil) {
+ FIRStorageMetadata *metadata =
+ [[FIRStorageMetadata alloc] initWithDictionary:responseDictionary];
+ [metadata setType:FIRStorageMetadataTypeFile];
+ if (callback) {
+ callback(metadata, nil);
+ }
+ } else {
+ NSString *returnedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ NSString *invalidDataString =
+ [NSString stringWithFormat:kFIRStorageInvalidDataFormat, returnedData];
+ NSDictionary *dict;
+ if (invalidDataString.length > 0) {
+ dict = @{NSLocalizedFailureReasonErrorKey : invalidDataString};
+ }
+ self.error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnknown infoDictionary:dict];
+ if (callback) {
+ callback(nil, self.error);
+ }
+ }
+ }];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageMetadata.h b/Firebase/Storage/FIRStorageMetadata.h
new file mode 100644
index 0000000..8d844f7
--- /dev/null
+++ b/Firebase/Storage/FIRStorageMetadata.h
@@ -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.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRStorageSwiftNameSupport.h"
+
+@class FIRStorageReference;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Class which represents the metadata on an object in Firebase Storage. This metadata is
+ * returned on successful operations, and can be used to retrieve download URLs, content types,
+ * and a FIRStorage reference to the object in question. Full documentation can be found at the GCS
+ * Objects#resource docs.
+ * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource
+ */
+FIR_SWIFT_NAME(StorageMetadata)
+@interface FIRStorageMetadata : NSObject<NSCopying>
+
+/**
+ * The name of the bucket containing this object.
+ */
+@property(copy, nonatomic, readonly) NSString *bucket;
+
+/**
+ * Cache-Control directive for the object data.
+ */
+@property(copy, nonatomic, nullable) NSString *cacheControl;
+
+/**
+ * Content-Disposition of the object data.
+ */
+@property(copy, nonatomic, nullable) NSString *contentDisposition;
+
+/**
+ * Content-Encoding of the object data.
+ */
+@property(copy, nonatomic, nullable) NSString *contentEncoding;
+
+/**
+ * Content-Language of the object data.
+ */
+@property(copy, nonatomic, nullable) NSString *contentLanguage;
+
+/**
+ * Content-Type of the object data.
+ */
+@property(copy, nonatomic, nullable) NSString *contentType;
+
+/**
+ * The content generation of this object. Used for object versioning.
+ */
+@property(readonly) int64_t generation;
+
+/**
+ * User-provided metadata, in key/value pairs.
+ */
+@property(copy, nonatomic, nullable) NSDictionary<NSString *, NSString *> *customMetadata;
+
+/**
+ * The version of the metadata for this object at this generation. Used
+ * for preconditions and for detecting changes in metadata. A metageneration number is only
+ * meaningful in the context of a particular generation of a particular object.
+ */
+@property(readonly) int64_t metageneration;
+
+/**
+ * The name of this object, in gs://bucket/path/to/object.txt, this is object.txt.
+ */
+@property(copy, nonatomic, readonly, nullable) NSString *name;
+
+/**
+ * The full path of this object, in gs://bucket/path/to/object.txt, this is path/to/object.txt.
+ */
+@property(copy, nonatomic, readonly, nullable) NSString *path;
+
+/**
+ * Content-Length of the data in bytes.
+ */
+@property(readonly) int64_t size;
+
+/**
+ * The creation time of the object in RFC 3339 format.
+ */
+@property(copy, nonatomic, readonly, nullable) NSDate *timeCreated;
+
+/**
+ * The modification time of the object metadata in RFC 3339 format.
+ */
+@property(copy, nonatomic, readonly, nullable) NSDate *updated;
+
+/**
+ * A reference to the object in Firebase Storage.
+ */
+@property(strong, nonatomic, readonly, nullable) FIRStorageReference *storageReference;
+
+/**
+ * An array containing all download URLs available for the object.
+ */
+@property(strong, nonatomic, readonly, nullable) NSArray<NSURL *> *downloadURLs;
+
+/**
+ * Creates an instanece of FIRStorageMetadata from the contents of a dictionary.
+ * @return An instance of FIRStorageMetadata that represents the contents of a dictionary.
+ */
+- (nullable instancetype)initWithDictionary:(NSDictionary <NSString *, id>*)dictionary
+ NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Creates an NSDictionary from the contents of the metadata.
+ * @return An NSDictionary that represents the contents of the metadata.
+ */
+- (NSDictionary <NSString *, id>*)dictionaryRepresentation;
+
+/**
+ * Determines if the current metadata represents a "file".
+ */
+@property(readonly, getter=isFile) BOOL file;
+
+/**
+ * Determines if the current metadata represents a "folder".
+ */
+@property(readonly, getter=isFolder) BOOL folder;
+
+/**
+ * Retrieves a download URL for the given object, or nil if none exist.
+ * Note that if there are many valid download tokens, this will always return the first
+ * valid token created.
+ */
+- (nullable NSURL *)downloadURL;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageMetadata.m b/Firebase/Storage/FIRStorageMetadata.m
new file mode 100644
index 0000000..6c85bbf
--- /dev/null
+++ b/Firebase/Storage/FIRStorageMetadata.m
@@ -0,0 +1,227 @@
+// 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 "FIRStorageMetadata.h"
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageUtils.h"
+
+// TODO: consider rewriting this using GTLR (GTLRStorageObjects.h)
+@implementation FIRStorageMetadata
+
+#pragma mark - Initializers
+
+- (instancetype)init {
+ return [self initWithDictionary:[NSDictionary dictionary]];
+}
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+ self = [super init];
+ if (self) {
+ _bucket = dictionary[kFIRStorageMetadataBucket];
+ _cacheControl = dictionary[kFIRStorageMetadataCacheControl];
+ _contentDisposition = dictionary[kFIRStorageMetadataContentDisposition];
+ _contentEncoding = dictionary[kFIRStorageMetadataContentEncoding];
+ _contentLanguage = dictionary[kFIRStorageMetadataContentLanguage];
+ _contentType = dictionary[kFIRStorageMetadataContentType];
+ _customMetadata = dictionary[kFIRStorageMetadataCustomMetadata];
+ _size = [dictionary[kFIRStorageMetadataSize] longLongValue];
+ _downloadURLs = dictionary[kFIRStorageMetadataDownloadURLs];
+ _generation = [dictionary[kFIRStorageMetadataGeneration] longLongValue];
+ _metageneration = [dictionary[kFIRStorageMetadataMetageneration] longLongValue];
+ _timeCreated = [self dateFromRFC3339String:dictionary[kFIRStorageMetadataTimeCreated]];
+ _updated = [self dateFromRFC3339String:dictionary[kFIRStorageMetadataUpdated]];
+ // GCS "name" is our path, our "name" is just the last path component of the path
+ _path = dictionary[kFIRStorageMetadataName];
+ _name = [_path lastPathComponent];
+ NSString *downloadTokens = dictionary[kFIRStorageMetadataDownloadTokens];
+ if (downloadTokens) {
+ NSArray<NSString *> *downloadStringArray = [downloadTokens componentsSeparatedByString:@","];
+ NSMutableArray<NSURL *> *downloadURLArray =
+ [[NSMutableArray alloc] initWithCapacity:[downloadStringArray count]];
+ [downloadStringArray enumerateObjectsUsingBlock:^(NSString *_Nonnull token, NSUInteger idx,
+ BOOL *_Nonnull stop) {
+ NSURLComponents *components = [[NSURLComponents alloc] init];
+ components.scheme = kFIRStorageScheme;
+ components.host = kFIRStorageHost;
+ NSString *path = [FIRStorageUtils GCSEscapedString:_path];
+ NSString *fullPath = [NSString stringWithFormat:kFIRStorageFullPathFormat, _bucket, path];
+ components.percentEncodedPath = fullPath;
+ components.query = [NSString stringWithFormat:@"alt=media&token=%@", token];
+
+ [downloadURLArray insertObject:[components URL] atIndex:idx];
+ }];
+ _downloadURLs = downloadURLArray;
+ }
+ }
+ return self;
+}
+
+#pragma mark - NSObject overrides
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+ return [[[self class] allocWithZone:zone] initWithDictionary:[self dictionaryRepresentation]];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FIRStorageMetadata class]]) {
+ return NO;
+ }
+
+ BOOL isEqualObject = [self isEqualToFIRStorageMetadata:(FIRStorageMetadata *)object];
+ return isEqualObject;
+}
+
+- (BOOL)isEqualToFIRStorageMetadata:(FIRStorageMetadata *)metadata {
+ return [[self dictionaryRepresentation] isEqualToDictionary:[metadata dictionaryRepresentation]];
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = [[self dictionaryRepresentation] hash];
+ return hash;
+}
+
+- (NSString *)description {
+ NSDictionary *metadataDictionary = [self dictionaryRepresentation];
+ return [NSString stringWithFormat:@"%@ %p: %@", [self class], self, metadataDictionary];
+}
+
+#pragma mark - Public methods
+
+- (NSDictionary *)dictionaryRepresentation {
+ NSMutableDictionary *metadataDictionary = [[NSMutableDictionary alloc] initWithCapacity:13];
+
+ if (_bucket) {
+ metadataDictionary[kFIRStorageMetadataBucket] = _bucket;
+ }
+
+ if (_cacheControl) {
+ metadataDictionary[kFIRStorageMetadataCacheControl] = _cacheControl;
+ }
+
+ if (_contentDisposition) {
+ metadataDictionary[kFIRStorageMetadataContentDisposition] = _contentDisposition;
+ }
+
+ if (_contentEncoding) {
+ metadataDictionary[kFIRStorageMetadataContentEncoding] = _contentEncoding;
+ }
+
+ if (_contentLanguage) {
+ metadataDictionary[kFIRStorageMetadataContentLanguage] = _contentLanguage;
+ }
+
+ if (_contentType) {
+ metadataDictionary[kFIRStorageMetadataContentType] = _contentType;
+ }
+
+ if (_customMetadata) {
+ metadataDictionary[kFIRStorageMetadataCustomMetadata] = _customMetadata;
+ }
+
+ if (_downloadURLs) {
+ NSMutableArray *downloadTokens = [[NSMutableArray alloc] init];
+ [_downloadURLs
+ enumerateObjectsUsingBlock:^(NSURL *_Nonnull URL, NSUInteger idx, BOOL *_Nonnull stop) {
+ NSArray *queryItems = [URL.query componentsSeparatedByString:@"&"];
+ [queryItems enumerateObjectsUsingBlock:^(NSString *queryString, NSUInteger idx,
+ BOOL *_Nonnull stop) {
+ NSString *key;
+ NSString *value;
+ NSScanner *scanner = [NSScanner scannerWithString:queryString];
+ [scanner scanUpToString:@"=" intoString:&key];
+ [scanner scanString:@"=" intoString:NULL];
+ [scanner scanUpToString:@"\n" intoString:&value];
+ if ([key isEqual:@"token"]) {
+ [downloadTokens addObject:value];
+ *stop = YES;
+ }
+ }];
+ }];
+ NSString *downloadTokenString = [downloadTokens componentsJoinedByString:@","];
+ metadataDictionary[kFIRStorageMetadataDownloadTokens] = downloadTokenString;
+ }
+
+ if (_generation) {
+ NSString *generationString = [NSString stringWithFormat:@"%lld", _generation];
+ metadataDictionary[kFIRStorageMetadataGeneration] = generationString;
+ }
+
+ if (_metageneration) {
+ NSString *metagenerationString = [NSString stringWithFormat:@"%lld", _metageneration];
+ metadataDictionary[kFIRStorageMetadataMetageneration] = metagenerationString;
+ }
+
+ if (_timeCreated) {
+ metadataDictionary[kFIRStorageMetadataTimeCreated] = [self RFC3339StringFromDate:_timeCreated];
+ }
+
+ if (_updated) {
+ metadataDictionary[kFIRStorageMetadataUpdated] = [self RFC3339StringFromDate:_updated];
+ }
+
+ if (_path) {
+ metadataDictionary[kFIRStorageMetadataName] = _path;
+ }
+
+ return [metadataDictionary copy];
+}
+
+- (BOOL)isFile {
+ return _type == FIRStorageMetadataTypeFile;
+}
+
+- (BOOL)isFolder {
+ return _type == FIRStorageMetadataTypeFolder;
+}
+
+- (nullable NSURL *)downloadURL {
+ return [_downloadURLs firstObject];
+}
+
+#pragma mark - RFC 3339 conversions
+
+static NSDateFormatter *sRFC3339DateFormatter;
+
+static void setupDateFormatterOnce(void) {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sRFC3339DateFormatter = [[NSDateFormatter alloc] init];
+ NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
+
+ [sRFC3339DateFormatter setLocale:enUSPOSIXLocale];
+ [sRFC3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSSZZZZZ"];
+ [sRFC3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
+ });
+}
+
+- (nullable NSDate *)dateFromRFC3339String:(NSString *)dateString {
+ setupDateFormatterOnce();
+ NSDate *rfc3339Date = [sRFC3339DateFormatter dateFromString:dateString];
+ return rfc3339Date;
+}
+
+- (nullable NSString *)RFC3339StringFromDate:(NSDate *)date {
+ setupDateFormatterOnce();
+ NSString *rfc3339String = [sRFC3339DateFormatter stringFromDate:date];
+ return rfc3339String;
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageObservableTask.h b/Firebase/Storage/FIRStorageObservableTask.h
new file mode 100644
index 0000000..502aba5
--- /dev/null
+++ b/Firebase/Storage/FIRStorageObservableTask.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 "FIRStorageSwiftNameSupport.h"
+#import "FIRStorageTask.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRStorageReference;
+@class FIRStorageTaskSnapshot;
+
+/**
+ * Extends FIRStorageTask to provide observable semantics such as adding and removing observers.
+ * Observers produce a FIRStorageHandle, which is used to keep track of and remove specific
+ * observers at a later date.
+ * This class is currently not thread safe and can only be called on the main thread.
+ */
+FIR_SWIFT_NAME(StorageObservableTask)
+@interface FIRStorageObservableTask : FIRStorageTask
+
+/**
+ * Observes changes in the upload status: Resume, Pause, Progress, Success, and Failure.
+ * @param status The FIRStorageTaskStatus change to observe.
+ * @param handler A callback that fires every time the status event occurs,
+ * returns a FIRStorageTaskSnapshot containing the state of the task.
+ * @return A task handle that can be used to remove the observer at a later date.
+ */
+- (FIRStorageHandle)observeStatus:(FIRStorageTaskStatus)status
+ handler:(void (^)(FIRStorageTaskSnapshot *snapshot))handler;
+
+/**
+ * Removes the single observer with the provided handle.
+ * @param handle The handle of the task to remove.
+ */
+- (void)removeObserverWithHandle:(FIRStorageHandle)handle;
+
+/**
+ * Removes all observers for a single status.
+ * @param status A FIRStorageTaskStatus to remove listeners for.
+ */
+- (void)removeAllObserversForStatus:(FIRStorageTaskStatus)status;
+
+/**
+ * Removes all observers.
+ */
+- (void)removeAllObservers;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageObservableTask.m b/Firebase/Storage/FIRStorageObservableTask.m
new file mode 100644
index 0000000..bac5924
--- /dev/null
+++ b/Firebase/Storage/FIRStorageObservableTask.m
@@ -0,0 +1,216 @@
+// 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 "FIRStorageObservableTask.h"
+#import "FIRStorageObservableTask_Private.h"
+#import "FIRStorageTask_Private.h"
+
+@implementation FIRStorageObservableTask {
+ @private
+ // Handlers for pause, resume, progress, success, and failure callbacks
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_resumeHandlers;
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_pauseHandlers;
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_progressHandlers;
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_successHandlers;
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_failureHandlers;
+ // Reverse map of fetcher handles to status types
+ NSMutableDictionary<NSString *, NSNumber *> *_handleToStatusMap;
+}
+
+@synthesize state = _state;
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _pauseHandlers = [[NSMutableDictionary alloc] init];
+ _resumeHandlers = [[NSMutableDictionary alloc] init];
+ _progressHandlers = [[NSMutableDictionary alloc] init];
+ _successHandlers = [[NSMutableDictionary alloc] init];
+ _failureHandlers = [[NSMutableDictionary alloc] init];
+ _handleToStatusMap = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+#pragma mark - Observers
+
+- (FIRStorageHandle)observeStatus:(FIRStorageTaskStatus)status
+ handler:(FIRStorageVoidSnapshot)handler {
+ FIRStorageVoidSnapshot callback = handler;
+ handler = nil;
+
+ // Note: self.snapshot is synchronized
+ FIRStorageTaskSnapshot *snapshot = self.snapshot;
+ // TODO: use an increasing counter instead of a random UUID
+ NSString *UUIDString = [[NSUUID UUID] UUIDString];
+ switch (status) {
+ case FIRStorageTaskStatusPause:
+ @synchronized(self) {
+ [_pauseHandlers setValue:callback forKey:UUIDString];
+ } // @synchronized(self)
+ if (_state == FIRStorageTaskStatePausing || _state == FIRStorageTaskStatePaused) {
+ [self fireHandlers:_pauseHandlers snapshot:snapshot];
+ }
+ break;
+
+ case FIRStorageTaskStatusResume:
+ @synchronized(self) {
+ [_resumeHandlers setValue:callback forKey:UUIDString];
+ } // @synchronized(self)
+ if (_state == FIRStorageTaskStateResuming || _state == FIRStorageTaskStateRunning) {
+ [self fireHandlers:_resumeHandlers snapshot:snapshot];
+ }
+ break;
+
+ case FIRStorageTaskStatusProgress:
+ @synchronized(self) {
+ [_progressHandlers setValue:callback forKey:UUIDString];
+ } // @synchronized(self)
+ if (_state == FIRStorageTaskStateRunning || _state == FIRStorageTaskStateProgress) {
+ [self fireHandlers:_progressHandlers snapshot:snapshot];
+ }
+ break;
+
+ case FIRStorageTaskStatusSuccess:
+ @synchronized(self) {
+ [_successHandlers setValue:callback forKey:UUIDString];
+ } // @synchronized(self)
+ if (_state == FIRStorageTaskStateSuccess) {
+ [self fireHandlers:_successHandlers snapshot:snapshot];
+ }
+ break;
+
+ case FIRStorageTaskStatusFailure:
+ @synchronized(self) {
+ [_failureHandlers setValue:callback forKey:UUIDString];
+ } // @synchronized(self)
+ if (_state == FIRStorageTaskStateFailing || _state == FIRStorageTaskStateFailed) {
+ [self fireHandlers:_failureHandlers snapshot:snapshot];
+ }
+ break;
+
+ case FIRStorageTaskStatusUnknown:
+ // Fall through to exception case if an unknown status is passed
+
+ default:
+ [NSException raise:NSInternalInconsistencyException
+ format:kFIRStorageInvalidObserverStatus, nil];
+ break;
+ }
+
+ @synchronized(self) {
+ _handleToStatusMap[UUIDString] = @(status);
+ } // @synchronized(self)
+
+ return UUIDString;
+}
+
+- (void)removeObserverWithHandle:(FIRStorageHandle)handle {
+ FIRStorageTaskStatus status = [_handleToStatusMap[handle] intValue];
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *observerDictionary =
+ [self handlerDictionaryForStatus:status];
+
+ @synchronized(self) {
+ [observerDictionary removeObjectForKey:handle];
+ [_handleToStatusMap removeObjectForKey:handle];
+ } // @synchronized(self)
+}
+
+- (void)removeAllObserversForStatus:(FIRStorageTaskStatus)status {
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *observerDictionary =
+ [self handlerDictionaryForStatus:status];
+ [self removeHandlersFromStatusMapForDictionary:observerDictionary];
+
+ @synchronized(self) {
+ [observerDictionary removeAllObjects];
+ } // @synchronized(self)
+}
+
+- (void)removeAllObservers {
+ @synchronized(self) {
+ [_pauseHandlers removeAllObjects];
+ [_resumeHandlers removeAllObjects];
+ [_progressHandlers removeAllObjects];
+ [_successHandlers removeAllObjects];
+ [_failureHandlers removeAllObjects];
+ [_handleToStatusMap removeAllObjects];
+ } // @synchronized(self)
+}
+
+- (NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *)handlerDictionaryForStatus:
+ (FIRStorageTaskStatus)status {
+ switch (status) {
+ case FIRStorageTaskStatusPause:
+ return _pauseHandlers;
+
+ case FIRStorageTaskStatusResume:
+ return _resumeHandlers;
+
+ case FIRStorageTaskStatusProgress:
+ return _progressHandlers;
+
+ case FIRStorageTaskStatusSuccess:
+ return _successHandlers;
+
+ case FIRStorageTaskStatusFailure:
+ return _failureHandlers;
+
+ case FIRStorageTaskStatusUnknown:
+ return [NSMutableDictionary dictionary];
+
+ default:
+ [NSException raise:NSInternalInconsistencyException
+ format:kFIRStorageInvalidObserverStatus, nil];
+ return nil;
+ }
+}
+
+- (void)removeHandlersFromStatusMapForDictionary:
+ (NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *)dict {
+ @synchronized(self) {
+ [_handleToStatusMap removeObjectsForKeys:dict.allKeys];
+ } // @synchronized(self)
+}
+
+- (void)fireHandlersForStatus:(FIRStorageTaskStatus)status
+ snapshot:(FIRStorageTaskSnapshot *)snapshot {
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *observerDictionary =
+ [self handlerDictionaryForStatus:status];
+ [self fireHandlers:observerDictionary snapshot:snapshot];
+}
+
+- (void)fireHandlers:(NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *)handlers
+ snapshot:(FIRStorageTaskSnapshot *)snapshot {
+ dispatch_queue_t callbackQueue = self.fetcherService.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ // TODO: iterate over this list in a consistent order
+ NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *handlersCopy;
+ @synchronized(self) {
+ handlersCopy = [handlers copy];
+ } // @synchronized(self)
+ [handlersCopy enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key,
+ FIRStorageVoidSnapshot _Nonnull handler,
+ BOOL *_Nonnull stop) {
+
+ dispatch_async(callbackQueue, ^{
+ handler(snapshot);
+ });
+ }];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStoragePath.m b/Firebase/Storage/FIRStoragePath.m
new file mode 100644
index 0000000..7188ab6
--- /dev/null
+++ b/Firebase/Storage/FIRStoragePath.m
@@ -0,0 +1,199 @@
+// 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 "FIRStoragePath.h"
+
+#import "FIRStorageConstants_Private.h"
+
+@implementation FIRStoragePath
+
+#pragma mark - Class methods
+
++ (nullable FIRStoragePath *)pathFromString:(NSString *)string {
+ if ([string hasPrefix:@"gs://"]) {
+ // "gs://bucket/path/to/object"
+ return [FIRStoragePath pathFromGSURI:string];
+ } else if ([string hasPrefix:@"http://"] || [string hasPrefix:@"https://"]) {
+ // "http[s]://firebasestorage.googleapis.com/bucket/path/to/object?signed_url_params"
+ return [FIRStoragePath pathFromHTTPURL:string];
+ } else {
+ // Invalid scheme, raise an exception!
+ [NSException raise:NSInternalInconsistencyException
+ format:@"URL scheme must be one of gs://, http://, or https:// "];
+ return nil;
+ }
+}
+
++ (nullable FIRStoragePath *)pathFromGSURI:(NSString *)aURIString {
+ NSString *bucketName;
+ NSString *objectName;
+ NSScanner *scanner = [NSScanner scannerWithString:aURIString];
+ BOOL isGSURI = [scanner scanString:@"gs://" intoString:NULL];
+ BOOL hasBucket = [scanner scanUpToString:@"/" intoString:&bucketName];
+ [scanner scanString:@"/" intoString:NULL];
+ [scanner scanUpToString:@"\n" intoString:&objectName];
+
+ if (!isGSURI || !hasBucket) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"URI must be in the form of gs://<bucket>/<path/to/object>"];
+ return nil;
+ }
+
+ return [[self alloc] initWithBucket:bucketName object:objectName];
+}
+
++ (nullable FIRStoragePath *)pathFromHTTPURL:(NSString *)aURLString {
+ NSString *bucketName;
+ NSString *objectName;
+ NSURL *httpsURL = [NSURL URLWithString:aURLString];
+ NSArray *pathComponents = httpsURL.pathComponents; // [/, v0, b, <bucket>, o, <objects/...>]
+
+ if ([httpsURL.host isEqual:kFIRStorageHost]) {
+ // Have a bucket name
+ if ([pathComponents count] > 3) {
+ bucketName = pathComponents[3];
+ }
+
+ // Have an object name
+ if ([pathComponents count] > 5) {
+ NSRange objectRange = NSMakeRange(5, [pathComponents count] - 5);
+ objectName = [[pathComponents subarrayWithRange:objectRange] componentsJoinedByString:@"/"];
+ }
+ }
+
+ if (bucketName.length == 0) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"URL must be in the form of "
+ @"http[s]://firebasestorage.googleapis.com/v0/b/<bucket>/o/<path/to/"
+ @"object>[?token=signed_url_params]"];
+ return nil;
+ }
+
+ if (objectName.length == 0) {
+ objectName = nil;
+ }
+
+ return [[self alloc] initWithBucket:bucketName object:objectName];
+}
+
+#pragma mark - Initializers
+
+- (instancetype)initWithBucket:(NSString *)bucket object:(nullable NSString *)object {
+ self = [super init];
+ if (self) {
+ _bucket = [bucket copy];
+ _object = [self standardizedPathForString:[object copy]];
+ }
+ return self;
+}
+
+#pragma mark - NSObject overrides
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+ return [[[self class] allocWithZone:zone] initWithBucket:_bucket object:_object];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FIRStoragePath class]]) {
+ return NO;
+ }
+
+ BOOL isObjectEqual = [self isEqualToFIRStoragePath:(FIRStoragePath *)object];
+ return isObjectEqual;
+}
+
+- (BOOL)isEqualToFIRStoragePath:(FIRStoragePath *)path {
+ BOOL isBucketEqual = _bucket == nil && path->_bucket == nil;
+ BOOL isObjectEqual = _object == nil && path->_object == nil;
+
+ if (_bucket && path->_bucket) {
+ isBucketEqual = [_bucket isEqual:path->_bucket];
+ }
+
+ if (_object && path.object) {
+ isObjectEqual = [_object isEqual:path->_object];
+ }
+
+ BOOL isEqual = isBucketEqual && isObjectEqual;
+ return isEqual;
+}
+
+- (NSUInteger)hash {
+ // "...because in those days, you could XOR anything with anything and get something useful..."
+ // https://www.usenix.org/system/files/1309_14-17_mickens.pdf
+ NSUInteger hash = [_bucket hash] ^ [_object hash];
+ return hash;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %p: %@", [self class], self, [self stringValue]];
+}
+
+- (NSString *)stringValue {
+ return [NSString stringWithFormat:@"gs://%@/%@", _bucket, _object ?: @""];
+}
+
+#pragma mark - Public methods
+
+- (FIRStoragePath *)child:(NSString *)path {
+ if (path.length == 0) {
+ return [self copy]; // Return a copy of the same path, nothing happened
+ }
+
+ NSString *childObject;
+ if (_object == nil) {
+ childObject = path;
+ } else {
+ childObject = [_object stringByAppendingPathComponent:path];
+ }
+
+ FIRStoragePath *childPath = [[FIRStoragePath alloc] initWithBucket:_bucket object:childObject];
+ return childPath;
+}
+
+- (nullable FIRStoragePath *)parent {
+ if (_object.length == 0) {
+ return nil;
+ }
+
+ NSString *parentObject = [_object stringByDeletingLastPathComponent];
+ FIRStoragePath *parentPath = [[FIRStoragePath alloc] initWithBucket:_bucket object:parentObject];
+ return parentPath;
+}
+
+- (FIRStoragePath *)root {
+ FIRStoragePath *rootPath = [[FIRStoragePath alloc] initWithBucket:_bucket object:nil];
+ return rootPath;
+}
+
+#pragma mark - Private methods
+
+// Removes leading and trailing slashes, and compresses multiple slashes
+// to create a canonical representation.
+// Example: /foo//bar///baz//// -> foo/bar/baz
+- (NSString *)standardizedPathForString:(NSString *)string {
+ NSMutableArray *components = [[string componentsSeparatedByString:@"/"] mutableCopy];
+ NSIndexSet *removedPaths =
+ [components indexesOfObjectsPassingTest:^BOOL(NSString *string, NSUInteger idx, BOOL *stop) {
+ return (string.length == 0);
+ }];
+ [components removeObjectsAtIndexes:removedPaths];
+ return [components componentsJoinedByString:@"/"];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageReference.h b/Firebase/Storage/FIRStorageReference.h
new file mode 100644
index 0000000..0b85267
--- /dev/null
+++ b/Firebase/Storage/FIRStorageReference.h
@@ -0,0 +1,244 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRStorage.h"
+#import "FIRStorageConstants.h"
+#import "FIRStorageDownloadTask.h"
+#import "FIRStorageMetadata.h"
+#import "FIRStorageSwiftNameSupport.h"
+#import "FIRStorageTask.h"
+#import "FIRStorageUploadTask.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FIRStorageReference represents a reference to a Google Cloud Storage object. Developers can
+ * upload and download objects, as well as get/set object metadata, and delete an object at the
+ * path.
+ * @see https://cloud.google.com/storage/
+ */
+FIR_SWIFT_NAME(StorageReference)
+@interface FIRStorageReference : NSObject
+
+/**
+ * The FIRStorage service object which created this reference.
+ */
+@property(nonatomic, readonly) FIRStorage *storage;
+
+/**
+ * The name of the Google Cloud Storage bucket associated with this reference,
+ * in gs://bucket/path/to/object.txt, the bucket would be: 'bucket'
+ */
+@property(nonatomic, readonly) NSString *bucket;
+
+/**
+ * The full path to this object, not including the Google Cloud Storage bucket.
+ * In gs://bucket/path/to/object.txt, the full path would be: 'path/to/object.txt'
+ */
+@property(nonatomic, readonly) NSString *fullPath;
+
+/**
+ * The short name of the object associated with this reference,
+ * in gs://bucket/path/to/object.txt, the name of the object would be: 'object.txt'
+ */
+@property(nonatomic, readonly) NSString *name;
+
+#pragma mark - Path Operations
+
+/**
+ * Creates a new FIRStorageReference pointing to the root object.
+ * @return A new FIRStorageReference pointing to the root object.
+ */
+- (FIRStorageReference *)root;
+
+/**
+ * Creates a new FIRStorageReference pointing to the parent of the current reference
+ * or nil if this instance references the root location.
+ * For example:
+ * path = foo/bar/baz parent = foo/bar
+ * path = foo parent = (root)
+ * path = (root) parent = nil
+ * @return A new FIRStorageReference pointing to the parent of the current reference.
+ */
+- (nullable FIRStorageReference *)parent;
+
+/**
+ * Creates a new FIRStorageReference pointing to a child object of the current reference.
+ * path = foo child = bar newPath = foo/bar
+ * path = foo/bar child = baz newPath = foo/bar/baz
+ * All leading and trailing slashes will be removed, and consecutive slashes will be
+ * compressed to single slashes. For example:
+ * child = /foo/bar newPath = foo/bar
+ * child = foo/bar/ newPath = foo/bar
+ * child = foo///bar newPath = foo/bar
+ * @param path Path to append to the current path.
+ * @return A new FIRStorageReference pointing to a child location of the current reference.
+ */
+- (FIRStorageReference *)child:(NSString *)path;
+
+#pragma mark - Uploads
+
+/**
+ * Asynchronously uploads data to the currently specified FIRStorageReference,
+ * without additional metadata.
+ * This is not recommended for large files, and one should instead upload a file from disk.
+ * @param uploadData The NSData to upload.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData FIR_SWIFT_NAME(putData(_:));
+
+/**
+ * Asynchronously uploads data to the currently specified FIRStorageReference.
+ * This is not recommended for large files, and one should instead upload a file from disk.
+ * @param uploadData The NSData to upload.
+ * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.)
+ * about the object being uploaded.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData
+ metadata:(nullable FIRStorageMetadata *)metadata
+ FIR_SWIFT_NAME(putData(_:metadata:));
+
+/**
+ * Asynchronously uploads data to the currently specified FIRStorageReference.
+ * This is not recommended for large files, and one should instead upload a file from disk.
+ * @param uploadData The NSData to upload.
+ * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.)
+ * about the object being uploaded.
+ * @param completion A completion block that either returns the object metadata on success,
+ * or an error on failure.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData
+ metadata:(nullable FIRStorageMetadata *)metadata
+ completion:(nullable void (^)(FIRStorageMetadata *_Nullable metadata,
+ NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(putData(_:metadata:completion:));
+
+/**
+ * Asynchronously uploads a file to the currently specified FIRStorageReference,
+ * without additional metadata.
+ * @param fileURL A URL representing the system file path of the object to be uploaded.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL FIR_SWIFT_NAME(putFile(from:));
+
+/**
+ * Asynchronously uploads a file to the currently specified FIRStorageReference.
+ * @param fileURL A URL representing the system file path of the object to be uploaded.
+ * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.)
+ * about the object being uploaded.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL metadata:(nullable FIRStorageMetadata *)metadata
+ FIR_SWIFT_NAME(putFile(from:metadata:));
+
+/**
+ * Asynchronously uploads a file to the currently specified FIRStorageReference.
+ * @param fileURL A URL representing the system file path of the object to be uploaded.
+ * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.)
+ * about the object being uploaded.
+ * @param completion A completion block that either returns the object metadata on success,
+ * or an error on failure.
+ * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload.
+ */
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL
+ metadata:(nullable FIRStorageMetadata *)metadata
+ completion:(nullable void (^)(FIRStorageMetadata *_Nullable metadata,
+ NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(putFile(from:metadata:completion:));
+
+#pragma mark - Downloads
+
+/**
+ * Asynchronously downloads the object at the FIRStorageReference to an NSData object in memory.
+ * An NSData of the provided max size will be allocated, so ensure that the device has enough free
+ * memory to complete the download. For downloading large files, writeToFile may be a better option.
+ * @param size The maximum size in bytes to download. If the download exceeds this size
+ * the task will be cancelled and an error will be returned.
+ * @param completion A completion block that either returns the object data on success,
+ * or an error on failure.
+ * @return An FIRStorageDownloadTask that can be used to monitor or manage the download.
+ */
+- (FIRStorageDownloadTask *)dataWithMaxSize:(int64_t)size
+ completion:(void (^)(NSData *_Nullable data,
+ NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(getData(maxSize:completion:));
+
+/**
+ * Asynchronously retrieves a long lived download URL with a revokable token.
+ * This can be used to share the file with others, but can be revoked by a developer
+ * in the Firebase Console if desired.
+ * @param completion A completion block that either returns the URL on success,
+ * or an error on failure.
+ */
+- (void)downloadURLWithCompletion:(void (^)(NSURL *_Nullable URL,
+ NSError *_Nullable error))completion;
+
+/**
+ * Asynchronously downloads the object at the current path to a specified system filepath.
+ * @param fileURL A file system URL representing the path the object should be downloaded to.
+ * @return An FIRStorageDownloadTask that can be used to monitor or manage the download.
+ */
+- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL;
+
+/**
+ * Asynchronously downloads the object at the current path to a specified system filepath.
+ * @param fileURL A file system URL representing the path the object should be downloaded to.
+ * @param completion A completion block that fires when the file download completes.
+ * Returns an NSURL pointing to the file path of the downloaded file on success,
+ * or an error on failure.
+ * @return An FIRStorageDownloadTask that can be used to monitor or manage the download.
+ */
+- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL
+ completion:(nullable void (^)(NSURL *_Nullable URL,
+ NSError *_Nullable error))completion;
+
+#pragma mark - Metadata Operations
+
+/**
+ * Retrieves metadata associated with an object at the current path.
+ * @param completion A completion block which returns the object metadata on success,
+ * or an error on failure.
+ */
+- (void)metadataWithCompletion:(void (^)(FIRStorageMetadata *_Nullable metadata,
+ NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(getMetadata(completion:));
+
+/**
+ * Updates the metadata associated with an object at the current path.
+ * @param metadata An FIRStorageMetadata object with the metadata to update.
+ * @param completion A completion block which returns the FIRStorageMetadata on success,
+ * or an error on failure.
+ */
+- (void)updateMetadata:(FIRStorageMetadata *)metadata
+ completion:(nullable void (^)(FIRStorageMetadata *_Nullable metadata,
+ NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(updateMetadata(_:completion:));
+
+#pragma mark - Delete
+
+/**
+ * Deletes the object at the current path.
+ * @param completion A completion block which returns nil on success, or an error on failure.
+ */
+- (void)deleteWithCompletion:(nullable void (^)(NSError *_Nullable error))completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageReference.m b/Firebase/Storage/FIRStorageReference.m
new file mode 100644
index 0000000..6e8105c
--- /dev/null
+++ b/Firebase/Storage/FIRStorageReference.m
@@ -0,0 +1,364 @@
+// 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 "FIRStorageReference.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageDeleteTask.h"
+#import "FIRStorageDownloadTask_Private.h"
+#import "FIRStorageGetMetadataTask.h"
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageReference_Private.h"
+#import "FIRStorageTaskSnapshot.h"
+#import "FIRStorageTaskSnapshot_Private.h"
+#import "FIRStorageTask_Private.h"
+#import "FIRStorageUpdateMetadataTask.h"
+#import "FIRStorageUploadTask_Private.h"
+#import "FIRStorageUtils.h"
+#import "FIRStorage_Private.h"
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import "GTMSessionFetcherService.h"
+
+@implementation FIRStorageReference
+
+- (instancetype)init {
+ FIRStorage *storage = [FIRStorage storage];
+ NSString *storageBucket = storage.app.options.storageBucket;
+ FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:storageBucket object:nil];
+ FIRStorageReference *reference = [self initWithStorage:storage path:path];
+ return reference;
+}
+
+- (instancetype)initWithStorage:(FIRStorage *)storage path:(FIRStoragePath *)path {
+ self = [super init];
+ if (self) {
+ _storage = storage;
+ _path = path;
+ }
+ return self;
+}
+
+#pragma mark - NSObject overrides
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+ FIRStorageReference *copiedReference =
+ [[[self class] allocWithZone:zone] initWithStorage:_storage path:_path];
+ return copiedReference;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FIRStorageReference class]]) {
+ return NO;
+ }
+
+ BOOL isObjectEqual = [self isEqualToFIRStorageReference:(FIRStorageReference *)object];
+ return isObjectEqual;
+}
+
+- (BOOL)isEqualToFIRStorageReference:(FIRStorageReference *)reference {
+ BOOL isEqual = [_storage isEqual:reference.storage] && [_path isEqual:reference.path];
+ return isEqual;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = [_storage hash] ^ [_path hash];
+ return hash;
+}
+
+- (NSString *)description {
+ return [self stringValue];
+}
+
+- (NSString *)stringValue {
+ NSString *value = [NSString stringWithFormat:@"gs://%@/%@", _path.bucket, _path.object ?: @""];
+ return value;
+}
+
+#pragma mark - Property Getters
+
+- (NSString *)bucket {
+ NSString *bucket = _path.bucket;
+ return bucket;
+}
+
+- (NSString *)fullPath {
+ NSString *path = _path.object;
+ if (!path) {
+ path = @"";
+ }
+ return path;
+}
+
+- (NSString *)name {
+ NSString *name = [_path.object lastPathComponent];
+ if (!name) {
+ name = @"";
+ }
+ return name;
+}
+
+#pragma mark - Path Operations
+
+- (FIRStorageReference *)root {
+ FIRStoragePath *rootPath = [_path root];
+ FIRStorageReference *rootReference =
+ [[FIRStorageReference alloc] initWithStorage:_storage path:rootPath];
+ return rootReference;
+}
+
+- (nullable FIRStorageReference *)parent {
+ FIRStoragePath *parentPath = [_path parent];
+ if (!parentPath) {
+ return nil;
+ }
+
+ FIRStorageReference *parentReference =
+ [[FIRStorageReference alloc] initWithStorage:_storage path:parentPath];
+ return parentReference;
+}
+
+- (FIRStorageReference *)child:(NSString *)path {
+ FIRStoragePath *childPath = [_path child:path];
+ FIRStorageReference *childReference =
+ [[FIRStorageReference alloc] initWithStorage:_storage path:childPath];
+ return childReference;
+}
+
+#pragma mark - Uploads
+
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData {
+ return [self putData:uploadData metadata:nil completion:nil];
+}
+
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData
+ metadata:(nullable FIRStorageMetadata *)metadata {
+ return [self putData:uploadData metadata:metadata completion:nil];
+}
+
+- (FIRStorageUploadTask *)putData:(NSData *)uploadData
+ metadata:(nullable FIRStorageMetadata *)metadata
+ completion:(nullable FIRStorageVoidMetadataError)completion {
+ if (!metadata) {
+ metadata = [[FIRStorageMetadata alloc] init];
+ }
+
+ metadata.path = _path.object;
+ metadata.name = [_path.object lastPathComponent];
+ FIRStorageUploadTask *task =
+ [[FIRStorageUploadTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ data:uploadData
+ metadata:metadata];
+
+ if (completion) {
+ dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(snapshot.metadata, nil);
+ });
+ }];
+ [task observeStatus:FIRStorageTaskStatusFailure
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(nil, snapshot.error);
+ });
+ }];
+ }
+ [task enqueue];
+ return task;
+}
+
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL {
+ return [self putFile:fileURL metadata:nil completion:nil];
+}
+
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL
+ metadata:(nullable FIRStorageMetadata *)metadata {
+ return [self putFile:fileURL metadata:metadata completion:nil];
+}
+
+- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL
+ metadata:(nullable FIRStorageMetadata *)metadata
+ completion:(nullable FIRStorageVoidMetadataError)completion {
+ if (!metadata) {
+ metadata = [[FIRStorageMetadata alloc] init];
+ }
+
+ metadata.path = _path.object;
+ metadata.name = [_path.object lastPathComponent];
+ FIRStorageUploadTask *task =
+ [[FIRStorageUploadTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ file:fileURL
+ metadata:metadata];
+
+ if (completion) {
+ dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(snapshot.metadata, nil);
+ });
+ }];
+ [task observeStatus:FIRStorageTaskStatusFailure
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(nil, snapshot.error);
+ });
+ }];
+ }
+ [task enqueue];
+ return task;
+}
+
+#pragma mark - Downloads
+
+- (FIRStorageDownloadTask *)dataWithMaxSize:(int64_t)size
+ completion:(FIRStorageVoidDataError)completion {
+ FIRStorageDownloadTask *task =
+ [[FIRStorageDownloadTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ file:nil];
+
+ dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ FIRStorageDownloadTask *task = snapshot.task;
+ dispatch_async(callbackQueue, ^{
+ completion(task.downloadData, nil);
+ });
+ }];
+ [task observeStatus:FIRStorageTaskStatusFailure
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(nil, snapshot.error);
+ });
+ }];
+ [task observeStatus:FIRStorageTaskStatusProgress
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ FIRStorageDownloadTask *task = snapshot.task;
+ if (task.progress.totalUnitCount > size ||
+ task.progress.completedUnitCount > size) {
+ NSDictionary *infoDictionary = @{
+ @"totalSize" : @(task.progress.totalUnitCount),
+ @"maxAllowedSize" : @(size)
+ };
+ NSError *error =
+ [FIRStorageErrors errorWithCode:FIRStorageErrorCodeDownloadSizeExceeded
+ infoDictionary:infoDictionary];
+ [task cancelWithError:error];
+ }
+ }];
+ [task enqueue];
+ return task;
+}
+
+- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL {
+ return [self writeToFile:fileURL completion:nil];
+}
+
+- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL
+ completion:(FIRStorageVoidURLError)completion {
+ FIRStorageDownloadTask *task =
+ [[FIRStorageDownloadTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ file:fileURL];
+ if (completion) {
+ dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ [task observeStatus:FIRStorageTaskStatusSuccess
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(fileURL, nil);
+ });
+ }];
+ [task observeStatus:FIRStorageTaskStatusFailure
+ handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) {
+ dispatch_async(callbackQueue, ^{
+ completion(nil, snapshot.error);
+ });
+ }];
+ }
+ [task enqueue];
+ return task;
+}
+
+- (void)downloadURLWithCompletion:(FIRStorageVoidURLError)completion {
+ dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ return [self metadataWithCompletion:^(FIRStorageMetadata *metadata, NSError *error) {
+ dispatch_async(callbackQueue, ^{
+ completion(metadata.downloadURL, error);
+ });
+ }];
+}
+
+#pragma mark - Metadata Operations
+
+- (void)metadataWithCompletion:(FIRStorageVoidMetadataError)completion {
+ FIRStorageGetMetadataTask *task =
+ [[FIRStorageGetMetadataTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ completion:completion];
+ [task enqueue];
+}
+
+- (void)updateMetadata:(FIRStorageMetadata *)metadata
+ completion:(nullable FIRStorageVoidMetadataError)completion {
+ FIRStorageUpdateMetadataTask *task =
+ [[FIRStorageUpdateMetadataTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ metadata:metadata
+ completion:completion];
+ [task enqueue];
+}
+
+#pragma mark - Delete
+
+- (void)deleteWithCompletion:(nullable FIRStorageVoidError)completion {
+ FIRStorageDeleteTask *task =
+ [[FIRStorageDeleteTask alloc] initWithReference:self
+ fetcherService:_storage.fetcherServiceForApp
+ completion:completion];
+ [task enqueue];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageSwiftNameSupport.h b/Firebase/Storage/FIRStorageSwiftNameSupport.h
new file mode 100644
index 0000000..529adf4
--- /dev/null
+++ b/Firebase/Storage/FIRStorageSwiftNameSupport.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.
+ */
+
+#ifndef FIR_SWIFT_NAME
+
+#import <Foundation/Foundation.h>
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// // Wrap it in our own macro if it's a non-compatible SDK.
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+
+#endif // FIR_SWIFT_NAME \ No newline at end of file
diff --git a/Firebase/Storage/FIRStorageTask.h b/Firebase/Storage/FIRStorageTask.h
new file mode 100644
index 0000000..0428220
--- /dev/null
+++ b/Firebase/Storage/FIRStorageTask.h
@@ -0,0 +1,76 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageMetadata.h"
+#import "FIRStorageSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A superclass to all FIRStorage*Tasks, including FIRStorageUploadTask
+ * and FIRStorageDownloadTask, to provide state transitions, event raising, and common storage
+ * or metadata and errors.
+ * Callbacks are always fired on the developer specified callback queue.
+ * If no queue is specified by the developer, it defaults to the main queue.
+ * Currently not thread safe, so only call methods on the main thread.
+ */
+FIR_SWIFT_NAME(StorageTask)
+@interface FIRStorageTask : NSObject
+
+/**
+ * An immutable view of the task and associated metadata, progress, error, etc.
+ */
+@property(strong, readonly, nonatomic, nonnull) FIRStorageTaskSnapshot *snapshot;
+
+@end
+
+/**
+ * Defines task operations such as pause, resume, cancel, and enqueue for all tasks.
+ * All tasks are required to implement enqueue, which begins the task, and may optionally
+ * implement pause, resume, and cancel, which operate on the task to pause, resume, and cancel
+ * operations.
+ */
+FIR_SWIFT_NAME(StorageTaskManagement)
+@protocol FIRStorageTaskManagement<NSObject>
+
+@required
+/**
+ * Prepares a task and begins execution.
+ */
+- (void)enqueue;
+
+@optional
+/**
+ * Pauses a task currently in progress.
+ */
+- (void)pause;
+
+/**
+ * Cancels a task currently in progress.
+ */
+- (void)cancel;
+
+/**
+ * Resumes a task that is paused.
+ */
+- (void)resume;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageTask.m b/Firebase/Storage/FIRStorageTask.m
new file mode 100644
index 0000000..3c1cf6f
--- /dev/null
+++ b/Firebase/Storage/FIRStorageTask.m
@@ -0,0 +1,65 @@
+// 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 "FIRStorageTask.h"
+
+#import "FIRStorage.h"
+#import "FIRStorage_Private.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageReference_Private.h"
+#import "FIRStorageTaskSnapshot.h"
+#import "FIRStorageTaskSnapshot_Private.h"
+#import "FIRStorageTask_Private.h"
+
+#import "GTMSessionFetcherService.h"
+
+@implementation FIRStorageTask
+
+- (instancetype)init {
+ FIRStorage *storage = [FIRStorage storage];
+ FIRStorageReference *reference = [storage reference];
+ FIRStorageTask *task =
+ [self initWithReference:reference fetcherService:storage.fetcherServiceForApp];
+ return task;
+}
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service {
+ self = [super init];
+ if (self) {
+ _reference = reference;
+ _baseRequest = [FIRStorageUtils defaultRequestForPath:reference.path];
+ _fetcherService = service;
+ _fetcherService.maxRetryInterval = _reference.storage.maxOperationRetryTime;
+ }
+ return self;
+}
+
+- (FIRStorageTaskSnapshot *)snapshot {
+ @synchronized(self) {
+ NSProgress *progress =
+ [NSProgress progressWithTotalUnitCount:self.progress.totalUnitCount];
+ progress.completedUnitCount = self.progress.completedUnitCount;
+ FIRStorageTaskSnapshot *snapshot =
+ [[FIRStorageTaskSnapshot alloc] initWithTask:self
+ state:self.state
+ metadata:self.metadata
+ reference:self.reference
+ progress:progress
+ error:[self.error copy]];
+ return snapshot;
+ }
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageTaskSnapshot.h b/Firebase/Storage/FIRStorageTaskSnapshot.h
new file mode 100644
index 0000000..b654c09
--- /dev/null
+++ b/Firebase/Storage/FIRStorageTaskSnapshot.h
@@ -0,0 +1,68 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRStorageMetadata;
+@class FIRStorageReference;
+@class FIRStorageTask;
+
+/**
+ * FIRStorageTaskSnapshot represents an immutable view of a task.
+ * A Snapshot contains a task, storage reference, metadata (if it exists),
+ * progress, and an error (if one occurred).
+ */
+FIR_SWIFT_NAME(StorageTaskSnapshot)
+@interface FIRStorageTaskSnapshot : NSObject
+
+/**
+ * Subclass of FIRStorageTask this snapshot represents.
+ */
+@property(readonly, copy, nonatomic) __kindof FIRStorageTask *task;
+
+/**
+ * Metadata returned by the task, or nil if no metadata returned.
+ */
+@property(readonly, copy, nonatomic, nullable) FIRStorageMetadata *metadata;
+
+/**
+ * FIRStorageReference this task is operates on.
+ */
+@property(readonly, copy, nonatomic) FIRStorageReference *reference;
+
+/**
+ * NSProgress object which tracks the progess of an upload or download.
+ */
+@property(readonly, strong, nonatomic, nullable) NSProgress *progress;
+
+/**
+ * Error during task execution, or nil if no error occurred.
+ */
+@property(readonly, copy, nonatomic, nullable) NSError *error;
+
+/**
+ * Status of the task.
+ */
+@property(readonly, nonatomic) FIRStorageTaskStatus status;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageTaskSnapshot.m b/Firebase/Storage/FIRStorageTaskSnapshot.m
new file mode 100644
index 0000000..050d05c
--- /dev/null
+++ b/Firebase/Storage/FIRStorageTaskSnapshot.m
@@ -0,0 +1,88 @@
+// 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 "FIRStorageTaskSnapshot.h"
+#import "FIRStorageTaskSnapshot_Private.h"
+
+#import "FIRStorageTask_Private.h"
+
+@implementation FIRStorageTaskSnapshot
+
+- (instancetype)initWithTask:(__kindof FIRStorageTask *)task
+ state:(FIRStorageTaskState)state
+ metadata:(nullable FIRStorageMetadata *)metadata
+ reference:(FIRStorageReference *)reference
+ progress:(nullable NSProgress *)progress
+ error:(nullable NSError *)error {
+ self = [super init];
+ if (self) {
+ _task = task;
+ _metadata = metadata;
+ _reference = reference;
+ _progress = progress;
+ _error = error;
+
+ switch (state) {
+ case FIRStorageTaskStateQueueing:
+ case FIRStorageTaskStateRunning:
+ case FIRStorageTaskStateResuming:
+ _status = FIRStorageTaskStatusResume;
+ break;
+
+ case FIRStorageTaskStateProgress:
+ _status = FIRStorageTaskStatusProgress;
+ break;
+
+ case FIRStorageTaskStatePaused:
+ case FIRStorageTaskStatePausing:
+ _status = FIRStorageTaskStatusPause;
+ break;
+
+ case FIRStorageTaskStateSuccess:
+ case FIRStorageTaskStateCompleting:
+ _status = FIRStorageTaskStatusSuccess;
+ break;
+
+ case FIRStorageTaskStateCancelled:
+ case FIRStorageTaskStateFailing:
+ case FIRStorageTaskStateFailed:
+ _status = FIRStorageTaskStatusFailure;
+ break;
+
+ default:
+ _status = FIRStorageTaskStatusUnknown;
+ }
+ }
+ return self;
+}
+
+
+-(NSString *)description {
+ switch (_status) {
+ case FIRStorageTaskStatusResume:
+ return @"<State: Resume>";
+ case FIRStorageTaskStatusProgress:
+ return [NSString stringWithFormat:@"<State: Progress, Progress: %@>", _progress];
+ case FIRStorageTaskStatusPause:
+ return @"<State: Paused>";
+ case FIRStorageTaskStatusSuccess:
+ return @"<State: Success>";
+ case FIRStorageTaskStatusFailure:
+ return [NSString stringWithFormat:@"<State: Failed, Error: %@>", _error];
+ default:
+ return @"<State: Unknown>";
+ };
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageTokenAuthorizer.m b/Firebase/Storage/FIRStorageTokenAuthorizer.m
new file mode 100644
index 0000000..36b94a9
--- /dev/null
+++ b/Firebase/Storage/FIRStorageTokenAuthorizer.m
@@ -0,0 +1,131 @@
+// 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 "../../Firebase/Core/Private/FIRAppInternal.h"
+
+#import "FIRStorageTokenAuthorizer.h"
+
+#import "FIRStorageConstants.h"
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageErrors.h"
+
+#import "FirebaseStorage.h"
+
+#import "FIRApp.h"
+#import "FIROptions.h"
+
+@implementation FIRStorageTokenAuthorizer {
+ @private
+ // Firebase App which vends tokens
+ FIRApp *_app;
+}
+
+@synthesize fetcherService = _fetcherService;
+
+- (instancetype)initWithApp:(FIRApp *)app fetcherService:(GTMSessionFetcherService *)service {
+ self = [super init];
+ if (self) {
+ _app = app;
+ _fetcherService = service;
+ }
+ return self;
+}
+
+#pragma mark - GTMFetcherAuthorizationProtocol methods
+
+- (void)authorizeRequest:(NSMutableURLRequest *)request
+ delegate:(id)delegate
+ didFinishSelector:(SEL)sel {
+ // Set version header on each request
+ NSString *versionString = [NSString stringWithFormat:@"ios/%s", FIRStorageVersionString];
+ [request setValue:versionString forHTTPHeaderField:@"x-firebase-storage-version"];
+
+ // Set GMP ID on each request
+ NSString *GMPAppId = _app.options.googleAppID;
+ [request setValue:GMPAppId forHTTPHeaderField:@"x-firebase-gmpid"];
+
+ if (delegate && sel) {
+ id selfParam = self;
+ NSMethodSignature *sig = [delegate methodSignatureForSelector:sel];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:sel];
+ [invocation setTarget:delegate];
+ [invocation setArgument:&selfParam atIndex:2];
+ [invocation setArgument:&request atIndex:3];
+
+ dispatch_queue_t callbackQueue = self.fetcherService.callbackQueue;
+ if (!callbackQueue) {
+ callbackQueue = dispatch_get_main_queue();
+ }
+
+ [invocation retainArguments];
+ if (_app.getTokenImplementation) {
+ [_app getTokenForcingRefresh:NO
+ withCallback:^(NSString *_Nullable token, NSError *_Nullable error) {
+ if (error) {
+ NSMutableDictionary *errorDictionary =
+ [NSMutableDictionary dictionaryWithDictionary:error.userInfo];
+ errorDictionary[kFIRStorageResponseErrorDomain] = error.domain;
+ errorDictionary[kFIRStorageResponseErrorCode] = @(error.code);
+
+ NSError *tokenError =
+ [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnauthenticated
+ infoDictionary:errorDictionary];
+ [invocation setArgument:&tokenError atIndex:4];
+ } else if (token) {
+ NSString *firebaseToken =
+ [NSString stringWithFormat:kFIRStorageAuthTokenFormat, token];
+ [request setValue:firebaseToken forHTTPHeaderField:@"Authorization"];
+ }
+ dispatch_async(callbackQueue, ^{
+ [invocation invoke];
+ });
+ }];
+ } else {
+ dispatch_async(callbackQueue, ^{
+ [invocation invoke];
+ });
+ }
+ }
+}
+
+// Note that stopAuthorization, isAuthorizingRequest, and userEmail
+// aren't relevant with the Firebase App/Auth implementation of tokens,
+// and thus aren't implemented. Token refresh is handled transparently
+// for us, and we don't allow the auth request to be stopped.
+// Auth is also not required so the world doesn't stop.
+- (void)stopAuthorization {
+ // Noop
+}
+
+- (void)stopAuthorizationForRequest:(NSURLRequest *)request {
+ // Noop
+}
+
+- (BOOL)isAuthorizingRequest:(NSURLRequest *)request {
+ return NO;
+}
+
+- (BOOL)isAuthorizedRequest:(NSURLRequest *)request {
+ NSString *authHeader = request.allHTTPHeaderFields[@"Authorization"];
+ BOOL isFirebaseToken = [authHeader hasPrefix:@"Firebase"];
+ return isFirebaseToken;
+}
+
+- (NSString *)userEmail {
+ // Noop
+ return nil;
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageUpdateMetadataTask.m b/Firebase/Storage/FIRStorageUpdateMetadataTask.m
new file mode 100644
index 0000000..dbd276b
--- /dev/null
+++ b/Firebase/Storage/FIRStorageUpdateMetadataTask.m
@@ -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 "FIRStorageUpdateMetadataTask.h"
+
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageTask_Private.h"
+
+@implementation FIRStorageUpdateMetadataTask {
+ @private
+ FIRStorageVoidMetadataError _completion;
+ // Metadata used in the update request
+ FIRStorageMetadata *_updateMetadata;
+}
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ metadata:(FIRStorageMetadata *)metadata
+ completion:(FIRStorageVoidMetadataError)completion {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _updateMetadata = [metadata copy];
+ _completion = [completion copy];
+ }
+ return self;
+}
+
+- (void)enqueue {
+ NSMutableURLRequest *request = [self.baseRequest mutableCopy];
+ NSDictionary *updateDictionary = [_updateMetadata dictionaryRepresentation];
+ NSData *updateData = [NSData frs_dataFromJSONDictionary:updateDictionary];
+ request.HTTPMethod = @"PATCH";
+ request.timeoutInterval = self.reference.storage.maxUploadRetryTime;
+ request.HTTPBody = updateData;
+ NSString *typeString = @"application/json; charset=UTF-8";
+ [request setValue:typeString forHTTPHeaderField:@"Content-Type"];
+ NSString *lengthString = [NSString stringWithFormat:@"%zu", (unsigned long)[updateData length]];
+ [request setValue:lengthString forHTTPHeaderField:@"Content-Length"];
+
+ FIRStorageVoidMetadataError callback = _completion;
+ _completion = nil;
+
+ GTMSessionFetcher *fetcher = [self.fetcherService fetcherWithRequest:request];
+ fetcher.comment = @"UpdateMetadataTask";
+ [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+ if (error) {
+ if (!self.error) {
+ self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference];
+ }
+ if (callback) {
+ callback(nil, self.error);
+ }
+ return;
+ }
+
+ NSDictionary *responseDictionary = [NSDictionary frs_dictionaryFromJSONData:data];
+ if (responseDictionary) {
+ FIRStorageMetadata *metadata =
+ [[FIRStorageMetadata alloc] initWithDictionary:responseDictionary];
+ [metadata setType:FIRStorageMetadataTypeFile];
+ if (callback){
+ callback(metadata, nil);
+ }
+ } else {
+ NSString *returnedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ NSString *invalidDataString =
+ [NSString stringWithFormat:kFIRStorageInvalidDataFormat, returnedData];
+ NSDictionary *dict;
+ if (invalidDataString.length > 0) {
+ dict = @{NSLocalizedFailureReasonErrorKey : invalidDataString};
+ }
+ self.error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnknown infoDictionary:dict];
+ if (callback) {
+ callback(nil, self.error);
+ }
+ }
+ }];
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageUploadTask.h b/Firebase/Storage/FIRStorageUploadTask.h
new file mode 100644
index 0000000..cdf1d29
--- /dev/null
+++ b/Firebase/Storage/FIRStorageUploadTask.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 <Foundation/Foundation.h>
+
+#import "FIRStorageObservableTask.h"
+#import "FIRStorageSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FIRStorageUploadTask implements resumable uploads to a file in Firebase Storage.
+ * Uploads can be returned on completion with a completion callback, and can be monitored
+ * by attaching observers, or controlled by calling FIRStorageTask#pause, FIRStorageTask#resume,
+ * or FIRStorageTask#cancel.
+ * Uploads can take NSData in memory, or an NSURL to a file on disk.
+ * Uploads are performed on a background queue, and callbacks are raised on the developer
+ * specified callbackQueue in FIRStorage, or the main queue if left unspecified.
+ * Currently all uploads must be initiated and managed on the main queue.
+ */
+FIR_SWIFT_NAME(StorageUploadTask)
+@interface FIRStorageUploadTask : FIRStorageObservableTask<FIRStorageTaskManagement>
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/FIRStorageUploadTask.m b/Firebase/Storage/FIRStorageUploadTask.m
new file mode 100644
index 0000000..74741b0
--- /dev/null
+++ b/Firebase/Storage/FIRStorageUploadTask.m
@@ -0,0 +1,199 @@
+// 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 "FIRStorageUploadTask.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStorageMetadata_Private.h"
+#import "FIRStorageObservableTask_Private.h"
+#import "FIRStorageTask_Private.h"
+#import "FIRStorageUploadTask_Private.h"
+
+#import "GTMSessionUploadFetcher.h"
+
+@implementation FIRStorageUploadTask
+
+@synthesize progress = _progress;
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ data:(NSData *)uploadData
+ metadata:(FIRStorageMetadata *)metadata {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _uploadMetadata = [metadata copy];
+ _uploadData = [uploadData copy];
+ _progress = [NSProgress progressWithTotalUnitCount:[_uploadData length]];
+
+ if (!_uploadMetadata.contentType) {
+ _uploadMetadata.contentType = @"application/octet-stream";
+ }
+ }
+ return self;
+}
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ file:(NSURL *)fileURL
+ metadata:(FIRStorageMetadata *)metadata {
+ self = [super initWithReference:reference fetcherService:service];
+ if (self) {
+ _uploadMetadata = [metadata copy];
+ _fileURL = [fileURL copy];
+ _progress = [NSProgress progressWithTotalUnitCount:0];
+
+ NSString *mimeType = [FIRStorageUtils MIMETypeForExtension:[_fileURL pathExtension]];
+
+ if (!_uploadMetadata.contentType) {
+ _uploadMetadata.contentType = mimeType ?: @"application/octet-stream";
+ }
+ }
+ return self;
+}
+
+- (void)enqueue {
+ NSAssert([NSThread isMainThread], @"Upload attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStateQueueing;
+
+ NSMutableURLRequest *request = [self.baseRequest mutableCopy];
+ request.HTTPMethod = @"POST";
+ request.timeoutInterval = self.reference.storage.maxUploadRetryTime;
+ NSData *bodyData = [NSData frs_dataFromJSONDictionary:[_uploadMetadata dictionaryRepresentation]];
+ request.HTTPBody = bodyData;
+ [request setValue:@"application/json; charset=UTF-8" forHTTPHeaderField:@"Content-Type"];
+ NSString *contentLengthString = [NSString stringWithFormat:@"%zu",
+ (unsigned long)[bodyData length]];
+ [request setValue:contentLengthString forHTTPHeaderField:@"Content-Length"];
+
+ NSURLComponents *components =
+ [NSURLComponents componentsWithURL:request.URL resolvingAgainstBaseURL:NO];
+
+ if ([components.host isEqual:kGCSHost]) {
+ [components setPercentEncodedPath:[@"/upload" stringByAppendingString:components.path]];
+ }
+
+ NSDictionary *queryParams = @{ @"uploadType" : @"resumable", @"name" : self.uploadMetadata.path };
+ [components setPercentEncodedQuery:[FIRStorageUtils queryStringForDictionary:queryParams]];
+ request.URL = components.URL;
+
+ GTMSessionUploadFetcher *uploadFetcher =
+ [GTMSessionUploadFetcher uploadFetcherWithRequest:request
+ uploadMIMEType:_uploadMetadata.contentType
+ chunkSize:kGTMSessionUploadFetcherStandardChunkSize
+ fetcherService:self.fetcherService];
+
+ if (_uploadData) {
+ [uploadFetcher setUploadData:_uploadData];
+ uploadFetcher.comment = @"Data UploadTask";
+ } else if (_fileURL) {
+ [uploadFetcher setUploadFileURL:_fileURL];
+ uploadFetcher.comment = @"File UploadTask";
+ }
+
+ uploadFetcher.maxRetryInterval = self.reference.storage.maxUploadRetryTime;
+
+ [uploadFetcher setSendProgressBlock:^(int64_t bytesSent, int64_t totalBytesSent,
+ int64_t totalBytesExpectedToSend) {
+ self.state = FIRStorageTaskStateProgress;
+ self.progress.completedUnitCount = totalBytesSent;
+ self.progress.totalUnitCount = totalBytesExpectedToSend;
+ self.metadata = _uploadMetadata;
+ [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:self.snapshot];
+ self.state = FIRStorageTaskStateRunning;
+ }];
+
+ _uploadFetcher = uploadFetcher;
+
+ // Process fetches
+ self.state = FIRStorageTaskStateRunning;
+ [_uploadFetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data,
+ NSError *_Nullable error) {
+ // Fire last progress updates
+ [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:self.snapshot];
+
+ // Handle potential issues with upload
+ if (error) {
+ self.state = FIRStorageTaskStateFailed;
+ self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference];
+ self.metadata = _uploadMetadata;
+ [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot];
+ [self removeAllObservers];
+ return;
+ }
+
+ // Upload completed successfully, fire completion callbacks
+ self.state = FIRStorageTaskStateSuccess;
+
+ NSDictionary *responseDictionary = [NSDictionary frs_dictionaryFromJSONData:data];
+ if (responseDictionary) {
+ FIRStorageMetadata *metadata =
+ [[FIRStorageMetadata alloc] initWithDictionary:responseDictionary];
+ [metadata setType:FIRStorageMetadataTypeFile];
+ self.metadata = metadata;
+ } else {
+ NSString *returnedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ NSString *invalidDataString =
+ [NSString stringWithFormat:kFIRStorageInvalidDataFormat, returnedData];
+ NSDictionary *dict;
+ if (invalidDataString.length > 0) {
+ dict = @{NSLocalizedFailureReasonErrorKey : invalidDataString};
+ }
+ self.error =
+ [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnknown infoDictionary:dict];
+ }
+
+ [self fireHandlersForStatus:FIRStorageTaskStatusSuccess snapshot:self.snapshot];
+ [self removeAllObservers];
+ }];
+}
+
+#pragma mark - Upload Management
+
+- (void)cancel {
+ NSAssert([NSThread isMainThread], @"Cancel attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStateCancelled;
+ [_uploadFetcher stopFetching];
+ if (self.state != FIRStorageTaskStateSuccess) {
+ self.metadata = _uploadMetadata;
+ }
+ self.error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeCancelled];
+ [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot];
+}
+
+- (void)pause {
+ NSAssert([NSThread isMainThread], @"Pause attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStatePaused;
+ [_uploadFetcher pauseFetching];
+ if (self.state != FIRStorageTaskStateSuccess) {
+ self.metadata = _uploadMetadata;
+ }
+ [self fireHandlersForStatus:FIRStorageTaskStatusPause snapshot:self.snapshot];
+}
+
+- (void)resume {
+ NSAssert([NSThread isMainThread], @"Resume attempting to execute on non main queue! Please only "
+ @"execute this method on the main queue.");
+ self.state = FIRStorageTaskStateResuming;
+ [_uploadFetcher resumeFetching];
+ if (self.state != FIRStorageTaskStateSuccess) {
+ self.metadata = _uploadMetadata;
+ }
+ [self fireHandlersForStatus:FIRStorageTaskStatusResume snapshot:self.snapshot];
+ self.state = FIRStorageTaskStateRunning;
+}
+
+@end
diff --git a/Firebase/Storage/FIRStorageUtils.m b/Firebase/Storage/FIRStorageUtils.m
new file mode 100644
index 0000000..e0abe0a
--- /dev/null
+++ b/Firebase/Storage/FIRStorageUtils.m
@@ -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.
+
+#import <MobileCoreServices/MobileCoreServices.h>
+
+#import "FIRStorageUtils.h"
+
+#import "FIRStorageConstants_Private.h"
+#import "FIRStoragePath.h"
+
+#import "FirebaseStorage.h"
+
+#import "GTMSessionFetcher.h"
+
+// This is the list at https://cloud.google.com/storage/docs/json_api/ without & and +.
+NSString *const kGCSObjectAllowedCharacterSet =
+ @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$'()*,;=:@";
+
+@implementation FIRStorageUtils
+
++ (nullable NSString *)GCSEscapedString:(NSString *)string {
+ NSCharacterSet *allowedCharacters =
+ [NSCharacterSet characterSetWithCharactersInString:kGCSObjectAllowedCharacterSet];
+
+ return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];
+}
+
++ (nullable NSString *)MIMETypeForExtension:(NSString *)extension {
+ if (extension == nil) {
+ return nil;
+ }
+
+ CFStringRef pathExtension = (__bridge_retained CFStringRef)extension;
+ CFStringRef type =
+ UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension, NULL);
+ NSString *mimeType =
+ (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(type, kUTTagClassMIMEType);
+ CFRelease(pathExtension);
+ if (type != NULL) {
+ CFRelease(type);
+ }
+
+ return mimeType;
+}
+
++ (NSString *)queryStringForDictionary:(nullable NSDictionary *)dictionary {
+ if (!dictionary) {
+ return @"";
+ }
+
+ __block NSMutableArray *queryItems = [[NSMutableArray alloc] initWithCapacity:[dictionary count]];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull name, NSString *_Nonnull value,
+ BOOL *_Nonnull stop) {
+ NSString *item =
+ [FIRStorageUtils GCSEscapedString:[NSString stringWithFormat:@"%@=%@", name, value]];
+ [queryItems addObject:item];
+ }];
+ return [queryItems componentsJoinedByString:@"&"];
+}
+
++ (NSURLRequest *)defaultRequestForPath:(FIRStoragePath *)path {
+ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
+ NSURLComponents *components = [[NSURLComponents alloc] init];
+ [components setScheme:kFIRStorageScheme];
+ [components setHost:kFIRStorageHost];
+ NSString *encodedPath = [self encodedURLForPath:path];
+ [components setPercentEncodedPath:encodedPath];
+ [request setURL:components.URL];
+ return request;
+}
+
++ (NSString *)encodedURLForPath:(FIRStoragePath *)path {
+ NSString *bucketName = [FIRStorageUtils GCSEscapedString:path.bucket];
+ NSString *objectName = [FIRStorageUtils GCSEscapedString:path.object];
+ NSString *bucketFormat = [NSString stringWithFormat:kFIRStorageBucketPathFormat, bucketName];
+ NSString *urlPath = [@"/" stringByAppendingPathComponent:bucketFormat];
+ if (objectName) {
+ NSString *objectFormat = [NSString stringWithFormat:kFIRStorageObjectPathFormat, objectName];
+ urlPath = [urlPath stringByAppendingFormat:@"/%@", objectFormat];
+ } else {
+ urlPath = [urlPath stringByAppendingString:@"/o"];
+ }
+ return [@"/" stringByAppendingString:[kFIRStorageVersionPath stringByAppendingString:urlPath]];
+}
+
+@end
+
+@implementation NSDictionary (FIRStorageNSDictionaryJSONHelpers)
+
++ (nullable instancetype)frs_dictionaryFromJSONData:(nullable NSData *)data {
+ if (!data) {
+ return nil;
+ }
+ return [NSJSONSerialization JSONObjectWithData:data
+ options:NSJSONReadingMutableContainers
+ error:nil];
+}
+
+@end
+
+@implementation NSData (FIRStorageNSDataJSONHelpers)
+
++ (nullable instancetype)frs_dataFromJSONDictionary:(nullable NSDictionary *)dictionary {
+ if (!dictionary) {
+ return nil;
+ }
+ return [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil];
+}
+
+@end \ No newline at end of file
diff --git a/Firebase/Storage/FirebaseStorage.h b/Firebase/Storage/FirebaseStorage.h
new file mode 100644
index 0000000..1e5dfa4
--- /dev/null
+++ b/Firebase/Storage/FirebaseStorage.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 "FIRStorage.h"
+#import "FIRStorageConstants.h"
+#import "FIRStorageDownloadTask.h"
+#import "FIRStorageMetadata.h"
+#import "FIRStorageObservableTask.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageTask.h"
+#import "FIRStorageTaskSnapshot.h"
+#import "FIRStorageUploadTask.h"
diff --git a/Firebase/Storage/FirebaseStorage.podspec b/Firebase/Storage/FirebaseStorage.podspec
new file mode 100644
index 0000000..69c6ddc
--- /dev/null
+++ b/Firebase/Storage/FirebaseStorage.podspec
@@ -0,0 +1,44 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseStorage'
+ s.version = '2.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]'
+ s.public_header_files =
+ 'FirebaseStorage.h',
+ 'FIRStorage.h',
+ 'FIRStorageConstants.h',
+ 'FIRStorageDownloadTask.h',
+ 'FIRStorageMetadata.h',
+ 'FIRStorageObservableTask.h',
+ 'FIRStorageReference.h',
+ 'FIRStorageSwiftNameSupport.h',
+ 'FIRStorageTask.h',
+ 'FIRStorageTaskSnapshot.h',
+ 'FIRStorageUploadTask.h'
+
+ s.framework = 'MobileCoreServices'
+# s.dependency 'FirebaseDev/Core'
+ s.dependency 'GTMSessionFetcher/Core', '~> 1.1'
+ s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' +
+ 'FIRStorage_VERSION=' + s.version.to_s }
+end
diff --git a/Firebase/Storage/Private/FIRStorageConstants_Private.h b/Firebase/Storage/Private/FIRStorageConstants_Private.h
new file mode 100644
index 0000000..50addb1
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageConstants_Private.h
@@ -0,0 +1,145 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FIRStorageMetadata;
+
+NS_ASSUME_NONNULL_BEGIN
+
+FOUNDATION_EXPORT NSString *const kGCSScheme;
+FOUNDATION_EXPORT NSString *const kGCSHost;
+FOUNDATION_EXPORT NSString *const kGCSUploadPath;
+FOUNDATION_EXPORT NSString *const kGCSStorageVersionPath;
+FOUNDATION_EXPORT NSString *const kGCSBucketPathFormat;
+FOUNDATION_EXPORT NSString *const kGCSObjectPathFormat;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageScheme;
+FOUNDATION_EXPORT NSString *const kFIRStorageHost;
+FOUNDATION_EXPORT NSString *const kFIRStorageVersionPath;
+FOUNDATION_EXPORT NSString *const kFIRStorageBucketPathFormat;
+FOUNDATION_EXPORT NSString *const kFIRStorageObjectPathFormat;
+FOUNDATION_EXPORT NSString *const kFIRStorageFullPathFormat;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageAuthTokenFormat;
+FOUNDATION_EXPORT NSString *const kFIRStorageDefaultBucketFormat;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageResponseErrorDomain;
+FOUNDATION_EXPORT NSString *const kFIRStorageResponseErrorCode;
+FOUNDATION_EXPORT NSString *const kFIRStorageResponseBody;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusResumeNotification;
+FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusPauseNotification;
+FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusProgressNotification;
+FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusCompleteNotification;
+FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusFailureNotification;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataBucket;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataCacheControl;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentDisposition;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentEncoding;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentLanguage;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentType;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataCustomMetadata;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataSize;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataDownloadURLs;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataGeneration;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataMetageneration;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataTimeCreated;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataUpdated;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataName;
+FOUNDATION_EXPORT NSString *const kFIRStorageMetadataDownloadTokens;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageInvalidDataFormat;
+FOUNDATION_EXPORT NSString *const kFIRStorageInvalidObserverStatus;
+
+FOUNDATION_EXPORT NSString *const kFIRStorageBundleIdentifier;
+
+/**
+ * Enum representing the internal state of an upload or download task.
+ */
+typedef NS_ENUM(NSInteger, FIRStorageTaskState) {
+ /**
+ * Unknown task state
+ */
+ FIRStorageTaskStateUnknown,
+
+ /**
+ * Task is being queued is ready to run
+ */
+ FIRStorageTaskStateQueueing,
+
+ /**
+ * Task is resuming from a paused state
+ */
+ FIRStorageTaskStateResuming,
+
+ /**
+ * Task is currently running
+ */
+ FIRStorageTaskStateRunning,
+
+ /**
+ * Task reporting a progress event
+ */
+ FIRStorageTaskStateProgress,
+
+ /**
+ * Task is pausing
+ */
+ FIRStorageTaskStatePausing,
+
+ /**
+ * Task is completing successfully
+ */
+ FIRStorageTaskStateCompleting,
+
+ /**
+ * Task is failing unrecoverably
+ */
+ FIRStorageTaskStateFailing,
+
+ /**
+ * Task paused successfully
+ */
+ FIRStorageTaskStatePaused,
+
+ /**
+ * Task cancelled successfully
+ */
+ FIRStorageTaskStateCancelled,
+
+ /**
+ * Task completed successfully
+ */
+ FIRStorageTaskStateSuccess,
+
+ /**
+ * Task failed unrecoverably
+ */
+ FIRStorageTaskStateFailed
+};
+
+/**
+ * Represents the various types of metadata: Files or Folders.
+ */
+typedef NS_ENUM(NSUInteger, FIRStorageMetadataType) {
+ FIRStorageMetadataTypeUnknown,
+ FIRStorageMetadataTypeFile,
+ FIRStorageMetadataTypeFolder,
+};
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageDeleteTask.h b/Firebase/Storage/Private/FIRStorageDeleteTask.h
new file mode 100644
index 0000000..c97fd27
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageDeleteTask.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "FIRStorageTask.h"
+
+@class GTMSessionFetcherService;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Task which provides the ability to delete an object in Firebase Storage.
+ */
+@interface FIRStorageDeleteTask : FIRStorageTask<FIRStorageTaskManagement>
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ completion:(FIRStorageVoidError)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageDownloadTask_Private.h b/Firebase/Storage/Private/FIRStorageDownloadTask_Private.h
new file mode 100644
index 0000000..293d1d5
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageDownloadTask_Private.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.
+ */
+
+@class GTMSessionFetcherService;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorageDownloadTask ()
+
+/**
+ * Bytes which have been downloaded so far.
+ */
+@property(readonly, nonatomic) NSData *downloadData;
+
+/**
+ * The file on disk to write to.
+ */
+@property(copy, nonatomic) NSURL *fileURL;
+
+/**
+ * Initializes a download task with a base FIRStorageReference and GTMSessionFetcherService.
+ * @param reference The base FIRStorageReference which fetchers use for configuration.
+ * @param service The GTMSessionFetcherService which will create fetchers.
+ * @param fileURL The system URL to download to. If nil, download in memory as bytes.
+ * @return Returns an instance of FIRStorageDownloadTask
+ */
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ file:(nullable NSURL *)fileURL;
+
+/**
+ * Cancels the download task and passes an appropriate error to the developer.
+ * @param error NSError to propegate to the developer.
+ */
+- (void)cancelWithError:(NSError *)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageErrors.h b/Firebase/Storage/Private/FIRStorageErrors.h
new file mode 100644
index 0000000..7c236d9
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageErrors.h
@@ -0,0 +1,54 @@
+/*
+ * 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 "FIRStorageConstants.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRStorageReference;
+
+/**
+ * Adds wrappers for common Firebase Storage errors (including creating errors from GCS errors).
+ * For more information on unwrapping GCS errors, see the GCS errors docs:
+ * https://cloud.google.com/storage/docs/json_api/v1/status-codes
+ * This is never publicly exposed to end developers (as they will simply see an NSError).
+ */
+@interface FIRStorageErrors : NSObject
+
+/**
+ * Creates a Firebase Storage error from a specific FIRStorageErrorCode.
+ */
++ (NSError *)errorWithCode:(FIRStorageErrorCode)code;
+
+/**
+ * Creates a Firebase Storage error from a specific FIRStorageErrorCode while adding
+ * custom info from an optionally provided info dictionary.
+ */
++ (NSError *)errorWithCode:(FIRStorageErrorCode)code
+ infoDictionary:(nullable NSDictionary *)dictionary;
+
+/**
+ * Creates a Firebase Storage error from a specific GCS error and FIRStorageReference.
+ * @param error Server error to wrap and return as a Firebase Storage error.
+ * @param reference FIRStorageReference which provides context about the request being made.
+ * @return Returns an Firebase Storage error, or nil if no error is provided.
+ */
++ (nullable NSError *)errorWithServerError:(nullable NSError *)error
+ reference:(nullable FIRStorageReference *)reference;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageGetMetadataTask.h b/Firebase/Storage/Private/FIRStorageGetMetadataTask.h
new file mode 100644
index 0000000..5f1dc8f
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageGetMetadataTask.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "FIRStorageTask.h"
+
+@class GTMSessionFetcherService;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Task which provides the ability to get metadata on an object in Firebase Storage.
+ */
+@interface FIRStorageGetMetadataTask : FIRStorageTask<FIRStorageTaskManagement>
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ completion:(FIRStorageVoidMetadataError)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageMetadata_Private.h b/Firebase/Storage/Private/FIRStorageMetadata_Private.h
new file mode 100644
index 0000000..629c935
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageMetadata_Private.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 "FIRStorageConstants_Private.h"
+
+@class FIRStorageReference;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorageMetadata ()
+
+@property(readwrite, nonatomic) NSString *name;
+
+@property(readwrite, nonatomic) NSString *path;
+
+@property(readwrite, nonatomic) FIRStorageReference *reference;
+
+/**
+ * The type of the object, either a "File" or a "Folder".
+ */
+@property(readwrite) FIRStorageMetadataType type;
+
+/**
+ * Returns an RFC3339 formatted date from a string.
+ * @param dateString An NSString of the form: yyyy-MM-ddTHH:mm:ss.SSSZ.
+ * @return An NSDate populated from the string or nil if conversion isn't possible.
+ */
+- (nullable NSDate *)dateFromRFC3339String:(NSString *)dateString;
+
+/**
+ * Returns an RFC3339 formatted string from an NSDate object.
+ * @param date The NSDate object to be converted to a string.
+ * @return An NSString of the form: yyyy-MM-ddTHH:mm:ss.SSSZ or nil if conversion isn't possible.
+ */
+- (nullable NSString *)RFC3339StringFromDate:(NSDate *)date;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageObservableTask_Private.h b/Firebase/Storage/Private/FIRStorageObservableTask_Private.h
new file mode 100644
index 0000000..e37b63f
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageObservableTask_Private.h
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRStorageTaskSnapshot;
+
+@class GTMSessionFetcherService;
+
+@interface FIRStorageObservableTask ()
+
+/**
+ * Creates a new FIRStorageTask initialized with a FIRStorageReference and GTMSessionFetcherService.
+ * @param reference A FIRStorageReference the task will be performed on.
+ * @param service A GTMSessionFetcherService which provides the fetchers and configuration for
+ * requests.
+ * @return A new FIRStorageTask representing the current task.
+ */
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service;
+
+/**
+ * Raise events for a given task status by passing along a snapshot of existing task state.
+ * @param status A FIRStorageTaskStatus to raise events for.
+ * @param snapshot A FIRStorageTaskSnapshot snapshot of task state to pass through the handler.
+ */
+- (void)fireHandlersForStatus:(FIRStorageTaskStatus)status
+ snapshot:(FIRStorageTaskSnapshot *)snapshot;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStoragePath.h b/Firebase/Storage/Private/FIRStoragePath.h
new file mode 100644
index 0000000..53ff7ef
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStoragePath.h
@@ -0,0 +1,106 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Represents a path in GCS, which can be represented as: gs://bucket/path/to/object
+ * or http[s]://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object?token=<12345>
+ * This class also includes helper methods to parse those URI/Ls, as well as to
+ * add and remove path segments.
+ */
+@interface FIRStoragePath : NSObject
+
+/**
+ * The GCS bucket in the path.
+ */
+@property(copy, nonatomic) NSString *bucket;
+
+/**
+ * The GCS object in the path.
+ */
+@property(copy, nonatomic, nullable) NSString *object;
+
+/**
+ * Parses a generic string (representing some URI or URL) and returns the appropriate path.
+ * @param string String which is parsed into a path.
+ * @return Returns an instance of FIRStoragePath or nil if one can't be created.
+ * @throws Throws an exception if the string is not a valid gs:// URI or http[s]:// URL.
+ */
++ (nullable FIRStoragePath *)pathFromString:(NSString *)string;
+
+/**
+ * Parses a gs://bucket/path/to/object URI into a GCS path.
+ * @param aURIString gs:// URI which is parsed into a path.
+ * @return Returns an instance of FIRStoragePath or nil if one can't be created.
+ * @throws Throws an exception if the string is not a valid gs:// URI.
+ */
++ (nullable FIRStoragePath *)pathFromGSURI:(NSString *)aURIString;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Constructs an FIRStoragePath object that represents the given bucket and object.
+ * @param bucket The name of the bucket.
+ * @param object The name of the object.
+ * @return An instance of FIRStoragePath representing the @a bucket and @a object.
+ */
+- (instancetype)initWithBucket:(NSString *)bucket
+ object:(nullable NSString *)object NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Parses a http[s]://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object...?token=<12345>
+ * URL into a GCS path.
+ * @param aURLString http[s]:// URL which is parsed into a path.
+ * string which is parsed into a path.
+ * @return Returns an instance of FIRStoragePath or nil if one can't be created.
+ * @throws Throws an exception if the string is not a valid http[s]:// URL.
+ */
++ (nullable FIRStoragePath *)pathFromHTTPURL:(NSString *)aURLString;
+
+/**
+ * Creates a new path based off of the current path and a string appended to it.
+ * Note that all slashes are compressed to a single slash, and leading and trailing slashes
+ * are removed.
+ * @param path String to append to the current path.
+ * @return Returns a new instance of FIRStoragePath with the new path appended.
+ */
+- (FIRStoragePath *)child:(NSString *)path;
+
+/**
+ * Creates a new path based off of the current path with the last path segment removed.
+ * @return Returns a new instance of FIRStoragePath pointing to the parent path,
+ * or nil if the current path points to the root.
+ */
+- (nullable FIRStoragePath *)parent;
+
+/**
+ * Creates a new path based off of the root of the bucket.
+ * @return Returns a new instance of FIRStoragePath pointing to the root of the bucket.
+ */
+- (FIRStoragePath *)root;
+
+/**
+ * Returns a GS URI representing the current path.
+ * @return Returns a gs://bucket/path/to/object URI representing the current path.
+ */
+- (NSString *)stringValue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageReference_Private.h b/Firebase/Storage/Private/FIRStorageReference_Private.h
new file mode 100644
index 0000000..825964d
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageReference_Private.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 "FIRStoragePath.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorageReference ()
+
+@property(nonatomic, readwrite) FIRStorage *storage;
+
+/**
+ * The current path which points to an object in the Google Cloud Storage bucket.
+ */
+@property(strong, nonatomic) FIRStoragePath *path;
+
+- (instancetype)initWithStorage:(FIRStorage *)storage
+ path:(FIRStoragePath *)path NS_DESIGNATED_INITIALIZER;
+
+- (NSString *)stringValue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.h b/Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.h
new file mode 100644
index 0000000..1762a61
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.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 "FIRStorageConstants_Private.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRStorageMetadata;
+@class FIRStorageReference;
+@class FIRStorageTask;
+
+@interface FIRStorageTaskSnapshot ()
+
+@property(readwrite, copy, nonatomic) FIRStorageTask *task;
+@property(readwrite, copy, nonatomic) FIRStorageMetadata *metadata;
+@property(readwrite, copy, nonatomic) FIRStorageReference *reference;
+@property(readwrite, strong, nonatomic) NSProgress *progress;
+@property(readwrite, copy, nonatomic) NSError *error;
+
+/**
+ * Creates a new task snapshot from the given properties.
+ * @param task The task being represented in this snapshot.
+ * @param state The current state of the parent task.
+ * @param metadata The FIRStorageMetadata of a task. Before upload/update, contains the metadata
+ * to be updated; after, contains the returned metadata. May be nil if no metadata is provided
+ * or returned.
+ * @param reference The FIRStorageReference that spawned the task this snapshot is based on.
+ * @param progress An NSProgress object containing progress of the task this snapshot is based on,
+ * or nil if the task doesn't report progress.
+ * @param error An NSError object containing an error that occurred during the task,
+ * if one occurred.
+ * @return Returns the constructed snapshot.
+ */
+- (instancetype)initWithTask:(__kindof FIRStorageTask *)task
+ state:(FIRStorageTaskState)state
+ metadata:(nullable FIRStorageMetadata *)metadata
+ reference:(FIRStorageReference *)reference
+ progress:(nullable NSProgress *)progress
+ error:(nullable NSError *)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageTask_Private.h b/Firebase/Storage/Private/FIRStorageTask_Private.h
new file mode 100644
index 0000000..598006b
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageTask_Private.h
@@ -0,0 +1,77 @@
+/*
+ * 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 "FIRStorageConstants_Private.h"
+#import "FIRStorageErrors.h"
+#import "FIRStorageReference.h"
+#import "FIRStorageReference_Private.h"
+#import "FIRStorageTaskSnapshot.h"
+#import "FIRStorageTaskSnapshot_Private.h"
+#import "FIRStorageUtils.h"
+
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import <GTMSessionFetcher/GTMSessionFetcherService.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorageTask ()
+
+/**
+ * State for the current task in progress.
+ */
+@property(atomic) FIRStorageTaskState state;
+
+/**
+ * FIRStorageMetadata for the task in progress, or nil if none present.
+ */
+@property(strong, nonatomic, nullable) FIRStorageMetadata *metadata;
+
+/**
+ * Error which occurred during task execution, or nil if no error occurred.
+ */
+@property(strong, nonatomic, nullable) NSError *error;
+
+/**
+ * NSProgress object which tracks the progess of an observable task.
+ */
+@property(strong, nonatomic) NSProgress *progress;
+
+/**
+ * Reference pointing to the location the task is being performed against.
+ */
+@property(strong, nonatomic) FIRStorageReference *reference;
+
+@property(strong, readwrite, nonatomic, nonnull) FIRStorageTaskSnapshot *snapshot;
+
+@property(readonly, copy, nonatomic) NSURLRequest *baseRequest;
+
+@property(strong, atomic) GTMSessionFetcher *fetcher;
+
+@property(readonly, nonatomic) GTMSessionFetcherService *fetcherService;
+
+/**
+ * Creates a new FIRStorageTask initialized with a FIRStorageReference and GTMSessionFetcherService.
+ * @param reference A FIRStorageReference the task will be performed on.
+ * @param service A GTMSessionFetcherService which provides the fetchers and configuration for
+ * requests.
+ * @return A new FIRStorageTask representing the current task.
+ */
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageTokenAuthorizer.h b/Firebase/Storage/Private/FIRStorageTokenAuthorizer.h
new file mode 100644
index 0000000..78a8218
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageTokenAuthorizer.h
@@ -0,0 +1,44 @@
+/*
+ * 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 <GTMSessionFetcher/GTMSessionFetcherService.h>
+
+@class FIRApp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Wrapper class for FIRApp that implements the GTMFetcherAuthorizationProtocol,
+ * so as to easily provide GTMSessionFetcher fetches a Firebase Authentication JWT
+ * for the current logged in user. Handles token expiration and other failure cases.
+ * If no authentication provider exists or no token is found, no token is added
+ * and the request is passed.
+ */
+@interface FIRStorageTokenAuthorizer : NSObject<GTMFetcherAuthorizationProtocol>
+
+/**
+ * Initializes the token authorizer with an instance of FIRApp.
+ * @param app An instance of FIRApp which provides auth tokens.
+ * @return Returns an instance of FIRStorageTokenAuthorizer which adds the appropriate
+ * "Authorization" header to all outbound requests. Note that a token may not be added
+ * if a getTokenImplementation doesn't exist on FIRApp. This allows for unauthenticated
+ * access, if Firebase Storage rules allow for it.
+ */
+- (instancetype)initWithApp:(FIRApp *)app fetcherService:(GTMSessionFetcherService *)service;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h b/Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h
new file mode 100644
index 0000000..2fcefdd
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h
@@ -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 "FIRStorageTask.h"
+
+@class GTMSessionFetcherService;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Task which provides the ability update the metadata on an object in Firebase Storage.
+ */
+@interface FIRStorageUpdateMetadataTask : FIRStorageTask<FIRStorageTaskManagement>
+
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ metadata:(FIRStorageMetadata *)metadata
+ completion:(FIRStorageVoidMetadataError)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageUploadTask_Private.h b/Firebase/Storage/Private/FIRStorageUploadTask_Private.h
new file mode 100644
index 0000000..468d9d3
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageUploadTask_Private.h
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+@class GTMSessionUploadFetcher;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorageUploadTask ()
+
+/**
+ * The data to be uploaded (if uploading bytes).
+ */
+@property(readonly, copy, nonatomic, nullable) NSData *uploadData;
+
+/**
+ * The name of a file on disk to be uploaded (if uploading from a file).
+ */
+@property(readonly, copy, nonatomic, nullable) NSURL *fileURL;
+
+/**
+ * The FIRStorageMetadata about the object being uploaded.
+ */
+@property(readonly, copy, nonatomic) FIRStorageMetadata *uploadMetadata;
+
+/**
+ * GTMSessionUploadFetcher used by all uploads.
+ */
+@property(strong, atomic) GTMSessionUploadFetcher *uploadFetcher;
+
+/**
+ * Initializes an upload task with a base FIRStorageReference and GTMSessionFetcherService.
+ * @param reference The base FIRStorageReference which fetchers use for configuration.
+ * @param service The GTMSessionFetcherService which will create fetchers.
+ * @param uploadData The NSData object to be uploaded.
+ * @return Returns an instance of FIRStorageUploadTask.
+ */
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ data:(NSData *)uploadData
+ metadata:(FIRStorageMetadata *)metadata;
+
+/**
+ * Initializes an upload task with a base FIRStorageReference and GTMSessionFetcherService.
+ * @param reference The base FIRStorageReference which fetchers use for configuration.
+ * @param service The GTMSessionFetcherService which will create fetchers.
+ * @param fileURL The system file URL to upload from.
+ * @return Returns an instance of FIRStorageUploadTask.
+ */
+- (instancetype)initWithReference:(FIRStorageReference *)reference
+ fetcherService:(GTMSessionFetcherService *)service
+ file:(NSURL *)fileURL
+ metadata:(FIRStorageMetadata *)metadata;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorageUtils.h b/Firebase/Storage/Private/FIRStorageUtils.h
new file mode 100644
index 0000000..e687c82
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorageUtils.h
@@ -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 <Foundation/Foundation.h>
+
+@class FIRStoragePath;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FIRStorageUtils provides a number of helper methods for commonly used operations
+ * in Firebase Storage, such as JSON parsing, escaping, and file extensions.
+ */
+@interface FIRStorageUtils : NSObject
+
+/**
+ * Returns a percent encoded string appropriate for GCS.
+ * See https://cloud.google.com/storage/docs/naming for more details.
+ * @param string A path to escape characters according to the GCS
+ * @return A percent encoded string appropriate for GCS operations or nil if string is nil
+ * or can't be escaped.
+ */
++ (nullable NSString *)GCSEscapedString:(NSString *)string;
+
+/**
+ * Returns the MIME type for a file extension.
+ * Example of how to get MIME type here: http://ddeville.me/2011/12/mime-to-UTI-cocoa/
+ * @param extension A file extension such as "txt", "png", etc.
+ * @return The MIME type for the input extension such as "text/plain", "image/png", etc.
+ * or nil if no type is found.
+ */
++ (nullable NSString *)MIMETypeForExtension:(NSString *)extension;
+
+/**
+ * Returns a properly escaped query string from a given dictionary of query items to values.
+ * @param dictionary A dictionary containing query items and associated values.
+ * @return A properly escaped query string or the empty string for a nil or empty dictionary.
+ */
++ (NSString *)queryStringForDictionary:(nullable NSDictionary *)dictionary;
+
+/**
+ * Returns a base NSURLRequest used by all tasks.
+ * @param path The FIRStoragePath to create a request for.
+ * @return Returns a properly formatted NSURLRequest of the form:
+ * scheme://host/version/b/<bucket>/o[/path/to/object]
+ */
++ (NSURLRequest *)defaultRequestForPath:(FIRStoragePath *)path;
+
+/**
+ * Creates the appropriate GCS percent escaped path for a given FIRStoragePath.
+ * @param path The FIRStoragePath to encode.
+ * @return Returns the GCS encoded URL for a given FIRStoragePath.
+ */
++ (NSString *)encodedURLForPath:(FIRStoragePath *)path;
+
+@end
+
+@interface NSDictionary (FIRStorageNSDictionaryJSONHelpers)
+
+/**
+ * Returns a dictionary representation of the data in @a data.
+ * @param data NSData containing JSON data.
+ * @return An NSDictionary representation of the JSON, or nil if serialization failed.
+ */
++ (nullable instancetype)frs_dictionaryFromJSONData:(nullable NSData *)data;
+
+@end
+
+@interface NSData (FIRStorageNSDataJSONHelpers)
+
+/**
+ * Returns an NSData instance containing JSON serialized from @a dictionary.
+ * @param dictionary An NSDictionary containing only types serializable to JSON.
+ * @return An NSData object representing the binary JSON, or nil if serialization failed.
+ */
++ (nullable instancetype)frs_dataFromJSONDictionary:(nullable NSDictionary *)dictionary;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Storage/Private/FIRStorage_Private.h b/Firebase/Storage/Private/FIRStorage_Private.h
new file mode 100644
index 0000000..aefe808
--- /dev/null
+++ b/Firebase/Storage/Private/FIRStorage_Private.h
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+@class FIRApp;
+@class GTMSessionFetcherService;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStorage ()
+
+@property(strong, nonatomic, readwrite) FIRApp *app;
+
+@property(strong, nonatomic) GTMSessionFetcherService *fetcherServiceForApp;
+
+@property(strong, nonatomic) NSString *storageBucket;
+
+/**
+ * Enables/disables GTMSessionFetcher HTTP logging
+ * @param isLoggingEnabled Boolean passed through to enable/disable GTMSessionFetcher logging
+ */
++ (void)setGTMSessionFetcherLoggingEnabled:(BOOL)isLoggingEnabled;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/FirebaseDev.podspec b/FirebaseDev.podspec
new file mode 100644
index 0000000..3dc09d7
--- /dev/null
+++ b/FirebaseDev.podspec
@@ -0,0 +1,140 @@
+Pod::Spec.new do |s|
+ s.name = 'FirebaseDev'
+ s.version = '4.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => 'LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '8.0'
+ s.default_subspec = 'Root'
+
+ s.subspec 'Root' do |sp|
+ sp.source_files = 'Firebase/Firebase/Firebase.h'
+ sp.public_header_files = 'Firebase/Firebase/Firebase.h'
+ sp.preserve_paths = 'Firebase/Firebase/module.modulemap'
+ sp.user_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}/Firebase/Firebase/Firebase"' }
+ end
+
+ s.subspec 'Core' do |sp|
+ sp.source_files = 'Firebase/Core/**/*.[mh]'
+ sp.public_header_files =
+ 'Firebase/Core/FirebaseCore.h',
+ 'Firebase/Core/FIRAnalyticsConfiguration.h',
+ 'Firebase/Core/FIRApp.h',
+ 'Firebase/Core/FIRConfiguration.h',
+ 'Firebase/Core/FIRLoggerLevel.h',
+ 'Firebase/Core/FIROptions.h',
+ 'Firebase/Core/FIRCoreSwiftNameSupport.h'
+ sp.dependency 'GoogleToolboxForMac/NSData+zlib', '~> 2.1'
+ sp.dependency 'FirebaseDev/Root'
+ end
+
+ s.subspec 'Auth' do |sp|
+ sp.source_files = 'Firebase/Auth/Source/**/*.[mh]'
+ sp.public_header_files =
+ 'Firebase/Auth/Source/FirebaseAuth.h',
+ 'Firebase/Auth/Source/FirebaseAuthVersion.h',
+ 'Firebase/Auth/Source/FIRAdditionalUserInfo.h',
+ 'Firebase/Auth/Source/FIRAuth.h',
+ 'Firebase/Auth/Source/FIRAuthAPNSTokenType.h',
+ 'Firebase/Auth/Source/FIRAuthCredential.h',
+ 'Firebase/Auth/Source/FIRAuthDataResult.h',
+ 'Firebase/Auth/Source/FIRAuthErrors.h',
+ 'Firebase/Auth/Source/FIRAuthSwiftNameSupport.h',
+ 'Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.h',
+ 'Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.h',
+ 'Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.h',
+ 'Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.h',
+ 'Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.h',
+ 'Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthCredential.h',
+ 'Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.h',
+ 'Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.h',
+ 'Firebase/Auth/Source/FIRUser.h',
+ 'Firebase/Auth/Source/FIRUserInfo.h'
+ sp.preserve_paths =
+ 'Firebase/Auth/README.md',
+ 'Firebase/Auth/CHANGELOG.md'
+ sp.xcconfig = { 'OTHER_CFLAGS' => '-DFIRAuth_VERSION=' + s.version.to_s +
+ ' -DFIRAuth_MINOR_VERSION=' + s.version.to_s.split(".")[0] + "." + s.version.to_s.split(".")[1]
+ }
+ sp.framework = 'Security'
+ sp.dependency 'FirebaseDev/Core'
+ sp.dependency 'GTMSessionFetcher/Core', '~> 1.1'
+ sp.dependency 'GoogleToolboxForMac/NSDictionary+URLArguments', '~> 2.1'
+ end
+
+ s.subspec 'Database' do |sp|
+ sp.source_files = 'Firebase/Database/**/*.[mh]',
+ 'Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm',
+ 'Firebase/Database/third_party/SocketRocket/fbase64.c'
+ sp.public_header_files =
+ 'Firebase/Database/Api/FirebaseDatabase.h',
+ 'Firebase/Database/Api/FIRDataEventType.h',
+ 'Firebase/Database/Api/FIRDataSnapshot.h',
+ 'Firebase/Database/Api/FIRDatabaseQuery.h',
+ 'Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h',
+ 'Firebase/Database/Api/FIRMutableData.h',
+ 'Firebase/Database/Api/FIRServerValue.h',
+ 'Firebase/Database/Api/FIRTransactionResult.h',
+ 'Firebase/Database/Api/FIRDatabase.h',
+ 'Firebase/Database/FIRDatabaseReference.h'
+ sp.library = 'c++'
+ sp.library = 'icucore'
+ sp.framework = 'CFNetwork'
+ sp.framework = 'Security'
+ sp.framework = 'SystemConfiguration'
+ sp.dependency 'leveldb-library'
+ sp.dependency 'FirebaseDev/Core'
+ sp.xcconfig = { 'OTHER_CFLAGS' => '-DFIRDatabase_VERSION=' + s.version.to_s }
+ end
+
+ s.subspec 'Messaging' do |sp|
+ sp.source_files = 'Firebase/Messaging/**/*.[mh]'
+ sp.requires_arc = 'Firebase/Messaging/*.m'
+
+ sp.public_header_files =
+ 'Firebase/Messaging/Public/FirebaseMessaging.h',
+ 'Firebase/Messaging/Public/FIRMessaging.h'
+ sp.library = 'sqlite3'
+ sp.xcconfig ={ 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' +
+ 'GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1 ' +
+ 'FIRMessaging_LIB_VERSION=' + String(s.version)
+ }
+ sp.framework = 'AddressBook'
+ sp.framework = 'SystemConfiguration'
+ sp.dependency 'FirebaseDev/Core'
+ sp.dependency 'GoogleToolboxForMac/Logger', '~> 2.1'
+ sp.dependency 'Protobuf', '~> 3.1'
+ end
+
+ s.subspec 'Storage' do |sp|
+ sp.source_files = 'Firebase/Storage/**/*.[mh]'
+ sp.public_header_files =
+ 'Firebase/Storage/FirebaseStorage.h',
+ 'Firebase/Storage/FIRStorage.h',
+ 'Firebase/Storage/FIRStorageConstants.h',
+ 'Firebase/Storage/FIRStorageDownloadTask.h',
+ 'Firebase/Storage/FIRStorageMetadata.h',
+ 'Firebase/Storage/FIRStorageObservableTask.h',
+ 'Firebase/Storage/FIRStorageReference.h',
+ 'Firebase/Storage/FIRStorageSwiftNameSupport.h',
+ 'Firebase/Storage/FIRStorageTask.h',
+ 'Firebase/Storage/FIRStorageTaskSnapshot.h',
+ 'Firebase/Storage/FIRStorageUploadTask.h'
+ sp.framework = 'MobileCoreServices'
+ sp.dependency 'FirebaseDev/Core'
+ sp.dependency 'GTMSessionFetcher/Core', '~> 1.1'
+ sp.xcconfig = { 'OTHER_CFLAGS' => '-DFIRStorage_VERSION=' + s.version.to_s }
+ end
+end
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..26eb9a9
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,32 @@
+### [READ] Step 1: Are you in the right place?
+
+ * For issues or feature requests related to __the code in this repository__
+ file a Github issue.
+ * If this is a __feature request__ make sure the issue title starts with "FR:".
+ * For general technical questions, post a question on [StackOverflow](http://stackoverflow.com/)
+ with the firebase tag.
+ * For general Firebase discussion, use the [firebase-talk](https://groups.google.com/forum/#!forum/firebase-talk)
+ google group.
+ * For help troubleshooting your application that does not fall under one
+ of the above categories, reach out to the personalized
+ [Firebase support channel](https://firebase.google.com/support/).
+
+### [REQUIRED] Step 2: Describe your environment
+
+ * XCode version: _____
+ * Firebase SDK version: _____
+ * Library version: _____
+ * Firebase Product: _____ (auth, database, storage, core, messaging, etc)
+
+### [REQUIRED] Step 3: Describe the problem
+
+#### Steps to reproduce:
+
+What happened? How can we make the problem occur?
+This could be a description, log/console output, etc.
+
+#### Relevant Code:
+
+```
+// TODO(you): code here to reproduce the problem
+```
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..2572425
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,19 @@
+Hey there! So you want to contribute to a Firebase SDK?
+Before you file this pull request, please read these guidelines:
+
+### Discussion
+
+ * Read the contribution guidelines (CONTRIBUTING.md).
+ * If this has been discussed in an issue, make sure to link to the issue here.
+ If not, go file an issue about this **before creating a pull request** to discuss.
+
+### Testing
+
+ * Make sure all existing tests in the repository pass after your change.
+ * If you fixed a bug or added a feature, add a new test to cover your code.
+
+### API Changes
+
+ * At this time we cannot accept changes that affect the public API. If you'd like to help
+ us make Firebase APIs better, please propose your change in a feature request so that we
+ can discuss it together.
diff --git a/README.md b/README.md
index 1a0b753..717e014 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,65 @@
-# firebase-ios-sdk \ No newline at end of file
+# Firebase iOS Open Source Development
+
+This repository contains a subset of the Firebase iOS SDK source. It currently
+includes FirebaseCore, FirebaseAuth, FirebaseDatabase, FirebaseMessaging, and
+FirebaseStorage.
+
+The code here is only for those interested in the SDK internals or those
+interested in contributing to Firebase.
+
+General Firebase information can be found at [https://firebase.google.com](https://firebase.google.com).
+
+## Usage
+
+```
+$ git clone git@github.com:FirebasePrivate/firebase-ios-sdk.git
+$ cd firebase-ios-sdk/Example
+$ pod update
+$ open Firebase.xcworkspace
+```
+### Running Unit Tests
+
+Select a scheme and press Command-u to build a component and run its unit tests.
+
+### 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 files without real values, but can be replaced with real plist files. To get your own `GoogleService-Info.plist` files:
+
+1. Go to the [Firebase Console](https://console.firebase.google.com/)
+2. Create a new Firebase project, if you don't already have one
+3. For each sample app you want to test, create a new Firebase app with the sample app's bundle identifier (e.g. `com.google.Database-Example`)
+4. Download the resulting `GoogleService-Info.plist` and replace the appropriate dummy plist file (e.g. in [Example/Database/App/](Example/Database/App/));
+
+Some sample apps like Firebase Messaging ([Example/Messaging/App](Example/Messaging/App)) require special Apple capabilities, and you will have to change the sample app to use a unique bundle identifier that you can control in your own Apple Developer account.
+
+See the sections below for any special instructions for those SDKs.
+
+## Firebase Auth
+
+If you're doing specific Firebase Auth development, see
+[AuthSamples/README.md](AuthSamples/README.md) for instructions about
+building and running the FirebaseAuth pod along with various samples and tests.
+
+## Firebase Database
+
+To run the Database Integration tests, make your database authentication rules
+[public](https://firebase.google.com/docs/database/security/quickstart).
+
+## Firebase Storage
+
+To run the Storage Integration tests, follow the instructions in
+[FIRStorageIntegrationTests.m](Example/Storage/Tests/Integration/FIRStorageIntegrationTests.m).
+
+## Firebase Messaging
+
+### Push Notifications
+
+Push notifications can only be delivered to specially provisioned App IDs in the developer portal. In order to actually test receiving push notifications, you will need to:
+
+1. Change the bundle identifier of the sample app to something you own in your Apple Developer account, and enable that App ID for push notifications.
+2. You'll also need to [upload your APNs Provider Authentication token or certificate to the Firebase Console](https://firebase.google.com/docs/cloud-messaging/ios/certs) at **Project Settings > Cloud Messaging > [Your Firebase App]**.
+3. Ensure your iOS device is added to your Apple Developer portal as a test device.
+
+### iOS Simulator
+
+The iOS Simulator cannot register for remote notifications, and will not receive push notifications. In order to receive push notifications, you'll have to follow the steps above and run the app on a physical device.
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..a093ddf
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+# 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.
+
+set -eo pipefail
+
+EXIT_STATUS=0
+
+(xcodebuild \
+ -workspace Example/Firebase.xcworkspace \
+ -scheme AllTests \
+ -sdk iphonesimulator \
+ -destination 'platform=iOS Simulator,name=iPhone 7' \
+ build \
+ test \
+ ONLY_ACTIVE_ARCH=YES \
+ CODE_SIGNING_REQUIRED=NO \
+ | xcpretty) || EXIT_STATUS=$?
+
+ exit $EXIT_STATUS