diff --git a/gma/CMakeLists.txt b/gma/CMakeLists.txt index 440d0db7f0..887139f1db 100644 --- a/gma/CMakeLists.txt +++ b/gma/CMakeLists.txt @@ -54,7 +54,7 @@ set(android_SRCS # Source files used by the iOS implementation. set(ios_SRCS - src/stub/ump/consent_info_internal_stub.cc + src/ios/ump/consent_info_internal_ios.mm src/ios/FADAdSize.mm src/ios/FADAdView.mm src/ios/FADInterstitialDelegate.mm @@ -133,11 +133,13 @@ elseif(IOS) firebase_gma POD_NAMES Google-Mobile-Ads-SDK + GoogleUserMessagingPlatform ) # GMA expects the header files to be in a subfolder, so set up a symlink to # accomplish that. symlink_pod_headers(firebase_gma Google-Mobile-Ads-SDK GoogleMobileAds) + symlink_pod_headers(firebase_gma GoogleUserMessagingPlatform UserMessagingPlatform) if (FIREBASE_XCODE_TARGET_FORMAT STREQUAL "frameworks") set_target_properties(firebase_gma PROPERTIES diff --git a/gma/integration_test/Info.plist b/gma/integration_test/Info.plist index d2403b9c2c..953571e326 100644 --- a/gma/integration_test/Info.plist +++ b/gma/integration_test/Info.plist @@ -24,6 +24,8 @@ UILaunchStoryboardName LaunchScreen + NSUserTrackingUsageDescription + This identifier will be used to deliver personalized ads to you. CFBundleURLTypes diff --git a/gma/integration_test/Podfile b/gma/integration_test/Podfile index 8f9a305cdf..b347ca3b39 100644 --- a/gma/integration_test/Podfile +++ b/gma/integration_test/Podfile @@ -6,6 +6,7 @@ use_frameworks! :linkage => :static target 'integration_test' do pod 'Firebase/CoreOnly', '10.13.0' pod 'Google-Mobile-Ads-SDK', '10.9.0' + pod 'GoogleUserMessagingPlatform', '2.1.0' end post_install do |installer| diff --git a/gma/integration_test/integration_test.xcodeproj/project.pbxproj b/gma/integration_test/integration_test.xcodeproj/project.pbxproj index 54ac389b36..35b4a36051 100644 --- a/gma/integration_test/integration_test.xcodeproj/project.pbxproj +++ b/gma/integration_test/integration_test.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ D640F3172819C85800AC956E /* empty.swift in Sources */ = {isa = PBXBuildFile; fileRef = D640F3162819C85800AC956E /* empty.swift */; }; D66B16871CE46E8900E5638A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D66B16861CE46E8900E5638A /* LaunchScreen.storyboard */; }; D67D355822BABD2200292C1D /* gtest-all.cc in Sources */ = {isa = PBXBuildFile; fileRef = D67D355622BABD2100292C1D /* gtest-all.cc */; }; + D686A3292A8B16F20034845A /* AppTrackingTransparency.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D686A3282A8B16F20034845A /* AppTrackingTransparency.framework */; }; D6C179E922CB322900C2651A /* ios_app_framework.mm in Sources */ = {isa = PBXBuildFile; fileRef = D6C179E722CB322900C2651A /* ios_app_framework.mm */; }; D6C179EA22CB322900C2651A /* ios_firebase_test_framework.mm in Sources */ = {isa = PBXBuildFile; fileRef = D6C179E822CB322900C2651A /* ios_firebase_test_framework.mm */; }; D6C179EE22CB323300C2651A /* firebase_test_framework.cc in Sources */ = {isa = PBXBuildFile; fileRef = D6C179EC22CB323300C2651A /* firebase_test_framework.cc */; }; @@ -39,6 +40,7 @@ D66B16861CE46E8900E5638A /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; D67D355622BABD2100292C1D /* gtest-all.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = "gtest-all.cc"; path = "external/googletest/src/googletest/src/gtest-all.cc"; sourceTree = ""; }; D67D355722BABD2100292C1D /* gtest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gtest.h; path = external/googletest/src/googletest/include/gtest/gtest.h; sourceTree = ""; }; + D686A3282A8B16F20034845A /* AppTrackingTransparency.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppTrackingTransparency.framework; path = System/Library/Frameworks/AppTrackingTransparency.framework; sourceTree = SDKROOT; }; D6C179E722CB322900C2651A /* ios_app_framework.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_app_framework.mm; path = src/ios/ios_app_framework.mm; sourceTree = ""; }; D6C179E822CB322900C2651A /* ios_firebase_test_framework.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_firebase_test_framework.mm; path = src/ios/ios_firebase_test_framework.mm; sourceTree = ""; }; D6C179EB22CB323300C2651A /* firebase_test_framework.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = firebase_test_framework.h; path = src/firebase_test_framework.h; sourceTree = ""; }; @@ -53,6 +55,7 @@ buildActionMask = 2147483647; files = ( 529226D81C85F68000C89379 /* CoreGraphics.framework in Frameworks */, + D686A3292A8B16F20034845A /* AppTrackingTransparency.framework in Frameworks */, 529226DA1C85F68000C89379 /* UIKit.framework in Frameworks */, 529226D61C85F68000C89379 /* Foundation.framework in Frameworks */, ); @@ -85,6 +88,7 @@ 529226D41C85F68000C89379 /* Frameworks */ = { isa = PBXGroup; children = ( + D686A3282A8B16F20034845A /* AppTrackingTransparency.framework */, 529226D51C85F68000C89379 /* Foundation.framework */, 529226D71C85F68000C89379 /* CoreGraphics.framework */, 529226D91C85F68000C89379 /* UIKit.framework */, diff --git a/gma/integration_test/src/integration_test.cc b/gma/integration_test/src/integration_test.cc index 7143237873..da158fa0ea 100644 --- a/gma/integration_test/src/integration_test.cc +++ b/gma/integration_test/src/integration_test.cc @@ -98,6 +98,8 @@ const char* kErrorDomain = "com.google.admob"; #endif // Sample test device IDs to use in making the request. +// You can replace these with actual device IDs for certain tests (e.g. UMP) +// to work on hardware devices. const std::vector kTestDeviceIDs = { "2077ef9a63d2b398840261c8221a0c9b", "098fe087d987c9a878965454a65654d7"}; @@ -136,6 +138,7 @@ static const std::vector kNeighboringContentURLs = { "test_url1", "test_url2", "test_url3"}; using app_framework::LogDebug; +using app_framework::LogInfo; using app_framework::LogWarning; using app_framework::ProcessEvents; @@ -2498,6 +2501,7 @@ void FirebaseGmaUmpTest::InitializeUmp(ResetOption reset) { void FirebaseGmaUmpTest::TerminateUmp() { if (consent_info_) { + consent_info_->Reset(); delete consent_info_; consent_info_ = nullptr; } @@ -2591,6 +2595,8 @@ TEST_F(FirebaseGmaUmpTest, TestUmpRequestConsentInfoUpdateDebugEEA) { params.tag_for_under_age_of_consent = false; params.debug_settings.debug_geography = firebase::gma::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); firebase::Future future = consent_info_->RequestConsentInfoUpdate(params); @@ -2610,6 +2616,8 @@ TEST_F(FirebaseGmaUmpTest, TestUmpRequestConsentInfoUpdateDebugNonEEA) { params.tag_for_under_age_of_consent = false; params.debug_settings.debug_geography = firebase::gma::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); firebase::Future future = consent_info_->RequestConsentInfoUpdate(params); @@ -2630,6 +2638,8 @@ TEST_F(FirebaseGmaUmpTest, TestUmpLoadForm) { params.tag_for_under_age_of_consent = false; params.debug_settings.debug_geography = firebase::gma::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), "RequestConsentInfoUpdate"); @@ -2663,6 +2673,8 @@ TEST_F(FirebaseGmaUmpTest, TestUmpShowForm) { params.tag_for_under_age_of_consent = false; params.debug_settings.debug_geography = firebase::gma::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), "RequestConsentInfoUpdate"); @@ -2678,7 +2690,8 @@ TEST_F(FirebaseGmaUmpTest, TestUmpShowForm) { EXPECT_EQ(consent_info_->GetConsentFormStatus(), firebase::gma::ump::kConsentFormStatusAvailable); - firebase::Future future = consent_info_->ShowConsentForm(nullptr); + firebase::Future future = + consent_info_->ShowConsentForm(app_framework::GetWindowController()); EXPECT_TRUE(future == consent_info_->ShowConsentFormLastResult()); @@ -2688,9 +2701,7 @@ TEST_F(FirebaseGmaUmpTest, TestUmpShowForm) { firebase::gma::ump::kConsentStatusObtained); } -TEST_F(FirebaseGmaUmpTest, TestUmpLoadFormUnavailableDueUnderAgeOfConsent) { - TEST_REQUIRES_USER_INTERACTION; - +TEST_F(FirebaseGmaUmpTest, TestUmpLoadFormUnavailableDueToUnderAgeOfConsent) { using firebase::gma::ump::ConsentDebugSettings; using firebase::gma::ump::ConsentFormStatus; using firebase::gma::ump::ConsentRequestParameters; @@ -2700,24 +2711,65 @@ TEST_F(FirebaseGmaUmpTest, TestUmpLoadFormUnavailableDueUnderAgeOfConsent) { params.tag_for_under_age_of_consent = true; params.debug_settings.debug_geography = firebase::gma::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), "RequestConsentInfoUpdate"); - EXPECT_EQ(consent_info_->GetConsentStatus(), - firebase::gma::ump::kConsentStatusRequired); + WaitForCompletion(consent_info_->LoadConsentForm(), "LoadConsentForm", + firebase::gma::ump::kConsentFormErrorUnavailable); +} - EXPECT_EQ(consent_info_->GetConsentFormStatus(), - firebase::gma::ump::kConsentFormStatusUnavailable); +TEST_F(FirebaseGmaUmpTest, TestUmpLoadFormUnavailableDebugNonEEA) { + using firebase::gma::ump::ConsentDebugSettings; + using firebase::gma::ump::ConsentFormStatus; + using firebase::gma::ump::ConsentRequestParameters; + using firebase::gma::ump::ConsentStatus; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::gma::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), + "RequestConsentInfoUpdate"); WaitForCompletion(consent_info_->LoadConsentForm(), "LoadConsentForm", firebase::gma::ump::kConsentFormErrorUnavailable); +} - EXPECT_EQ(consent_info_->GetConsentFormStatus(), - firebase::gma::ump::kConsentFormStatusUnavailable); +TEST_F(FirebaseGmaUmpTest, TestUmpLoadAndShowIfRequiredDebugNonEEA) { + using firebase::gma::ump::ConsentDebugSettings; + using firebase::gma::ump::ConsentRequestParameters; + using firebase::gma::ump::ConsentStatus; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::gma::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), + "RequestConsentInfoUpdate"); + + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::gma::ump::kConsentStatusNotRequired); + + firebase::Future future = + consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()); + + EXPECT_TRUE(future == + consent_info_->LoadAndShowConsentFormIfRequiredLastResult()); + + WaitForCompletion(future, "LoadAndShowConsentFormIfRequired"); } -TEST_F(FirebaseGmaUmpTest, TestUmpLoadAndShowIfRequired) { +TEST_F(FirebaseGmaUmpTest, TestUmpLoadAndShowIfRequiredDebugEEA) { using firebase::gma::ump::ConsentDebugSettings; using firebase::gma::ump::ConsentRequestParameters; using firebase::gma::ump::ConsentStatus; @@ -2728,6 +2780,8 @@ TEST_F(FirebaseGmaUmpTest, TestUmpLoadAndShowIfRequired) { params.tag_for_under_age_of_consent = false; params.debug_settings.debug_geography = firebase::gma::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), "RequestConsentInfoUpdate"); @@ -2736,7 +2790,8 @@ TEST_F(FirebaseGmaUmpTest, TestUmpLoadAndShowIfRequired) { firebase::gma::ump::kConsentStatusRequired); firebase::Future future = - consent_info_->LoadAndShowConsentFormIfRequired(nullptr); + consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()); EXPECT_TRUE(future == consent_info_->LoadAndShowConsentFormIfRequiredLastResult()); @@ -2759,6 +2814,8 @@ TEST_F(FirebaseGmaUmpTest, TestUmpPrivacyOptions) { params.tag_for_under_age_of_consent = false; params.debug_settings.debug_geography = firebase::gma::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), "RequestConsentInfoUpdate"); @@ -2768,28 +2825,30 @@ TEST_F(FirebaseGmaUmpTest, TestUmpPrivacyOptions) { EXPECT_FALSE(consent_info_->CanRequestAds()); - WaitForCompletion(consent_info_->LoadAndShowConsentFormIfRequired(nullptr), + WaitForCompletion(consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()), "LoadAndShowConsentFormIfRequired"); EXPECT_EQ(consent_info_->GetConsentStatus(), firebase::gma::ump::kConsentStatusObtained); - EXPECT_TRUE(consent_info_->CanRequestAds()); + EXPECT_TRUE(consent_info_->CanRequestAds()) << "After consent obtained"; + + LogInfo( + "******** On the Privacy Options screen that is about to appear, please " + "select DO NOT CONSENT."); + + ProcessEvents(5000); EXPECT_EQ(consent_info_->GetPrivacyOptionsRequirementStatus(), firebase::gma::ump::kPrivacyOptionsRequirementStatusRequired); - firebase::Future future = - consent_info_->ShowPrivacyOptionsForm(nullptr); + firebase::Future future = consent_info_->ShowPrivacyOptionsForm( + app_framework::GetWindowController()); EXPECT_TRUE(future == consent_info_->ShowPrivacyOptionsFormLastResult()); WaitForCompletion(future, "ShowPrivacyOptionsForm"); - - EXPECT_EQ(consent_info_->GetConsentStatus(), - firebase::gma::ump::kConsentStatusRequired); - - EXPECT_FALSE(consent_info_->CanRequestAds()); } TEST_F(FirebaseGmaUmpTest, TestCanRequestAdsNonEEA) { @@ -2797,12 +2856,12 @@ TEST_F(FirebaseGmaUmpTest, TestCanRequestAdsNonEEA) { using firebase::gma::ump::ConsentRequestParameters; using firebase::gma::ump::ConsentStatus; - TEST_REQUIRES_USER_INTERACTION; - ConsentRequestParameters params; params.tag_for_under_age_of_consent = false; params.debug_settings.debug_geography = firebase::gma::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), "RequestConsentInfoUpdate"); @@ -2818,12 +2877,12 @@ TEST_F(FirebaseGmaUmpTest, TestCanRequestAdsEEA) { using firebase::gma::ump::ConsentRequestParameters; using firebase::gma::ump::ConsentStatus; - TEST_REQUIRES_USER_INTERACTION; - ConsentRequestParameters params; params.tag_for_under_age_of_consent = false; params.debug_settings.debug_geography = firebase::gma::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), "RequestConsentInfoUpdate"); @@ -2834,7 +2893,30 @@ TEST_F(FirebaseGmaUmpTest, TestCanRequestAdsEEA) { EXPECT_FALSE(consent_info_->CanRequestAds()); } -TEST_F(FirebaseGmaUmpTest, TestUmpCleanup) { +TEST_F(FirebaseGmaUmpTest, TestUmpCleanupWithDelay) { + using firebase::gma::ump::ConsentFormStatus; + using firebase::gma::ump::ConsentRequestParameters; + using firebase::gma::ump::ConsentStatus; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + firebase::Future future_request = + consent_info_->RequestConsentInfoUpdate(params); + firebase::Future future_load = consent_info_->LoadConsentForm(); + firebase::Future future_show = + consent_info_->ShowConsentForm(app_framework::GetWindowController()); + + ProcessEvents(5000); + + delete consent_info_; + consent_info_ = nullptr; + + EXPECT_EQ(future_request.status(), firebase::kFutureStatusInvalid); + EXPECT_EQ(future_load.status(), firebase::kFutureStatusInvalid); + EXPECT_EQ(future_show.status(), firebase::kFutureStatusInvalid); +} + +TEST_F(FirebaseGmaUmpTest, TestUmpCleanupRaceCondition) { using firebase::gma::ump::ConsentFormStatus; using firebase::gma::ump::ConsentRequestParameters; using firebase::gma::ump::ConsentStatus; @@ -2844,7 +2926,8 @@ TEST_F(FirebaseGmaUmpTest, TestUmpCleanup) { firebase::Future future_request = consent_info_->RequestConsentInfoUpdate(params); firebase::Future future_load = consent_info_->LoadConsentForm(); - firebase::Future future_show = consent_info_->ShowConsentForm(nullptr); + firebase::Future future_show = + consent_info_->ShowConsentForm(app_framework::GetWindowController()); delete consent_info_; consent_info_ = nullptr; diff --git a/gma/src/common/ump/consent_info_internal.cc b/gma/src/common/ump/consent_info_internal.cc index ef58873575..3a3e9cd4b8 100644 --- a/gma/src/common/ump/consent_info_internal.cc +++ b/gma/src/common/ump/consent_info_internal.cc @@ -45,7 +45,7 @@ const char* ConsentInfoInternal::GetConsentRequestErrorMessage( return "Network error"; case kConsentRequestErrorInternal: return "Internal error"; - case kConsentRequestErrorCodeMisconfiguration: + case kConsentRequestErrorMisconfiguration: return "A misconfiguration exists in the UI"; case kConsentRequestErrorUnknown: return "Unknown error"; @@ -72,8 +72,8 @@ const char* ConsentInfoInternal::GetConsentFormErrorMessage( return "Internal error"; case kConsentFormErrorUnknown: return "Unknown error"; - case kConsentFormErrorCodeAlreadyUsed: - return "Code already used"; + case kConsentFormErrorAlreadyUsed: + return "The form was already used"; case kConsentFormErrorInvalidOperation: return "Invalid operation"; case kConsentFormErrorOperationInProgress: diff --git a/gma/src/common/ump/consent_info_internal.h b/gma/src/common/ump/consent_info_internal.h index 3438526ef0..88160a341c 100644 --- a/gma/src/common/ump/consent_info_internal.h +++ b/gma/src/common/ump/consent_info_internal.h @@ -46,8 +46,8 @@ class ConsentInfoInternal { // platform-specific subclass. static ConsentInfoInternal* CreateInstance(); - virtual ConsentStatus GetConsentStatus() const = 0; - virtual ConsentFormStatus GetConsentFormStatus() const = 0; + virtual ConsentStatus GetConsentStatus() = 0; + virtual ConsentFormStatus GetConsentFormStatus() = 0; virtual Future RequestConsentInfoUpdate( const ConsentRequestParameters& params) = 0; diff --git a/gma/src/include/firebase/gma/ump/types.h b/gma/src/include/firebase/gma/ump/types.h index bf380d16be..af29182d16 100644 --- a/gma/src/include/firebase/gma/ump/types.h +++ b/gma/src/include/firebase/gma/ump/types.h @@ -114,7 +114,7 @@ enum ConsentRequestError { /// An internal error occurred. kConsentRequestErrorInternal, /// A misconfiguration exists in the UI. - kConsentRequestErrorCodeMisconfiguration, + kConsentRequestErrorMisconfiguration, /// An unknown error occurred. kConsentRequestErrorUnknown, /// An invalid operation occurred. Try again. @@ -151,7 +151,7 @@ enum ConsentFormError { /// The form is unavailable. kConsentFormErrorUnavailable, /// This form was already used. - kConsentFormErrorCodeAlreadyUsed, + kConsentFormErrorAlreadyUsed, /// An invalid operation occurred. Try again. kConsentFormErrorInvalidOperation, /// The operation is already in progress. Call diff --git a/gma/src/ios/ump/consent_info_internal_ios.h b/gma/src/ios/ump/consent_info_internal_ios.h new file mode 100644 index 0000000000..5472c91d4a --- /dev/null +++ b/gma/src/ios/ump/consent_info_internal_ios.h @@ -0,0 +1,68 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIREBASE_GMA_SRC_IOS_UMP_CONSENT_INFO_INTERNAL_IOS_H_ +#define FIREBASE_GMA_SRC_IOS_UMP_CONSENT_INFO_INTERNAL_IOS_H_ + +#include + +#include "firebase/internal/mutex.h" +#include "gma/src/common/ump/consent_info_internal.h" + +namespace firebase { +namespace gma { +namespace ump { +namespace internal { + +class ConsentInfoInternalIos : public ConsentInfoInternal { + public: + ConsentInfoInternalIos(); + ~ConsentInfoInternalIos() override; + + ConsentStatus GetConsentStatus() override; + Future RequestConsentInfoUpdate( + const ConsentRequestParameters& params) override; + + ConsentFormStatus GetConsentFormStatus() override; + Future LoadConsentForm() override; + Future ShowConsentForm(FormParent parent) override; + + Future LoadAndShowConsentFormIfRequired(FormParent parent) override; + + PrivacyOptionsRequirementStatus GetPrivacyOptionsRequirementStatus() override; + Future ShowPrivacyOptionsForm(FormParent parent) override; + + bool CanRequestAds() override; + + void Reset() override; + + private: + static ConsentInfoInternalIos* s_instance; + static firebase::Mutex s_instance_mutex; + + void SetLoadedForm(UMPConsentForm *form) { + loaded_form_ = form; + } + + UMPConsentForm *loaded_form_; +}; + +} // namespace internal +} // namespace ump +} // namespace gma +} // namespace firebase + +#endif // FIREBASE_GMA_SRC_IOS_UMP_CONSENT_INFO_INTERNAL_IOS_H_ diff --git a/gma/src/ios/ump/consent_info_internal_ios.mm b/gma/src/ios/ump/consent_info_internal_ios.mm new file mode 100644 index 0000000000..08d2a0d004 --- /dev/null +++ b/gma/src/ios/ump/consent_info_internal_ios.mm @@ -0,0 +1,295 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "gma/src/ios/ump/consent_info_internal_ios.h" + +#include "app/src/assert.h" +#include "app/src/thread.h" +#include "app/src/util_ios.h" + +namespace firebase { +namespace gma { +namespace ump { +namespace internal { + +ConsentInfoInternalIos* ConsentInfoInternalIos::s_instance = nullptr; +firebase::Mutex ConsentInfoInternalIos::s_instance_mutex; + + +// This explicitly implements the constructor for the outer class, +// ConsentInfoInternal. +ConsentInfoInternal* ConsentInfoInternal::CreateInstance() { + return new ConsentInfoInternalIos(); +} + +ConsentInfoInternalIos::ConsentInfoInternalIos() + : loaded_form_(nil) { + MutexLock lock(s_instance_mutex); + FIREBASE_ASSERT(s_instance == nullptr); + s_instance = this; +} + +ConsentInfoInternalIos::~ConsentInfoInternalIos() { + MutexLock lock(s_instance_mutex); + s_instance = nullptr; +} + +static ConsentRequestError CppRequestErrorFromIosRequestError(NSInteger code) { + switch(code) { + case UMPRequestErrorCodeInternal: + return kConsentRequestErrorInternal; + case UMPRequestErrorCodeInvalidAppID: + return kConsentRequestErrorInvalidAppId; + case UMPRequestErrorCodeMisconfiguration: + return kConsentRequestErrorMisconfiguration; + case UMPRequestErrorCodeNetwork: + return kConsentRequestErrorNetwork; + default: + LogWarning("GMA: Unknown UMPRequestErrorCode returned by UMP iOS SDK: %d", + (int)code); + return kConsentRequestErrorUnknown; + } +} + +static ConsentFormError CppFormErrorFromIosFormError(NSInteger code) { + switch(code) { + case UMPFormErrorCodeInternal: + return kConsentFormErrorInternal; + case UMPFormErrorCodeAlreadyUsed: + return kConsentFormErrorAlreadyUsed; + case UMPFormErrorCodeUnavailable: + return kConsentFormErrorUnavailable; + case UMPFormErrorCodeTimeout: + return kConsentFormErrorTimeout; + case UMPFormErrorCodeInvalidViewController: + return kConsentFormErrorInvalidOperation; + default: + LogWarning("GMA: Unknown UMPFormErrorCode returned by UMP iOS SDK: %d", + (int)code); + return kConsentFormErrorUnknown; + } +} + +Future ConsentInfoInternalIos::RequestConsentInfoUpdate( + const ConsentRequestParameters& params) { + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnRequestConsentInfoUpdate); + + UMPRequestParameters *ios_parameters = [[UMPRequestParameters alloc] init]; + ios_parameters.tagForUnderAgeOfConsent = params.tag_for_under_age_of_consent ? YES : NO; + UMPDebugSettings *ios_debug_settings = [[UMPDebugSettings alloc] init]; + + switch(params.debug_settings.debug_geography) { + case kConsentDebugGeographyEEA: + ios_debug_settings.geography = UMPDebugGeographyEEA; + break; + case kConsentDebugGeographyNonEEA: + ios_debug_settings.geography = UMPDebugGeographyNotEEA; + break; + case kConsentDebugGeographyDisabled: + ios_debug_settings.geography = UMPDebugGeographyDisabled; + break; + } + if (params.debug_settings.debug_device_ids.size() > 0) { + ios_debug_settings.testDeviceIdentifiers = + firebase::util::StringVectorToNSMutableArray(params.debug_settings.debug_device_ids); + } + ios_parameters.debugSettings = ios_debug_settings; + + util::DispatchAsyncSafeMainQueue(^{ + [UMPConsentInformation.sharedInstance + requestConsentInfoUpdateWithParameters:ios_parameters + completionHandler:^(NSError *_Nullable error){ + if (!error) { + CompleteFuture(handle, kConsentRequestSuccess); + } else { + CompleteFuture(handle, CppRequestErrorFromIosRequestError(error.code), error.localizedDescription.UTF8String); + } + }]; + }); + + return MakeFuture(futures(), handle); +} + +ConsentStatus ConsentInfoInternalIos::GetConsentStatus() { + UMPConsentStatus ios_status = + UMPConsentInformation.sharedInstance.consentStatus; + switch(ios_status) { + case UMPConsentStatusNotRequired: + return kConsentStatusNotRequired; + case UMPConsentStatusRequired: + return kConsentStatusRequired; + case UMPConsentStatusObtained: + return kConsentStatusObtained; + case UMPConsentStatusUnknown: + return kConsentStatusUnknown; + default: + LogWarning("GMA: Unknown UMPConsentStatus returned by UMP iOS SDK: %d", + (int)ios_status); + return kConsentStatusUnknown; + } +} + + +ConsentFormStatus ConsentInfoInternalIos::GetConsentFormStatus() { + UMPFormStatus ios_status = + UMPConsentInformation.sharedInstance.formStatus; + switch(ios_status) { + case UMPFormStatusAvailable: + return kConsentFormStatusAvailable; + case UMPFormStatusUnavailable: + return kConsentFormStatusUnavailable; + case UMPFormStatusUnknown: + return kConsentFormStatusUnknown; + default: + LogWarning("GMA: Unknown UMPFormConsentStatus returned by UMP iOS SDK: %d", + (int)ios_status); + return kConsentFormStatusUnknown; + } +} + +Future ConsentInfoInternalIos::LoadConsentForm() { + SafeFutureHandle handle = CreateFuture(kConsentInfoFnLoadConsentForm); + loaded_form_ = nil; + + util::DispatchAsyncSafeMainQueue(^{ + [UMPConsentForm + loadWithCompletionHandler:^(UMPConsentForm *_Nullable form, NSError *_Nullable error){ + if (form) { + MutexLock lock(s_instance_mutex); + if (s_instance) { + SetLoadedForm(form); + CompleteFuture(handle, kConsentFormSuccess, "Success"); + } + } else if (error) { + MutexLock lock(s_instance_mutex); + if (s_instance) { + CompleteFuture(handle, CppFormErrorFromIosFormError(error.code), error.localizedDescription.UTF8String); + } + } else { + MutexLock lock(s_instance_mutex); + if (s_instance) { + CompleteFuture(handle, kConsentFormErrorUnknown, "An unknown error occurred."); + } + } + }]; + }); + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalIos::ShowConsentForm(FormParent parent) { + SafeFutureHandle handle = CreateFuture(kConsentInfoFnShowConsentForm); + + if (!loaded_form_) { + CompleteFuture(handle, kConsentFormErrorInvalidOperation, + "You must call LoadConsentForm() prior to calling ShowConsentForm()."); + } else { + util::DispatchAsyncSafeMainQueue(^{ + [loaded_form_ presentFromViewController:parent + completionHandler:^(NSError *_Nullable error){ + if (!error) { + MutexLock lock(s_instance_mutex); + if (s_instance) { + CompleteFuture(handle, kConsentRequestSuccess); + } + } else { + MutexLock lock(s_instance_mutex); + if (s_instance) { + CompleteFuture(handle, CppFormErrorFromIosFormError(error.code), error.localizedDescription.UTF8String); + } + } + }]; + }); + } + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalIos::LoadAndShowConsentFormIfRequired( + FormParent parent) { + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnLoadAndShowConsentFormIfRequired); + + util::DispatchAsyncSafeMainQueue(^{ + [UMPConsentForm loadAndPresentIfRequiredFromViewController:parent + completionHandler:^(NSError *_Nullable error){ + if (!error) { + MutexLock lock(s_instance_mutex); + if (s_instance) { + CompleteFuture(handle, kConsentRequestSuccess); + } + } else { + MutexLock lock(s_instance_mutex); + if (s_instance) { + CompleteFuture(handle, CppFormErrorFromIosFormError(error.code), error.localizedDescription.UTF8String); + } + } + }]; + }); + return MakeFuture(futures(), handle); +} + +PrivacyOptionsRequirementStatus +ConsentInfoInternalIos::GetPrivacyOptionsRequirementStatus() { + UMPPrivacyOptionsRequirementStatus ios_status = + UMPConsentInformation.sharedInstance.privacyOptionsRequirementStatus; + switch(ios_status) { + case UMPPrivacyOptionsRequirementStatusRequired: + return kPrivacyOptionsRequirementStatusRequired; + case UMPPrivacyOptionsRequirementStatusNotRequired: + return kPrivacyOptionsRequirementStatusNotRequired; + case UMPPrivacyOptionsRequirementStatusUnknown: + return kPrivacyOptionsRequirementStatusUnknown; + default: + LogWarning("GMA: Unknown UMPPrivacyOptionsRequirementStatus returned by UMP iOS SDK: %d", + (int)ios_status); + return kPrivacyOptionsRequirementStatusUnknown; + } +} + +Future ConsentInfoInternalIos::ShowPrivacyOptionsForm(FormParent parent) { + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnShowPrivacyOptionsForm); + util::DispatchAsyncSafeMainQueue(^{ + [UMPConsentForm presentPrivacyOptionsFormFromViewController:parent + completionHandler:^(NSError *_Nullable error){ + if (!error) { + MutexLock lock(s_instance_mutex); + if (s_instance) { + CompleteFuture(handle, kConsentRequestSuccess); + } + } else { + MutexLock lock(s_instance_mutex); + if (s_instance) { + CompleteFuture(handle, CppFormErrorFromIosFormError(error.code), error.localizedDescription.UTF8String); + } + } + }]; + }); + return MakeFuture(futures(), handle); +} + +bool ConsentInfoInternalIos::CanRequestAds() { + return (UMPConsentInformation.sharedInstance.canRequestAds == YES ? true : false); +} + +void ConsentInfoInternalIos::Reset() { + [UMPConsentInformation.sharedInstance reset]; +} + +} // namespace internal +} // namespace ump +} // namespace gma +} // namespace firebase diff --git a/gma/src/stub/ump/consent_info_internal_stub.cc b/gma/src/stub/ump/consent_info_internal_stub.cc index 8f4d033896..6bc92ec08c 100644 --- a/gma/src/stub/ump/consent_info_internal_stub.cc +++ b/gma/src/stub/ump/consent_info_internal_stub.cc @@ -58,8 +58,10 @@ Future ConsentInfoInternalStub::RequestConsentInfoUpdate( consent_status_ = new_consent_status; under_age_of_consent_ = params.tag_for_under_age_of_consent; - consent_form_status_ = under_age_of_consent_ ? kConsentFormStatusUnavailable - : kConsentFormStatusAvailable; + consent_form_status_ = + (under_age_of_consent_ || consent_status_ != kConsentStatusRequired) + ? kConsentFormStatusUnavailable + : kConsentFormStatusAvailable; debug_geo_ = params.debug_settings.debug_geography; privacy_options_requirement_status_ = kPrivacyOptionsRequirementStatusNotRequired; @@ -104,7 +106,8 @@ Future ConsentInfoInternalStub::LoadAndShowConsentFormIfRequired( SafeFutureHandle handle = CreateFuture(kConsentInfoFnLoadAndShowConsentFormIfRequired); - if (consent_form_status_ != kConsentFormStatusAvailable) { + if (consent_status_ == kConsentStatusRequired && + consent_form_status_ != kConsentFormStatusAvailable) { CompleteFuture(handle, kConsentFormErrorUnavailable); return MakeFuture(futures(), handle); } diff --git a/gma/src/stub/ump/consent_info_internal_stub.h b/gma/src/stub/ump/consent_info_internal_stub.h index 23d44dfbaa..83368de284 100644 --- a/gma/src/stub/ump/consent_info_internal_stub.h +++ b/gma/src/stub/ump/consent_info_internal_stub.h @@ -51,8 +51,8 @@ class ConsentInfoInternalStub : public ConsentInfoInternal { ConsentInfoInternalStub(); ~ConsentInfoInternalStub() override; - ConsentStatus GetConsentStatus() const override { return consent_status_; } - ConsentFormStatus GetConsentFormStatus() const override { + ConsentStatus GetConsentStatus() override { return consent_status_; } + ConsentFormStatus GetConsentFormStatus() override { return consent_form_status_; } diff --git a/ios_pod/Podfile b/ios_pod/Podfile index 85d0daf610..fc5a040227 100644 --- a/ios_pod/Podfile +++ b/ios_pod/Podfile @@ -6,6 +6,7 @@ target 'GetPods' do pod 'Firebase/Core', '10.13.0' pod 'Google-Mobile-Ads-SDK', '10.9.0' + pod 'GoogleUserMessagingPlatform', '2.1.0' pod 'Firebase/Analytics', '10.13.0' pod 'Firebase/AppCheck', '10.13.0' pod 'Firebase/Auth', '10.13.0' diff --git a/testing/test_framework/src/android/android_firebase_test_framework.cc b/testing/test_framework/src/android/android_firebase_test_framework.cc index d1289d0750..03513c0924 100644 --- a/testing/test_framework/src/android/android_firebase_test_framework.cc +++ b/testing/test_framework/src/android/android_firebase_test_framework.cc @@ -267,4 +267,9 @@ int FirebaseTest::GetGooglePlayServicesVersion() { return static_cast(result); } +std::string FirebaseTest::GetDebugDeviceId() { + // TODO(jsimantov): Add this for Android. + return "placeholder-device-id"; +} + } // namespace firebase_test_framework diff --git a/testing/test_framework/src/desktop/desktop_firebase_test_framework.cc b/testing/test_framework/src/desktop/desktop_firebase_test_framework.cc index 31fc42184d..01e4a4e7c9 100644 --- a/testing/test_framework/src/desktop/desktop_firebase_test_framework.cc +++ b/testing/test_framework/src/desktop/desktop_firebase_test_framework.cc @@ -59,4 +59,6 @@ int FirebaseTest::GetGooglePlayServicesVersion() { return 0; } +std::string FirebaseTest::GetDebugDeviceId() { return "placeholder-device-id"; } + } // namespace firebase_test_framework diff --git a/testing/test_framework/src/firebase_test_framework.h b/testing/test_framework/src/firebase_test_framework.h index cff9ef5aec..4d03fe317f 100644 --- a/testing/test_framework/src/firebase_test_framework.h +++ b/testing/test_framework/src/firebase_test_framework.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "app_framework.h" // NOLINT @@ -192,6 +193,23 @@ namespace firebase_test_framework { #define SKIP_TEST_ON_ANDROID ((void)0) #endif // defined(ANDROID) +// Skip on physical mobile device. +#if !defined(ANDROID) && !(defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) +// Allow desktop. +#define SKIP_TEST_ON_MOBILE_HARDWARE ((void)0) +#else +// Android needs to determine emulator at runtime, so we can't just use #ifdef. +#define SKIP_TEST_ON_MOBILE_HARDWARE \ + { \ + if (!IsRunningOnEmulator()) { \ + app_framework::LogInfo("Skipping %s on mobile hardware.", \ + test_info_->name()); \ + GTEST_SKIP(); \ + return; \ + } \ + } +#endif // !defined(ANDROID) && !(defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + // Android needs to determine emulator at runtime, so we can't just use #ifdef. #define SKIP_TEST_ON_SIMULATOR \ { \ @@ -538,6 +556,8 @@ class FirebaseTest : public testing::Test { // false if it failed. static bool Base64Decode(const std::string& input, std::string* output); + static std::string GetDebugDeviceId(); + firebase::App* app_; static int argc_; static char** argv_; diff --git a/testing/test_framework/src/ios/ios_firebase_test_framework.mm b/testing/test_framework/src/ios/ios_firebase_test_framework.mm index 36295d0b76..0a14eb4f46 100644 --- a/testing/test_framework/src/ios/ios_firebase_test_framework.mm +++ b/testing/test_framework/src/ios/ios_firebase_test_framework.mm @@ -205,4 +205,8 @@ static bool SendHttpRequest(const char* method, const char* url, return 0; } +std::string FirebaseTest::GetDebugDeviceId() { + return UIDevice.currentDevice.identifierForVendor.UUIDString.UTF8String; +} + } // namespace firebase_test_framework