From 5ce51e1b8b225bb509b4a0d444f42fa4c5f3effc Mon Sep 17 00:00:00 2001 From: Matin Zadeh Dolatabad <24797481+matinzd@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:06:30 +0200 Subject: [PATCH] feat(analytics): initiate on device measurement with sha256-hashed values (#7963) --- .../analytics/__tests__/analytics.test.ts | 59 +++++++++++++++++++ packages/analytics/e2e/analytics.e2e.js | 19 ++++++ .../ios/RNFBAnalytics/RNFBAnalyticsModule.m | 28 +++++++++ packages/analytics/lib/index.d.ts | 36 +++++++++-- packages/analytics/lib/index.js | 38 ++++++++++++ packages/analytics/lib/modular/index.d.ts | 41 +++++++++++-- packages/analytics/lib/modular/index.js | 32 ++++++++++ 7 files changed, 245 insertions(+), 8 deletions(-) diff --git a/packages/analytics/__tests__/analytics.test.ts b/packages/analytics/__tests__/analytics.test.ts index 0a69069051..86520877b4 100644 --- a/packages/analytics/__tests__/analytics.test.ts +++ b/packages/analytics/__tests__/analytics.test.ts @@ -50,7 +50,9 @@ import { logViewSearchResults, setDefaultEventParameters, initiateOnDeviceConversionMeasurementWithEmailAddress, + initiateOnDeviceConversionMeasurementWithHashedEmailAddress, initiateOnDeviceConversionMeasurementWithPhoneNumber, + initiateOnDeviceConversionMeasurementWithHashedPhoneNumber, isSupported, setConsent, settings, @@ -650,6 +652,38 @@ describe('Analytics', function () { }); }); + describe('initiateOnDeviceConversionMeasurementWithHashedEmailAddress()', function () { + it('throws if not a string', function () { + expect(() => + // @ts-ignore + firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(true), + ).toThrowError( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a string value.", + ); + }); + }); + + describe('initiateOnDeviceConversionMeasurementWithHashedPhoneNumber()', function () { + it('throws if not a string', function () { + expect(() => + // @ts-ignore + firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(1234), + ).toThrowError( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.", + ); + }); + + it('throws if hashed value is a phone number in E.164 format', function () { + expect(() => + firebase + .analytics() + .initiateOnDeviceConversionMeasurementWithHashedPhoneNumber('+1234567890'), + ).toThrowError( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.", + ); + }); + }); + describe('modular', function () { it('`getAnalytics` function is properly exposed to end user', function () { expect(getAnalytics).toBeDefined(); @@ -843,10 +877,35 @@ describe('Analytics', function () { expect(initiateOnDeviceConversionMeasurementWithEmailAddress).toBeDefined(); }); + it('`initiateOnDeviceConversionMeasurementWithHashedEmailAddress` function is properly exposed to end user', function () { + expect(initiateOnDeviceConversionMeasurementWithHashedEmailAddress).toBeDefined(); + }); + + it('`initiateOnDeviceConversionMeasurementWithHashedEmailAddress` throws if not a string', function () { + expect(() => + // @ts-ignore + initiateOnDeviceConversionMeasurementWithHashedEmailAddress(getAnalytics(), true), + ).toThrowError( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a string value.", + ); + }); + + it('`initiateOnDeviceConversionMeasurementWithHashedPhoneNumber` should throw if the value is in E.164 format', function () { + expect(() => + initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(getAnalytics(), '+1234567890'), + ).toThrowError( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.", + ); + }); + it('`initiateOnDeviceConversionMeasurementWithPhoneNumber` function is properly exposed to end user', function () { expect(initiateOnDeviceConversionMeasurementWithPhoneNumber).toBeDefined(); }); + it('`initiateOnDeviceConversionMeasurementWithHashedPhoneNumber` function is properly exposed to end user', function () { + expect(initiateOnDeviceConversionMeasurementWithHashedPhoneNumber).toBeDefined(); + }); + it('`isSupported` function is properly exposed to end user', function () { expect(isSupported).toBeDefined(); }); diff --git a/packages/analytics/e2e/analytics.e2e.js b/packages/analytics/e2e/analytics.e2e.js index 0b1e736edc..fb3bc1d4aa 100644 --- a/packages/analytics/e2e/analytics.e2e.js +++ b/packages/analytics/e2e/analytics.e2e.js @@ -489,6 +489,17 @@ describe('analytics() modular', function () { .analytics() .initiateOnDeviceConversionMeasurementWithEmailAddress('conversionTest@example.com'); }); + + it('calls native API successfully with hashed email', async function () { + // Normalized email address: 'conversiontest@example.com' + // echo -n 'conversiontest@example.com' | shasum -a 256 + + await firebase + .analytics() + .initiateOnDeviceConversionMeasurementWithHashedEmailAddress( + '73914334417d04bc2922331e5fb3b3572ab88debfa0c63beb0c56f7b31d4aaed', + ); + }); }); // Test this last so it does not stop delivery to DebugView @@ -499,6 +510,14 @@ describe('analytics() modular', function () { .initiateOnDeviceConversionMeasurementWithPhoneNumber('+14155551212'); }); + it('calls native API successfully with hashed phone', async function () { + await firebase + .analytics() + .initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( + '5dce05f429bc23dbd9e2caa03f336b56d4ee2aa374d8708f4f12eb4e10204c2b', + ); + }); + it('handles mal-formatted phone number', async function () { try { await firebase diff --git a/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m b/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m index 61b7dfa1b7..22e1c7f221 100644 --- a/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m +++ b/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m @@ -164,6 +164,20 @@ - (dispatch_queue_t)methodQueue { return resolve([NSNull null]); } +RCT_EXPORT_METHOD(initiateOnDeviceConversionMeasurementWithHashedEmailAddress + : (NSString *)hashedEmailAddress resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { + @try { + NSData *emailAddress = [hashedEmailAddress dataUsingEncoding:NSUTF8StringEncoding]; + [FIRAnalytics initiateOnDeviceConversionMeasurementWithHashedEmailAddress:emailAddress]; + } @catch (NSException *exception) { + return [RNFBSharedUtils rejectPromiseWithExceptionDict:reject exception:exception]; + } + + return resolve([NSNull null]); +} + RCT_EXPORT_METHOD(initiateOnDeviceConversionMeasurementWithPhoneNumber : (NSString *)phoneNumber resolver : (RCTPromiseResolveBlock)resolve rejecter @@ -177,6 +191,20 @@ - (dispatch_queue_t)methodQueue { return resolve([NSNull null]); } +RCT_EXPORT_METHOD(initiateOnDeviceConversionMeasurementWithHashedPhoneNumber + : (NSString *)hashedPhoneNumber resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { + @try { + NSData *phoneNumber = [hashedPhoneNumber dataUsingEncoding:NSUTF8StringEncoding]; + [FIRAnalytics initiateOnDeviceConversionMeasurementWithHashedPhoneNumber:phoneNumber]; + } @catch (NSException *exception) { + return [RNFBSharedUtils rejectPromiseWithExceptionDict:reject exception:exception]; + } + + return resolve([NSNull null]); +} + RCT_EXPORT_METHOD(setConsent : (NSDictionary *)consentSettings resolver : (RCTPromiseResolveBlock)resolve rejecter diff --git a/packages/analytics/lib/index.d.ts b/packages/analytics/lib/index.d.ts index fd78994c71..cac30af0ce 100644 --- a/packages/analytics/lib/index.d.ts +++ b/packages/analytics/lib/index.d.ts @@ -1755,23 +1755,51 @@ export namespace FirebaseAnalyticsTypes { setDefaultEventParameters(params?: { [key: string]: any }): Promise; /** - * start privacy-sensitive on-device conversion management. + * Start privacy-sensitive on-device conversion management. * This is iOS-only. - * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile. * + * @platform ios * @param emailAddress email address, properly formatted complete with domain name e.g, 'user@example.com' */ initiateOnDeviceConversionMeasurementWithEmailAddress(emailAddress: string): Promise; /** - * start privacy-sensitive on-device conversion management. + * Start privacy-sensitive on-device conversion management. * This is iOS-only. - * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile. + * You need to pass the sha256-hashed of normalized email address to this function. See [this link](https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials) for more information. * + * @platform ios + * @param hashedEmailAddress sha256-hashed of normalized email address, properly formatted complete with domain name e.g, 'user@example.com' + */ + initiateOnDeviceConversionMeasurementWithHashedEmailAddress( + hashedEmailAddress: string, + ): Poromise; + + /** + * Start privacy-sensitive on-device conversion management. + * This is iOS-only. + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile. + * + * @platform ios * @param phoneNumber phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces. */ initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber: string): Promise; + /** + * Start privacy-sensitive on-device conversion management. + * This is iOS-only. + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile. + * You need to pass the sha256-hashed of phone number in E.164 format. See [this link](https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials) for more information. + * + * @platform ios + * @param hashedPhoneNumber sha256-hashed of normalized phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces. + */ + initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( + hashedPhoneNumber: string, + ): Promise; + /** * For Consent Mode! * diff --git a/packages/analytics/lib/index.js b/packages/analytics/lib/index.js index ec4a71ebba..1cf5543551 100644 --- a/packages/analytics/lib/index.js +++ b/packages/analytics/lib/index.js @@ -743,6 +743,22 @@ class FirebaseAnalyticsModule extends FirebaseModule { return this.native.initiateOnDeviceConversionMeasurementWithEmailAddress(emailAddress); } + initiateOnDeviceConversionMeasurementWithHashedEmailAddress(hashedEmailAddress) { + if (!isString(hashedEmailAddress)) { + throw new Error( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a string value.", + ); + } + + if (!isIOS) { + return; + } + + return this.native.initiateOnDeviceConversionMeasurementWithHashedEmailAddress( + hashedEmailAddress, + ); + } + initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber) { if (!isE164PhoneNumber(phoneNumber)) { throw new Error( @@ -756,6 +772,28 @@ class FirebaseAnalyticsModule extends FirebaseModule { return this.native.initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber); } + + initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(hashedPhoneNumber) { + if (isE164PhoneNumber(hashedPhoneNumber)) { + throw new Error( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.", + ); + } + + if (!isString(hashedPhoneNumber)) { + throw new Error( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.", + ); + } + + if (!isIOS) { + return; + } + + return this.native.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( + hashedPhoneNumber, + ); + } } // import { SDK_VERSION } from '@react-native-firebase/analytics'; diff --git a/packages/analytics/lib/modular/index.d.ts b/packages/analytics/lib/modular/index.d.ts index a79a7a0fd6..4287318daf 100644 --- a/packages/analytics/lib/modular/index.d.ts +++ b/packages/analytics/lib/modular/index.d.ts @@ -1143,6 +1143,7 @@ export function logViewSearchResults( * For Web, the values passed persist on the current page and are passed with all * subsequent events. * + * @platform ios * @param analytics Analytics instance. * @param params Parameters to be added to the map of parameters added to every event. */ @@ -1152,10 +1153,11 @@ export function setDefaultEventParameters( ): Promise; /** - * start privacy-sensitive on-device conversion management. + * Start privacy-sensitive on-device conversion management. * This is iOS-only. - * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile. * + * @platform ios * @param analytics Analytics instance. * @param emailAddress email address, properly formatted complete with domain name e.g, 'user@example.com' */ @@ -1165,10 +1167,26 @@ export function initiateOnDeviceConversionMeasurementWithEmailAddress( ): Promise; /** - * start privacy-sensitive on-device conversion management. + * Start privacy-sensitive on-device conversion management. * This is iOS-only. - * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile. + * You need to pass the sha256-hashed of normalized email address to this function. See [this link](https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials) for more information. * + * @platform ios + * @param analytics Analytics instance. + * @param hashedEmailAddress sha256-hashed of normalized email address, properly formatted complete with domain name e.g, 'user@example.com' + */ +export function initiateOnDeviceConversionMeasurementWithHashedEmailAddress( + analytics: Analytics, + hashedEmailAddress: string, +): Poromise; + +/** + * Start privacy-sensitive on-device conversion management. + * This is iOS-only. + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile. + * + * @platform ios * @param analytics Analytics instance. * @param phoneNumber phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces. */ @@ -1177,6 +1195,21 @@ export function initiateOnDeviceConversionMeasurementWithPhoneNumber( phoneNumber: string, ): Promise; +/** + * Start privacy-sensitive on-device conversion management. + * This is iOS-only. + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile. + * You need to pass the sha256-hashed of phone number in E.164 format. See [this link](https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials) for more information. + * + * @platform ios + * @param analytics Analytics instance. + * @param hashedPhoneNumber sha256-hashed of normalized phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces. + */ +export function initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( + analytics: Analytics, + hashedPhoneNumber: string, +): Promise; + /** * Checks four different things. * 1. Checks if it's not a browser extension environment. diff --git a/packages/analytics/lib/modular/index.js b/packages/analytics/lib/modular/index.js index ba70f07ba0..ed69a32b3a 100644 --- a/packages/analytics/lib/modular/index.js +++ b/packages/analytics/lib/modular/index.js @@ -524,6 +524,22 @@ export function initiateOnDeviceConversionMeasurementWithEmailAddress(analytics, return analytics.initiateOnDeviceConversionMeasurementWithEmailAddress(emailAddress); } +/** + * start privacy-sensitive on-device conversion management. + * This is iOS-only. + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile + * + * @param analytics Analytics instance. + * @param hashedEmailAddress sha256-hashed of normalized email address, properly formatted complete with domain name e.g, 'user@example.com' + * @link https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials + */ +export function initiateOnDeviceConversionMeasurementWithHashedEmailAddress( + analytics, + hashedEmailAddress, +) { + return analytics.initiateOnDeviceConversionMeasurementWithHashedEmailAddress(hashedEmailAddress); +} + /** * start privacy-sensitive on-device conversion management. * This is iOS-only. @@ -535,6 +551,22 @@ export function initiateOnDeviceConversionMeasurementWithPhoneNumber(analytics, return analytics.initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber); } +/** + * start privacy-sensitive on-device conversion management. + * This is iOS-only. + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile + * + * @param analytics Analytics instance. + * @param hashedPhoneNumber sha256-hashed of normalized phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces. + * @link https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials + */ +export function initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( + analytics, + hashedPhoneNumber, +) { + return analytics.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(hashedPhoneNumber); +} + /** * Checks four different things. * 1. Checks if it's not a browser extension environment.