Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(analytics): initiate on device measurement with sha256-hashed values #7963

59 changes: 59 additions & 0 deletions packages/analytics/__tests__/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ import {
logViewSearchResults,
setDefaultEventParameters,
initiateOnDeviceConversionMeasurementWithEmailAddress,
initiateOnDeviceConversionMeasurementWithHashedEmailAddress,
initiateOnDeviceConversionMeasurementWithPhoneNumber,
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber,
isSupported,
setConsent,
settings,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -843,10 +877,35 @@ describe('Analytics', function () {
expect(initiateOnDeviceConversionMeasurementWithEmailAddress).toBeDefined();
});

it('`initiateOnDeviceConversionMeasurementWithHashedEmailAddress` function is properly exposed to end user', function () {
matinzd marked this conversation as resolved.
Show resolved Hide resolved
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.",
);
});

matinzd marked this conversation as resolved.
Show resolved Hide resolved
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();
});
Expand Down
19 changes: 19 additions & 0 deletions packages/analytics/e2e/analytics.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
36 changes: 32 additions & 4 deletions packages/analytics/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1755,23 +1755,51 @@ export namespace FirebaseAnalyticsTypes {
setDefaultEventParameters(params?: { [key: string]: any }): Promise<void>;

/**
* 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<void>;

/**
* 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<void>;

/**
* 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<void>;

/**
* 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<void>;

/**
* For Consent Mode!
*
Expand Down
38 changes: 38 additions & 0 deletions packages/analytics/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,22 @@
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(

Check warning on line 757 in packages/analytics/lib/index.js

View check run for this annotation

Codecov / codecov/patch

packages/analytics/lib/index.js#L757

Added line #L757 was not covered by tests
hashedEmailAddress,
);
}

initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber) {
if (!isE164PhoneNumber(phoneNumber)) {
throw new Error(
Expand All @@ -756,6 +772,28 @@

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(

Check warning on line 793 in packages/analytics/lib/index.js

View check run for this annotation

Codecov / codecov/patch

packages/analytics/lib/index.js#L793

Added line #L793 was not covered by tests
hashedPhoneNumber,
);
}
}

// import { SDK_VERSION } from '@react-native-firebase/analytics';
Expand Down
41 changes: 37 additions & 4 deletions packages/analytics/lib/modular/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -1152,10 +1153,11 @@ export function setDefaultEventParameters(
): Promise<void>;

/**
* 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'
*/
Expand All @@ -1165,10 +1167,26 @@ export function initiateOnDeviceConversionMeasurementWithEmailAddress(
): Promise<void>;

/**
* 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<void>;

/**
* 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.
*/
Expand All @@ -1177,6 +1195,21 @@ export function initiateOnDeviceConversionMeasurementWithPhoneNumber(
phoneNumber: string,
): Promise<void>;

/**
* 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<void>;

/**
* Checks four different things.
* 1. Checks if it's not a browser extension environment.
Expand Down
32 changes: 32 additions & 0 deletions packages/analytics/lib/modular/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
Loading