diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index af81e742..c72203c8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - with: + with: persist-credentials: false token: ${{ secrets.GH_TOKEN }} @@ -40,7 +40,7 @@ jobs: - name: Set Working Directory if: github.event.inputs.workspace != '' - env: + env: IS_PLUGIN: ${{ startsWith(github.event.inputs.workspace, 'plugin-') }} run: | if ${IS_PLUGIN}; then @@ -57,7 +57,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GH_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - + - name: Publish (All) if: github.event.inputs.workspace == '' run: yarn workspaces run release diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 221ef768..fc29c269 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,5 +14,9 @@ export { } from './util'; export { SegmentClient } from './analytics'; export { SegmentDestination } from './plugins/SegmentDestination'; +export { + CategoryConsentStatusProvider, + ConsentPlugin, +} from './plugins/ConsentPlugin'; export * from './flushPolicies'; export * from './errors'; diff --git a/packages/core/src/plugins/ConsentPlugin.ts b/packages/core/src/plugins/ConsentPlugin.ts new file mode 100644 index 00000000..f5ab2470 --- /dev/null +++ b/packages/core/src/plugins/ConsentPlugin.ts @@ -0,0 +1,134 @@ +import { + Plugin, + type SegmentClient, + type DestinationPlugin, + IntegrationSettings, + PluginType, + SegmentAPIIntegration, + SegmentEvent, + TrackEventType, +} from '..'; + +import { SEGMENT_DESTINATION_KEY } from './SegmentDestination'; + +const CONSENT_PREF_UPDATE_EVENT = 'Segment Consent Preference'; + +export interface CategoryConsentStatusProvider { + setApplicableCategories(categories: string[]): void; + getConsentStatus(): Promise>; + onConsentChange(cb: (updConsent: Record) => void): void; + shutdown?(): void; +} + +/** + * This plugin interfaces with the consent provider and it: + * + * - stamps all events with the consent metadata. + * - augments all destinations with a consent filter plugin that prevents events from reaching them if + * they are not compliant current consent setup + * - listens for consent change from the provider and notifies Segment + */ +export class ConsentPlugin extends Plugin { + type = PluginType.before; + + constructor( + private consentCategoryProvider: CategoryConsentStatusProvider, + private categories: string[] + ) { + super(); + } + + configure(analytics: SegmentClient): void { + super.configure(analytics); + analytics.getPlugins().forEach(this.injectConsentFilterIfApplicable); + analytics.onPluginLoaded(this.injectConsentFilterIfApplicable); + this.consentCategoryProvider.setApplicableCategories(this.categories); + this.consentCategoryProvider.onConsentChange((categoryPreferences) => { + this.analytics + ?.track(CONSENT_PREF_UPDATE_EVENT, { + consent: { + categoryPreferences, + }, + }) + .catch((e) => { + throw e; + }); + }); + } + + async execute(event: SegmentEvent): Promise { + if ((event as TrackEventType).event === CONSENT_PREF_UPDATE_EVENT) { + return event; + } + + event.context = { + ...event.context, + consent: { + categoryPreferences: + await this.consentCategoryProvider.getConsentStatus(), + }, + }; + + return event; + } + + shutdown(): void { + this.consentCategoryProvider.shutdown?.(); + } + + private injectConsentFilterIfApplicable = (plugin: Plugin) => { + if ( + this.isDestinationPlugin(plugin) && + plugin.key !== SEGMENT_DESTINATION_KEY + ) { + const settings = this.analytics?.settings.get()?.[plugin.key]; + + plugin.add( + new ConsentFilterPlugin( + this.containsConsentSettings(settings) + ? settings.consentSettings.categories + : [] + ) + ); + } + }; + + private isDestinationPlugin(plugin: Plugin): plugin is DestinationPlugin { + return plugin.type === PluginType.destination; + } + + private containsConsentSettings = ( + settings: IntegrationSettings | undefined + ): settings is Required> => { + return ( + typeof (settings as SegmentAPIIntegration)?.consentSettings + ?.categories === 'object' + ); + }; +} + +/** + * This plugin reads the consent metadata set on the context object and then drops the events + * if they are going into a destination which violates's set consent preferences + */ +class ConsentFilterPlugin extends Plugin { + type = PluginType.before; + + constructor(private categories: string[]) { + super(); + } + + execute(event: SegmentEvent): SegmentEvent | undefined { + const preferences = event.context?.consent?.categoryPreferences; + + // if consent plugin is active but the setup isn't properly configured - events are blocked by default + if (!preferences || this.categories.length === 0) { + return undefined; + } + + // all categories this destination is tagged with must be present, and allowed in consent preferences + return this.categories.every((category) => preferences?.[category]) + ? event + : undefined; + } +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index af4d581b..709d0adc 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -213,6 +213,9 @@ export type Context = { timezone: string; traits: UserTraits; instanceId: string; + consent?: { + categoryPreferences: Record; + }; }; /** @@ -248,10 +251,13 @@ export type NativeContextInfo = { advertisingId?: string; // ios only }; -export type SegmentAPIIntegration = { +export type SegmentAPIIntegration = { apiKey: string; apiHost: string; -}; + consentSettings?: { + categories: string[]; + }; +} & T; type SegmentAmplitudeIntegration = { session_id: number; @@ -273,9 +279,8 @@ export type SegmentBrazeSettings = { export type IntegrationSettings = // Strongly typed known integration settings - | SegmentAPIIntegration - | SegmentAmplitudeIntegration - | SegmentAdjustSettings + | SegmentAPIIntegration + | SegmentAPIIntegration // Support any kind of configuration in the future | Record // enable/disable the integration at cloud level diff --git a/packages/plugins/plugin-onetrust/LICENSE b/packages/plugins/plugin-onetrust/LICENSE new file mode 100644 index 00000000..1aa03ee8 --- /dev/null +++ b/packages/plugins/plugin-onetrust/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Segment + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/plugins/plugin-onetrust/README.md b/packages/plugins/plugin-onetrust/README.md new file mode 100644 index 00000000..85a9a88e --- /dev/null +++ b/packages/plugins/plugin-onetrust/README.md @@ -0,0 +1,76 @@ +# @segment/analytics-react-native-plugin-onetrust + +Plugin for adding support for [OneTrust](https://onetrust.com/) CMP to your React Native application. + +## Installation + +You will need to install the `@segment/analytics-react-native-plugin-onetrust` package as a dependency in your project: + +Using NPM: + +```bash +npm install --save @segment/analytics-react-native-plugin-onetrust react-native-onetrust-cmp +``` + +Using Yarn: + +```bash +yarn add @segment/analytics-react-native-plugin-onetrust react-native-onetrust-cmp +``` + +## Usage + +Follow the [instructions for adding plugins](https://github.com/segmentio/analytics-react-native#adding-plugins) on the main Analytics client: + +After you create your segment client add `OneTrustPlugin` as a plugin, order doesn't matter, this plugin will apply to all device mode destinations you add before and after this plugin is added: + +```ts +import { createClient } from '@segment/analytics-react-native'; +import { OneTrustPlugin } from '@segment/analytics-react-native-plugin-onetrust'; +import OTPublishersNativeSDK from 'react-native-onetrust-cmp'; + +const segment = createClient({ + writeKey: 'SEGMENT_KEY', +}); + +segment.add({ + plugin: new OneTrust(OTPublishersNativeSDK, ['C001', 'C002', '...']), +}); + +// device mode destinations +segment.add({ plugin: new BrazePlugin() }); +``` + +## Support + +Please use Github issues, Pull Requests, or feel free to reach out to our [support team](https://segment.com/help/). + +## Integrating with Segment + +Interested in integrating your service with us? Check out our [Partners page](https://segment.com/partners/) for more details. + +## License + +``` +MIT License + +Copyright (c) 2021 Segment + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/packages/plugins/plugin-onetrust/babel.config.js b/packages/plugins/plugin-onetrust/babel.config.js new file mode 100644 index 00000000..f842b77f --- /dev/null +++ b/packages/plugins/plugin-onetrust/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], +}; diff --git a/packages/plugins/plugin-onetrust/jest.config.js b/packages/plugins/plugin-onetrust/jest.config.js new file mode 100644 index 00000000..65f6dddc --- /dev/null +++ b/packages/plugins/plugin-onetrust/jest.config.js @@ -0,0 +1,16 @@ +const { pathsToModuleNameMapper } = require('ts-jest'); +const { compilerOptions } = require('./tsconfig'); + +module.exports = { + preset: 'react-native', + roots: [''], + setupFiles: ['../../core/src/__tests__/__helpers__/setup.ts'], + testPathIgnorePatterns: ['.../../core/src/__tests__/__helpers__/'], + modulePathIgnorePatterns: ['/lib/'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + modulePaths: [compilerOptions.baseUrl], + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), +}; diff --git a/packages/plugins/plugin-onetrust/package.json b/packages/plugins/plugin-onetrust/package.json new file mode 100644 index 00000000..84d6457a --- /dev/null +++ b/packages/plugins/plugin-onetrust/package.json @@ -0,0 +1,85 @@ +{ + "name": "@segment/analytics-react-native-plugin-onetrust", + "version": "0.0.0", + "description": "Add OneTrust to Segment analytics in your React-Native app.", + "main": "lib/commonjs/index", + "module": "lib/module/index", + "types": "lib/typescript/src/index.d.ts", + "react-native": "src/index", + "source": "src/index", + "files": [ + "src", + "lib", + "android", + "ios", + "cpp", + "package.json", + "!android/build", + "!ios/build", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__" + ], + "scripts": { + "build": "bob build", + "test": "jest", + "typescript": "tsc --noEmit", + "clean": "rimraf lib node_modules", + "release": "semantic-release" + }, + "keywords": [ + "segment", + "react-native", + "ios", + "android" + ], + "repository": { + "type": "git", + "url": "https://github.com/segmentio/analytics-react-native.git", + "directory": "packages/plugins/plugin-onetrust" + }, + "author": "Segment (https://segment.com/)", + "license": "MIT", + "bugs": { + "url": "https://github.com/segmentio/analytics-react-native/issues" + }, + "homepage": "https://github.com/segmentio/analytics-react-native/tree/master/packages/plugins/plugin-onetrust#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "peerDependencies": { + "@segment/analytics-react-native": "*", + "@segment/sovran-react-native": "*", + "react-native-onetrust-cmp": "^202308.2.0" + }, + "devDependencies": { + "@semantic-release/changelog": "^6.0.1", + "@semantic-release/commit-analyzer": "^9.0.2", + "@semantic-release/git": "^10.0.1", + "@semantic-release/github": "^8.0.4", + "@semantic-release/npm": "^9.0.1", + "@semantic-release/release-notes-generator": "^10.0.3", + "@types/jest": "^27.0.3", + "conventional-changelog-conventionalcommits": "^5.0.0", + "on-change": "^3.0.2", + "rimraf": "^3.0.2", + "semantic-release": "^19.0.3", + "semantic-release-monorepo": "^7.0.5", + "ts-jest": "^27.0.7", + "typescript": "^4.4.4" + }, + "jest": { + "setupFiles": [ + "/src/methods/__tests__/jest_setup.ts" + ] + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + "commonjs", + "module", + "typescript" + ] + } +} diff --git a/packages/plugins/plugin-onetrust/release.config.js b/packages/plugins/plugin-onetrust/release.config.js new file mode 100644 index 00000000..57ec0e5b --- /dev/null +++ b/packages/plugins/plugin-onetrust/release.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['../../../release.config.js', 'semantic-release-monorepo'], +}; diff --git a/packages/plugins/plugin-onetrust/src/OTProvider.ts b/packages/plugins/plugin-onetrust/src/OTProvider.ts new file mode 100644 index 00000000..7aa29172 --- /dev/null +++ b/packages/plugins/plugin-onetrust/src/OTProvider.ts @@ -0,0 +1,76 @@ +import type { CategoryConsentStatusProvider } from '@segment/analytics-react-native'; + +enum ConsentStatus { + Granted = 1, + Denied = 0, + Unknown = -1, +} + +type OnConsentChangeCb = (v: Record) => void; + +/** Interface derived from https://www.npmjs.com/package/react-native-onetrust-cmp */ +export interface OTPublishersNativeSDK { + getConsentStatusForCategory(categoryId: string): Promise; + setBroadcastAllowedValues(categoryIds: string[]): void; + listenForConsentChanges( + categoryId: string, + callback: (cid: string, status: ConsentStatus) => void + ): void; + stopListeningForConsentChanges(): void; +} + +export class OTCategoryConsentProvider + implements CategoryConsentStatusProvider +{ + getConsentStatus!: () => Promise>; + private onConsentChangeCallback!: OnConsentChangeCb; + + constructor(private oneTrust: OTPublishersNativeSDK) {} + + onConsentChange(cb: (updConsent: Record) => void): void { + this.onConsentChangeCallback = cb; + } + + setApplicableCategories(categories: string[]): void { + const initialStatusesP = Promise.all( + categories.map((categoryId) => + this.oneTrust + .getConsentStatusForCategory(categoryId) + .then<[string, boolean]>((status) => [ + categoryId, + status === ConsentStatus.Granted, + ]) + ) + ).then((entries) => Object.fromEntries(entries)); + + let latestStatuses: Record | null; + + this.getConsentStatus = () => + Promise.resolve(latestStatuses ?? initialStatusesP); + + this.oneTrust.stopListeningForConsentChanges(); + this.oneTrust.setBroadcastAllowedValues(categories); + + categories.forEach((categoryId) => { + this.oneTrust.listenForConsentChanges(categoryId, (_, status) => { + initialStatusesP + .then((initialStatuses) => { + latestStatuses = { + ...initialStatuses, + ...latestStatuses, + [categoryId]: status === ConsentStatus.Granted, + }; + + this.onConsentChangeCallback(latestStatuses); + }) + .catch((e) => { + throw e; + }); + }); + }); + } + + shutdown() { + this.oneTrust.stopListeningForConsentChanges(); + } +} diff --git a/packages/plugins/plugin-onetrust/src/OneTrust.ts b/packages/plugins/plugin-onetrust/src/OneTrust.ts new file mode 100644 index 00000000..2287283c --- /dev/null +++ b/packages/plugins/plugin-onetrust/src/OneTrust.ts @@ -0,0 +1,9 @@ +import { ConsentPlugin } from '@segment/analytics-react-native'; + +import { OTPublishersNativeSDK, OTCategoryConsentProvider } from './OTProvider'; + +export class OneTrustPlugin extends ConsentPlugin { + constructor(oneTrustSDK: OTPublishersNativeSDK, categories: string[]) { + super(new OTCategoryConsentProvider(oneTrustSDK), categories); + } +} diff --git a/packages/plugins/plugin-onetrust/src/__tests__/OneTrust.test.ts b/packages/plugins/plugin-onetrust/src/__tests__/OneTrust.test.ts new file mode 100644 index 00000000..a3d81f68 --- /dev/null +++ b/packages/plugins/plugin-onetrust/src/__tests__/OneTrust.test.ts @@ -0,0 +1,189 @@ +import { + DestinationPlugin, + Plugin, + PluginType, + SegmentClient, +} from '@segment/analytics-react-native'; +import { OneTrustPlugin } from '../OneTrust'; +import onChange from 'on-change'; +import type { OTPublishersNativeSDK } from '../OTProvider'; +import { createTestClient } from '@segment/analytics-react-native/src/__tests__/__helpers__/setupSegmentClient'; + +class MockDestination extends DestinationPlugin { + track = jest.fn(); + + constructor(public readonly key: string) { + super(); + } +} + +class MockOneTrustSDK implements OTPublishersNativeSDK { + private readonly DEFAULT_CONSENT_STATUSES = { + C001: 1, + C002: 0, + C003: 1, + C004: -1, + }; + + private changeCallbacks = new Map< + string, + ((cid: string, status: number) => void)[] + >(); + + mockConsentStatuses: Record = onChange( + this.DEFAULT_CONSENT_STATUSES, + (key, value) => { + this.changeCallbacks.get(key)?.forEach((cb) => cb(key, value as number)); + } + ); + + getConsentStatusForCategory(categoryId: string): Promise { + return Promise.resolve(this.mockConsentStatuses[categoryId]); + } + + setBroadcastAllowedValues(): void { + return; + } + + listenForConsentChanges( + categoryId: string, + callback: (cid: string, status: number) => void + ): void { + this.changeCallbacks.set(categoryId, [ + ...(this.changeCallbacks.get(categoryId) || []), + callback, + ]); + } + + stopListeningForConsentChanges(): void { + this.changeCallbacks.clear(); + } +} + +describe('OneTrustPlugin', () => { + let client: SegmentClient; + let mockOneTrust: MockOneTrustSDK; + const mockBraze = new MockDestination('Braze'); + const mockAmplitude = new MockDestination('Amplitude'); + + beforeEach(async () => { + const testClient = createTestClient(); + testClient.store.reset(); + jest.clearAllMocks(); + client = testClient.client; + mockOneTrust = new MockOneTrustSDK(); + client.add({ + plugin: new OneTrustPlugin( + mockOneTrust, + Object.keys(mockOneTrust.mockConsentStatuses) + ), + }); + + client.add({ + plugin: mockBraze, + settings: { + consentSettings: { + categories: ['C002', 'C004'], + }, + }, + }); + + client.add({ + plugin: mockAmplitude, + settings: { + consentSettings: { + categories: ['C002'], + }, + }, + }); + + await client.init(); + }); + + it('stamps each event with consent statuses as provided by onetrust', async () => { + // we'll use a before plugin to tap into the timeline and confirm the stamps are applied as early as possible + class TapPlugin extends Plugin { + type = PluginType.before; + execute = jest.fn(); + } + + const tapPlugin = new TapPlugin(); + client.add({ + plugin: tapPlugin, + }); + + await client.track('Test event'); + + expect(tapPlugin.execute).toHaveBeenCalledWith( + expect.objectContaining({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + context: expect.objectContaining({ + consent: { + categoryPreferences: { + C001: true, + C002: false, + C003: true, + C004: false, + }, + }, + }), + }) + ); + }); + + it('prevents an event from reaching non-compliant destinations', async () => { + await client.track('Test event'); + + expect(mockBraze.track).not.toHaveBeenCalled(); + expect(mockAmplitude.track).not.toHaveBeenCalled(); + }); + + it('allows an event to reach destinations once consent is granted later on', async () => { + await client.track('Test event'); + + expect(mockBraze.track).not.toHaveBeenCalled(); + expect(mockAmplitude.track).not.toHaveBeenCalled(); + + mockOneTrust.mockConsentStatuses.C002 = 1; + + await client.track('Test event'); + + // this destination will now receive events + expect(mockAmplitude.track).toHaveBeenCalledTimes(1); + // but one of the tagged categories on this destination is still not consented + expect(mockBraze.track).not.toHaveBeenCalled(); + + mockOneTrust.mockConsentStatuses.C004 = 1; + + await client.track('Test event'); + + // now both have been consented + expect(mockAmplitude.track).toHaveBeenCalledTimes(2); + expect(mockBraze.track).toHaveBeenCalledTimes(1); + }); + + it('relays consent change within onetrust to Segment', async () => { + const spy = jest.spyOn(client, 'track'); + + await client.track('Test event'); + + mockOneTrust.mockConsentStatuses.C002 = 1; + + // await one tick + await new Promise((res) => setTimeout(res, 0)); + + // this is to make sure there are no unneccessary Consent Preference track calls + expect(spy).toHaveBeenCalledTimes(2); + + expect(spy).toHaveBeenLastCalledWith('Segment Consent Preference', { + consent: { + categoryPreferences: { + C001: true, + C002: true, + C003: true, + C004: false, + }, + }, + }); + }); +}); diff --git a/packages/plugins/plugin-onetrust/src/index.tsx b/packages/plugins/plugin-onetrust/src/index.tsx new file mode 100644 index 00000000..878a3ba4 --- /dev/null +++ b/packages/plugins/plugin-onetrust/src/index.tsx @@ -0,0 +1 @@ +export { OneTrustPlugin } from './OneTrust'; diff --git a/packages/plugins/plugin-onetrust/tsconfig.json b/packages/plugins/plugin-onetrust/tsconfig.json new file mode 100644 index 00000000..5695a71a --- /dev/null +++ b/packages/plugins/plugin-onetrust/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "lib/typescript", + "baseUrl": ".", + "paths": { + "@segment/analytics-react-native": ["/../../core/src/index"] + } + }, + "references": [{ "path": "../../core" }] +} diff --git a/yarn.lock b/yarn.lock index 79c5672a..3cfa7934 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1497,27 +1497,6 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@expo/config-plugins@^4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-4.1.5.tgz#9d357d2cda9c095e511b51583ede8a3b76174068" - integrity sha512-RVvU40RtZt12HavuDAe+LDIq9lHj7sheOfMEHdmpJ/uTA8pgvkbc56XF6JHQD+yRr6+uhhb+JnAasGq49dsQbw== - dependencies: - "@expo/config-types" "^45.0.0" - "@expo/json-file" "8.2.36" - "@expo/plist" "0.0.18" - "@expo/sdk-runtime-versions" "^1.0.0" - "@react-native/normalize-color" "^2.0.0" - chalk "^4.1.2" - debug "^4.3.1" - find-up "~5.0.0" - getenv "^1.0.0" - glob "7.1.6" - resolve-from "^5.0.0" - semver "^7.3.5" - slash "^3.0.0" - xcode "^3.0.1" - xml2js "0.4.23" - "@expo/config-plugins@^5.0.4": version "5.0.4" resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-5.0.4.tgz#216fea6558fe66615af1370de55193f4181cb23e" @@ -1539,11 +1518,6 @@ xcode "^3.0.1" xml2js "0.4.23" -"@expo/config-types@^45.0.0": - version "45.0.0" - resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-45.0.0.tgz#963c2fdce8fbcbd003758b92ed8a25375f437ef6" - integrity sha512-/QGhhLWyaGautgEyU50UJr5YqKJix5t77ePTwreOVAhmZH+ff3nrrtYTTnccx+qF08ZNQmfAyYMCD3rQfzpiJA== - "@expo/config-types@^47.0.0": version "47.0.0" resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-47.0.0.tgz#99eeabe0bba7a776e0f252b78beb0c574692c38d" @@ -2463,10 +2437,10 @@ resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa" integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ== -"@segment/tsub@^0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@segment/tsub/-/tsub-0.2.0.tgz#456345ad04bca81f6c3fefacac722fd9eb4923ce" - integrity sha512-DThHEnZ+1PlSjj59Q7Ns/ESevjfztDV4PtKMcoVp1PWSWaaPcaktWGdTeDgi8ucsJO91qtY9N4aM2cbfGr/yWA== +"@segment/tsub@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@segment/tsub/-/tsub-2.0.0.tgz#321e781a38fcd3720853f2da7574b523b447a296" + integrity sha512-NzkBK8GwPsyQ74AceLjENbUoaFrObnzEKOX4ko2wZDuIyK+DnDm3B//8xZYI2LCKt+wUD55l6ygfjCoVs8RMWw== dependencies: "@stdlib/math-base-special-ldexp" "^0.0.5" dlv "^1.1.3" @@ -9926,6 +9900,11 @@ object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" +on-change@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/on-change/-/on-change-3.0.2.tgz#e40eb57b455e71ad3144aab06337d91e91d4a7d6" + integrity sha512-rdt5YfIfo86aFNwvQqzzHMpaPPyVQ/XjcGK01d46chZh47G8Xzvoao79SgFb03GZfxRGREzNQVJuo31drqyIlA== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -10712,12 +10691,11 @@ react-native-codegen@^0.0.8: jscodeshift "^0.11.0" nullthrows "^1.1.1" -react-native-fbsdk-next@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/react-native-fbsdk-next/-/react-native-fbsdk-next-10.1.0.tgz#e26c4e2505ffe34050ffdd44b8e6bbb8eaf736a4" - integrity sha512-M7G4G0WIyg6HY72eViROSiV/9DVCdovoLSTdED+Ovj+otTPwDCniYPOirBBES14GTzN0Ehl6rZkwX6qAspAhqA== +react-native-fbsdk-next@^11: + version "11.3.0" + resolved "https://registry.yarnpkg.com/react-native-fbsdk-next/-/react-native-fbsdk-next-11.3.0.tgz#a1fb9e177acd11dc9118ec398ad1855e3e7c33c2" + integrity sha512-Mb08KqlHh9mIAiIProAvxY8paKEcP4FGR2Hiz9QPxzkM+AxDk8DLcPv3QZrB/nkMOuSNYi4gtwdpao92WNzeUw== dependencies: - "@expo/config-plugins" "^4.1.5" xml2js "^0.4.23" react-native@^0.67.2: