diff --git a/.env.local b/.env.local index 40519e1afd3758e4c988073e64f0c07693e5b8b2..21a665d1124ad1c401fcd3ee6f7a7b0c16be33d6 100644 --- a/.env.local +++ b/.env.local @@ -77,4 +77,9 @@ export TYPESENSE_HOST=dev-search.holi.social export TYPESENSE_PORT=443 export TYPESENSE_PROTOCOL=https export TYPESENSE_API_KEY=WAxFP8Gv9RrNvG18RhYgXCNNj39LfAu2 -export TYPESENSE_COLLECTION=holi_search_staging \ No newline at end of file +export TYPESENSE_COLLECTION=holi_search_staging + +# Facebook Tracking +export FACEBOOK_APP_ID=655863986950215 +export FACEBOOK_CLIENT_TOKEN="095ea0ea5f60749ee3fb10bec46da095" +export FACEBOOK_APP_DISPLAY_NAME="holi (dev)" \ No newline at end of file diff --git a/.env.production b/.env.production index 1dd026488464ddf5b1986dfd375b61b642875118..314e978a4ca288a48675da05b0c0f7b1e06b767a 100644 --- a/.env.production +++ b/.env.production @@ -50,3 +50,8 @@ export TYPESENSE_PORT=443 export TYPESENSE_PROTOCOL=https export TYPESENSE_API_KEY=jtOdwRfhEFW7UmCpDKjnQaS71uU28YOq export TYPESENSE_COLLECTION=holi_search_production + +# Facebook Tracking +export FACEBOOK_APP_ID=655863986950215 +export FACEBOOK_CLIENT_TOKEN="095ea0ea5f60749ee3fb10bec46da095" +export FACEBOOK_APP_DISPLAY_NAME="holi" \ No newline at end of file diff --git a/.env.staging b/.env.staging index 36f11ebb7c3be1403cb9515fa297b70ba4da4429..21f56be7753b09058c0b90c6e3779c5d9bc5822c 100644 --- a/.env.staging +++ b/.env.staging @@ -50,3 +50,8 @@ export TYPESENSE_PORT=443 export TYPESENSE_PROTOCOL=https export TYPESENSE_API_KEY=WAxFP8Gv9RrNvG18RhYgXCNNj39LfAu2 export TYPESENSE_COLLECTION=holi_search_staging + +# Facebook Tracking +export FACEBOOK_APP_ID=655863986950215 +export FACEBOOK_CLIENT_TOKEN="095ea0ea5f60749ee3fb10bec46da095" +export FACEBOOK_APP_DISPLAY_NAME="holi (dev)" diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 0b9a58a95430876e91f52428923a02a2ab1e54a0..d8d9e7ecbe4d62d26cc8c9640921382c74af5b7e 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -143,6 +143,7 @@ const config: ExpoConfig = { softwareKeyboardLayoutMode: 'pan', }, plugins: [ + 'expo-tracking-transparency', 'expo-localization', 'expo-secure-store', [ @@ -172,6 +173,18 @@ const config: ExpoConfig = { calendarPermission: 'Allow access to your calendar', }, ], + [ + 'react-native-fbsdk-next', + { + appID: requiredEnvVar('FACEBOOK_APP_ID'), + displayName: process.env.FACEBOOK_APP_DISPLAY_NAME ?? 'holi', + clientToken: requiredEnvVar('FACEBOOK_CLIENT_TOKEN'), + scheme: `fb${requiredEnvVar('FACEBOOK_APP_ID')}`, + autoLogAppEventsEnabled: false, + isAutoInitEnabled: false, + advertiserIDCollectionEnabled: false, + }, + ], // HOLI-9792: Create a new development build to bundle fonts // [ // 'expo-font', diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json index f739b79f89471da71f02e3e53958e7e589754c36..c2b7a424602d0d62276ce6b623d80f3a5a00b30e 100644 --- a/apps/mobile/eas.json +++ b/apps/mobile/eas.json @@ -17,6 +17,8 @@ "APP_VARIANT": "development", "E2E_USER_ID": "5dbeb15e-1fac-4292-96fc-9fbe2e6edbb0", "ENVIRONMENT_ID": "staging", + "FACEBOOK_APP_DISPLAY_NAME": "holi (dev)", + "FACEBOOK_APP_ID": "655863986950215", "HOLI_API_GATEWAY": "https://staging.unified.apis.holi.social", "HOLI_API_URL": "https://staging.unified.apis.holi.social/graphql", "HOLI_CHAT_SERVER_NAME": "development-chat.holi.social", @@ -50,6 +52,8 @@ "APP_VARIANT": "development", "E2E_USER_ID": "5dbeb15e-1fac-4292-96fc-9fbe2e6edbb0", "ENVIRONMENT_ID": "staging", + "FACEBOOK_APP_DISPLAY_NAME": "holi (dev)", + "FACEBOOK_APP_ID": "655863986950215", "HOLI_API_GATEWAY": "https://staging.unified.apis.holi.social", "HOLI_API_URL": "https://staging.unified.apis.holi.social/graphql", "HOLI_CHAT_SERVER_NAME": "development-chat.holi.social", @@ -86,6 +90,8 @@ "APP_VARIANT": "development", "E2E_USER_ID": "5dbeb15e-1fac-4292-96fc-9fbe2e6edbb0", "ENVIRONMENT_ID": "staging", + "FACEBOOK_APP_DISPLAY_NAME": "holi (staging)", + "FACEBOOK_APP_ID": "655863986950215", "HOLI_API_GATEWAY": "https://staging.unified.apis.holi.social", "HOLI_API_URL": "https://staging.unified.apis.holi.social/graphql", "HOLI_CHAT_SERVER_NAME": "development-chat.holi.social", @@ -117,6 +123,8 @@ "env": { "APP_VARIANT": "production", "ENVIRONMENT_ID": "production", + "FACEBOOK_APP_DISPLAY_NAME": "holi", + "FACEBOOK_APP_ID": "655863986950215", "HOLI_API_GATEWAY": "https://production.unified.apis.holi.social", "HOLI_API_URL": "https://production.unified.apis.holi.social/graphql", "HOLI_CHAT_SERVER_NAME": "holi.social", @@ -155,6 +163,8 @@ "APP_VARIANT": "development", "E2E_USER_ID": "5dbeb15e-1fac-4292-96fc-9fbe2e6edbb0", "ENVIRONMENT_ID": "staging", + "FACEBOOK_APP_DISPLAY_NAME": "holi (dev)", + "FACEBOOK_APP_ID": "655863986950215", "HOLI_API_GATEWAY": "https://staging.unified.apis.holi.social", "HOLI_API_URL": "https://staging.unified.apis.holi.social/graphql", "HOLI_CHAT_SERVER_NAME": "development-chat.holi.social", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index a2c2281edf727e9e57c3b03a477c13213e5acc34..9ed7bff44b68c1214eaa53f41e82ebb2b910ffcf 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -42,6 +42,7 @@ "expo-splash-screen": "~0.27.6", "expo-status-bar": "~1.12.1", "expo-task-manager": "~11.8.2", + "expo-tracking-transparency": "~4.0.2", "expo-updates": "~0.25.27", "fast-text-encoding": "^1.0.6", "global": "^4.4.0", @@ -63,6 +64,7 @@ "react-native-animated-pagination-dot": "^0.4.0", "react-native-calendars": "^1.1306.0", "react-native-controlled-mentions": "^2.2.5", + "react-native-fbsdk-next": "^13.4.1", "react-native-gesture-handler": "~2.16.1", "react-native-get-random-values": "^1.11.0", "react-native-image-crop-picker": "^0.41.2", diff --git a/core/components/TrackingConsentModal/ManageOptionsContent.tsx b/core/components/TrackingConsentModal/ManageOptionsContent.tsx index c0e7fdffa90273393cefc0db603bedb7bd6a9338..e9c115fe360169e87ed3dea9cd3a731b6247cc8e 100644 --- a/core/components/TrackingConsentModal/ManageOptionsContent.tsx +++ b/core/components/TrackingConsentModal/ManageOptionsContent.tsx @@ -4,7 +4,7 @@ import { StyleSheet, View } from 'react-native' import OptionSection from '@holi/core/components/OptionSection' import { HoliGap } from '@holi/ui/components/atoms/HoliGap' -import { HoliTheme, useTheme } from '@holi/ui/styles/theme' +import { type HoliTheme, useTheme } from '@holi/ui/styles/theme' import { Text } from 'holi-bricks/components/text' interface Props { @@ -12,6 +12,8 @@ interface Props { onTogglePersonalization: () => void allowProductAnalytics: boolean onToggleProductAnalytics: () => void + allowAdPartnerAnalytics: boolean + onToggleAdPartnerAnalytics: () => void } const ManageOptionsContent = ({ @@ -19,6 +21,8 @@ const ManageOptionsContent = ({ onTogglePersonalization, allowProductAnalytics, onToggleProductAnalytics, + allowAdPartnerAnalytics, + onToggleAdPartnerAnalytics, }: Props) => { const { t } = useTranslation() @@ -47,6 +51,17 @@ const ManageOptionsContent = ({ /> </View> + <HoliGap size="sm" /> + + <View style={styles.optionBox}> + <OptionSection + title={t('tracking.consentModal.manageOptions.adPartnerAnalyticsBox.title')} + description={t('tracking.consentModal.manageOptions.adPartnerAnalyticsBox.description')} + value={allowAdPartnerAnalytics} + onToggle={onToggleAdPartnerAnalytics} + /> + </View> + <HoliGap size="m" /> <Text size="md">{t('tracking.consentModal.manageOptions.note')}</Text> diff --git a/core/components/TrackingConsentModal/TrackingConsentModalDesktop.tsx b/core/components/TrackingConsentModal/TrackingConsentModalDesktop.tsx index c2282bf04e374109336ae2d4bc3eae2b26644d1f..25e428e172df1e916dfe80b5206ce478d8d0a0b4 100644 --- a/core/components/TrackingConsentModal/TrackingConsentModalDesktop.tsx +++ b/core/components/TrackingConsentModal/TrackingConsentModalDesktop.tsx @@ -26,8 +26,10 @@ export const TrackingConsentModalDesktop = () => { const { allowPersonalization, allowProductAnalytics, + allowAdPartnerAnalytics, onTogglePersonalization, onToggleProductAnalytics, + onToggleAdPartnerAnalytics, acceptAll, denyAll, acceptMyChoices, @@ -46,6 +48,8 @@ export const TrackingConsentModalDesktop = () => { onTogglePersonalization={onTogglePersonalization} allowProductAnalytics={allowProductAnalytics} onToggleProductAnalytics={onToggleProductAnalytics} + allowAdPartnerAnalytics={allowAdPartnerAnalytics} + onToggleAdPartnerAnalytics={onToggleAdPartnerAnalytics} /> </HoliBox> </ScrollView> diff --git a/core/components/TrackingConsentModal/TrackingConsentModalMobile.tsx b/core/components/TrackingConsentModal/TrackingConsentModalMobile.tsx index 0d084c038929b3aea7d823f42bd3e4cd44feb5c7..9d42ab2498fd7c01319a913b00b7d3477f237344 100644 --- a/core/components/TrackingConsentModal/TrackingConsentModalMobile.tsx +++ b/core/components/TrackingConsentModal/TrackingConsentModalMobile.tsx @@ -42,8 +42,10 @@ export const TrackingConsentModalMobile = () => { const { allowPersonalization, allowProductAnalytics, + allowAdPartnerAnalytics, onTogglePersonalization, onToggleProductAnalytics, + onToggleAdPartnerAnalytics, acceptAll, denyAll, acceptMyChoices, @@ -99,6 +101,8 @@ export const TrackingConsentModalMobile = () => { onTogglePersonalization={onTogglePersonalization} allowProductAnalytics={allowProductAnalytics} onToggleProductAnalytics={onToggleProductAnalytics} + allowAdPartnerAnalytics={allowAdPartnerAnalytics} + onToggleAdPartnerAnalytics={onToggleAdPartnerAnalytics} /> <HoliGap size="ml" /> diff --git a/core/components/TrackingConsentModal/__tests__/TrackingConsentModalDesktop.test.tsx b/core/components/TrackingConsentModal/__tests__/TrackingConsentModalDesktop.test.tsx index e818f012e41164495bf65bc0bab71099e69715cf..bd34cdbd1e06f48e7ef9bbb60c46f561dfc4af09 100644 --- a/core/components/TrackingConsentModal/__tests__/TrackingConsentModalDesktop.test.tsx +++ b/core/components/TrackingConsentModal/__tests__/TrackingConsentModalDesktop.test.tsx @@ -42,10 +42,12 @@ describe('TrackingConsentModalDesktop', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Given, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_accepted_all') }) }) + describe('when denying tracking for all purposes', () => { it('updates consent state and calls hooks', async () => { const user = userEvent.setup() @@ -60,12 +62,14 @@ describe('TrackingConsentModalDesktop', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_denied_all') }) }) + describe('when confirming tracking choices', () => { - it('[given personalization + denied analytics] updates consent state and calls hooks', async () => { + it('[given personalization + denied analytics + denied ad partners] updates consent state and calls hooks', async () => { const user = userEvent.setup() render(<TrackingConsentModalDesktop />) @@ -79,10 +83,12 @@ describe('TrackingConsentModalDesktop', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_personalization') }) - it('[denied personalization + given analytics] updates consent state and calls hooks', async () => { + + it('[denied personalization + given analytics + denied ad partners] updates consent state and calls hooks', async () => { const user = userEvent.setup() render(<TrackingConsentModalDesktop />) @@ -96,10 +102,12 @@ describe('TrackingConsentModalDesktop', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_analytics') }) - it('[given personalization + given analytics] updates consent state and calls hooks', async () => { + + it('[given personalization + given analytics + denied ad partners] updates consent state and calls hooks', async () => { const user = userEvent.setup() render(<TrackingConsentModalDesktop />) @@ -114,10 +122,35 @@ describe('TrackingConsentModalDesktop', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_analytics_personalization') }) - it('[denied personalization + denied analytics] updates consent state and calls hooks', async () => { + + it('[given personalization + given analytics + given ad partners] updates consent state and calls hooks', async () => { + const user = userEvent.setup() + + render(<TrackingConsentModalDesktop />) + act(jest.runAllTimers) + + expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_shown') + + await user.press(screen.getByText('tracking.consentModal.manageOptions.personalizationBox.title')) + await user.press(screen.getByText('tracking.consentModal.manageOptions.productAnalyticsBox.title')) + await user.press(screen.getByText('tracking.consentModal.manageOptions.adPartnerAnalyticsBox.title')) + await user.press(screen.getByLabelText('tracking.consentModal.manageOptions.button.confirmMyChoices')) + + expect(mockUpdateConsentState).toHaveBeenCalledWith({ + [TrackingPurpose.Analytics]: Consent.Given, + [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Given, + }) + expect(mockAnonCount).toHaveBeenCalledWith( + 'tracking_consent_modal_options_accepted_analytics_personalization_ad_partners' + ) + }) + + it('[denied personalization + denied analytics + denied ad partners] updates consent state and calls hooks', async () => { const user = userEvent.setup() render(<TrackingConsentModalDesktop />) @@ -130,6 +163,7 @@ describe('TrackingConsentModalDesktop', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_denied_all') }) diff --git a/core/components/TrackingConsentModal/__tests__/TrackingConsentModalMobile.test.tsx b/core/components/TrackingConsentModal/__tests__/TrackingConsentModalMobile.test.tsx index 468893f248ad29a14c913952258f68677e669387..65b87b1b7b07ee75fffb61d933f151fc96f27324 100644 --- a/core/components/TrackingConsentModal/__tests__/TrackingConsentModalMobile.test.tsx +++ b/core/components/TrackingConsentModal/__tests__/TrackingConsentModalMobile.test.tsx @@ -42,6 +42,7 @@ describe('TrackingConsentModalMobile', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Given, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_accepted_all') }) @@ -60,12 +61,13 @@ describe('TrackingConsentModalMobile', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_denied_all') }) }) describe('when confirming tracking choices', () => { - it('[given personalization + denied analytics] updates consent state and calls hooks', async () => { + it('[given personalization + denied analytics + denied ad partners] updates consent state and calls hooks', async () => { const user = userEvent.setup() render(<TrackingConsentModalMobile />) @@ -82,10 +84,12 @@ describe('TrackingConsentModalMobile', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_personalization') }) - it('[denied personalization + given analytics] updates consent state and calls hooks', async () => { + + it('[denied personalization + given analytics + denied ad partners] updates consent state and calls hooks', async () => { const user = userEvent.setup() render(<TrackingConsentModalMobile />) @@ -102,10 +106,12 @@ describe('TrackingConsentModalMobile', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_analytics') }) - it('[given personalization + given analytics] updates consent state and calls hooks', async () => { + + it('[given personalization + given analytics + denied ad partners] updates consent state and calls hooks', async () => { const user = userEvent.setup() render(<TrackingConsentModalMobile />) @@ -123,10 +129,12 @@ describe('TrackingConsentModalMobile', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_analytics_personalization') }) - it('[denied personalization + denied analytics] updates consent state and calls hooks', async () => { + + it('[denied personalization + denied analytics + denied ad partners] updates consent state and calls hooks', async () => { const user = userEvent.setup() render(<TrackingConsentModalMobile />) @@ -142,8 +150,37 @@ describe('TrackingConsentModalMobile', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, + }) + expect(mockAnonCount).toHaveBeenCalledWith( + 'tracking_consent_modal_options_denied_analytics_personalization_ad_partners' + ) + }) + + it('[given personalization + given analytics + given ad partners] updates consent state and calls hooks', async () => { + const user = userEvent.setup() + + render(<TrackingConsentModalMobile />) + act(jest.runAllTimers) + + expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_shown') + + await user.press(screen.getByLabelText('tracking.consentModal.general.button.manageOptions')) + act(jest.runAllTimers) + + await user.press(screen.getByText('tracking.consentModal.manageOptions.personalizationBox.title')) + await user.press(screen.getByText('tracking.consentModal.manageOptions.productAnalyticsBox.title')) + await user.press(screen.getByText('tracking.consentModal.manageOptions.adPartnerAnalyticsBox.title')) + await user.press(screen.getByLabelText('tracking.consentModal.manageOptions.button.confirmMyChoices')) + + expect(mockUpdateConsentState).toHaveBeenCalledWith({ + [TrackingPurpose.Analytics]: Consent.Given, + [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Given, }) - expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_denied_analytics_personalization') + expect(mockAnonCount).toHaveBeenCalledWith( + 'tracking_consent_modal_options_accepted_analytics_personalization_ad_partners' + ) }) }) }) diff --git a/core/components/TrackingConsentModal/__tests__/useConsentModal.test.ts b/core/components/TrackingConsentModal/__tests__/useConsentModal.test.ts index ebd5a16798e3a23f37e9172df2927625de9c1dc7..418a25aedb21bed8508485bfd97ba17f9af13b70 100644 --- a/core/components/TrackingConsentModal/__tests__/useConsentModal.test.ts +++ b/core/components/TrackingConsentModal/__tests__/useConsentModal.test.ts @@ -24,6 +24,7 @@ const mockUseModalState = jest.mocked(useModalState) const randomConsent = () => (Math.random() > 0.5 ? Consent.Given : Consent.Denied) const randomKnownConsentState = () => ({ [TrackingPurpose.Analytics]: randomConsent(), + [TrackingPurpose.AdPartners]: randomConsent(), [TrackingPurpose.Personalization]: randomConsent(), }) diff --git a/core/components/TrackingConsentModal/__tests__/useConsentModalState.test.ts b/core/components/TrackingConsentModal/__tests__/useConsentModalState.test.ts index c1d202862911da9ff31bdfd3b40382352de60373..2ebbae3e36dabc08bd15b97de37281991004e10a 100644 --- a/core/components/TrackingConsentModal/__tests__/useConsentModalState.test.ts +++ b/core/components/TrackingConsentModal/__tests__/useConsentModalState.test.ts @@ -25,8 +25,11 @@ describe('useConsentModalState', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, }) - expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_denied_analytics_personalization') + expect(mockAnonCount).toHaveBeenCalledWith( + 'tracking_consent_modal_options_denied_analytics_personalization_ad_partners' + ) expect(onFinish).toHaveBeenCalled() }) describe('when accepting tracking for all purposes', () => { @@ -38,6 +41,7 @@ describe('useConsentModalState', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Given, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_accepted_all') expect(onFinish).toHaveBeenCalled() @@ -52,11 +56,13 @@ describe('useConsentModalState', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_denied_all') expect(onFinish).toHaveBeenCalled() }) }) + describe('when confirming tracking choices', () => { it('[personalization only] updates consent state and calls hooks', async () => { const { result } = renderHook(() => useConsentModalState(onFinish)) @@ -67,10 +73,12 @@ describe('useConsentModalState', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_personalization') expect(onFinish).toHaveBeenCalled() }) + it('[analytics only] updates consent state and calls hooks', async () => { const { result } = renderHook(() => useConsentModalState(onFinish)) @@ -80,10 +88,25 @@ describe('useConsentModalState', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_analytics') expect(onFinish).toHaveBeenCalled() }) + + it('[ad partners only] updates consent state and calls hooks', async () => { + const { result } = renderHook(() => useConsentModalState(onFinish)) + + act(() => result.current.onToggleAdPartnerAnalytics()) + act(() => result.current.acceptMyChoices()) + + expect(mockUpdateConsentState).toHaveBeenCalledWith({ + [TrackingPurpose.Analytics]: Consent.Denied, + [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Given, + }) + }) + it('[personalization + analytics] updates consent state and calls hooks', async () => { const { result } = renderHook(() => useConsentModalState(onFinish)) @@ -94,9 +117,60 @@ describe('useConsentModalState', () => { expect(mockUpdateConsentState).toHaveBeenCalledWith({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, }) expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_analytics_personalization') expect(onFinish).toHaveBeenCalled() }) + + it('[personalization + ad partners] updates consent state and calls hooks', async () => { + const { result } = renderHook(() => useConsentModalState(onFinish)) + + act(() => result.current.onTogglePersonalization()) + act(() => result.current.onToggleAdPartnerAnalytics()) + act(() => result.current.acceptMyChoices()) + + expect(mockUpdateConsentState).toHaveBeenCalledWith({ + [TrackingPurpose.Analytics]: Consent.Denied, + [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Given, + }) + expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_personalization_ad_partners') + expect(onFinish).toHaveBeenCalled() + }) + it('[analytics + ad partners] updates consent state and calls hooks', async () => { + const { result } = renderHook(() => useConsentModalState(onFinish)) + + act(() => result.current.onToggleProductAnalytics()) + act(() => result.current.onToggleAdPartnerAnalytics()) + act(() => result.current.acceptMyChoices()) + + expect(mockUpdateConsentState).toHaveBeenCalledWith({ + [TrackingPurpose.Analytics]: Consent.Given, + [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Given, + }) + expect(mockAnonCount).toHaveBeenCalledWith('tracking_consent_modal_options_accepted_analytics_ad_partners') + expect(onFinish).toHaveBeenCalled() + }) + + it('[personalization + analytics + ad partners] updates consent state and calls hooks', async () => { + const { result } = renderHook(() => useConsentModalState(onFinish)) + + act(() => result.current.onTogglePersonalization()) + act(() => result.current.onToggleProductAnalytics()) + act(() => result.current.onToggleAdPartnerAnalytics()) + act(() => result.current.acceptMyChoices()) + + expect(mockUpdateConsentState).toHaveBeenCalledWith({ + [TrackingPurpose.Analytics]: Consent.Given, + [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Given, + }) + expect(mockAnonCount).toHaveBeenCalledWith( + 'tracking_consent_modal_options_accepted_analytics_personalization_ad_partners' + ) + expect(onFinish).toHaveBeenCalled() + }) }) }) diff --git a/core/components/TrackingConsentModal/useConsentModalState.ts b/core/components/TrackingConsentModal/useConsentModalState.ts index 01ea7e1f64a1944b578d66f606054e56c6a61391..58c52c019bbad8089dfc55419d7c9bb8e3f86352 100644 --- a/core/components/TrackingConsentModal/useConsentModalState.ts +++ b/core/components/TrackingConsentModal/useConsentModalState.ts @@ -5,9 +5,10 @@ import { Consent, TrackingPurpose } from '@holi/core/tracking/types' import { useAnonCount } from '@holi/core/domain/shared/hooks' export const useConsentModalState = (onFinish: () => void) => { - const [{ allowPersonalization, allowProductAnalytics }, setState] = useState({ + const [{ allowPersonalization, allowProductAnalytics, allowAdPartnerAnalytics }, setState] = useState({ allowPersonalization: false, allowProductAnalytics: false, + allowAdPartnerAnalytics: false, }) const anonCount = useAnonCount() @@ -23,22 +24,30 @@ export const useConsentModalState = (onFinish: () => void) => { allowProductAnalytics: !allowProductAnalytics, })) + const onToggleAdPartnerAnalytics = () => + setState((prevState) => ({ + ...prevState, + allowAdPartnerAnalytics: !allowAdPartnerAnalytics, + })) + // Event handlers const acceptAll = () => { - setState({ allowPersonalization: true, allowProductAnalytics: true }) + setState({ allowPersonalization: true, allowProductAnalytics: true, allowAdPartnerAnalytics: true }) updateConsentState({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Given, }) anonCount('tracking_consent_modal_accepted_all') onFinish() } const denyAll = () => { - setState({ allowPersonalization: false, allowProductAnalytics: false }) + setState({ allowPersonalization: false, allowProductAnalytics: false, allowAdPartnerAnalytics: true }) updateConsentState({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, }) anonCount('tracking_consent_modal_denied_all') onFinish() @@ -48,16 +57,25 @@ export const useConsentModalState = (onFinish: () => void) => { const cs = { [TrackingPurpose.Analytics]: allowProductAnalytics ? Consent.Given : Consent.Denied, [TrackingPurpose.Personalization]: allowPersonalization ? Consent.Given : Consent.Denied, + [TrackingPurpose.AdPartners]: allowAdPartnerAnalytics ? Consent.Given : Consent.Denied, } updateConsentState(cs) - if (allowPersonalization && allowProductAnalytics) { + if (allowPersonalization && allowProductAnalytics && allowAdPartnerAnalytics) { + anonCount('tracking_consent_modal_options_accepted_analytics_personalization_ad_partners') + } else if (allowPersonalization && allowProductAnalytics) { anonCount('tracking_consent_modal_options_accepted_analytics_personalization') + } else if (allowPersonalization && allowAdPartnerAnalytics) { + anonCount('tracking_consent_modal_options_accepted_personalization_ad_partners') + } else if (allowProductAnalytics && allowAdPartnerAnalytics) { + anonCount('tracking_consent_modal_options_accepted_analytics_ad_partners') } else if (allowProductAnalytics) { anonCount('tracking_consent_modal_options_accepted_analytics') } else if (allowPersonalization) { anonCount('tracking_consent_modal_options_accepted_personalization') + } else if (allowAdPartnerAnalytics) { + anonCount('tracking_consent_modal_options_accepted_ad_partners') } else { - anonCount('tracking_consent_modal_options_denied_analytics_personalization') + anonCount('tracking_consent_modal_options_denied_analytics_personalization_ad_partners') } onFinish() } @@ -65,8 +83,10 @@ export const useConsentModalState = (onFinish: () => void) => { return { allowPersonalization, allowProductAnalytics, + allowAdPartnerAnalytics, onTogglePersonalization, onToggleProductAnalytics, + onToggleAdPartnerAnalytics, acceptAll, denyAll, acceptMyChoices, diff --git a/core/domain/shared/__tests__/useSortedUserTerms.test.ts b/core/domain/shared/__tests__/useSortedUserTerms.test.ts index 6573c79316b70a541bc2f23e3ebab60b90dc2bb8..3cd4243c323d9737376bd0b729de24362e44a266 100644 --- a/core/domain/shared/__tests__/useSortedUserTerms.test.ts +++ b/core/domain/shared/__tests__/useSortedUserTerms.test.ts @@ -17,6 +17,7 @@ describe('useSortedUserTerms', () => { user: { isEmployee: false, id: '', + email: '', identity: '', avatarDefaultColor: '', avatarLabel: '', diff --git a/core/domain/shared/queries.ts b/core/domain/shared/queries.ts index 7e19dac7031ea2ca100d1b8161105ba36d4bec58..d162eafafb6d549c101cf5da171412f21bfed0a2 100644 --- a/core/domain/shared/queries.ts +++ b/core/domain/shared/queries.ts @@ -55,7 +55,9 @@ export const FullAuthenticatedUserFragment = gql` connectionStatusToMyself trackingConsentAnalytics trackingConsentPersonalization + trackingConsentAdPartners isEmployee + email } ` export const authenticatedUserV2Query = gql` diff --git a/core/domain/shared/types.ts b/core/domain/shared/types.ts index 792447b2afaac6601b5db4685cc599b3c6d7c9b4..05a6ef58caa41c00bf07f3e4533a176eb4cda91e 100644 --- a/core/domain/shared/types.ts +++ b/core/domain/shared/types.ts @@ -168,7 +168,9 @@ export interface User { } export interface AuthenticatedUser extends User { + email: string trackingConsentAnalytics?: boolean | null trackingConsentPersonalization?: boolean | null + trackingConsentAdPartners?: boolean | null isEmployee: boolean } diff --git a/core/i18n/locales/de.json b/core/i18n/locales/de.json index c52f3bb07f78ec52b11e1911ed7e696e701c6fcb..21391d35941fcfe82986e52c97620f7cc4540ce0 100644 --- a/core/i18n/locales/de.json +++ b/core/i18n/locales/de.json @@ -718,6 +718,7 @@ "profile.settings.resetOnboardingState.title": "Onboarding-Status zurücksetzen", "profile.settings.tracking.dataManagement.description": "Du kannst deine Daten aus unseren Systemen löschen, indem du eine E-Mail an <0>support@holi.social</0> sendest", "profile.settings.tracking.dataManagement.title": "Datenmanagement", + "profile.settings.tracking.purpose.adPartners": "Werbepartneranalysen erlauben", "profile.settings.tracking.purpose.all": "Für alle Zwecke erlauben", "profile.settings.tracking.purpose.analytics": "Produktanalyse erlauben", "profile.settings.tracking.purpose.personalization": "Personalisierung erlauben", @@ -1543,6 +1544,8 @@ "tracking.consentModal.general.description.check_2": "Hilfst du uns das Nutzungserlebnis zu verbessern", "tracking.consentModal.general.description.check_3": "Deine Zustimmung kannst du in den Einstellungen jederzeit ändern oder widerrufen", "tracking.consentModal.general.headline": "Deine Daten sind bei uns sicher. Was möchtest du teilen?", + "tracking.consentModal.manageOptions.adPartnerAnalyticsBox.description": "Hilf uns zu Wachsen! Um möglichst passende Menschen auf externen Seiten und Netzwerken (wie Facebook oder Instragram) mit holi Werbung ansprechen zu können, analysieren wir mit deiner Zustimmung dein Nutzungsverhalten und deine Interaktionen. Mit Hilfe des daraus abgeleiteten Nutzungsprofils können wir dich in passende Zielgruppen und Werbekategorien einordnen, die verwendet werden, um unsere Werbung auf den externen Seiten zielgerichteter auszuspielen. Daher freuen wir uns, wenn du uns mit deinen Daten dabei hilfst, holi wachsen zu lassen.", + "tracking.consentModal.manageOptions.adPartnerAnalyticsBox.title": "Werbepartneranalysen erlauben", "tracking.consentModal.manageOptions.button.acceptAll": "Gesamte Verarbeitung akzeptieren", "tracking.consentModal.manageOptions.button.confirmMyChoices": "Auswahl bestätigen", "tracking.consentModal.manageOptions.headline": "Optionen anpassen", diff --git a/core/i18n/locales/en.json b/core/i18n/locales/en.json index 6f9e60c802ee5b3e2a8ffa5da9424c316ed71c16..88a6397b751c756a7afffd6e44926fbaec0cb0f2 100644 --- a/core/i18n/locales/en.json +++ b/core/i18n/locales/en.json @@ -717,6 +717,7 @@ "profile.settings.resetOnboardingState.title": "Reset onboarding state", "profile.settings.tracking.dataManagement.description": "You can delete your data from our systems by sending an email to <0>support@holi.social</0>", "profile.settings.tracking.dataManagement.title": "Data management", + "profile.settings.tracking.purpose.adPartners": "Allow advertising partner analyses", "profile.settings.tracking.purpose.all": "Allow all purposes", "profile.settings.tracking.purpose.analytics": "Allow product analytics", "profile.settings.tracking.purpose.personalization": "Allow personalization", @@ -1539,6 +1540,8 @@ "topic.sports-recreation": "Sports & Recreation", "topic.technology-innovation": "Technology & Innovation", "topic.youth-development": "Youth Development", + "tracking.consentModal.manageOptions.adPartnerAnalyticsBox.description": "Help us grow! In order to address people on external sites and networks (such as Facebook or Instagram) with holi advertising that are as suitable as possible, we analyse your usage behaviour and interactions with your consent. With the help of the usage profile derived from this, we can classify you into suitable target groups and advertising categories that are used to display our advertising on external sites in a more targeted manner. We would therefore be grateful if you would help us with your data to grow holi.", + "tracking.consentModal.manageOptions.adPartnerAnalyticsBox.title": "Allow advertising partner analyses", "tracking.consentModal.general.button.acceptAll": "Accept full data processing", "tracking.consentModal.general.button.denyAll": "Decline all", "tracking.consentModal.general.button.manageOptions": "Manage options", diff --git a/core/package.json b/core/package.json index 893d8870b9bee116dfd381de69bf460490312cb3..eb43de4285d5e758764316132bb10556fdfe6394 100644 --- a/core/package.json +++ b/core/package.json @@ -41,7 +41,9 @@ "expo-calendar": "^13.0.5", "expo-image-picker": "~15.0.7", "expo-linking": "~6.3.1", + "expo-tracking-transparency": "^5.1.1", "graphql-request": "^7.1.0", + "react-native-fbsdk-next": "^13.4.1", "react-native-reanimated": "~3.10.1", "react-native-svg": "15.2.0", "react-zoom-pan-pinch": "^2.1.3" diff --git a/core/screens/onboarding/Onboarding.tsx b/core/screens/onboarding/Onboarding.tsx index 4820740f36298c90e23a260f4a8e0b7d538fc186..8530d042c69895a059889b368f055db1f15ec766 100644 --- a/core/screens/onboarding/Onboarding.tsx +++ b/core/screens/onboarding/Onboarding.tsx @@ -37,6 +37,7 @@ import { useRedirect } from '@holi/core/navigation/hooks/useRedirect' import { clearOnboardingUserDataAsyncStore } from './helpers/onboardingUserDataAsyncStore' import { useCallback, useEffect } from 'react' import { showOnboardingFinishModal } from '@holi/core/screens/onboarding/hooks/useOnboardingFinishModalState' +import { useFacebookSdkCrossPlatform } from '@holi/core/tracking/FacebookSdkCrossPlatform' const Onboarding = () => { const { t } = useTranslation() @@ -44,6 +45,7 @@ const Onboarding = () => { const { top: safeAreaTop, bottom: safeAreaBottom } = useSafeAreaInsets() const onboardingState = useOnboardingState() const { track } = useTracking() + const facebook = useFacebookSdkCrossPlatform() const { isLoggedIn } = useLoggedInUser() const [currentStep, setCurrentStep] = useOnboardingCurrentStepKey() @@ -181,6 +183,7 @@ const Onboarding = () => { }} onRegister={() => { track(SignupStepCompleted({ step: 'emailAndPassword', answer: 'signUp' })) + facebook.trackSignUp() setCurrentStep('EMAIL_VERIFICATION') }} testID="onboarding-account-creation" diff --git a/core/screens/userprofile/TrackingSettings.tsx b/core/screens/userprofile/TrackingSettings.tsx index 568bf791e54c835952b451403586184b504e1ab3..2fc2ebc4f3b90d9698d8625114893fcf997f12ca 100644 --- a/core/screens/userprofile/TrackingSettings.tsx +++ b/core/screens/userprofile/TrackingSettings.tsx @@ -23,13 +23,16 @@ const logger = getLogger('Tracking') const updateUserConsentState = ({ personalization = false, productAnalytics = false, + adPartnerAnalytics = false, }: { personalization: boolean | null productAnalytics: boolean | null + adPartnerAnalytics: boolean | null }) => { updateConsentState({ [TrackingPurpose.Personalization]: personalization ? Consent.Given : Consent.Denied, [TrackingPurpose.Analytics]: productAnalytics ? Consent.Given : Consent.Denied, + [TrackingPurpose.AdPartners]: adPartnerAnalytics ? Consent.Given : Consent.Denied, }) } @@ -53,22 +56,28 @@ const TrackingSettings = () => { const { [TrackingPurpose.Personalization]: personalizationConsent = Consent.Unknown, [TrackingPurpose.Analytics]: productAnalyticsConsent = Consent.Unknown, + [TrackingPurpose.AdPartners]: adPartnerAnalyticsConsent = Consent.Unknown, } = consentState ?? {} const personalization = Consent.toBooleanOrNull(personalizationConsent) const productAnalytics = Consent.toBooleanOrNull(productAnalyticsConsent) + const adPartnerAnalytics = Consent.toBooleanOrNull(adPartnerAnalyticsConsent) const onAllPurposesChange = (value: boolean) => { - updateUserConsentState({ personalization: value, productAnalytics: value }) + updateUserConsentState({ personalization: value, productAnalytics: value, adPartnerAnalytics: value }) logger.debug('TrackingSettings', 'updated personalization and analytics consent') } const onPersonalizationChange = (value: boolean) => { - updateUserConsentState({ personalization: value, productAnalytics }) + updateUserConsentState({ personalization: value, productAnalytics, adPartnerAnalytics }) logger.debug('TrackingSettings', 'updated personalization consent') } const onProductAnalyticsChange = (value: boolean) => { - updateUserConsentState({ personalization, productAnalytics: value }) + updateUserConsentState({ personalization, productAnalytics: value, adPartnerAnalytics }) logger.debug('TrackingSettings', 'updated analytics consent') } + const onAdPartnerAnalyticsChange = (value: boolean) => { + updateUserConsentState({ personalization, productAnalytics, adPartnerAnalytics: value }) + logger.debug('TrackingSettings', 'updated ad partner consent') + } return ( <ScrollView style={{ backgroundColor: colors.background20 }}> @@ -92,11 +101,12 @@ const TrackingSettings = () => { [ [ t('profile.settings.tracking.purpose.all'), - !!(personalization && productAnalytics), + !!(personalization && productAnalytics && adPartnerAnalytics), onAllPurposesChange, ], [t('profile.settings.tracking.purpose.personalization'), !!personalization, onPersonalizationChange], [t('profile.settings.tracking.purpose.analytics'), !!productAnalytics, onProductAnalyticsChange], + [t('profile.settings.tracking.purpose.adPartners'), !!adPartnerAnalytics, onAdPartnerAnalyticsChange], ] as [string, boolean, (value: boolean) => void][] ).map(([text, value, onChange], idx) => ( <View style={[styles.optionBox, idx !== 0 && styles.underlined]} key={idx}> diff --git a/core/screens/userprofile/components/__tests__/UserTermsRow.test.tsx b/core/screens/userprofile/components/__tests__/UserTermsRow.test.tsx index 33ac995287c6ea87a1c59c842984c6442b740d6a..467543803ab103d34af95d8c5ded860b5df365ef 100644 --- a/core/screens/userprofile/components/__tests__/UserTermsRow.test.tsx +++ b/core/screens/userprofile/components/__tests__/UserTermsRow.test.tsx @@ -18,6 +18,7 @@ describe('UserTermsRow', () => { user: { isEmployee: false, id: '', + email: '', identity: '', avatarDefaultColor: '', avatarLabel: '', diff --git a/core/screens/userprofile/queries.ts b/core/screens/userprofile/queries.ts index 516a9a13a4c3b4edcab47b84b820def3212fdc9b..fc7e38074676b8112d003e0dd2a714cd0b0d2a05 100644 --- a/core/screens/userprofile/queries.ts +++ b/core/screens/userprofile/queries.ts @@ -23,6 +23,7 @@ export const UpdateAuthenticatedUserInputSchema = z.object({ skillsV2: z.array(z.string()).optional(), trackingConsentAnalytics: z.boolean().optional().nullable(), trackingConsentPersonalization: z.boolean().optional().nullable(), + trackingConsentAdPartners: z.boolean().optional().nullable(), }) export type UpdateAuthenticatedUserInput = z.infer<typeof UpdateAuthenticatedUserInputSchema> diff --git a/core/tracking/FacebookSdkCrossPlatform.ts b/core/tracking/FacebookSdkCrossPlatform.ts new file mode 100644 index 0000000000000000000000000000000000000000..d63c1d147db6a4eba8f5fc49a20cf7c04e0acb31 --- /dev/null +++ b/core/tracking/FacebookSdkCrossPlatform.ts @@ -0,0 +1,97 @@ +import { AppEventsLogger, Settings } from 'react-native-fbsdk-next' +import { Consent, type ConsentState, TrackingPurpose, type FacebookSdkCrossPlatform } from '@holi/core/tracking/types' +import { + getTrackingPermissionsAsync, + type PermissionResponse, + PermissionStatus, + requestTrackingPermissionsAsync, +} from 'expo-tracking-transparency' +import { getConsentState } from '@holi/core/tracking/TrackingInitializer' +import { getLogger } from '@holi/core/helpers/logging' +import { AuthenticatedUser } from '@holi/core/domain/shared/types' + +export class FacebookSdkReactNative implements FacebookSdkCrossPlatform { + private readonly logger = getLogger('FacebookSdk') + + async enabled(): Promise<boolean> { + const [trackingPermission, trackingConsent] = await this.getTrackingPermissionStatus() + + return ( + trackingConsent[TrackingPurpose.AdPartners] === Consent.Given && + trackingPermission.status === PermissionStatus.GRANTED + ) + } + + async initialize(user?: AuthenticatedUser): Promise<void> { + const [trackingPermission, trackingConsent] = await this.getTrackingPermissionStatus() + + if (trackingConsent[TrackingPurpose.AdPartners] !== Consent.Given) { + // don't do anything if the user has not given us their consent + this.logger.debug('initialize', 'ad partner tracking consent not given') + return + } + + // from here we can assume consent has been given + + if (trackingPermission.status === PermissionStatus.GRANTED) { + this.logger.debug('initialize', 'tracking permission and consent granted, initializing SDK') + await this.optIn(user) + return + } + + if (trackingPermission.status === PermissionStatus.UNDETERMINED || trackingPermission.canAskAgain) { + this.logger.debug('initialize', 'requesting tracking permission') + const { granted } = await requestTrackingPermissionsAsync() + if (granted) { + this.logger.debug('initialize', 'tracking permission and consent granted, initializing SDK') + await this.optIn(user) + return + } + } + + this.logger.debug('initialize', 'Tracking permission not granted') + } + + async optIn(user?: AuthenticatedUser): Promise<void> { + Settings.initializeSDK() + + Settings.setAdvertiserIDCollectionEnabled(true) + Settings.setAutoLogAppEventsEnabled(true) + await Settings.setAdvertiserTrackingEnabled(true) + + if (!user) { + return + } + + AppEventsLogger.setUserData({ + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + }) + } + + async optOut(): Promise<void> { + Settings.setAdvertiserIDCollectionEnabled(false) + Settings.setAutoLogAppEventsEnabled(false) + await Settings.setAdvertiserTrackingEnabled(false) + } + + async trackSignUp(): Promise<void> { + if (!(await this.enabled())) { + return + } + + AppEventsLogger.logEvent(AppEventsLogger.AppEvents.CompletedRegistration, { + action_source: 'app', + }) + } + + private async getTrackingPermissionStatus(): Promise<[PermissionResponse, ConsentState]> { + this.logger.debug('getTrackingPermissionStatus', 'getting tracking permission status') + return [await getTrackingPermissionsAsync(), getConsentState()] + } +} + +export const useFacebookSdkCrossPlatform = (): FacebookSdkCrossPlatform => { + return new FacebookSdkReactNative() +} diff --git a/core/tracking/FacebookSdkCrossPlatform.web.ts b/core/tracking/FacebookSdkCrossPlatform.web.ts new file mode 100644 index 0000000000000000000000000000000000000000..608a1e2eed93d1713cd5570f034b3e935ebb86d5 --- /dev/null +++ b/core/tracking/FacebookSdkCrossPlatform.web.ts @@ -0,0 +1,15 @@ +import type { FacebookSdkCrossPlatform } from '@holi/core/tracking/types' + +export class FacebookSdkWeb implements FacebookSdkCrossPlatform { + async initialize(): Promise<void> {} + async optIn(): Promise<void> {} + async optOut(): Promise<void> {} + async trackSignUp(): Promise<void> {} + async enabled(): Promise<boolean> { + return false + } +} + +export const useFacebookSdkCrossPlatform = (): FacebookSdkCrossPlatform => { + return new FacebookSdkWeb() +} diff --git a/core/tracking/TrackingInitializer.tsx b/core/tracking/TrackingInitializer.tsx index 0d284d6c2a73b2cde2bd678a7e64d4cd3bb102a3..ee835878961a4db6cc338ab1317b204c2f8afd01 100644 --- a/core/tracking/TrackingInitializer.tsx +++ b/core/tracking/TrackingInitializer.tsx @@ -19,15 +19,23 @@ import { } from '@holi/core/screens/userprofile/queries' import { consentStore } from '@holi/core/tracking/ConsentStore' import { usePosthogCrossPlatform } from '@holi/core/tracking/PosthogCrossPlatform' -import { Consent, ConsentState, type PosthogCrossPlatform, TrackingPurpose } from '@holi/core/tracking/types' +import { + Consent, + ConsentState, + type FacebookSdkCrossPlatform, + type PosthogCrossPlatform, + TrackingPurpose, +} from '@holi/core/tracking/types' import { TrackingEvent } from '@holi/core/tracking/events' import useTracking from '@holi/core/tracking/hooks/useTracking' +import { useFacebookSdkCrossPlatform } from '@holi/core/tracking/FacebookSdkCrossPlatform' const logger = getLogger('Tracking') class Tracking { constructor( private posthog: PosthogCrossPlatform, + private facebook: FacebookSdkCrossPlatform, private apollo: ApolloClient<object> ) {} @@ -53,6 +61,15 @@ class Tracking { await this.posthog.optOut() break } + + if (consentState[TrackingPurpose.AdPartners] !== Consent.Given) { + logger.debug('TrackingInitializer.initWithLoggedInUser', 'ad partner consent not given - opting out') + await this.facebook.optOut() + } else { + // the Facebook SDK wrapper's `initialize` method handles consent and permissions internally + logger.debug('TrackingInitializer.initWithLoggedInUser', 'initializing facebook sdk') + await this.facebook.initialize(user) + } } public async initWithLoggedOutUser(consentState: ConsentState): Promise<void> { @@ -73,6 +90,14 @@ class Tracking { await this.posthog.optOut() break } + + if (consentState[TrackingPurpose.AdPartners] !== Consent.Given) { + logger.debug('TrackingInitializer.initWithLoggedOutUser', 'ad partner consent not given - opting out') + await this.facebook.optOut() + } else { + // the Facebook SDK wrapper's `initialize` method handles consent and permissions internally + await this.facebook.initialize() + } } public async clearConsentAfterUserLoggedOut(): Promise<void> { @@ -82,6 +107,8 @@ class Tracking { } finally { this.posthog.reset() logger.debug('TrackingInitializer.clearConsentAfterUserLoggedOut', 'posthog reset') + await this.facebook.optOut() + logger.debug('TrackingInitializer.clearConsentAfterUserLoggedOut', 'facebook opted out') } } @@ -118,6 +145,7 @@ class Tracking { input: { trackingConsentAnalytics: Consent.toBooleanOrNull(consentState[TrackingPurpose.Analytics]), trackingConsentPersonalization: Consent.toBooleanOrNull(consentState[TrackingPurpose.Personalization]), + trackingConsentAdPartners: Consent.toBooleanOrNull(consentState[TrackingPurpose.AdPartners]), }, }, }) @@ -243,7 +271,8 @@ const TrackingInitializerInner = ({ }: TrackingInitializerInnerProps) => { const posthog: PosthogCrossPlatform = usePosthogCrossPlatform() const apollo: ApolloClient<object> = useApolloClient() - const tracking = useMemo(() => new Tracking(posthog, apollo), [posthog, apollo]) + const facebook: FacebookSdkCrossPlatform = useFacebookSdkCrossPlatform() + const tracking = useMemo(() => new Tracking(posthog, facebook, apollo), [posthog, facebook, apollo]) const state = useReactiveVar(initializerStateVar) const { track } = useTracking() diff --git a/core/tracking/__tests__/TrackingInitializer.test.tsx b/core/tracking/__tests__/TrackingInitializer.test.tsx index 90495100e38b6800269a95b4b32f2e3a36201dbc..a0facf9177d3f1a7b4a79c08f195219bf57e4f6c 100644 --- a/core/tracking/__tests__/TrackingInitializer.test.tsx +++ b/core/tracking/__tests__/TrackingInitializer.test.tsx @@ -46,6 +46,34 @@ const posthogCrossPlatformMock = { } as PosthogCrossPlatform const usePosthogCrossPlatformMock = usePosthogCrossPlatform as jest.Mock +jest.mock('expo-tracking-transparency', () => ({ + PermissionStatus: { + GRANTED: 'granted', + }, + getTrackingPermissionsAsync: async () => ({ + granted: true, + expires: 'never', + canAskAgain: true, + status: 'granted', + }), +})) + +jest.mock('react-native-fbsdk-next', () => ({ + Settings: { + initializeSDK: jest.fn(), + setAdvertiserIDCollectionEnabled: jest.fn(), + setAutoLogAppEventsEnabled: jest.fn(), + setAdvertiserTrackingEnabled: jest.fn(), + }, + AppEventsLogger: { + setUserData: jest.fn(), + logEvent: jest.fn(), + AppEvents: { + CompletedRegistration: 'SomeThing', + }, + }, +})) + jest.mock('next/router', () => ({ useRouter: () => ({ events: { @@ -61,6 +89,7 @@ const mockUser = (consentState: ConsentState): AuthenticatedUser => { id: 'some-user-identity-id', trackingConsentAnalytics: Consent.toBooleanOrNull(consentState[TrackingPurpose.Analytics]), trackingConsentPersonalization: Consent.toBooleanOrNull(consentState[TrackingPurpose.Personalization]), + trackingConsentAdPartners: Consent.toBooleanOrNull(consentState[TrackingPurpose.AdPartners]), isEmployee: faker.datatype.boolean(), } as AuthenticatedUser } @@ -77,22 +106,14 @@ jest.mock('@react-native-async-storage/async-storage', () => { }) const mockLocalStorage = (consentState?: ConsentState): void => { - const consentToString = (consent: Consent): string => { - switch (consent) { - case Consent.Given: - return 'given' - case Consent.Denied: - return 'denied' - case Consent.Unknown: - return 'unknown' - } - } const key = 'HOLI_TRACKING_CONSENT' - const value = consentState - ? `{"ANALYTICS":"${consentToString(consentState[TrackingPurpose.Analytics])}","PERSONALIZATION":"${consentToString( - consentState[TrackingPurpose.Personalization] - )}"}` - : null + const value = + consentState && + JSON.stringify({ + [TrackingPurpose.Analytics]: consentState[TrackingPurpose.Analytics], + [TrackingPurpose.Personalization]: consentState[TrackingPurpose.Personalization], + [TrackingPurpose.AdPartners]: consentState[TrackingPurpose.AdPartners], + }) mockStorageGetItem.mockImplementation((getKey) => { if (getKey === key) { return Promise.resolve(value) @@ -132,6 +153,7 @@ const mockUpdateAuthenticatedUserResponse = (user: AuthenticatedUser, consentSta input: { trackingConsentAnalytics: Consent.toBooleanOrNull(consentState[TrackingPurpose.Analytics]), trackingConsentPersonalization: Consent.toBooleanOrNull(consentState[TrackingPurpose.Personalization]), + trackingConsentAdPartners: Consent.toBooleanOrNull(consentState[TrackingPurpose.AdPartners]), }, }, }, @@ -141,6 +163,7 @@ const mockUpdateAuthenticatedUserResponse = (user: AuthenticatedUser, consentSta ...user, trackingConsentAnalytics: Consent.toBooleanOrNull(consentState[TrackingPurpose.Analytics]), trackingConsentPersonalization: Consent.toBooleanOrNull(consentState[TrackingPurpose.Personalization]), + trackingConsentAdPartners: Consent.toBooleanOrNull(consentState[TrackingPurpose.AdPartners]), __typename: 'AuthenticatedUser', }, }, @@ -194,10 +217,12 @@ describe('TrackingInitializer', () => { mockLocalStorage({ [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, }) const user = mockUser({ [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Given, }) const onUserLoggedOutCompleted = jest.fn() @@ -247,6 +272,7 @@ describe('TrackingInitializer', () => { const expectedConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockEnv() mockLocalStorage(expectedConsentState) @@ -273,6 +299,7 @@ describe('TrackingInitializer', () => { const expectedConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockEnv() mockLocalStorage(expectedConsentState) @@ -299,6 +326,7 @@ describe('TrackingInitializer', () => { const expectedConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockEnv() mockLocalStorage(expectedConsentState) @@ -351,6 +379,7 @@ describe('TrackingInitializer', () => { const expectedConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockEnv() const user = mockUser(expectedConsentState) @@ -384,6 +413,7 @@ describe('TrackingInitializer', () => { const expectedConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockEnv() const user = mockUser(expectedConsentState) @@ -411,6 +441,7 @@ describe('TrackingInitializer', () => { const expectedConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockEnv() const user = mockUser(expectedConsentState) @@ -443,10 +474,12 @@ describe('TrackingInitializer', () => { const consentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, } const updatedConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockEnv() const user = mockUser(consentState) @@ -494,10 +527,12 @@ describe('TrackingInitializer', () => { const consentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, } const updatedConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockEnv() const user = mockUser(consentState) @@ -550,10 +585,12 @@ describe('TrackingInitializer', () => { const initialConsentState: ConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, } const updatedConsentState: ConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Denied, } const user = mockUser(initialConsentState) @@ -604,6 +641,7 @@ describe('TrackingInitializer', () => { const loggedInConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Unknown, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockEnv() mockLocalStorage() @@ -652,6 +690,7 @@ describe('TrackingInitializer', () => { const localStorageConsentState: ConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockLocalStorage(localStorageConsentState) const user = mockUser(ConsentState.unknown) @@ -709,10 +748,12 @@ describe('TrackingInitializer', () => { const localStorageConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Given, + [TrackingPurpose.AdPartners]: Consent.Given, } const userProfileConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Denied, } mockEnv() mockLocalStorage(localStorageConsentState) @@ -760,10 +801,12 @@ describe('TrackingInitializer', () => { const localStorageConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Unknown, + [TrackingPurpose.AdPartners]: Consent.Unknown, } const userProfileConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Unknown, + [TrackingPurpose.AdPartners]: Consent.Unknown, } mockEnv() mockLocalStorage(localStorageConsentState) @@ -811,10 +854,12 @@ describe('TrackingInitializer', () => { const localStorageConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Unknown, + [TrackingPurpose.AdPartners]: Consent.Unknown, } const userProfileConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Unknown, } mockEnv() mockLocalStorage(localStorageConsentState) @@ -866,6 +911,7 @@ describe('TrackingInitializer', () => { const localStorageConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Unknown, + [TrackingPurpose.AdPartners]: Consent.Unknown, } mockEnv() mockLocalStorage(localStorageConsentState) @@ -894,6 +940,7 @@ describe('TrackingInitializer', () => { const updatedConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Unknown, } await act(() => { updateConsentState(updatedConsentState) @@ -904,7 +951,7 @@ describe('TrackingInitializer', () => { expect(posthogCrossPlatformMock.identify).not.toHaveBeenCalled() expect(mockStorageSetItem).toHaveBeenCalledWith( 'HOLI_TRACKING_CONSENT', - '{"ANALYTICS":"given","PERSONALIZATION":"denied"}' + '{"ANALYTICS":"given","PERSONALIZATION":"denied","AD_PARTNERS":"unknown"}' ) const [consentStateAfter] = trackingInitializerTesting.consentStateVar() expect(consentStateAfter).toEqual(updatedConsentState) @@ -917,6 +964,7 @@ describe('TrackingInitializer', () => { const localStorageConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Unknown, + [TrackingPurpose.AdPartners]: Consent.Unknown, } mockLocalStorage(localStorageConsentState) const user = mockUser(ConsentState.unknown) @@ -969,10 +1017,12 @@ describe('TrackingInitializer', () => { const localStorageConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Unknown, + [TrackingPurpose.AdPartners]: Consent.Unknown, } const userProfileConsentState = { [TrackingPurpose.Analytics]: Consent.Given, [TrackingPurpose.Personalization]: Consent.Unknown, + [TrackingPurpose.AdPartners]: Consent.Unknown, } mockEnv() mockLocalStorage(localStorageConsentState) @@ -1020,10 +1070,12 @@ describe('TrackingInitializer', () => { const localStorageConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Unknown, + [TrackingPurpose.AdPartners]: Consent.Unknown, } const userProfileConsentState = { [TrackingPurpose.Analytics]: Consent.Denied, [TrackingPurpose.Personalization]: Consent.Denied, + [TrackingPurpose.AdPartners]: Consent.Unknown, } mockEnv() mockLocalStorage(localStorageConsentState) diff --git a/core/tracking/types.ts b/core/tracking/types.ts index 0ed72c81b33ed2ea361674b251f394ffdb6996c0..d653f71218e94dc2c07f37bb2dd6779f0fe3c033 100644 --- a/core/tracking/types.ts +++ b/core/tracking/types.ts @@ -4,6 +4,7 @@ import type { TrackingEvent } from '@holi/core/tracking/events' export enum TrackingPurpose { Analytics = 'ANALYTICS', Personalization = 'PERSONALIZATION', + AdPartners = 'AD_PARTNERS', } export enum Consent { @@ -41,6 +42,7 @@ export namespace Consent { export interface ConsentState extends Record<TrackingPurpose, Consent> { [TrackingPurpose.Analytics]: Consent [TrackingPurpose.Personalization]: Consent + [TrackingPurpose.AdPartners]: Consent } // eslint-disable-next-line @typescript-eslint/no-namespace @@ -48,12 +50,14 @@ export namespace ConsentState { export const unknown: ConsentState = { [TrackingPurpose.Analytics]: Consent.Unknown, [TrackingPurpose.Personalization]: Consent.Unknown, + [TrackingPurpose.AdPartners]: Consent.Unknown, } export const isUnknown = (cs: ConsentState): boolean => Object.values(cs).every((value) => value === Consent.Unknown) export const fromUser = (user: AuthenticatedUser): ConsentState => ({ [TrackingPurpose.Analytics]: Consent.fromBooleanOrNullOrUndefined(user.trackingConsentAnalytics), [TrackingPurpose.Personalization]: Consent.fromBooleanOrNullOrUndefined(user.trackingConsentPersonalization), + [TrackingPurpose.AdPartners]: Consent.fromBooleanOrNullOrUndefined(user.trackingConsentAdPartners), }) // can be used to attach the current consent state for every purpose to events sent to PostHog. This way, when @@ -62,6 +66,7 @@ export namespace ConsentState { export const toEventProperties = (cs: ConsentState): Record<string, boolean | null> => ({ trackingConsentAnalytics: Consent.toBooleanOrNull(cs[TrackingPurpose.Analytics]), trackingConsentPersonalization: Consent.toBooleanOrNull(cs[TrackingPurpose.Personalization]), + trackingConsentAdPartners: Consent.toBooleanOrNull(cs[TrackingPurpose.AdPartners]), }) } @@ -94,6 +99,14 @@ export interface PosthogCrossPlatform { flush: () => Promise<void> } +export interface FacebookSdkCrossPlatform { + initialize(user?: AuthenticatedUser): Promise<void> + optIn(user?: AuthenticatedUser): Promise<void> + optOut(): Promise<void> + enabled(): Promise<boolean> + trackSignUp(): Promise<void> +} + export type SignupModalPressedTrigger = | 'JOIN_SPACE' | 'FOLLOW_SPACE' diff --git a/holi-apps/volunteering/components/__tests__/VolunteeringRecos.test.tsx b/holi-apps/volunteering/components/__tests__/VolunteeringRecos.test.tsx index 22879dd3c501fff5bafcd9f3083f1604f51a8d3c..7049794179a31d95492473836db5037d9fec1511 100644 --- a/holi-apps/volunteering/components/__tests__/VolunteeringRecos.test.tsx +++ b/holi-apps/volunteering/components/__tests__/VolunteeringRecos.test.tsx @@ -33,6 +33,7 @@ const testUser1 = ( lastName: faker.person.lastName(), fullName: faker.person.firstName(), avatarDefaultColor: faker.color.rgb(), + email: faker.internet.email(), avatarLabel: faker.person.jobTitle(), interestsV2: interests, skillsV2: skills, diff --git a/packages/api/graphql/graphql-codegen.ts b/packages/api/graphql/graphql-codegen.ts index 22cc17ee9928c026866d29b80ccf7bac636cb02d..1155ff0db970965a3aebfe83f3b4296423198302 100644 --- a/packages/api/graphql/graphql-codegen.ts +++ b/packages/api/graphql/graphql-codegen.ts @@ -325,6 +325,7 @@ export type AuthenticatedUser = { avatarLabel: Scalars['String']['output'] connectionStatusToMyself?: Maybe<UserConnectionStatus> connectionStatusToSpace: Array<Maybe<SpaceUserConnectionType>> + email: Scalars['String']['output'] engagementLevel?: Maybe<Scalars['String']['output']> firstName?: Maybe<Scalars['String']['output']> /** Can be an empty string */ @@ -349,6 +350,7 @@ export type AuthenticatedUser = { /** @deprecated Deprecated since 1.52. Use skills_v2 instead. */ skills: Array<Maybe<Skill>> skillsV2: Array<Maybe<Scalars['String']['output']>> + trackingConsentAdPartners?: Maybe<Scalars['Boolean']['output']> trackingConsentAnalytics?: Maybe<Scalars['Boolean']['output']> trackingConsentPersonalization?: Maybe<Scalars['Boolean']['output']> } @@ -2211,6 +2213,7 @@ export type UpdateAuthenticatedUserInput = { sdgs?: InputMaybe<Array<InputMaybe<Scalars['UUID']['input']>>> skills?: InputMaybe<Array<InputMaybe<Scalars['UUID']['input']>>> skillsV2?: InputMaybe<Array<Scalars['String']['input']>> + trackingConsentAdPartners?: InputMaybe<Scalars['Boolean']['input']> trackingConsentAnalytics?: InputMaybe<Scalars['Boolean']['input']> trackingConsentPersonalization?: InputMaybe<Scalars['Boolean']['input']> } diff --git a/packages/ui/helper/fakeUsers.ts b/packages/ui/helper/fakeUsers.ts index d828ac2551d54d0ba21999ddf87fd8f72f59e709..9f6f2d4ced76ccfa660247d6cd716d4a37001dea 100644 --- a/packages/ui/helper/fakeUsers.ts +++ b/packages/ui/helper/fakeUsers.ts @@ -5,6 +5,7 @@ import { UserConnectionStatus } from '@holi/core/screens/userprofile/types' export const getRandomAuthenticatedUser = (): AuthenticatedUser => ({ ...getRandomUser(), + email: faker.internet.email(), isEmployee: faker.datatype.boolean(), }) diff --git a/yarn.lock b/yarn.lock index d9d0682c5889e1e52e90cef1d4178761c4f695d1..cb354907ae418a38d2bddb888fd7ede0f2be5951 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5550,7 +5550,9 @@ __metadata: expo-calendar: ^13.0.5 expo-image-picker: ~15.0.7 expo-linking: ~6.3.1 + expo-tracking-transparency: ^5.1.1 graphql-request: ^7.1.0 + react-native-fbsdk-next: ^13.4.1 react-native-reanimated: ~3.10.1 react-native-svg: 15.2.0 react-zoom-pan-pinch: ^2.1.3 @@ -5659,6 +5661,7 @@ __metadata: expo-splash-screen: "npm:~0.27.6" expo-status-bar: "npm:~1.12.1" expo-task-manager: "npm:~11.8.2" + expo-tracking-transparency: "npm:~4.0.2" expo-updates: "npm:~0.25.27" fast-text-encoding: "npm:^1.0.6" find-yarn-workspace-root: "npm:^2.0.0" @@ -5684,6 +5687,7 @@ __metadata: react-native-animated-pagination-dot: "npm:^0.4.0" react-native-calendars: "npm:^1.1306.0" react-native-controlled-mentions: "npm:^2.2.5" + react-native-fbsdk-next: "npm:^13.4.1" react-native-gesture-handler: "npm:~2.16.1" react-native-get-random-values: "npm:^1.11.0" react-native-image-crop-picker: "npm:^0.41.2" @@ -19636,6 +19640,15 @@ __metadata: languageName: node linkType: hard +"expo-tracking-transparency@npm:~4.0.2": + version: 4.0.2 + resolution: "expo-tracking-transparency@npm:4.0.2" + peerDependencies: + expo: "*" + checksum: 10c0/86092b53f42000b956a6a8cdd5df0ca71e84dd918614ff71eae8b61ff233e170f1cc77738be59da5c0dd60d2937b3871b8870e9f3303aa56ff5a4c2a9c8cc898 + languageName: node + linkType: hard + "expo-updates-interface@npm:~0.16.2": version: 0.16.2 resolution: "expo-updates-interface@npm:0.16.2" @@ -28652,6 +28665,19 @@ __metadata: languageName: node linkType: hard +"react-native-fbsdk-next@npm:^13.4.1": + version: 13.4.1 + resolution: "react-native-fbsdk-next@npm:13.4.1" + peerDependencies: + expo: ">=47.0.0" + react-native: ">=0.63.3" + peerDependenciesMeta: + expo: + optional: true + checksum: 10c0/f38b8d360986e47dede9b5bcd6166e750f93c4f6d38b85013499ce6fa7d0a181b20cc009b6a05167930df0f56ed8dc79223f9cc4bee9969c8d928405fc750576 + languageName: node + linkType: hard + "react-native-fit-image@npm:^1.5.5": version: 1.5.5 resolution: "react-native-fit-image@npm:1.5.5"