Skip to content

Commit 5ce51e1

Browse files
authored
feat(analytics): initiate on device measurement with sha256-hashed values (#7963)
1 parent c1ac022 commit 5ce51e1

File tree

7 files changed

+245
-8
lines changed

7 files changed

+245
-8
lines changed

packages/analytics/__tests__/analytics.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ import {
5050
logViewSearchResults,
5151
setDefaultEventParameters,
5252
initiateOnDeviceConversionMeasurementWithEmailAddress,
53+
initiateOnDeviceConversionMeasurementWithHashedEmailAddress,
5354
initiateOnDeviceConversionMeasurementWithPhoneNumber,
55+
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber,
5456
isSupported,
5557
setConsent,
5658
settings,
@@ -650,6 +652,38 @@ describe('Analytics', function () {
650652
});
651653
});
652654

655+
describe('initiateOnDeviceConversionMeasurementWithHashedEmailAddress()', function () {
656+
it('throws if not a string', function () {
657+
expect(() =>
658+
// @ts-ignore
659+
firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(true),
660+
).toThrowError(
661+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a string value.",
662+
);
663+
});
664+
});
665+
666+
describe('initiateOnDeviceConversionMeasurementWithHashedPhoneNumber()', function () {
667+
it('throws if not a string', function () {
668+
expect(() =>
669+
// @ts-ignore
670+
firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(1234),
671+
).toThrowError(
672+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.",
673+
);
674+
});
675+
676+
it('throws if hashed value is a phone number in E.164 format', function () {
677+
expect(() =>
678+
firebase
679+
.analytics()
680+
.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber('+1234567890'),
681+
).toThrowError(
682+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.",
683+
);
684+
});
685+
});
686+
653687
describe('modular', function () {
654688
it('`getAnalytics` function is properly exposed to end user', function () {
655689
expect(getAnalytics).toBeDefined();
@@ -843,10 +877,35 @@ describe('Analytics', function () {
843877
expect(initiateOnDeviceConversionMeasurementWithEmailAddress).toBeDefined();
844878
});
845879

880+
it('`initiateOnDeviceConversionMeasurementWithHashedEmailAddress` function is properly exposed to end user', function () {
881+
expect(initiateOnDeviceConversionMeasurementWithHashedEmailAddress).toBeDefined();
882+
});
883+
884+
it('`initiateOnDeviceConversionMeasurementWithHashedEmailAddress` throws if not a string', function () {
885+
expect(() =>
886+
// @ts-ignore
887+
initiateOnDeviceConversionMeasurementWithHashedEmailAddress(getAnalytics(), true),
888+
).toThrowError(
889+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a string value.",
890+
);
891+
});
892+
893+
it('`initiateOnDeviceConversionMeasurementWithHashedPhoneNumber` should throw if the value is in E.164 format', function () {
894+
expect(() =>
895+
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(getAnalytics(), '+1234567890'),
896+
).toThrowError(
897+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.",
898+
);
899+
});
900+
846901
it('`initiateOnDeviceConversionMeasurementWithPhoneNumber` function is properly exposed to end user', function () {
847902
expect(initiateOnDeviceConversionMeasurementWithPhoneNumber).toBeDefined();
848903
});
849904

905+
it('`initiateOnDeviceConversionMeasurementWithHashedPhoneNumber` function is properly exposed to end user', function () {
906+
expect(initiateOnDeviceConversionMeasurementWithHashedPhoneNumber).toBeDefined();
907+
});
908+
850909
it('`isSupported` function is properly exposed to end user', function () {
851910
expect(isSupported).toBeDefined();
852911
});

packages/analytics/e2e/analytics.e2e.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,17 @@ describe('analytics() modular', function () {
489489
.analytics()
490490
.initiateOnDeviceConversionMeasurementWithEmailAddress('[email protected]');
491491
});
492+
493+
it('calls native API successfully with hashed email', async function () {
494+
// Normalized email address: '[email protected]'
495+
// echo -n '[email protected]' | shasum -a 256
496+
497+
await firebase
498+
.analytics()
499+
.initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
500+
'73914334417d04bc2922331e5fb3b3572ab88debfa0c63beb0c56f7b31d4aaed',
501+
);
502+
});
492503
});
493504

494505
// Test this last so it does not stop delivery to DebugView
@@ -499,6 +510,14 @@ describe('analytics() modular', function () {
499510
.initiateOnDeviceConversionMeasurementWithPhoneNumber('+14155551212');
500511
});
501512

513+
it('calls native API successfully with hashed phone', async function () {
514+
await firebase
515+
.analytics()
516+
.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
517+
'5dce05f429bc23dbd9e2caa03f336b56d4ee2aa374d8708f4f12eb4e10204c2b',
518+
);
519+
});
520+
502521
it('handles mal-formatted phone number', async function () {
503522
try {
504523
await firebase

packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,20 @@ - (dispatch_queue_t)methodQueue {
164164
return resolve([NSNull null]);
165165
}
166166

167+
RCT_EXPORT_METHOD(initiateOnDeviceConversionMeasurementWithHashedEmailAddress
168+
: (NSString *)hashedEmailAddress resolver
169+
: (RCTPromiseResolveBlock)resolve rejecter
170+
: (RCTPromiseRejectBlock)reject) {
171+
@try {
172+
NSData *emailAddress = [hashedEmailAddress dataUsingEncoding:NSUTF8StringEncoding];
173+
[FIRAnalytics initiateOnDeviceConversionMeasurementWithHashedEmailAddress:emailAddress];
174+
} @catch (NSException *exception) {
175+
return [RNFBSharedUtils rejectPromiseWithExceptionDict:reject exception:exception];
176+
}
177+
178+
return resolve([NSNull null]);
179+
}
180+
167181
RCT_EXPORT_METHOD(initiateOnDeviceConversionMeasurementWithPhoneNumber
168182
: (NSString *)phoneNumber resolver
169183
: (RCTPromiseResolveBlock)resolve rejecter
@@ -177,6 +191,20 @@ - (dispatch_queue_t)methodQueue {
177191
return resolve([NSNull null]);
178192
}
179193

194+
RCT_EXPORT_METHOD(initiateOnDeviceConversionMeasurementWithHashedPhoneNumber
195+
: (NSString *)hashedPhoneNumber resolver
196+
: (RCTPromiseResolveBlock)resolve rejecter
197+
: (RCTPromiseRejectBlock)reject) {
198+
@try {
199+
NSData *phoneNumber = [hashedPhoneNumber dataUsingEncoding:NSUTF8StringEncoding];
200+
[FIRAnalytics initiateOnDeviceConversionMeasurementWithHashedPhoneNumber:phoneNumber];
201+
} @catch (NSException *exception) {
202+
return [RNFBSharedUtils rejectPromiseWithExceptionDict:reject exception:exception];
203+
}
204+
205+
return resolve([NSNull null]);
206+
}
207+
180208
RCT_EXPORT_METHOD(setConsent
181209
: (NSDictionary *)consentSettings resolver
182210
: (RCTPromiseResolveBlock)resolve rejecter

packages/analytics/lib/index.d.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,23 +1755,51 @@ export namespace FirebaseAnalyticsTypes {
17551755
setDefaultEventParameters(params?: { [key: string]: any }): Promise<void>;
17561756

17571757
/**
1758-
* start privacy-sensitive on-device conversion management.
1758+
* Start privacy-sensitive on-device conversion management.
17591759
* This is iOS-only.
1760-
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile
1760+
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile.
17611761
*
1762+
* @platform ios
17621763
* @param emailAddress email address, properly formatted complete with domain name e.g, '[email protected]'
17631764
*/
17641765
initiateOnDeviceConversionMeasurementWithEmailAddress(emailAddress: string): Promise<void>;
17651766

17661767
/**
1767-
* start privacy-sensitive on-device conversion management.
1768+
* Start privacy-sensitive on-device conversion management.
17681769
* This is iOS-only.
1769-
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile
1770+
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile.
1771+
* 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.
17701772
*
1773+
* @platform ios
1774+
* @param hashedEmailAddress sha256-hashed of normalized email address, properly formatted complete with domain name e.g, '[email protected]'
1775+
*/
1776+
initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
1777+
hashedEmailAddress: string,
1778+
): Poromise<void>;
1779+
1780+
/**
1781+
* Start privacy-sensitive on-device conversion management.
1782+
* This is iOS-only.
1783+
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile.
1784+
*
1785+
* @platform ios
17711786
* @param phoneNumber phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces.
17721787
*/
17731788
initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber: string): Promise<void>;
17741789

1790+
/**
1791+
* Start privacy-sensitive on-device conversion management.
1792+
* This is iOS-only.
1793+
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile.
1794+
* 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.
1795+
*
1796+
* @platform ios
1797+
* @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.
1798+
*/
1799+
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
1800+
hashedPhoneNumber: string,
1801+
): Promise<void>;
1802+
17751803
/**
17761804
* For Consent Mode!
17771805
*

packages/analytics/lib/index.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,22 @@ class FirebaseAnalyticsModule extends FirebaseModule {
743743
return this.native.initiateOnDeviceConversionMeasurementWithEmailAddress(emailAddress);
744744
}
745745

746+
initiateOnDeviceConversionMeasurementWithHashedEmailAddress(hashedEmailAddress) {
747+
if (!isString(hashedEmailAddress)) {
748+
throw new Error(
749+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a string value.",
750+
);
751+
}
752+
753+
if (!isIOS) {
754+
return;
755+
}
756+
757+
return this.native.initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
758+
hashedEmailAddress,
759+
);
760+
}
761+
746762
initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber) {
747763
if (!isE164PhoneNumber(phoneNumber)) {
748764
throw new Error(
@@ -756,6 +772,28 @@ class FirebaseAnalyticsModule extends FirebaseModule {
756772

757773
return this.native.initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber);
758774
}
775+
776+
initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(hashedPhoneNumber) {
777+
if (isE164PhoneNumber(hashedPhoneNumber)) {
778+
throw new Error(
779+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.",
780+
);
781+
}
782+
783+
if (!isString(hashedPhoneNumber)) {
784+
throw new Error(
785+
"firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.",
786+
);
787+
}
788+
789+
if (!isIOS) {
790+
return;
791+
}
792+
793+
return this.native.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
794+
hashedPhoneNumber,
795+
);
796+
}
759797
}
760798

761799
// import { SDK_VERSION } from '@react-native-firebase/analytics';

packages/analytics/lib/modular/index.d.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,7 @@ export function logViewSearchResults(
11431143
* For Web, the values passed persist on the current page and are passed with all
11441144
* subsequent events.
11451145
*
1146+
* @platform ios
11461147
* @param analytics Analytics instance.
11471148
* @param params Parameters to be added to the map of parameters added to every event.
11481149
*/
@@ -1152,10 +1153,11 @@ export function setDefaultEventParameters(
11521153
): Promise<void>;
11531154

11541155
/**
1155-
* start privacy-sensitive on-device conversion management.
1156+
* Start privacy-sensitive on-device conversion management.
11561157
* This is iOS-only.
1157-
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile
1158+
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile.
11581159
*
1160+
* @platform ios
11591161
* @param analytics Analytics instance.
11601162
* @param emailAddress email address, properly formatted complete with domain name e.g, '[email protected]'
11611163
*/
@@ -1165,10 +1167,26 @@ export function initiateOnDeviceConversionMeasurementWithEmailAddress(
11651167
): Promise<void>;
11661168

11671169
/**
1168-
* start privacy-sensitive on-device conversion management.
1170+
* Start privacy-sensitive on-device conversion management.
11691171
* This is iOS-only.
1170-
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile
1172+
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile.
1173+
* 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.
11711174
*
1175+
* @platform ios
1176+
* @param analytics Analytics instance.
1177+
* @param hashedEmailAddress sha256-hashed of normalized email address, properly formatted complete with domain name e.g, '[email protected]'
1178+
*/
1179+
export function initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
1180+
analytics: Analytics,
1181+
hashedEmailAddress: string,
1182+
): Poromise<void>;
1183+
1184+
/**
1185+
* Start privacy-sensitive on-device conversion management.
1186+
* This is iOS-only.
1187+
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile.
1188+
*
1189+
* @platform ios
11721190
* @param analytics Analytics instance.
11731191
* @param phoneNumber phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces.
11741192
*/
@@ -1177,6 +1195,21 @@ export function initiateOnDeviceConversionMeasurementWithPhoneNumber(
11771195
phoneNumber: string,
11781196
): Promise<void>;
11791197

1198+
/**
1199+
* Start privacy-sensitive on-device conversion management.
1200+
* This is iOS-only.
1201+
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile.
1202+
* 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.
1203+
*
1204+
* @platform ios
1205+
* @param analytics Analytics instance.
1206+
* @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.
1207+
*/
1208+
export function initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
1209+
analytics: Analytics,
1210+
hashedPhoneNumber: string,
1211+
): Promise<void>;
1212+
11801213
/**
11811214
* Checks four different things.
11821215
* 1. Checks if it's not a browser extension environment.

packages/analytics/lib/modular/index.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,22 @@ export function initiateOnDeviceConversionMeasurementWithEmailAddress(analytics,
524524
return analytics.initiateOnDeviceConversionMeasurementWithEmailAddress(emailAddress);
525525
}
526526

527+
/**
528+
* start privacy-sensitive on-device conversion management.
529+
* This is iOS-only.
530+
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile
531+
*
532+
* @param analytics Analytics instance.
533+
* @param hashedEmailAddress sha256-hashed of normalized email address, properly formatted complete with domain name e.g, '[email protected]'
534+
* @link https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials
535+
*/
536+
export function initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
537+
analytics,
538+
hashedEmailAddress,
539+
) {
540+
return analytics.initiateOnDeviceConversionMeasurementWithHashedEmailAddress(hashedEmailAddress);
541+
}
542+
527543
/**
528544
* start privacy-sensitive on-device conversion management.
529545
* This is iOS-only.
@@ -535,6 +551,22 @@ export function initiateOnDeviceConversionMeasurementWithPhoneNumber(analytics,
535551
return analytics.initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber);
536552
}
537553

554+
/**
555+
* start privacy-sensitive on-device conversion management.
556+
* This is iOS-only.
557+
* This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile
558+
*
559+
* @param analytics Analytics instance.
560+
* @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.
561+
* @link https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials
562+
*/
563+
export function initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
564+
analytics,
565+
hashedPhoneNumber,
566+
) {
567+
return analytics.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(hashedPhoneNumber);
568+
}
569+
538570
/**
539571
* Checks four different things.
540572
* 1. Checks if it's not a browser extension environment.

0 commit comments

Comments
 (0)