diff --git a/.circleci/config.yml b/.circleci/config.yml index 8a5c94809..36ffd0eed 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,12 +25,19 @@ jobs: command: sudo corepack enable - run: - name: Install Node.js and Yarn Dependencies + name: Install Root Dependencies command: yarn install --immutable --network-timeout 300000 no_output_timeout: 20m environment: NODE_OPTIONS: "--max-old-space-size=4096" + - run: + name: Install AEPSampleApp Dependencies + command: cd apps/AEPSampleApp && yarn install --immutable --network-timeout 300000 + no_output_timeout: 20m + environment: + NODE_OPTIONS: "--max-old-space-size=4096" + - run: name: Build the Project command: yarn run build diff --git a/.gitignore b/.gitignore index 48e6d3b41..3bc48f015 100644 --- a/.gitignore +++ b/.gitignore @@ -152,3 +152,21 @@ out/ # ignore ds store .DS_Store + +# Claude Code machine-specific files (never commit these) +.claude/ +**/.claude/ + +# Internal AI agent documentation (kept locally, not for public PRs) +agent-docs/ + +# Local smoke test run reports +smoke-tests/ + +# Compiled TypeScript build artifacts in package source/spec directories +packages/*/src/**/*.js +packages/*/src/**/*.js.map +packages/*/src/**/*.d.ts +packages/*/specs/**/*.js +packages/*/specs/**/*.js.map +packages/*/specs/**/*.d.ts diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 000000000..788c786cb --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["node_modules", ".git", "ios/build", "android/build", "android/.gradle", ".gradle"] +} diff --git a/.yarnrc.yml b/.yarnrc.yml index e0c77d49a..809e7bf2f 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -5,3 +5,4 @@ enableGlobalCache: false nmHoistingLimits: workspaces nodeLinker: node-modules + diff --git a/apps/AEPSampleApp/extensions/OptimizeView.tsx b/apps/AEPSampleApp/extensions/OptimizeView.tsx index 888402813..a37813fd9 100644 --- a/apps/AEPSampleApp/extensions/OptimizeView.tsx +++ b/apps/AEPSampleApp/extensions/OptimizeView.tsx @@ -24,7 +24,7 @@ import { View, Image, TouchableOpacity, - Dimensions, + useWindowDimensions, TextInput, StyleSheet, } from 'react-native'; @@ -49,7 +49,7 @@ const defaultPropositions = { export default ({navigation}: any) => { const [version, setVersion] = useState('0.0.0'); - const [customScopeInput, setCustomScopeInput] = useState('demoLoc3'); + const [customScopeInput, setCustomScopeInput] = useState('mboxAug'); const [textProposition, setTextProposition] = useState(); const [imageProposition, setImageProposition] = useState(); const [htmlProposition, setHtmlProposition] = useState(); @@ -68,13 +68,13 @@ export default ({navigation}: any) => { const decisionScopeJson = new DecisionScope( 'eyJ4ZG06YWN0aXZpdHlJZCI6Inhjb3JlOm9mZmVyLWFjdGl2aXR5OjE0MWM4NTg2MmRiMDQ4YzkiLCJ4ZG06cGxhY2VtZW50SWQiOiJ4Y29yZTpvZmZlci1wbGFjZW1lbnQ6MTQxYzZkN2VjOTZmOTg2ZCJ9', ); - const decisionScopeTargetMbox = new DecisionScope(customScopeInput.trim() || 'demoLoc3'); + const decisionScopeTargetMbox = new DecisionScope(customScopeInput.trim() || 'mboxAug'); const decisionScopes = [ - decisionScopeText, - decisionScopeImage, - decisionScopeHtml, - decisionScopeJson, + // decisionScopeText, + // decisionScopeImage, + // decisionScopeHtml, + // decisionScopeJson, decisionScopeTargetMbox, ]; @@ -217,7 +217,7 @@ export default ({navigation}: any) => { return data1 !== data2; }); - var {width} = Dimensions.get('window'); + const { width } = useWindowDimensions(); const inputStyles = StyleSheet.create({ label: {fontWeight: '600', marginTop: 8, marginBottom: 2, color: '#333', alignSelf: 'flex-start'}, input: {borderWidth: 1, borderColor: '#ccc', borderRadius: 6, padding: 8, fontSize: 13, marginBottom: 2, backgroundColor: '#fff', width: width - 32}, diff --git a/apps/AEPSampleApp/ios/Podfile.lock b/apps/AEPSampleApp/ios/Podfile.lock index c0f106cae..f13d69dd7 100644 --- a/apps/AEPSampleApp/ios/Podfile.lock +++ b/apps/AEPSampleApp/ios/Podfile.lock @@ -77,6 +77,7 @@ PODS: - RCTAEPOptimize (7.1.1): - AEPOptimize (< 6.0.0, >= 5.0.0) - React + - React-Codegen - RCTAEPPlaces (7.0.1): - AEPPlaces (< 6.0.0, >= 5.0.0) - React @@ -109,6 +110,7 @@ PODS: - React-RCTText (= 0.85.0) - React-RCTVibration (= 0.85.0) - React-callinvoker (0.85.0) + - React-Codegen (0.1.0) - React-Core (0.85.0): - hermes-engine - RCTDeprecation @@ -2298,6 +2300,7 @@ SPEC REPOS: - AEPSignal - AEPTarget - AEPUserProfile + - React-Codegen EXTERNAL SOURCES: FBLazyVector: @@ -2517,7 +2520,7 @@ SPEC CHECKSUMS: RCTAEPEdgeConsent: 94d66ceefac0058a1586b0b876e37f178d3733d2 RCTAEPEdgeIdentity: 2ed3fdc9a3150e9bcf268b128c22f293ab4df6aa RCTAEPMessaging: d97f2750b4698d54953e6a9b824506a18be1695c - RCTAEPOptimize: 160b9d7aa20c4524e70b930934dbd5c0f7a58f14 + RCTAEPOptimize: cf583da4e45b6db6e641c4771f5a4204882521f1 RCTAEPPlaces: 4050bda86286605a8683b4c078baac1662ca013a RCTAEPTarget: e8bcf8864758faf07635719ffa0eff6a06861910 RCTAEPUserProfile: 799d618ffbbfc389175c8a8cd5afcc36d852709a @@ -2528,6 +2531,7 @@ SPEC CHECKSUMS: RCTTypeSafety: abdf2eaed5501a52f2000de668ccfc60b78c3b27 React: 1b1536b9099195944034e65b1830f463caaa8390 React-callinvoker: 6dff6d17d1d6cc8fdf85468a649bafed473c65f5 + React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a React-Core: 39ee05b5798296f433dd3c3624c57a187c1510e3 React-Core-prebuilt: 0eb00afc411cea82c86a1a369b68da88299926f0 React-CoreModules: e78bfd2617075bc0e50c689df4a29232bd72ad82 diff --git a/apps/AEPSampleAppNewArchEnabled/.watchmanconfig b/apps/AEPSampleAppNewArchEnabled/.watchmanconfig new file mode 100644 index 000000000..e4bde3c05 --- /dev/null +++ b/apps/AEPSampleAppNewArchEnabled/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["node_modules", ".git", "ios/build", "android/build", "android/.gradle"] +} diff --git a/apps/AEPSampleAppNewArchEnabled/app.json b/apps/AEPSampleAppNewArchEnabled/app.json index ed5753893..525f52bae 100644 --- a/apps/AEPSampleAppNewArchEnabled/app.json +++ b/apps/AEPSampleAppNewArchEnabled/app.json @@ -57,7 +57,13 @@ } ], "expo-font", - "expo-web-browser" + "expo-web-browser", + [ + "./plugins/withInteropRoot", + { + "value": "0" + } + ] ], "experiments": { "typedRoutes": true diff --git a/apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx b/apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx index 71a5d9ade..f294785be 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx @@ -67,13 +67,13 @@ export default () => { const decisionScopeJson = new DecisionScope( 'eyJ4ZG06YWN0aXZpdHlJZCI6Inhjb3JlOm9mZmVyLWFjdGl2aXR5OjE0MWM4NTg2MmRiMDQ4YzkiLCJ4ZG06cGxhY2VtZW50SWQiOiJ4Y29yZTpvZmZlci1wbGFjZW1lbnQ6MTQxYzZkN2VjOTZmOTg2ZCJ9', ); - const decisionScopeTargetMbox = new DecisionScope('demoLoc3'); + const decisionScopeTargetMbox = new DecisionScope('mboxAug'); const decisionScopes = [ - decisionScopeText, - decisionScopeImage, - decisionScopeHtml, - decisionScopeJson, + // decisionScopeText, + // decisionScopeImage, + // decisionScopeHtml, + // decisionScopeJson, decisionScopeTargetMbox ]; @@ -126,7 +126,9 @@ export default () => { const onPropositionUpdate = () => Optimize.onPropositionUpdate({ call(propositions: Map) { + console.log('onPropositionUpdate called'); if (propositions) { + console.log('propositions bnana', JSON.stringify(Object.fromEntries(propositions), null, 2)); setTextProposition(propositions.get(decisionScopeText.getName())); setImageProposition(propositions.get(decisionScopeImage.getName())); setHtmlProposition(propositions.get(decisionScopeHtml.getName())); diff --git a/apps/AEPSampleAppNewArchEnabled/app/_layout.tsx b/apps/AEPSampleAppNewArchEnabled/app/_layout.tsx index c57b698e4..ed96660f6 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/_layout.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/_layout.tsx @@ -39,7 +39,7 @@ export default function RootLayout() { // For functional components, use useEffect with an empty dependency array. // For class components, call initializeWithAppId inside componentDidMount. MobileCore.setLogLevel(LogLevel.DEBUG); - MobileCore.initializeWithAppId("YOUR-APP-ID") + MobileCore.initializeWithAppId("3149c49c3910/0f12baf27522/launch-0d096c129660-development") .then(() => { console.log("AEP SDK Initialized"); diff --git a/apps/AEPSampleAppNewArchEnabled/metro.config.js b/apps/AEPSampleAppNewArchEnabled/metro.config.js index bcc3133ba..bdf59a2d4 100644 --- a/apps/AEPSampleAppNewArchEnabled/metro.config.js +++ b/apps/AEPSampleAppNewArchEnabled/metro.config.js @@ -1,52 +1,83 @@ const { getDefaultConfig } = require('expo/metro-config'); -const { FileStore } = require('metro-cache'); -const path = require('path'); - -const config = getDefaultConfig(__dirname); +const { resolve, join } = require('path'); const projectRoot = __dirname; -const monorepoRoot = path.resolve(projectRoot, '../..'); +const monorepoRoot = resolve(projectRoot, '../..'); + +const ADOBE_PACKAGE_FOLDERS = [ + 'assurance', + 'campaignclassic', + 'core', + 'edge', + 'edgebridge', + 'edgeconsent', + 'edgeidentity', + 'messaging', + 'optimize', + 'places', + 'target', + 'userprofile', +]; -// Watch all files in the monorepo -config.watchFolders = [monorepoRoot]; +const extraNodeModules = Object.fromEntries( + ADOBE_PACKAGE_FOLDERS.map((folder) => [ + `@adobe/react-native-aep${folder}`, + join(monorepoRoot, 'packages', folder), + ]), +); -// Let Metro know where to resolve packages -config.resolver.nodeModulesPaths = [ - path.resolve(projectRoot, 'node_modules'), - path.resolve(monorepoRoot, 'node_modules'), +const config = getDefaultConfig(projectRoot); + +config.watchFolders = [ + resolve(monorepoRoot, 'packages'), + resolve(monorepoRoot, 'node_modules'), ]; -// Exclude problematic nested node_modules to prevent the bundling error +// After merging main, root node_modules gained react-native@0.85 (devDep for jest). +// This app uses react-native@0.81 (Expo 54). Block root's react-native so Metro +// always resolves it from the app's own node_modules instead. +// Escape special regex chars in absolute paths used for blockList patterns. +const escapePath = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +// Block root node_modules react-native/react (root devDeps for Jest) so Metro +// always uses the app's own Expo-managed versions. +// Block expo-router's nested @react-navigation so only ONE copy (the app's +// top-level versions) is bundled — prevents "multiple NavigationContainer" crash. config.resolver.blockList = [ - // Block nested node_modules inside packages - /packages\/.*\/node_modules\/.*/, - // Block any nested node_modules directories - /.*\/node_modules\/.*\/node_modules\/.*/, + new RegExp(`^${escapePath(resolve(monorepoRoot, 'node_modules/react-native'))}/.*`), + new RegExp(`^${escapePath(resolve(monorepoRoot, 'node_modules/react'))}/.*`), + new RegExp(`^${escapePath(join(projectRoot, 'node_modules/expo-router/node_modules/@react-navigation'))}/.*`), ]; -// Don't try to transpile react-native's internal source files -config.resolver.disableHierarchicalLookup = true; -// Use a separate cache for the monorepo to avoid conflicts -config.cacheStores = [ - new FileStore({ - root: path.join(projectRoot, 'node_modules', '.cache', 'metro'), - }), +config.resolver.nodeModulesPaths = [ + resolve(projectRoot, 'node_modules'), + resolve(monorepoRoot, 'node_modules'), ]; -// Explicitly map workspace packages to their built versions -config.resolver.alias = { - '@adobe/react-native-aepassurance': path.resolve(monorepoRoot, 'packages/assurance'), - '@adobe/react-native-aepcampaignclassic': path.resolve(monorepoRoot, 'packages/campaignclassic'), - '@adobe/react-native-aepcore': path.resolve(monorepoRoot, 'packages/core'), - '@adobe/react-native-aepedge': path.resolve(monorepoRoot, 'packages/edge'), - '@adobe/react-native-aepedgebridge': path.resolve(monorepoRoot, 'packages/edgebridge'), - '@adobe/react-native-aepedgeconsent': path.resolve(monorepoRoot, 'packages/edgeconsent'), - '@adobe/react-native-aepedgeidentity': path.resolve(monorepoRoot, 'packages/edgeidentity'), - '@adobe/react-native-aepmessaging': path.resolve(monorepoRoot, 'packages/messaging'), - '@adobe/react-native-aepoptimize': path.resolve(monorepoRoot, 'packages/optimize'), - '@adobe/react-native-aepplaces': path.resolve(monorepoRoot, 'packages/places'), - '@adobe/react-native-aeptarget': path.resolve(monorepoRoot, 'packages/target'), - '@adobe/react-native-aepuserprofile': path.resolve(monorepoRoot, 'packages/userprofile'), +// Watchman's daemon socket is unavailable in this sandboxed environment, which +// makes `watch-project` hang indefinitely. Fall back to Metro's Node-based +// file watcher so bundling can proceed. +config.resolver.useWatchman = false; + +config.resolver.extraNodeModules = { + ...extraNodeModules, + '@babel/runtime': join(monorepoRoot, 'node_modules/@babel/runtime'), + // Pin singleton packages to the app's own node_modules so there is never more + // than one copy in the bundle. + // + // react / react-native: blocked from root node_modules (root has 0.85 devDep + // for Jest while this app uses 0.81 via Expo 54). + // + // @react-navigation/*: expo-router ships its own older nested copies + // (@react-navigation/core@7.14 vs app's 7.17, native@7.1.28 vs 7.2.2). + // Pinning here + blockList above forces a single instance → fixes + // "Couldn't register the navigator / multiple copies" crash. + 'react-native': join(projectRoot, 'node_modules/react-native'), + 'react': join(projectRoot, 'node_modules/react'), + '@react-navigation/core': join(projectRoot, 'node_modules/@react-navigation/core'), + '@react-navigation/native': join(projectRoot, 'node_modules/@react-navigation/native'), + '@react-navigation/routers': join(projectRoot, 'node_modules/@react-navigation/routers'), + '@react-navigation/elements': join(projectRoot, 'node_modules/@react-navigation/elements'), }; -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/apps/AEPSampleAppNewArchEnabled/package.json b/apps/AEPSampleAppNewArchEnabled/package.json index 1f6d49751..673865128 100644 --- a/apps/AEPSampleAppNewArchEnabled/package.json +++ b/apps/AEPSampleAppNewArchEnabled/package.json @@ -78,5 +78,14 @@ ] } }, - "private": true + "private": true, + "resolutions": { + "@react-navigation/core": "7.17.2", + "@react-navigation/native": "7.2.2", + "@react-navigation/routers": "7.5.3", + "@react-navigation/elements": "2.9.5", + "@react-navigation/bottom-tabs": "7.15.9", + "@react-navigation/drawer": "7.7.13", + "@react-navigation/native-stack": "7.14.10" + } } diff --git a/apps/AEPSampleAppNewArchEnabled/plugins/withInteropRoot.js b/apps/AEPSampleAppNewArchEnabled/plugins/withInteropRoot.js new file mode 100644 index 000000000..b5a1817ff --- /dev/null +++ b/apps/AEPSampleAppNewArchEnabled/plugins/withInteropRoot.js @@ -0,0 +1,57 @@ +const { withDangerousMod } = require('@expo/config-plugins'); +const fs = require('fs'); +const path = require('path'); + +/** + * Sets `ENV['USE_INTEROP_ROOT']` in the iOS Podfile. + * + * The AEP Optimize podspec (`packages/optimize/RCTAEPOptimize.podspec`) reads + * this env var at `pod install` time to choose the iOS compile path: + * "0" = Turbo Module (New Architecture, default) + * "1" = interop layer (legacy bridge / RCTEventEmitter) + * + * Expo has no built-in app.json key for forwarding an arbitrary env var to a + * third-party podspec, so we inject it into the Podfile. Because this runs on + * every `expo prebuild`, it survives Podfile regeneration. + * + * Uses `||=` so a shell override (e.g. `USE_INTEROP_ROOT=1 pod install`) still + * takes precedence for one-off builds. + * + * Usage in app.json: + * ["./plugins/withInteropRoot", { "value": "0" }] + */ +const START = '# >>> USE_INTEROP_ROOT (managed by withInteropRoot plugin)'; +const END = '# <<< USE_INTEROP_ROOT'; + +const withInteropRoot = (config, { value = '0' } = {}) => { + return withDangerousMod(config, [ + 'ios', + (cfg) => { + const podfilePath = path.join( + cfg.modRequest.platformProjectRoot, + 'Podfile', + ); + let contents = fs.readFileSync(podfilePath, 'utf8'); + + const block = `${START}\nENV['USE_INTEROP_ROOT'] ||= '${value}'\n${END}\n`; + + // Drop any previously managed block so the value stays in sync. + const blockRegex = new RegExp(`${START}[\\s\\S]*?${END}\\n?`); + contents = contents.replace(blockRegex, ''); + + // Insert just before prepare_react_native_project! (always present in + // Expo Podfiles); fall back to prepending if the anchor moves. + const anchor = 'prepare_react_native_project!'; + if (contents.includes(anchor)) { + contents = contents.replace(anchor, `${block}\n${anchor}`); + } else { + contents = `${block}\n${contents}`; + } + + fs.writeFileSync(podfilePath, contents); + return cfg; + }, + ]); +}; + +module.exports = withInteropRoot; diff --git a/jest.config.js b/jest.config.js index fb1a5d8d5..91475c113 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,7 @@ const { defaults: tsjPreset } = require("ts-jest/presets"); /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { ...tsjPreset, - preset: "@react-native/jest-preset", + preset: "/apps/AEPSampleApp/node_modules/react-native", testEnvironment: "node", transform: { "^.+\\.jsx$": "babel-jest", @@ -17,7 +17,9 @@ module.exports = { setupFiles: ["/tests/jest/setup.ts"], moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], moduleNameMapper: { - "^react-native$": "/node_modules/react-native", + // Resolve react-native from AEPSampleApp's node_modules — react-native is not hoisted + // to root (nmHoistingLimits: workspaces), so we point both preset and mapper here. + "^react-native$": "/apps/AEPSampleApp/node_modules/react-native", }, testPathIgnorePatterns: [ "/node_modules/", diff --git a/package.json b/package.json index be7e81e89..2189a2a8a 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,6 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lerna": "^8.2.2", - "react": "18.3.1", - "react-native": "^0.85.0", "ts-jest": "^29.1.1", "tslib": "^2.3.1", "typescript": "^5.0.0" diff --git a/packages/optimize/RCTAEPOptimize.podspec b/packages/optimize/RCTAEPOptimize.podspec index 308bf5f73..b3b947f7e 100644 --- a/packages/optimize/RCTAEPOptimize.podspec +++ b/packages/optimize/RCTAEPOptimize.podspec @@ -1,6 +1,11 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +# Build-time toggle — mirrors Android's buildConfigField "boolean", "USE_INTEROP_ROOT", "false" +# USE_INTEROP_ROOT=1 pod install → interop layer (old arch, RCTEventEmitter) +# USE_INTEROP_ROOT=0 (default) → Turbo Module (new arch, getTurboModule:) +use_interop_root = ENV.key?('USE_INTEROP_ROOT') ? ENV['USE_INTEROP_ROOT'].to_i : 0 + Pod::Spec.new do |s| s.name = "RCTAEPOptimize" s.version = package["version"] @@ -12,11 +17,19 @@ Pod::Spec.new do |s| s.license = "Apache 2.0 License" s.platform = :ios, '12.0' - s.source = { :git => "https://github.com/adobe/aepsdk-react-native.git", :branch => "optimize" } + s.source = { :git => "https://github.com/adobe/aepsdk-react-native.git", :tag => "#{s.version}" } - s.source_files = 'ios/**/*.{h,m}' + s.source_files = 'ios/**/*.{h,m,mm}' s.requires_arc = true - s.dependency "React" + s.dependency "React" + s.dependency "React-Codegen" s.dependency "AEPOptimize", ">= 5.0.0", "< 6.0.0" + + s.pod_target_xcconfig = { + "CLANG_ENABLE_MODULES" => "YES", + "OTHER_CPLUSPLUSFLAGS" => "$(inherited) -fcxx-modules", + "HEADER_SEARCH_PATHS" => "$(inherited) \"$(PODS_ROOT)/../build/generated/ios\" \"$(PODS_ROOT)/../build/generated/ios/ReactCodegen\" \"$(PODS_ROOT)/Headers/Public/ReactCodegen\"", + "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) USE_INTEROP_ROOT=#{use_interop_root}" + } end diff --git a/packages/optimize/__tests__/OptimizeTests.ts b/packages/optimize/__tests__/OptimizeTests.ts index cf75f9a17..3d8f528eb 100644 --- a/packages/optimize/__tests__/OptimizeTests.ts +++ b/packages/optimize/__tests__/OptimizeTests.ts @@ -10,6 +10,14 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ +// Map the TurboModule to the same mock object as NativeModules.AEPOptimize so that +// existing jest.spyOn(NativeModules.AEPOptimize, ...) calls continue to fire when +// Optimize.ts routes calls through NativeAEPOptimize (TurboModule path). +jest.mock('../src/NativeAEPOptimize', () => { + const rn = jest.requireMock('react-native'); + return { __esModule: true, default: rn.NativeModules.AEPOptimize }; +}); + import { NativeModules } from 'react-native'; import { Optimize, Proposition, DecisionScope, Offer } from '../src'; import offerJson from './offer.json'; @@ -23,13 +31,15 @@ describe('Optimize', () => { }); it('AEPOptimize onPropositionUpdate is called with correct parameters', async () => { - const spy = jest.spyOn(NativeModules.AEPOptimize, 'onPropositionsUpdate'); + const registerSpy = jest.spyOn(NativeModules.AEPOptimize, 'onPropositionsUpdate'); + const subscribeSpy = jest.spyOn(NativeModules.AEPOptimize, 'onPropositionsUpdated'); let adobeCallback = { call(_: Map): void {} }; await Optimize.onPropositionUpdate(adobeCallback); - expect(spy).toHaveBeenCalled(); + expect(subscribeSpy).toHaveBeenCalled(); + expect(registerSpy).toHaveBeenCalled(); }); it('AEPOptimize clearCachedProposition is called', async () => { diff --git a/packages/optimize/android/build.gradle b/packages/optimize/android/build.gradle index ec277b198..bac81bfb7 100644 --- a/packages/optimize/android/build.gradle +++ b/packages/optimize/android/build.gradle @@ -1,12 +1,9 @@ -buildscript { - repositories { - google() - mavenCentral() - } +def isNewArchitectureEnabled() { + return rootProject.hasProperty("newArchEnabled") && rootProject.newArchEnabled == "true" +} - dependencies { - classpath 'com.android.tools.build:gradle:8.1.1' - } +if (isNewArchitectureEnabled()) { + apply plugin: 'com.facebook.react' } apply plugin: 'com.android.library' @@ -16,7 +13,7 @@ def safeExtGet(prop, fallback) { } android { - compileSdk safeExtGet('compileSdk', 34) + compileSdk safeExtGet('compileSdkVersion', 34) def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger() if (agpVersion >= 8) { @@ -28,6 +25,10 @@ android { targetSdkVersion safeExtGet('targetSdkVersion', 34) versionCode 1 versionName "1.0" + buildConfigField "boolean", "USE_INTEROP_ROOT", "false" + } + buildFeatures { + buildConfig true } lintOptions { abortOnError false @@ -43,4 +44,4 @@ dependencies { implementation "com.facebook.react:react-native:+" implementation platform("com.adobe.marketing.mobile:sdk-bom:3.+") api "com.adobe.marketing.mobile:optimize" -} \ No newline at end of file +} diff --git a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/NativeAEPOptimizeModule.java b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/NativeAEPOptimizeModule.java new file mode 100644 index 000000000..bcee31cb1 --- /dev/null +++ b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/NativeAEPOptimizeModule.java @@ -0,0 +1,182 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * Turbo Native Module implementation per https://reactnative.dev/docs/turbo-native-modules-introduction + * Extends the spec base (NativeAEPOptimizeSpec); does not extend or reference RCTAEPOptimizeModule. + */ + +package com.adobe.marketing.mobile.reactnative.optimize; + +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.adobe.marketing.mobile.AdobeCallback; +import com.adobe.marketing.mobile.AdobeCallbackWithError; +import com.adobe.marketing.mobile.AdobeError; +import com.adobe.marketing.mobile.optimize.AdobeCallbackWithOptimizeError; +import com.adobe.marketing.mobile.optimize.AEPOptimizeError; +import com.adobe.marketing.mobile.optimize.DecisionScope; +import com.adobe.marketing.mobile.optimize.Optimize; +import com.adobe.marketing.mobile.optimize.OptimizeProposition; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Turbo Native Module implementation. Extends the Codegen spec base; + * used when USE_INTEROP_ROOT is false. + */ +public class NativeAEPOptimizeModule extends NativeAEPOptimizeSpec { + + public static final String NAME = "NativeAEPOptimize"; + private static final String TAG = "NativeAEPOptimizeModule"; + + private final Map propositionCache = new ConcurrentHashMap<>(); + + public NativeAEPOptimizeModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public void invalidate() { + propositionCache.clear(); + } + + @Override + public void extensionVersion(Promise promise) { + promise.resolve(Optimize.extensionVersion()); + } + + @Override + public void clearCachedPropositions() { + propositionCache.clear(); + Optimize.clearCachedPropositions(); + } + + @Override + public void getPropositions(ReadableArray decisionScopesArray, Promise promise) { + List decisionScopeList = RCTAEPOptimizeUtil.createDecisionScopes(decisionScopesArray); + Optimize.getPropositions(decisionScopeList, new AdobeCallbackWithError>() { + @Override + public void fail(AdobeError adobeError) { + promise.reject(String.valueOf(adobeError.getErrorCode()), adobeError.getErrorName()); + } + + @Override + public void call(Map decisionScopePropositionMap) { + RCTAEPOptimizeUtil.cachePropositionOffers(decisionScopePropositionMap, propositionCache); + promise.resolve(RCTAEPOptimizeUtil.createCallbackResponse(decisionScopePropositionMap)); + } + }); + } + + @Override + public void updatePropositions( + ReadableArray decisionScopesArray, + @Nullable ReadableMap xdm, + @Nullable ReadableMap data, + @Nullable Callback successCallback, + @Nullable Callback errorCallback) { + Log.d(TAG, "updatePropositions called"); + List decisionScopeList = RCTAEPOptimizeUtil.createDecisionScopes(decisionScopesArray); + Map mapXdm = xdm != null ? RCTAEPOptimizeUtil.convertReadableMapToMap(xdm) : Collections.emptyMap(); + Map mapData = data != null ? RCTAEPOptimizeUtil.convertReadableMapToMap(data) : Collections.emptyMap(); + + Optimize.updatePropositions(decisionScopeList, mapXdm, mapData, new AdobeCallbackWithOptimizeError>() { + @Override + public void fail(AEPOptimizeError adobeError) { + Log.e(TAG, "updatePropositions callback failed"); + if (errorCallback != null) { + WritableMap response = RCTAEPOptimizeUtil.convertAEPOptimizeErrorToWritableMap(adobeError); + errorCallback.invoke(response); + } + } + + @Override + public void call(Map decisionScopePropositionMap) { + RCTAEPOptimizeUtil.cachePropositionOffers(decisionScopePropositionMap, propositionCache); + Log.d(TAG, "updatePropositions callback success."); + if (successCallback != null) { + successCallback.invoke(RCTAEPOptimizeUtil.createCallbackResponse(decisionScopePropositionMap)); + } + } + }); + } + + @Override + public void onPropositionsUpdate() { + Optimize.onPropositionsUpdate(new AdobeCallback>() { + @Override + public void call(Map decisionScopePropositionMap) { + RCTAEPOptimizeUtil.cachePropositionOffers(decisionScopePropositionMap, propositionCache); + // Wrap in { propositions: ... } to match PropositionsPayload spec type. + // Delivered via JSI EventEmitterCallback (CodegenTypes.EventEmitter), not RCTDeviceEventEmitter. + WritableMap payload = Arguments.createMap(); + payload.putMap("propositions", RCTAEPOptimizeUtil.createCallbackResponse(decisionScopePropositionMap)); + emitOnPropositionsUpdated(payload); + } + }); + } + + @Override + public void multipleOffersDisplayed(ReadableArray offersArray) { + RCTAEPOptimizeUtil.multipleOffersDisplayed(offersArray, propositionCache); + } + + @Override + public void multipleOffersGenerateDisplayInteractionXdm(ReadableArray offersArray, Promise promise) { + RCTAEPOptimizeUtil.multipleOffersGenerateDisplayInteractionXdm(offersArray, propositionCache, promise); + } + + @Override + public void offerDisplayed(String offerId, ReadableMap propositionMap) { + RCTAEPOptimizeUtil.offerDisplayed(offerId, propositionMap); + } + + @Override + public void offerTapped(String offerId, ReadableMap propositionMap) { + RCTAEPOptimizeUtil.offerTapped(offerId, propositionMap); + } + + @Override + public void generateDisplayInteractionXdm(String offerId, ReadableMap propositionMap, Promise promise) { + RCTAEPOptimizeUtil.generateDisplayInteractionXdm(offerId, propositionMap, promise); + } + + @Override + public void generateTapInteractionXdm(String offerId, ReadableMap propositionMap, Promise promise) { + RCTAEPOptimizeUtil.generateTapInteractionXdm(offerId, propositionMap, promise); + } + + @Override + public void generateReferenceXdm(ReadableMap propositionMap, Promise promise) { + RCTAEPOptimizeUtil.generateReferenceXdm(propositionMap, promise); + } + + // addListener/removeListeners kept for NativeEventEmitter compatibility (interop path). + @Override + public void addListener(String eventName) { + } + + @Override + public void removeListeners(double count) { + } +} diff --git a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeModule.java b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeModule.java index d69b1a231..29f1d867b 100644 --- a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeModule.java +++ b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeModule.java @@ -59,7 +59,7 @@ public RCTAEPOptimizeModule(ReactApplicationContext reactContext) { @Override public String getName() { - return "AEPOptimize"; + return "NativeAEPOptimize"; } @ReactMethod diff --git a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizePackage.java b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizePackage.java index 2e032814a..cfe131c5b 100644 --- a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizePackage.java +++ b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizePackage.java @@ -10,29 +10,56 @@ */ package com.adobe.marketing.mobile.reactnative.optimize; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.HashMap; +import java.util.Map; -import com.facebook.react.ReactPackage; +import com.facebook.react.BaseReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.uimanager.ViewManager; -import com.facebook.react.bridge.JavaScriptModule; -public class RCTAEPOptimizePackage implements ReactPackage { +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; - @Override - public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new RCTAEPOptimizeModule(reactContext)); - } +/** + * Registers the Optimize native module per React Native Turbo Module doc. + * Build-time switch: only one root is registered, but both paths register under + * the same JS-visible name "NativeAEPOptimize" so the codegen JS spec + * (`TurboModuleRegistry.getEnforcing('NativeAEPOptimize')`) resolves on either path. + * Matches iOS, where both paths expose moduleName "NativeAEPOptimize". + * + * USE_INTEROP_ROOT true -> RCTAEPOptimizeModule (classic bridge); isTurboModule = false. + * USE_INTEROP_ROOT false -> NativeAEPOptimizeModule (Turbo); isTurboModule = true. + */ +public class RCTAEPOptimizePackage extends BaseReactPackage { - // Deprecated from RN 0.47 - public List> createJSModules() { - return Collections.emptyList(); + private static final String MODULE_NAME = "NativeAEPOptimize"; + + @Override + public NativeModule getModule(String name, ReactApplicationContext reactContext) { + if (!MODULE_NAME.equals(name)) { + return null; + } + return BuildConfig.USE_INTEROP_ROOT + ? new RCTAEPOptimizeModule(reactContext) + : new NativeAEPOptimizeModule(reactContext); } @Override - public List createViewManagers(ReactApplicationContext reactContext) { - return Collections.emptyList(); + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return new ReactModuleInfoProvider() { + @Override + public Map getReactModuleInfos() { + Map map = new HashMap<>(); + boolean isTurboModule = !BuildConfig.USE_INTEROP_ROOT; + map.put(MODULE_NAME, new ReactModuleInfo( + MODULE_NAME, + MODULE_NAME, + false, + false, + false, + isTurboModule + )); + return map; + } + }; } } diff --git a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeUtil.java b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeUtil.java index 2ba1b3896..4bb67d6ed 100644 --- a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeUtil.java +++ b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeUtil.java @@ -9,14 +9,18 @@ governing permissions and limitations under the License. */ - package com.adobe.marketing.mobile.reactnative.optimize; +package com.adobe.marketing.mobile.reactnative.optimize; + import android.util.Log; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.optimize.DecisionScope; import com.adobe.marketing.mobile.optimize.Offer; +import com.adobe.marketing.mobile.optimize.OfferUtils; import com.adobe.marketing.mobile.optimize.OptimizeProposition; import com.adobe.marketing.mobile.optimize.AEPOptimizeError; +import com.adobe.marketing.mobile.util.DataReader; +import com.adobe.marketing.mobile.util.DataReaderException; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; @@ -25,16 +29,23 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; + /** - * Utility class for converting data models to {@link com.facebook.react.bridge.WritableMap} + * Shared utility class used by both {@link RCTAEPOptimizeModule} (Interop) and + * {@link NativeAEPOptimizeModule} (Turbo). */ class RCTAEPOptimizeUtil { private static final String TAG = "RCTAEPOptimize"; private RCTAEPOptimizeUtil() {} + + // ---- Proposition / Offer conversion ---- + static WritableMap convertPropositionToWritableMap(final OptimizeProposition proposition) { final WritableMap propositionWritableMap = new WritableNativeMap(); if (proposition == null) { @@ -56,6 +67,7 @@ static WritableMap convertPropositionToWritableMap(final OptimizeProposition pro } return propositionWritableMap; } + static WritableMap convertOfferToWritableMap(final Offer offer) { final WritableMap offerWritableMap = new WritableNativeMap(); if (offer == null) { @@ -83,6 +95,9 @@ static WritableMap convertOfferToWritableMap(final Offer offer) { offerWritableMap.putMap("data", dataWritableMap); return offerWritableMap; } + + // ---- Generic bridge conversions ---- + static WritableArray convertListToWritableArray(final List objectList) { final WritableArray writableArray = new WritableNativeArray(); for (final Object object : objectList) { @@ -102,6 +117,7 @@ static WritableArray convertListToWritableArray(final List objectList) { } return writableArray; } + static WritableMap convertMapToWritableMap(final Map map) { final WritableMap writableMap = new WritableNativeMap(); for (final Map.Entry entry : map.entrySet()) { @@ -122,6 +138,7 @@ static WritableMap convertMapToWritableMap(final Map map) { } return writableMap; } + static List createDecisionScopes(final ReadableArray decisionScopesArray) { final List decisionScopeList = new ArrayList<>(decisionScopesArray.size()); for (int i = 0; i < decisionScopesArray.size(); i++) { @@ -132,12 +149,7 @@ static List createDecisionScopes(final ReadableArray decisionScop } return decisionScopeList; } - /** - * Converts {@link ReadableMap} Map to {@link Map} - * - * @param readableMap instance of {@code ReadableMap} - * @return instance of {@code Map} - */ + static Map convertReadableMapToMap(final ReadableMap readableMap) { ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); Map map = new HashMap<>(); @@ -166,11 +178,12 @@ static Map convertReadableMapToMap(final ReadableMap readableMap } return map; } + private static List convertReadableArrayToList(final ReadableArray readableArray) { final List list = new ArrayList<>(readableArray.size()); for (int i = 0; i < readableArray.size(); i++) { ReadableType indexType = readableArray.getType(i); - switch(indexType) { + switch (indexType) { case Boolean: list.add(i, readableArray.getBoolean(i)); break; @@ -195,33 +208,27 @@ private static List convertReadableArrayToList(final ReadableArray reada static List getNativeOffers(final ReadableArray offersArray, Map propositionCache) { List nativeOffers = new ArrayList<>(); - if (offersArray == null || offersArray.size() == 0) { Log.d(TAG, "getNativeOffers: offersArray is null or empty"); return nativeOffers; } - for (int i = 0; i < offersArray.size(); i++) { ReadableMap offer = offersArray.getMap(i); if (offer == null) { Log.d(TAG, "getNativeOffers: offer is null for index: " + i); continue; } - String uniquePropositionId = offer.getString(RCTAEPOptimizeConstants.UNIQUE_PROPOSITION_ID_KEY); String offerId = offer.getString("id"); - if (uniquePropositionId == null || offerId == null) { Log.d(TAG, "getNativeOffers: uniquePropositionId or offerId is null for offer: " + offer.toString()); continue; } - OptimizeProposition proposition = propositionCache.get(uniquePropositionId); if (proposition == null) { Log.d(TAG, "getNativeOffers: proposition not found in cache for uniquePropositionId: " + uniquePropositionId); continue; } - for (Offer propositionOffer : proposition.getOffers()) { if (propositionOffer.getId().equalsIgnoreCase(offerId)) { nativeOffers.add(propositionOffer); @@ -229,30 +236,19 @@ static List getNativeOffers(final ReadableArray offersArray, Map propositionsMap) { final WritableMap propositionsWritableMap = new WritableNativeMap(); - if (propositionsMap != null && !propositionsMap.isEmpty()) { for (final Map.Entry entry : propositionsMap.entrySet()) { propositionsWritableMap.putMap(entry.getKey().getName(), RCTAEPOptimizeUtil.convertPropositionToWritableMap(entry.getValue())); } } - return propositionsWritableMap; } - /** - * Converts an AEPOptimizeError to a WritableMap for React Native error callback - */ static WritableMap convertAEPOptimizeErrorToWritableMap(final AEPOptimizeError error) { final WritableMap errorMap = new WritableNativeMap(); if (error == null) { @@ -272,4 +268,118 @@ static WritableMap convertAEPOptimizeErrorToWritableMap(final AEPOptimizeError e } return errorMap; } -} \ No newline at end of file + + // ---- Shared helpers used by both Module variants ---- + + static void generateDisplayInteractionXdm(String offerId, ReadableMap propositionMap, Promise promise) { + Map eventData = convertReadableMapToMap(propositionMap); + OptimizeProposition proposition = OptimizeProposition.fromEventData(eventData); + Offer offer = findOfferById(proposition, offerId); + if (offer != null) { + promise.resolve(convertMapToWritableMap(offer.generateDisplayInteractionXdm())); + } else { + promise.reject("generateDisplayInteractionXdm", "Error in generating Display interaction XDM for offer with id: " + offerId); + } + } + + static void generateTapInteractionXdm(String offerId, ReadableMap propositionMap, Promise promise) { + Map eventData = convertReadableMapToMap(propositionMap); + OptimizeProposition proposition = OptimizeProposition.fromEventData(eventData); + Offer offer = findOfferById(proposition, offerId); + if (offer != null) { + promise.resolve(convertMapToWritableMap(offer.generateTapInteractionXdm())); + } else { + promise.reject("generateTapInteractionXdm", "Error in generating Tap interaction XDM for offer with id: " + offerId); + } + } + + static void generateReferenceXdm(ReadableMap propositionMap, Promise promise) { + Map propositionEventData = convertReadableMapToMap(propositionMap); + OptimizeProposition proposition = OptimizeProposition.fromEventData(propositionEventData); + if (proposition != null) { + promise.resolve(convertMapToWritableMap(proposition.generateReferenceXdm())); + } else { + promise.reject("generateReferenceXdm", "Error in generating Reference XDM."); + } + } + + private static Offer findOfferById(OptimizeProposition proposition, String offerId) { + if (proposition == null) return null; + for (Offer offer : proposition.getOffers()) { + if (offer.getId().equalsIgnoreCase(offerId)) return offer; + } + return null; + } + + static void offerDisplayed(String offerId, ReadableMap propositionMap) { + Offer offer = findOfferById(OptimizeProposition.fromEventData(convertReadableMapToMap(propositionMap)), offerId); + if (offer != null) offer.displayed(); + } + + static void offerTapped(String offerId, ReadableMap propositionMap) { + Offer offer = findOfferById(OptimizeProposition.fromEventData(convertReadableMapToMap(propositionMap)), offerId); + if (offer != null) offer.tapped(); + } + + static void multipleOffersDisplayed(ReadableArray offersArray, Map propositionCache) { + List nativeOffers = getNativeOffers(offersArray, propositionCache); + if (!nativeOffers.isEmpty()) { + OfferUtils.displayed(nativeOffers); + } + } + + static void multipleOffersGenerateDisplayInteractionXdm( + ReadableArray offersArray, + Map propositionCache, + Promise promise) { + List nativeOffers = getNativeOffers(offersArray, propositionCache); + if (!nativeOffers.isEmpty()) { + promise.resolve(convertMapToWritableMap(OfferUtils.generateDisplayInteractionXdm(nativeOffers))); + } else { + promise.reject("multipleOffersGenerateDisplayInteractionXdm", + "Error in generating Display interaction XDM for multiple offers: " + offersArray.toString()); + } + } + + static void cachePropositionOffers( + final Map decisionScopePropositionMap, + final Map cache) { + for (final Map.Entry entry : decisionScopePropositionMap.entrySet()) { + OptimizeProposition proposition = entry.getValue(); + if (proposition == null) continue; + + String activityId = null; + try { + Map activity = proposition.getActivity(); + if (activity != null && activity.containsKey("id")) { + activityId = DataReader.getString(activity, "id"); + } else { + Map scopeDetails = proposition.getScopeDetails(); + if (scopeDetails != null && scopeDetails.containsKey("activity")) { + Map scopeDetailsActivity = DataReader.getTypedMap(Object.class, scopeDetails, "activity"); + if (scopeDetailsActivity != null && scopeDetailsActivity.containsKey("id")) { + activityId = DataReader.getString(scopeDetailsActivity, "id"); + } + } + } + } catch (DataReaderException e) { + Log.w(TAG, "Failed to extract activity ID from proposition: " + e.getMessage()); + continue; + } + if (activityId != null) { + cache.put(activityId, proposition); + } + } + } + + static void emitOnPropositionsUpdate( + final ReactApplicationContext context, + final Map decisionScopePropositionMap, + final boolean checkActiveInstance) { + WritableMap writableMap = createCallbackResponse(decisionScopePropositionMap); + if (!checkActiveInstance || context.hasActiveReactInstance()) { + context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("onPropositionsUpdate", writableMap); + } + } +} diff --git a/packages/optimize/ios/src/RCTAEPOptimize.h b/packages/optimize/ios/src/RCTAEPOptimize.h index 377992c58..da2a9201c 100644 --- a/packages/optimize/ios/src/RCTAEPOptimize.h +++ b/packages/optimize/ios/src/RCTAEPOptimize.h @@ -9,11 +9,19 @@ governing permissions and limitations under the License. */ -#import #import -#import +#import -@interface RCTAEPOptimize : RCTEventEmitter +// NativeAEPOptimizeSpecBase (codegen-generated) provides emitOnPropositionsUpdated: +// for JSI-native event emission. Used on BOTH turbo and interop paths because: +// +// 1. getTurboModule: is required on both paths (RCTModuleProviders.mm checks it) +// 2. getTurboModule: → RCTTurboModuleManager creates module with callableJSModules:nil +// 3. callableJSModules:nil → sendEventWithName: silently drops events +// 4. Therefore RCTEventEmitter's sendEventWithName: is dead for any turbo-registered module +// 5. emitOnPropositionsUpdated: bypasses callableJSModules — uses JSI EventEmitterCallback +// +// See: https://reactnative.dev/docs/the-new-architecture/native-modules-custom-events +@interface RCTAEPOptimize : NativeAEPOptimizeSpecBase @end - diff --git a/packages/optimize/ios/src/RCTAEPOptimize.m b/packages/optimize/ios/src/RCTAEPOptimize.m deleted file mode 100644 index 518f7908a..000000000 --- a/packages/optimize/ios/src/RCTAEPOptimize.m +++ /dev/null @@ -1,523 +0,0 @@ -/* - Copyright 2022 Adobe. All rights reserved. - This file is licensed to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance with the License. - You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or - agreed to in writing, software distributed under the License is distributed on - an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either - express or implied. See the License for the specific language governing - permissions and limitations under the License. - */ - -#import "RCTAEPOptimize.h" -#import -@import AEPOptimize; -@import AEPServices; -@import Foundation; - -static NSString *const TAG = @"RCTAEPOptimize"; - -@implementation RCTAEPOptimize { - bool hasListeners; - NSMutableDictionary *propositionCache; -} - -- (instancetype)init { - self = [super init]; - hasListeners = false; - propositionCache = [[NSMutableDictionary alloc] init]; - return self; -} - -+ (BOOL)requiresMainQueueSetup { - return NO; -} - -RCT_EXPORT_MODULE(AEPOptimize); - -- (dispatch_queue_t)methodQueue { - return dispatch_get_main_queue(); -} - -RCT_EXPORT_METHOD(extensionVersion - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) { - [AEPLog traceWithLabel:TAG message:@"extensionVersion is called."]; - resolve([AEPMobileOptimize extensionVersion]); -} - -RCT_EXPORT_METHOD(clearCachedPropositions) { - [AEPLog traceWithLabel:TAG message:@"clearCachedPropositions is called."]; - // clear the react native cache - [self clearPropositionsCache]; - // clear the native cache - [AEPMobileOptimize clearCachedPropositions]; -} - -// Helper method to handle proposition dictionary creation -- (NSDictionary *> *)createPropositionDictionary:(NSDictionary *)decisionScopePropositionDict { - NSMutableDictionary *> *propositionDictionary = [[NSMutableDictionary alloc] initWithCapacity:decisionScopePropositionDict.count]; - - for (AEPDecisionScope *key in decisionScopePropositionDict) { - AEPOptimizeProposition *proposition = decisionScopePropositionDict[key]; - if (proposition) { - [propositionDictionary setValue:[self convertPropositionToDict:proposition] forKey:key.name]; - } - } - return propositionDictionary; -} - -// Unified method that handles both callback and non-callback cases -RCT_EXPORT_METHOD(updatePropositions:(NSArray *)decisionScopesArray - withXdm:(NSDictionary *)xdm - andData:(NSDictionary *)data - successCallback:(RCTResponseSenderBlock)successCallback - errorCallback:(RCTResponseSenderBlock)errorCallback) { - [AEPLog traceWithLabel:TAG message:@"updatePropositions is called."]; - NSArray *scopes = [self createDecisionScopesArray:decisionScopesArray]; - [AEPMobileOptimize updatePropositions:scopes - withXdm:xdm - andData:data - completion:^(NSDictionary *decisionScopePropositionDict, NSError *error) { - if (error) { - NSDictionary *errorDict = [self convertNSErrorToOptimizeErrorDict:error]; - if (errorCallback != nil) { - errorCallback(@[errorDict]); - } - } - if (decisionScopePropositionDict) { - [self cachePropositions:decisionScopePropositionDict]; - NSDictionary *propositions = [self createCallbackResponse:decisionScopePropositionDict]; - if (successCallback != nil) { - successCallback(@[propositions]); - } - } - }]; -} - - -RCT_EXPORT_METHOD(getPropositions - : (NSArray *)decisionScopes resolver - : (RCTPromiseResolveBlock)resolve rejector - : (RCTPromiseRejectBlock)reject) { - - [AEPLog traceWithLabel:TAG message:@"getPropositions is called."]; - NSArray *decisionScopesArray = - [self createDecisionScopesArray:decisionScopes]; - [AEPMobileOptimize - getPropositions:decisionScopesArray - completion:^(NSDictionary - *decisionScopePropositionDict, - NSError *error) { - if (error) { - reject([NSString stringWithFormat:@"%ld", (long)error.code], - error.description, nil); - } else { - [self cachePropositions:decisionScopePropositionDict]; - - NSDictionary *> - *propositionDictionary = [[NSMutableDictionary alloc] init]; - - for (AEPDecisionScope *key in decisionScopePropositionDict) { - AEPOptimizeProposition *proposition = - decisionScopePropositionDict[key]; - [propositionDictionary - setValue:[self convertPropositionToDict:proposition] - forKey:key.name]; - } - resolve(propositionDictionary); - } - }]; -} - -RCT_EXPORT_METHOD(onPropositionsUpdate) { - [AEPLog traceWithLabel:TAG message:@"onPropositionsUpdate is called."]; - [AEPMobileOptimize onPropositionsUpdate:^( - NSDictionary - *decisionScopePropositionDict) { - - [self cachePropositions:decisionScopePropositionDict]; - NSDictionary *> - *propositionDictionary = [[NSMutableDictionary alloc] init]; - - for (AEPDecisionScope *key in decisionScopePropositionDict) { - AEPOptimizeProposition *proposition = decisionScopePropositionDict[key]; - [propositionDictionary - setValue:[self convertPropositionToDict:proposition] - forKey:key.name]; - } - - if (self->hasListeners) { - [self sendEventWithName:@"onPropositionsUpdate" body:propositionDictionary]; - } - }]; -} - -RCT_EXPORT_METHOD(offerTapped - : (NSString *)offerId propositionDictionary - : (NSDictionary *)dictionary) { - [AEPLog debugWithLabel:TAG message:@"Offer Tapped"]; - AEPOptimizeProposition *proposition = [AEPOptimizeProposition initFromData:dictionary]; - NSArray *offers = [proposition offers]; - for (AEPOffer *offer in offers) { - if ([[offer id] isEqualToString:offerId]) { - [offer tapped]; - break; - } - } -} - -RCT_EXPORT_METHOD(offerDisplayed - : (NSString *)offerId propositionDictionary - : (NSDictionary *)dictionary) { - [AEPLog debugWithLabel:TAG message:@"Offer Displayed"]; - AEPOptimizeProposition *proposition = [AEPOptimizeProposition initFromData:dictionary]; - NSArray *offers = [proposition offers]; - for (AEPOffer *offer in offers) { - if ([[offer id] isEqualToString:offerId]) { - [offer displayed]; - break; - } - } -} - -RCT_EXPORT_METHOD(generateReferenceXdm - : (NSDictionary *)dictionary resolver - : (RCTPromiseResolveBlock)resolve rejector - : (RCTPromiseRejectBlock)reject) { - [AEPLog debugWithLabel:TAG message:@"Proposition generateReferenceXdm"]; - AEPOptimizeProposition *proposition = [AEPOptimizeProposition initFromData:dictionary]; - NSDictionary *referenceXDM = - [proposition generateReferenceXdm]; - resolve(referenceXDM); -} - -RCT_EXPORT_METHOD(generateTapInteractionXdm - : (NSString *)offerId propositionDictionary - : (NSDictionary *)dictionary resolver - : (RCTPromiseResolveBlock)resolve rejector - : (RCTPromiseRejectBlock)reject) { - [AEPLog debugWithLabel:TAG message:@"generateTapInteractionXdm"]; - AEPOptimizeProposition *proposition = [AEPOptimizeProposition initFromData:dictionary]; - NSArray *offers = [proposition offers]; - AEPOffer *offerInteracted = nil; - for (AEPOffer *offer in offers) { - if ([[offer id] isEqualToString:offerId]) { - offerInteracted = offer; - break; - } - } - - if (offerInteracted != nil) { - NSDictionary *tapInteractionXdm = - [offerInteracted generateTapInteractionXdm]; - resolve(tapInteractionXdm); - } else { - reject(@"generateTapInteractionXdm", - [NSString stringWithFormat:@"Error in generating Tap interaction " - @"XDM for offer with id: %@", - offerId], - nil); - } -} - -RCT_EXPORT_METHOD(generateDisplayInteractionXdm - : (NSString *)offerId propositionDictionary - : (NSDictionary *)dictionary resolver - : (RCTPromiseResolveBlock)resolve rejector - : (RCTPromiseRejectBlock)reject) { - [AEPLog debugWithLabel:TAG message:@"generateDisplayInteractionXdm"]; - AEPOptimizeProposition *proposition = [AEPOptimizeProposition initFromData:dictionary]; - NSArray *offers = [proposition offers]; - AEPOffer *offerDisplayed = nil; - for (AEPOffer *offer in offers) { - if ([[offer id] isEqualToString:offerId]) { - offerDisplayed = offer; - break; - } - } - - if (offerDisplayed != nil) { - NSDictionary *displayInteractionXdm = - [offerDisplayed generateDisplayInteractionXdm]; - resolve(displayInteractionXdm); - } else { - reject(@"generateDisplayInteractionXdm", - [NSString stringWithFormat:@"Error in generating Display " - @"interaction XDM for offer with id: %@", - offerId], - nil); - } -} - -RCT_EXPORT_METHOD(multipleOffersDisplayed - : (NSArray *> *)offersArray) { - - [AEPLog debugWithLabel:TAG message:@"multipleOffersDisplayed is called."]; - - NSMutableArray *nativeOffers = [self getNativeOffersFromOffersArray:offersArray]; - - if ([nativeOffers count] > 0) { - [AEPLog debugWithLabel:TAG message:[NSString stringWithFormat:@"multipleOffersDisplayed: calling display for: %lu offers", (unsigned long)[nativeOffers count]]]; - [AEPMobileOptimize displayed:nativeOffers]; - } -} - -RCT_EXPORT_METHOD(multipleOffersGenerateDisplayInteractionXdm - : (NSArray *> *)offersArray resolver - : (RCTPromiseResolveBlock)resolve rejector - : (RCTPromiseRejectBlock)reject) { - - [AEPLog debugWithLabel:TAG message:@"multipleOffersGenerateDisplayInteractionXdm is called."]; - - NSMutableArray *nativeOffers = [self getNativeOffersFromOffersArray:offersArray]; - - if ([nativeOffers count] > 0) { - [AEPLog debugWithLabel:TAG message:[NSString stringWithFormat:@"multipleOffersGenerateDisplayInteractionXdm: calling display for: %lu offers", (unsigned long)[nativeOffers count]]]; - NSDictionary *displayInteractionXdm = [AEPMobileOptimize generateDisplayInteractionXdm:nativeOffers]; - - resolve(displayInteractionXdm); - } else { - reject(@"generateDisplayInteractionXdmForMultipleOffers", @"Error in generating Display interaction XDM for multiple offers.", nil); - } -} - -#pragma mark - Helper methods - -- (NSMutableArray *)getNativeOffersFromOffersArray:(NSArray *> *)offersArray { - NSMutableArray *nativeOffers = [[NSMutableArray alloc] init]; - - if (!offersArray || [offersArray count] == 0) { - [AEPLog debugWithLabel:TAG message:@"getNativeOffersFromOffersArray: offersArray is null or empty"]; - return nativeOffers; - } - - for (NSDictionary *offerDict in offersArray) { - if (!offerDict) { - [AEPLog debugWithLabel:TAG message:@"getNativeOffersFromOffersArray: offer is null"]; - continue; - } - - NSString *uniquePropositionId = [offerDict objectForKey:@"uniquePropositionId"]; - NSString *offerId = [offerDict objectForKey:@"id"]; - - if (!uniquePropositionId || !offerId) { - [AEPLog debugWithLabel:TAG message:[NSString stringWithFormat:@"getNativeOffersFromOffersArray: uniquePropositionId or offerId is null for offer: %@", offerDict]]; - continue; - } - - AEPOptimizeProposition *proposition = [propositionCache objectForKey:uniquePropositionId]; - if (!proposition) { - [AEPLog debugWithLabel:TAG message:[NSString stringWithFormat:@"getNativeOffersFromOffersArray: proposition not found in cache for uniquePropositionId: %@", uniquePropositionId]]; - continue; - } - - NSArray *offers = [proposition offers]; - for (AEPOffer *propositionOffer in offers) { - if ([[propositionOffer id] isEqualToString:offerId]) { - [nativeOffers addObject:propositionOffer]; - break; - } - } - } - - return nativeOffers; -} - -#pragma mark - Cache Management - -- (void)cachePropositions:(NSDictionary *)decisionScopePropositionDict { - for (AEPDecisionScope *key in decisionScopePropositionDict) { - AEPOptimizeProposition *proposition = decisionScopePropositionDict[key]; - if (!proposition) { - [AEPLog debugWithLabel:TAG message:[NSString stringWithFormat:@"cachePropositions: proposition is null for decisionScope: %@", key]]; - continue; - } - - NSString *activityId = nil; - - NSDictionary *propositionDict = [self convertPropositionToDict:proposition]; - NSDictionary *activity = [propositionDict valueForKey:@"activity"]; - if ([propositionDict objectForKey:@"activity"]) { - if (activity && [activity objectForKey:@"id"]) { - activityId = [activity objectForKey:@"id"]; - } - } else { - NSDictionary *scopeDetails = [propositionDict valueForKey:@"scopeDetails"]; - if (scopeDetails && [scopeDetails objectForKey:@"activity"]) { - NSDictionary *scopeDetailsActivity = [scopeDetails objectForKey:@"activity"]; - if (scopeDetailsActivity && [scopeDetailsActivity objectForKey:@"id"]) { - activityId = [scopeDetailsActivity objectForKey:@"id"]; - } - } - } - - if (activityId) { - [propositionCache setObject:proposition forKey:activityId]; - } - } -} - -- (void)clearPropositionsCache { - [propositionCache removeAllObjects]; -} - -#pragma mark - Helper methods - -- (NSArray *)createDecisionScopesArray: - (NSArray *)decisionScopes { - NSMutableArray *decisionScopesArray = - [[NSMutableArray alloc] init]; - for (NSString *decisionScopeName in decisionScopes) { - [decisionScopesArray - addObject:[[AEPDecisionScope alloc] initWithName:decisionScopeName]]; - } - return decisionScopesArray; -} - -- (NSDictionary *> *) - convertPropositionToDict:(AEPOptimizeProposition *)proposition { - NSDictionary *propositionDict = - [[NSMutableDictionary alloc] init]; - if (!proposition) { - return propositionDict; - } - - [propositionDict setValue:proposition.id forKey:@"id"]; - [propositionDict setValue:proposition.scope forKey:@"scope"]; - [propositionDict setValue:[proposition scopeDetails] forKey:@"scopeDetails"]; - - NSMutableArray *> *offersArray = - [[NSMutableArray alloc] init]; - for (AEPOffer *offer in proposition.offers) { - [offersArray addObject:[self convertOfferToDict:offer]]; - } - [propositionDict setValue:offersArray forKey:@"items"]; - - if ([proposition activity]) { - [propositionDict setValue:[proposition activity] forKey:@"activity"]; - } - if ([proposition placement]) { - [propositionDict setValue:[proposition placement] forKey:@"placement"]; - } - return propositionDict; -} - -- (NSDictionary *)convertOfferToDict:(AEPOffer *)offer { - NSMutableDictionary *offerDict = - [[NSMutableDictionary alloc] init]; - if (!offer) { - return offerDict; - } - - [offerDict setValue:offer.id forKey:@"id"]; - if ([offer etag] != nil) { - [offerDict setValue:[offer etag] forKey:@"etag"]; - } - if ([offer meta] != nil) { - [offerDict setValue:[offer meta] forKey:@"meta"]; - } - [offerDict setValue:[offer schema] forKey:@"schema"]; - [offerDict setValue:@([offer score]) forKey:@"score"]; - - NSDictionary *data = [[NSMutableDictionary alloc] init]; - [data setValue:[offer id] forKey:@"id"]; - [data setValue:[self convertOfferTypeToString:[offer type]] forKey:@"format"]; - [data setValue:[offer content] forKey:@"content"]; - if ([offer language] != nil) { - [data setValue:[offer language] forKey:@"language"]; - } - if ([offer characteristics] != nil) { - [data setValue:[offer characteristics] forKey:@"characteristics"]; - } - - [offerDict setValue:data forKey:@"data"]; - return offerDict; -} - -- (NSString *)convertOfferTypeToString:(AEPOfferType)offerType { - switch (offerType) { - case AEPOfferTypeHtml: - return @"text/html"; - case AEPOfferTypeJson: - return @"application/json"; - case AEPOfferTypeText: - return @"text/plain"; - case AEPOfferTypeImage: - return @"image/*"; - default: - return @""; - } -} - -// Helper to convert NSError to a structured error dictionary for JS -- (NSDictionary *)convertNSErrorToOptimizeErrorDict:(NSError *)error { - if (!error) return @{}; - NSMutableDictionary *errorDict = [NSMutableDictionary dictionary]; - - // Log for debugging - NSLog(@"[AEPOptimize] NSError.domain: %@", error.domain); - NSLog(@"[AEPOptimize] NSError.code: %ld", (long)error.code); - NSLog(@"[AEPOptimize] NSError.userInfo: %@", error.userInfo); - NSLog(@"[AEPOptimize] NSError.localizedDescription: %@", error.localizedDescription); - - // Extract AEPOptimizeError properties from userInfo (matches Android structure) - NSDictionary *userInfo = error.userInfo; - - errorDict[@"type"] = userInfo[@"type"] ?: @""; - errorDict[@"status"] = userInfo[@"status"] ?: @(error.code); - errorDict[@"title"] = userInfo[@"title"] ?: @""; - errorDict[@"detail"] = userInfo[@"detail"] ?: @""; - errorDict[@"report"] = userInfo[@"report"] ?: @{}; - - // Handle aepError - check for both nil and NSNull - id aepErrorValue = userInfo[@"aepError"]; - if (aepErrorValue && aepErrorValue != [NSNull null]) { - errorDict[@"aepError"] = aepErrorValue; - } else { - errorDict[@"aepError"] = @"general.unexpected"; - } - - return errorDict; -} - -// Helper method to create standardized response for callbacks -- (NSDictionary *)createCallbackResponse:(NSDictionary *)decisionScopePropositionDict { - - if (decisionScopePropositionDict && [decisionScopePropositionDict count] > 0) { - // Return the propositions map directly - return [self createPropositionDictionary:decisionScopePropositionDict]; - } - - return @{}; -} - -- (void)handleError:(NSError *)error rejecter:(RCTPromiseRejectBlock)reject { - if (!error || !reject) { - return; - } - - NSDictionary *userInfo = [error userInfo]; - NSString *errorString = - [[userInfo objectForKey:NSUnderlyingErrorKey] localizedDescription]; - - reject([NSString stringWithFormat:@"%lu", (long)error.code], errorString, - error); -} - -#pragma mark - RCTEventEmitter functions - -- (NSArray *)supportedEvents { - return @[ @"onPropositionsUpdate" ]; -} - -- (void)startObserving { - hasListeners = true; -} - -- (void)stopObserving { - hasListeners = false; -} - -@end diff --git a/packages/optimize/ios/src/RCTAEPOptimize.mm b/packages/optimize/ios/src/RCTAEPOptimize.mm new file mode 100644 index 000000000..676875b1b --- /dev/null +++ b/packages/optimize/ios/src/RCTAEPOptimize.mm @@ -0,0 +1,396 @@ +/* + Copyright 2022 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or + agreed to in writing, software distributed under the License is distributed on + an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either + express or implied. See the License for the specific language governing + permissions and limitations under the License. + */ + +#import "RCTAEPOptimize.h" +@import AEPOptimize; +@import AEPServices; +@import Foundation; + +static NSString *const TAG = @"RCTAEPOptimize"; + +@implementation RCTAEPOptimize { + NSMutableDictionary *propositionCache; +} + +- (instancetype)init { + self = [super init]; + propositionCache = [[NSMutableDictionary alloc] init]; + return self; +} + ++ (BOOL)requiresMainQueueSetup { + return NO; +} + +- (dispatch_queue_t)methodQueue { + return dispatch_get_main_queue(); +} + +// Module name for TurboModuleRegistry resolution. ++ (NSString *)moduleName { return @"NativeAEPOptimize"; } + +// Required on both paths: RCTModuleProviders.mm (codegen-generated) checks +// respondsToSelector:@selector(getTurboModule:) at startup. Without it, +// the module is skipped and TurboModuleRegistry returns null. +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +#pragma mark - NativeAEPOptimizeSpec protocol methods + +- (void)extensionVersion:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [AEPLog traceWithLabel:TAG message:@"extensionVersion is called."]; + resolve([AEPMobileOptimize extensionVersion]); +} + +- (void)clearCachedPropositions { + [AEPLog traceWithLabel:TAG message:@"clearCachedPropositions is called."]; + [self clearPropositionsCache]; + [AEPMobileOptimize clearCachedPropositions]; +} + +- (void)getPropositions:(NSArray *)decisionScopeNames + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [AEPLog traceWithLabel:TAG message:@"getPropositions is called."]; + NSArray *decisionScopesArray = + [self createDecisionScopesArray:decisionScopeNames]; + [AEPMobileOptimize + getPropositions:decisionScopesArray + completion:^(NSDictionary + *decisionScopePropositionDict, + NSError *error) { + if (error) { + reject([NSString stringWithFormat:@"%ld", (long)error.code], + error.description, nil); + } else { + [self cachePropositions:decisionScopePropositionDict]; + NSMutableDictionary *> + *propositionDictionary = [[NSMutableDictionary alloc] init]; + for (AEPDecisionScope *key in decisionScopePropositionDict) { + AEPOptimizeProposition *proposition = decisionScopePropositionDict[key]; + [propositionDictionary + setValue:[self convertPropositionToDict:proposition] + forKey:key.name]; + } + resolve(propositionDictionary); + } + }]; +} + +- (void)updatePropositions:(NSArray *)decisionScopeNames + xdm:(NSDictionary *)xdm + data:(NSDictionary *)data + onSuccess:(RCTResponseSenderBlock)onSuccess + onError:(RCTResponseSenderBlock)onError { + [AEPLog traceWithLabel:TAG message:@"updatePropositions is called."]; + NSArray *scopes = [self createDecisionScopesArray:decisionScopeNames]; + [AEPMobileOptimize updatePropositions:scopes + withXdm:xdm + andData:data + completion:^(NSDictionary *decisionScopePropositionDict, NSError *error) { + if (error) { + NSDictionary *errorDict = [self convertNSErrorToOptimizeErrorDict:error]; + if (onError != nil) { onError(@[errorDict]); } + } + if (decisionScopePropositionDict) { + [self cachePropositions:decisionScopePropositionDict]; + NSDictionary *propositions = [self createCallbackResponse:decisionScopePropositionDict]; + if (onSuccess != nil) { onSuccess(@[propositions]); } + } + }]; +} + +- (void)onPropositionsUpdate { + [AEPLog traceWithLabel:TAG message:@"onPropositionsUpdate is called."]; + [AEPMobileOptimize onPropositionsUpdate:^( + NSDictionary + *decisionScopePropositionDict) { + [self cachePropositions:decisionScopePropositionDict]; + NSMutableDictionary *> + *propositionDictionary = [[NSMutableDictionary alloc] init]; + for (AEPDecisionScope *key in decisionScopePropositionDict) { + AEPOptimizeProposition *proposition = decisionScopePropositionDict[key]; + [propositionDictionary setValue:[self convertPropositionToDict:proposition] + forKey:key.name]; + } + // Emit via codegen-generated emitOnPropositionsUpdated: (JSI-native). + // Guard: _eventEmitterCallback may be nil if the SDK fires the callback + // before JS has subscribed (e.g. cached propositions from a prior update). + if (_eventEmitterCallback) { + [self emitOnPropositionsUpdated:@{@"propositions": propositionDictionary}]; + } + }]; +} + +- (void)multipleOffersDisplayed:(NSArray *)offersArray { + [AEPLog debugWithLabel:TAG message:@"multipleOffersDisplayed is called."]; + NSMutableArray *nativeOffers = [self getNativeOffersFromOffersArray:offersArray]; + if ([nativeOffers count] > 0) { + [AEPMobileOptimize displayed:nativeOffers]; + } +} + +- (void)multipleOffersGenerateDisplayInteractionXdm:(NSArray *)offersArray + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [AEPLog debugWithLabel:TAG message:@"multipleOffersGenerateDisplayInteractionXdm is called."]; + NSMutableArray *nativeOffers = [self getNativeOffersFromOffersArray:offersArray]; + if ([nativeOffers count] > 0) { + resolve([AEPMobileOptimize generateDisplayInteractionXdm:nativeOffers]); + } else { + reject(@"generateDisplayInteractionXdmForMultipleOffers", @"Error in generating Display interaction XDM for multiple offers.", nil); + } +} + +- (void)offerDisplayed:(NSString *)offerId + propositionMap:(NSDictionary *)dictionary { + [AEPLog debugWithLabel:TAG message:@"Offer Displayed"]; + AEPOptimizeProposition *proposition = [AEPOptimizeProposition initFromData:dictionary]; + NSArray *offers = [proposition offers]; + for (AEPOffer *offer in offers) { + if ([[offer id] isEqualToString:offerId]) { [offer displayed]; break; } + } +} + +- (void)offerTapped:(NSString *)offerId + propositionMap:(NSDictionary *)dictionary { + [AEPLog debugWithLabel:TAG message:@"Offer Tapped"]; + AEPOptimizeProposition *proposition = [AEPOptimizeProposition initFromData:dictionary]; + NSArray *offers = [proposition offers]; + for (AEPOffer *offer in offers) { + if ([[offer id] isEqualToString:offerId]) { [offer tapped]; break; } + } +} + +- (void)generateDisplayInteractionXdm:(NSString *)offerId + propositionMap:(NSDictionary *)dictionary + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [AEPLog debugWithLabel:TAG message:@"generateDisplayInteractionXdm"]; + AEPOptimizeProposition *proposition = [AEPOptimizeProposition initFromData:dictionary]; + NSArray *offers = [proposition offers]; + AEPOffer *offerDisplayed = nil; + for (AEPOffer *offer in offers) { + if ([[offer id] isEqualToString:offerId]) { offerDisplayed = offer; break; } + } + if (offerDisplayed != nil) { + resolve([offerDisplayed generateDisplayInteractionXdm]); + } else { + reject(@"generateDisplayInteractionXdm", + [NSString stringWithFormat:@"Error in generating Display interaction XDM for offer with id: %@", offerId], nil); + } +} + +- (void)generateTapInteractionXdm:(NSString *)offerId + propositionMap:(NSDictionary *)dictionary + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [AEPLog debugWithLabel:TAG message:@"generateTapInteractionXdm"]; + AEPOptimizeProposition *proposition = [AEPOptimizeProposition initFromData:dictionary]; + NSArray *offers = [proposition offers]; + AEPOffer *offerInteracted = nil; + for (AEPOffer *offer in offers) { + if ([[offer id] isEqualToString:offerId]) { offerInteracted = offer; break; } + } + if (offerInteracted != nil) { + resolve([offerInteracted generateTapInteractionXdm]); + } else { + reject(@"generateTapInteractionXdm", + [NSString stringWithFormat:@"Error in generating Tap interaction XDM for offer with id: %@", offerId], nil); + } +} + +- (void)generateReferenceXdm:(NSDictionary *)dictionary + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [AEPLog debugWithLabel:TAG message:@"Proposition generateReferenceXdm"]; + AEPOptimizeProposition *proposition = [AEPOptimizeProposition initFromData:dictionary]; + resolve([proposition generateReferenceXdm]); +} + +// addListener/removeListeners kept in spec for backward compatibility. +// No-ops — event emission uses CodegenTypes.EventEmitter (JSI), not bridge. +- (void)addListener:(NSString *)eventName { +} + +- (void)removeListeners:(double)count { +} + +#pragma mark - Shared helper methods + +- (NSMutableArray *)getNativeOffersFromOffersArray:(NSArray *)offersArray { + NSMutableArray *nativeOffers = [[NSMutableArray alloc] init]; + if (!offersArray || [offersArray count] == 0) { + [AEPLog debugWithLabel:TAG message:@"getNativeOffersFromOffersArray: offersArray is null or empty"]; + return nativeOffers; + } + for (NSDictionary *offerDict in offersArray) { + if (!offerDict) { continue; } + NSString *uniquePropositionId = [offerDict objectForKey:@"uniquePropositionId"]; + NSString *offerId = [offerDict objectForKey:@"id"]; + if (!uniquePropositionId || !offerId) { + [AEPLog debugWithLabel:TAG message:[NSString stringWithFormat:@"getNativeOffersFromOffersArray: uniquePropositionId or offerId is null for offer: %@", offerDict]]; + continue; + } + AEPOptimizeProposition *proposition = [propositionCache objectForKey:uniquePropositionId]; + if (!proposition) { continue; } + NSArray *offers = [proposition offers]; + for (AEPOffer *propositionOffer in offers) { + if ([[propositionOffer id] isEqualToString:offerId]) { + [nativeOffers addObject:propositionOffer]; + break; + } + } + } + return nativeOffers; +} + +#pragma mark - Cache Management + +- (void)cachePropositions:(NSDictionary *)decisionScopePropositionDict { + for (AEPDecisionScope *key in decisionScopePropositionDict) { + AEPOptimizeProposition *proposition = decisionScopePropositionDict[key]; + if (!proposition) { continue; } + NSString *activityId = nil; + NSDictionary *propositionDict = [self convertPropositionToDict:proposition]; + // Try top-level activity.id first + NSDictionary *activity = [propositionDict valueForKey:@"activity"]; + if (activity && [activity objectForKey:@"id"]) { + activityId = [activity objectForKey:@"id"]; + } + // Fallback to scopeDetails.activity.id (Target mbox propositions have + // an empty top-level activity {} but scopeDetails.activity.id = "563703") + if (!activityId) { + NSDictionary *scopeDetails = [propositionDict valueForKey:@"scopeDetails"]; + if (scopeDetails && [scopeDetails objectForKey:@"activity"]) { + NSDictionary *scopeDetailsActivity = [scopeDetails objectForKey:@"activity"]; + if (scopeDetailsActivity && [scopeDetailsActivity objectForKey:@"id"]) { + activityId = [scopeDetailsActivity objectForKey:@"id"]; + } + } + } + if (activityId) { + [propositionCache setObject:proposition forKey:activityId]; + } + } +} + +- (void)clearPropositionsCache { + [propositionCache removeAllObjects]; +} + +- (NSArray *)createDecisionScopesArray: + (NSArray *)decisionScopes { + NSMutableArray *decisionScopesArray = [[NSMutableArray alloc] init]; + for (NSString *decisionScopeName in decisionScopes) { + [decisionScopesArray addObject:[[AEPDecisionScope alloc] initWithName:decisionScopeName]]; + } + return decisionScopesArray; +} + +- (NSDictionary *> *) + convertPropositionToDict:(AEPOptimizeProposition *)proposition { + NSDictionary *propositionDict = [[NSMutableDictionary alloc] init]; + if (!proposition) { return propositionDict; } + [propositionDict setValue:proposition.id forKey:@"id"]; + [propositionDict setValue:proposition.scope forKey:@"scope"]; + [propositionDict setValue:[proposition scopeDetails] forKey:@"scopeDetails"]; + NSMutableArray *> *offersArray = [[NSMutableArray alloc] init]; + for (AEPOffer *offer in proposition.offers) { + [offersArray addObject:[self convertOfferToDict:offer]]; + } + [propositionDict setValue:offersArray forKey:@"items"]; + if ([proposition activity]) { + [propositionDict setValue:[proposition activity] forKey:@"activity"]; + } + if ([proposition placement]) { + [propositionDict setValue:[proposition placement] forKey:@"placement"]; + } + return propositionDict; +} + +- (NSDictionary *)convertOfferToDict:(AEPOffer *)offer { + NSMutableDictionary *offerDict = [[NSMutableDictionary alloc] init]; + if (!offer) { return offerDict; } + [offerDict setValue:offer.id forKey:@"id"]; + if ([offer etag] != nil) { [offerDict setValue:[offer etag] forKey:@"etag"]; } + if ([offer meta] != nil) { [offerDict setValue:[offer meta] forKey:@"meta"]; } + [offerDict setValue:[offer schema] forKey:@"schema"]; + [offerDict setValue:@([offer score]) forKey:@"score"]; + NSDictionary *data = [[NSMutableDictionary alloc] init]; + [data setValue:[offer id] forKey:@"id"]; + [data setValue:[self convertOfferTypeToString:[offer type]] forKey:@"format"]; + [data setValue:[offer content] forKey:@"content"]; + if ([offer language] != nil) { [data setValue:[offer language] forKey:@"language"]; } + if ([offer characteristics] != nil) { [data setValue:[offer characteristics] forKey:@"characteristics"]; } + [offerDict setValue:data forKey:@"data"]; + return offerDict; +} + +- (NSString *)convertOfferTypeToString:(AEPOfferType)offerType { + switch (offerType) { + case AEPOfferTypeHtml: return @"text/html"; + case AEPOfferTypeJson: return @"application/json"; + case AEPOfferTypeText: return @"text/plain"; + case AEPOfferTypeImage: return @"image/*"; + default: return @""; + } +} + +- (NSDictionary *)convertNSErrorToOptimizeErrorDict:(NSError *)error { + if (!error) return @{}; + NSMutableDictionary *errorDict = [NSMutableDictionary dictionary]; + NSDictionary *userInfo = error.userInfo; + errorDict[@"type"] = userInfo[@"type"] ?: @""; + errorDict[@"status"] = userInfo[@"status"] ?: @(error.code); + errorDict[@"title"] = userInfo[@"title"] ?: @""; + errorDict[@"detail"] = userInfo[@"detail"] ?: @""; + errorDict[@"report"] = userInfo[@"report"] ?: @{}; + id aepErrorValue = userInfo[@"aepError"]; + if (aepErrorValue && aepErrorValue != [NSNull null]) { + errorDict[@"aepError"] = aepErrorValue; + } else { + errorDict[@"aepError"] = @"general.unexpected"; + } + return errorDict; +} + +- (NSDictionary *> *)createPropositionDictionary:(NSDictionary *)decisionScopePropositionDict { + NSMutableDictionary *> *propositionDictionary = [[NSMutableDictionary alloc] initWithCapacity:decisionScopePropositionDict.count]; + for (AEPDecisionScope *key in decisionScopePropositionDict) { + AEPOptimizeProposition *proposition = decisionScopePropositionDict[key]; + if (proposition) { + [propositionDictionary setValue:[self convertPropositionToDict:proposition] forKey:key.name]; + } + } + return propositionDictionary; +} + +- (NSDictionary *)createCallbackResponse:(NSDictionary *)decisionScopePropositionDict { + if (decisionScopePropositionDict && [decisionScopePropositionDict count] > 0) { + return [self createPropositionDictionary:decisionScopePropositionDict]; + } + return @{}; +} + +- (void)handleError:(NSError *)error rejecter:(RCTPromiseRejectBlock)reject { + if (!error || !reject) { return; } + NSDictionary *userInfo = [error userInfo]; + NSString *errorString = [[userInfo objectForKey:NSUnderlyingErrorKey] localizedDescription]; + reject([NSString stringWithFormat:@"%lu", (long)error.code], errorString, error); +} + +@end diff --git a/packages/optimize/package.json b/packages/optimize/package.json index 2f2a708f9..aed35c2de 100644 --- a/packages/optimize/package.json +++ b/packages/optimize/package.json @@ -38,5 +38,18 @@ }, "installConfig": { "hoistingLimits": "dependencies" + }, + "codegenConfig": { + "name": "NativeAEPOptimizeSpec", + "type": "modules", + "jsSrcsDir": "specs", + "android": { + "javaPackageName": "com.adobe.marketing.mobile.reactnative.optimize" + }, + "ios": { + "modulesProvider": { + "NativeAEPOptimize": "RCTAEPOptimize" + } + } } } diff --git a/packages/optimize/specs/NativeAEPOptimize.ts b/packages/optimize/specs/NativeAEPOptimize.ts new file mode 100644 index 000000000..5ee96fd7f --- /dev/null +++ b/packages/optimize/specs/NativeAEPOptimize.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + */ + +import type { TurboModule, CodegenTypes } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export type PropositionsPayload = { + propositions: Object; +}; + +export interface Spec extends TurboModule { + extensionVersion(): Promise; + clearCachedPropositions(): void; + getPropositions(decisionScopeNames: Array): Promise; + updatePropositions( + decisionScopeNames: Array, + xdm?: Object, + data?: Object, + onSuccess?: (propositions: Object) => void, + onError?: (error: Object) => void + ): void; + onPropositionsUpdate(): void; + multipleOffersDisplayed(offersArray: Array): void; + multipleOffersGenerateDisplayInteractionXdm(offersArray: Array): Promise; + offerDisplayed(offerId: string, propositionMap: Object): void; + offerTapped(offerId: string, propositionMap: Object): void; + generateDisplayInteractionXdm(offerId: string, propositionMap: Object): Promise; + generateTapInteractionXdm(offerId: string, propositionMap: Object): Promise; + generateReferenceXdm(propositionMap: Object): Promise; + // Legacy event support (required for interop path's NativeEventEmitter) + addListener(eventName: string): void; + removeListeners(count: number): void; + // TurboModule event support (codegen generates emitOnPropositionsUpdated:) + // Different name from the legacy sendEventWithName:@"onPropositionsUpdate" + // to avoid conflict between bridge and JSI event channels. + readonly onPropositionsUpdated: CodegenTypes.EventEmitter; +} + +export default TurboModuleRegistry.getEnforcing('NativeAEPOptimize'); diff --git a/packages/optimize/src/NativeAEPOptimize.ts b/packages/optimize/src/NativeAEPOptimize.ts new file mode 100644 index 000000000..5a4378d7d --- /dev/null +++ b/packages/optimize/src/NativeAEPOptimize.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + */ + +import type { TurboModule, EventSubscription } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export type PropositionsPayload = { + propositions: Object; +}; + +export interface Spec extends TurboModule { + extensionVersion(): Promise; + clearCachedPropositions(): void; + getPropositions(decisionScopeNames: Array): Promise; + updatePropositions( + decisionScopeNames: Array, + xdm?: Object, + data?: Object, + onSuccess?: (propositions: Object) => void, + onError?: (error: Object) => void + ): void; + onPropositionsUpdate(): void; + multipleOffersDisplayed(offersArray: Array): void; + multipleOffersGenerateDisplayInteractionXdm(offersArray: Array): Promise; + offerDisplayed(offerId: string, propositionMap: Object): void; + offerTapped(offerId: string, propositionMap: Object): void; + generateDisplayInteractionXdm(offerId: string, propositionMap: Object): Promise; + generateTapInteractionXdm(offerId: string, propositionMap: Object): Promise; + generateReferenceXdm(propositionMap: Object): Promise; + addListener(eventName: string): void; + removeListeners(count: number): void; + // TurboModule event delivery — codegen generates emitOnPropositionsUpdated: (iOS / Android). + // Declared as an explicit function type because @types/react-native (0.66 era) + // predates the CodegenTypes namespace; functionally equivalent to CodegenTypes.EventEmitter. + readonly onPropositionsUpdated: (handler: (event: PropositionsPayload) => void) => EventSubscription; +} + +export default TurboModuleRegistry.getEnforcing('NativeAEPOptimize'); diff --git a/packages/optimize/src/Optimize.ts b/packages/optimize/src/Optimize.ts index ade9b6088..9d091a674 100644 --- a/packages/optimize/src/Optimize.ts +++ b/packages/optimize/src/Optimize.ts @@ -10,14 +10,13 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { EventSubscription, NativeModules } from 'react-native'; -import { NativeEventEmitter } from 'react-native'; +import { EventSubscription } from 'react-native'; import Proposition from './models/Proposition'; import DecisionScope from './models/DecisionScope'; import Offer from './models/Offer'; -import { AdobePropositionCallback } from './models/AdobePropositionCallback'; +import { AdobePropositionCallback } from './models/AdobePropositionCallback'; import AEPOptimizeError from './models/AEPOptimizeError'; - +import NativeAEPOptimize from './NativeAEPOptimize'; interface IOptimize { extensionVersion: () => Promise; @@ -35,11 +34,7 @@ interface IOptimize { generateDisplayInteractionXdm: (offers: Array) => Promise>; } -const RCTAEPOptimize = NativeModules.AEPOptimize; - -declare var onPropositionUpdateSubscription: EventSubscription; - -var onPropositionUpdateSubscription: EventSubscription; +var onPropositionUpdateSubscription: EventSubscription | null = null; /** @@ -50,35 +45,40 @@ const Optimize: IOptimize = { * Returns the version of the AEPOptimize extension * @return {string} - Promise a promise that resolves with the extension version */ - extensionVersion(): Promise { - return Promise.resolve(RCTAEPOptimize.extensionVersion()); + extensionVersion(): Promise { + return Promise.resolve(NativeAEPOptimize.extensionVersion()); }, /** * This API registers a permanent callback which is invoked whenever the Edge extension dispatches a response Event received from the Experience Edge Network upon a personalization query. * @param {Object} onPropositionUpdateCallback - the callback that will be called with the updated Propositions. */ - onPropositionUpdate(adobeCallback: AdobePropositionCallback) { - if(onPropositionUpdateSubscription) { + onPropositionUpdate(adobeCallback: AdobePropositionCallback) { + // Remove previous subscription + if (onPropositionUpdateSubscription) { onPropositionUpdateSubscription.remove(); + onPropositionUpdateSubscription = null; } - const eventEmitter = new NativeEventEmitter(RCTAEPOptimize); - onPropositionUpdateSubscription = eventEmitter.addListener("onPropositionsUpdate", (propositions: Proposition[]) => { + // CodegenTypes.EventEmitter: payload is { propositions: { scopeName: proposition, ... } } + // on both iOS and Android (JSI-native delivery, no NativeEventEmitter needed). + onPropositionUpdateSubscription = NativeAEPOptimize.onPropositionsUpdated((payload: { propositions: any }) => { const map = new Map(); - for (const [key, value] of Object.entries(propositions)) { - map.set(key, new Proposition(value)); - } + for (const [key, value] of Object.entries(payload.propositions)) { + map.set(key, new Proposition(value as any)); + } adobeCallback.call(map); - }); - RCTAEPOptimize.onPropositionsUpdate(); - }, + }); + + // Register the listener on the native AEP SDK side + NativeAEPOptimize.onPropositionsUpdate(); + }, /** * Clears the client-side in-memory propositions cache. */ - clearCachedPropositions() { - RCTAEPOptimize.clearCachedPropositions(); + clearCachedPropositions() { + NativeAEPOptimize.clearCachedPropositions(); }, /** @@ -89,11 +89,11 @@ const Optimize: IOptimize = { getPropositions(decisionScopes: Array): Promise> { var decisionScopeNames: Array = decisionScopes.map(decisionScope => decisionScope.getName()); return new Promise((resolve, reject) => { - RCTAEPOptimize.getPropositions(decisionScopeNames).then((propositions: Proposition[]) => { + NativeAEPOptimize.getPropositions(decisionScopeNames).then((propositions: any) => { const map = new Map(); for (const [key, value] of Object.entries(propositions)) { - map.set(key, new Proposition(value)); - } + map.set(key, new Proposition(value as any)); + } resolve(map); }).catch((error: any) => reject(error)); }); @@ -115,7 +115,7 @@ const Optimize: IOptimize = { onError?: (error: AEPOptimizeError) => void ) { var decisionScopeNames: Array = decisionScopes.map(decisionScope => decisionScope.getName()); - RCTAEPOptimize.updatePropositions( + NativeAEPOptimize.updatePropositions( decisionScopeNames, xdm, data, @@ -136,7 +136,7 @@ const Optimize: IOptimize = { * @param {Array} offers - an array of Proposition Offers */ displayed(offers: Array) { - RCTAEPOptimize.multipleOffersDisplayed(offers); + NativeAEPOptimize.multipleOffersDisplayed(offers); }, /** @@ -146,7 +146,7 @@ const Optimize: IOptimize = { * @return {Promise>} - a promise that resolves to xdm map */ generateDisplayInteractionXdm(offers: Array) { - return RCTAEPOptimize.multipleOffersGenerateDisplayInteractionXdm(offers); + return NativeAEPOptimize.multipleOffersGenerateDisplayInteractionXdm(offers) as Promise>; }, }; diff --git a/packages/optimize/src/models/Offer.ts b/packages/optimize/src/models/Offer.ts index 07b1c92ce..30b101a17 100644 --- a/packages/optimize/src/models/Offer.ts +++ b/packages/optimize/src/models/Offer.ts @@ -10,9 +10,8 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import Proposition from'./Proposition'; -import { NativeModules } from 'react-native'; -const { AEPOptimize: RCTAEPOptimize } = NativeModules; +import Proposition from './Proposition'; +import NativeAEPOptimize from '../NativeAEPOptimize'; interface OfferData { id: string; @@ -75,7 +74,7 @@ class Offer { displayed(proposition: Proposition): void { const entries = Object.entries(proposition).filter(([_, value]) => typeof(value) !== "function"); const cleanedProposition = Object.fromEntries(entries); - RCTAEPOptimize.offerDisplayed(this.id, cleanedProposition); + NativeAEPOptimize.offerDisplayed(this.id, cleanedProposition); }; /** @@ -87,7 +86,7 @@ class Offer { console.log("Offer is tapped"); const entries = Object.entries(proposition).filter(([_, value]) => typeof(value) !== "function"); const cleanedProposition = Object.fromEntries(entries); - RCTAEPOptimize.offerTapped(this.id, cleanedProposition); + NativeAEPOptimize.offerTapped(this.id, cleanedProposition); }; /** @@ -101,8 +100,8 @@ class Offer { generateDisplayInteractionXdm(proposition: Proposition): Promise> { const entries = Object.entries(proposition).filter(([_, value]) => typeof(value) !== "function"); const cleanedProposition = Object.fromEntries(entries); - return Promise.resolve(RCTAEPOptimize.generateDisplayInteractionXdm(this.id, cleanedProposition)); - }; + return NativeAEPOptimize.generateDisplayInteractionXdm(this.id, cleanedProposition) as Promise>; + }; /** * Generates a map containing XDM formatted data for {Experience Event - Proposition Interactions} field group from this proposition arguement. @@ -115,7 +114,7 @@ class Offer { generateTapInteractionXdm(proposition: Proposition): Promise> { const entries = Object.entries(proposition).filter(([_, value]) => typeof(value) !== "function"); const cleanedProposition = Object.fromEntries(entries); - return Promise.resolve(RCTAEPOptimize.generateTapInteractionXdm(this.id, cleanedProposition)); + return NativeAEPOptimize.generateTapInteractionXdm(this.id, cleanedProposition) as Promise>; }; }; diff --git a/packages/optimize/src/models/Proposition.ts b/packages/optimize/src/models/Proposition.ts index a81ef1912..713154e02 100644 --- a/packages/optimize/src/models/Proposition.ts +++ b/packages/optimize/src/models/Proposition.ts @@ -11,8 +11,7 @@ governing permissions and limitations under the License. */ import Offer from './Offer'; - -const RCTAEPOptimize = require('react-native').NativeModules.AEPOptimize; +import NativeAEPOptimize from '../NativeAEPOptimize'; interface Activity { id?: string; @@ -80,7 +79,7 @@ class Proposition { generateReferenceXdm(): Promise> { const entries = Object.entries(this).filter(([_,value]) => typeof(value) !== "function"); const proposition = Object.fromEntries(entries); - return Promise.resolve(RCTAEPOptimize.generateReferenceXdm(proposition)); + return NativeAEPOptimize.generateReferenceXdm(proposition) as Promise>; }; } diff --git a/packages/optimize/tsconfig.json b/packages/optimize/tsconfig.json index 399d6463d..0b2eca741 100644 --- a/packages/optimize/tsconfig.json +++ b/packages/optimize/tsconfig.json @@ -5,5 +5,5 @@ "rootDir": "./src", "outDir": "./dist" }, - "exclude": ["__tests__", "dist"] + "exclude": ["__tests__", "dist", "specs"] } \ No newline at end of file diff --git a/tests/jest/setup.ts b/tests/jest/setup.ts index 99c13c7e1..210a7a4bd 100644 --- a/tests/jest/setup.ts +++ b/tests/jest/setup.ts @@ -171,6 +171,7 @@ jest.doMock('react-native', () => { () => new Promise((resolve) => resolve('')) ), onPropositionsUpdate: jest.fn(), + onPropositionsUpdated: jest.fn().mockReturnValue({ remove: jest.fn() }), clearCachedPropositions: jest.fn(), getPropositions: jest.fn( () => diff --git a/yarn.lock b/yarn.lock index 0140f555f..8817ef592 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3591,13 +3591,6 @@ __metadata: languageName: node linkType: hard -"@react-native/assets-registry@npm:0.85.3": - version: 0.85.3 - resolution: "@react-native/assets-registry@npm:0.85.3" - checksum: 10/614892869515ec035b5e64c4ef310c6bd531bcd623941a40db08255e2b2ba7d988801092b5014a558af2e899388b63d1a818cf42dcb5d014bc87ec72dcc8d336 - languageName: node - linkType: hard - "@react-native/babel-plugin-codegen@npm:0.81.5": version: 0.81.5 resolution: "@react-native/babel-plugin-codegen@npm:0.81.5" @@ -3697,23 +3690,6 @@ __metadata: languageName: node linkType: hard -"@react-native/codegen@npm:0.85.3": - version: 0.85.3 - resolution: "@react-native/codegen@npm:0.85.3" - dependencies: - "@babel/core": "npm:^7.25.2" - "@babel/parser": "npm:^7.29.0" - hermes-parser: "npm:0.33.3" - invariant: "npm:^2.2.4" - nullthrows: "npm:^1.1.1" - tinyglobby: "npm:^0.2.15" - yargs: "npm:^17.6.2" - peerDependencies: - "@babel/core": "*" - checksum: 10/71986731526148205e43d624b4d02348a55da03d05c302cad3a314c7f216bad4703abc8280dfb77c17c787bce1e1c980f1ea516b404a238ae858b1307717a1c9 - languageName: node - linkType: hard - "@react-native/community-cli-plugin@npm:0.81.5": version: 0.81.5 resolution: "@react-native/community-cli-plugin@npm:0.81.5" @@ -3760,29 +3736,6 @@ __metadata: languageName: node linkType: hard -"@react-native/community-cli-plugin@npm:0.85.3": - version: 0.85.3 - resolution: "@react-native/community-cli-plugin@npm:0.85.3" - dependencies: - "@react-native/dev-middleware": "npm:0.85.3" - debug: "npm:^4.4.0" - invariant: "npm:^2.2.4" - metro: "npm:^0.84.3" - metro-config: "npm:^0.84.3" - metro-core: "npm:^0.84.3" - semver: "npm:^7.1.3" - peerDependencies: - "@react-native-community/cli": "*" - "@react-native/metro-config": 0.85.3 - peerDependenciesMeta: - "@react-native-community/cli": - optional: true - "@react-native/metro-config": - optional: true - checksum: 10/e2a108d09d323208a7879cda0a622bb4e41d2960302cbea12c46e91cf40a861cc56bb83534354182e9149c2ff5ed8d8609a576da1f77220b4a2e6a859d447f54 - languageName: node - linkType: hard - "@react-native/debugger-frontend@npm:0.81.5": version: 0.81.5 resolution: "@react-native/debugger-frontend@npm:0.81.5" @@ -3797,24 +3750,6 @@ __metadata: languageName: node linkType: hard -"@react-native/debugger-frontend@npm:0.85.3": - version: 0.85.3 - resolution: "@react-native/debugger-frontend@npm:0.85.3" - checksum: 10/c778ae789b23102c74113b08914f213b1da66327b1840bdb2d21600a6916c6d062f53d95bfea7001772a10be279a0038a577b5e4945d1159183658d5b1614668 - languageName: node - linkType: hard - -"@react-native/debugger-shell@npm:0.85.3": - version: 0.85.3 - resolution: "@react-native/debugger-shell@npm:0.85.3" - dependencies: - cross-spawn: "npm:^7.0.6" - debug: "npm:^4.4.0" - fb-dotslash: "npm:0.5.8" - checksum: 10/5c6871ff071a1ad030d6791892d55e7003d67656a7e26bf30ea5a1d23cb71b9c81e55768a754e1fbab19e3ec5ed9b246f9409e84963b0877a61d73f6d193b9e1 - languageName: node - linkType: hard - "@react-native/dev-middleware@npm:0.81.5": version: 0.81.5 resolution: "@react-native/dev-middleware@npm:0.81.5" @@ -3853,26 +3788,6 @@ __metadata: languageName: node linkType: hard -"@react-native/dev-middleware@npm:0.85.3": - version: 0.85.3 - resolution: "@react-native/dev-middleware@npm:0.85.3" - dependencies: - "@isaacs/ttlcache": "npm:^1.4.1" - "@react-native/debugger-frontend": "npm:0.85.3" - "@react-native/debugger-shell": "npm:0.85.3" - chrome-launcher: "npm:^0.15.2" - chromium-edge-launcher: "npm:^0.3.0" - connect: "npm:^3.6.5" - debug: "npm:^4.4.0" - invariant: "npm:^2.2.4" - nullthrows: "npm:^1.1.1" - open: "npm:^7.0.3" - serve-static: "npm:^1.16.2" - ws: "npm:^7.5.10" - checksum: 10/ff4d698edda3e205dc1de64961628a358f2f2cecbc3c50f4351761fff3eef87608f998ebaa3454b41b1ef6a1bfd5307cd1676fccd5eca348c7b54dac16f2fabc - languageName: node - linkType: hard - "@react-native/gradle-plugin@npm:0.81.5": version: 0.81.5 resolution: "@react-native/gradle-plugin@npm:0.81.5" @@ -3887,13 +3802,6 @@ __metadata: languageName: node linkType: hard -"@react-native/gradle-plugin@npm:0.85.3": - version: 0.85.3 - resolution: "@react-native/gradle-plugin@npm:0.85.3" - checksum: 10/c838736286d27a13d90fd14cf81cf2165360da01e43d1f95b0ea4fb5c7716b02153e1b2604a168a39dead6f77a66eaba155ea997b9db46f4887669fbf78f18df - languageName: node - linkType: hard - "@react-native/jest-preset@npm:^0.85.0": version: 0.85.3 resolution: "@react-native/jest-preset@npm:0.85.3" @@ -3944,13 +3852,6 @@ __metadata: languageName: node linkType: hard -"@react-native/normalize-colors@npm:0.85.3": - version: 0.85.3 - resolution: "@react-native/normalize-colors@npm:0.85.3" - checksum: 10/6ed3f85bf2405c72809ddc4c320910d4afaa399b9eb50ffa0ddd2e5ff478649abaa890517d6b7aeb431dd7d2e1a6412ac4df9da36acc74a8d470ffee44aeaed8 - languageName: node - linkType: hard - "@react-native/normalize-colors@npm:^0.74.1": version: 0.74.89 resolution: "@react-native/normalize-colors@npm:0.74.89" @@ -3992,23 +3893,6 @@ __metadata: languageName: node linkType: hard -"@react-native/virtualized-lists@npm:0.85.3": - version: 0.85.3 - resolution: "@react-native/virtualized-lists@npm:0.85.3" - dependencies: - invariant: "npm:^2.2.4" - nullthrows: "npm:^1.1.1" - peerDependencies: - "@types/react": ^19.2.0 - react: "*" - react-native: 0.85.3 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/a1ec317056cca51978c7947c89bf60ceb5c86e75a55b52035f4604281796ab4858b4cdb2a9350dd1eab161ab356c53094dde6448ebe5921e739de8f5c707e145 - languageName: node - linkType: hard - "@react-navigation/bottom-tabs@npm:^7.0.0": version: 7.15.9 resolution: "@react-navigation/bottom-tabs@npm:7.15.9" @@ -4835,8 +4719,6 @@ __metadata: jest: "npm:^29.7.0" jest-environment-jsdom: "npm:^29.7.0" lerna: "npm:^8.2.2" - react: "npm:18.3.1" - react-native: "npm:^0.85.0" ts-jest: "npm:^29.1.1" tslib: "npm:^2.3.1" typescript: "npm:^5.0.0" @@ -5237,15 +5119,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-syntax-hermes-parser@npm:0.33.3": - version: 0.33.3 - resolution: "babel-plugin-syntax-hermes-parser@npm:0.33.3" - dependencies: - hermes-parser: "npm:0.33.3" - checksum: 10/250394dbe9fc7b6b2235ed7d0eaed287c811fbb79ab122a6d1a74f212dd85307273a06ae72e0b7f164f908f57d93f45f06183236f51d9fc704083cc67bce78c6 - languageName: node - linkType: hard - "babel-plugin-transform-flow-enums@npm:^0.0.2": version: 0.0.2 resolution: "babel-plugin-transform-flow-enums@npm:0.0.2" @@ -5736,19 +5609,6 @@ __metadata: languageName: node linkType: hard -"chromium-edge-launcher@npm:^0.3.0": - version: 0.3.0 - resolution: "chromium-edge-launcher@npm:0.3.0" - dependencies: - "@types/node": "npm:*" - escape-string-regexp: "npm:^4.0.0" - is-wsl: "npm:^2.2.0" - lighthouse-logger: "npm:^1.0.0" - mkdirp: "npm:^1.0.4" - checksum: 10/1df5a42cb8bbcc01486b8ab4739341d493075e09715c2039fb2646056d6a6e533048a4274e53b7c4dcd477f205133e3dd4d8d095e0caaf08cefc2dc2627af9dc - languageName: node - linkType: hard - "ci-info@npm:^2.0.0": version: 2.0.0 resolution: "ci-info@npm:2.0.0" @@ -7282,15 +7142,6 @@ __metadata: languageName: node linkType: hard -"fb-dotslash@npm:0.5.8": - version: 0.5.8 - resolution: "fb-dotslash@npm:0.5.8" - bin: - dotslash: bin/dotslash - checksum: 10/9335e6835b6bb6d12807fe60e37af197295d26d671c20f355df188f3359188dda3d3bf93b978e3df93f67c4f67a281122399b828f0e49360302431db23480dee - languageName: node - linkType: hard - "fb-watchman@npm:^2.0.0": version: 2.0.2 resolution: "fb-watchman@npm:2.0.2" @@ -7979,13 +7830,6 @@ __metadata: languageName: node linkType: hard -"hermes-compiler@npm:250829098.0.10": - version: 250829098.0.10 - resolution: "hermes-compiler@npm:250829098.0.10" - checksum: 10/7687ad73483d6f25e9056da647ade37e434dbb7f85700f0900f902078c106c9b0498a064446191347d16c20cf29c083f560805179caf49af21b12b8b6be1f16b - languageName: node - linkType: hard - "hermes-estree@npm:0.23.1": version: 0.23.1 resolution: "hermes-estree@npm:0.23.1" @@ -8007,13 +7851,6 @@ __metadata: languageName: node linkType: hard -"hermes-estree@npm:0.33.3": - version: 0.33.3 - resolution: "hermes-estree@npm:0.33.3" - checksum: 10/dfaac7eb91e282cf04f26c8f557fcadbfb78f630062c7abc1e75b9765918103ebee1359dffbe6c5e42a52c7cee0b14420affda984d534f76ba3d7e8d9ba98215 - languageName: node - linkType: hard - "hermes-estree@npm:0.35.0": version: 0.35.0 resolution: "hermes-estree@npm:0.35.0" @@ -8048,15 +7885,6 @@ __metadata: languageName: node linkType: hard -"hermes-parser@npm:0.33.3": - version: 0.33.3 - resolution: "hermes-parser@npm:0.33.3" - dependencies: - hermes-estree: "npm:0.33.3" - checksum: 10/709dac7283a9eab706f3fff5c6f09deee5197a1a38751da66fdf499a307120ba3ef14ce734715430a838145531973a8c0b69874bf5bc615cca10059ee87f5ff3 - languageName: node - linkType: hard - "hermes-parser@npm:0.35.0": version: 0.35.0 resolution: "hermes-parser@npm:0.35.0" @@ -9921,7 +9749,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -10150,19 +9978,6 @@ __metadata: languageName: node linkType: hard -"metro-babel-transformer@npm:0.84.4": - version: 0.84.4 - resolution: "metro-babel-transformer@npm:0.84.4" - dependencies: - "@babel/core": "npm:^7.25.2" - flow-enums-runtime: "npm:^0.0.6" - hermes-parser: "npm:0.35.0" - metro-cache-key: "npm:0.84.4" - nullthrows: "npm:^1.1.1" - checksum: 10/5e3c1b49d88db6e6219f3c47a1fa61dd6cf38def566d9f24a430a8117853009fb0e3f975c7fa5aa20c7af7f142b37ef37b4a22838f0d18324a92002237630fad - languageName: node - linkType: hard - "metro-cache-key@npm:0.80.12": version: 0.80.12 resolution: "metro-cache-key@npm:0.80.12" @@ -10190,15 +10005,6 @@ __metadata: languageName: node linkType: hard -"metro-cache-key@npm:0.84.4": - version: 0.84.4 - resolution: "metro-cache-key@npm:0.84.4" - dependencies: - flow-enums-runtime: "npm:^0.0.6" - checksum: 10/381f330ec25ad3823ae843e5c21ed75aa5e34f4c92231aead526f4936f4280e1a73977a8d10fecc2b1ef8f11fc884323a76b650a93c699d6b02c706c17eea7ca - languageName: node - linkType: hard - "metro-cache@npm:0.80.12": version: 0.80.12 resolution: "metro-cache@npm:0.80.12" @@ -10234,18 +10040,6 @@ __metadata: languageName: node linkType: hard -"metro-cache@npm:0.84.4": - version: 0.84.4 - resolution: "metro-cache@npm:0.84.4" - dependencies: - exponential-backoff: "npm:^3.1.1" - flow-enums-runtime: "npm:^0.0.6" - https-proxy-agent: "npm:^7.0.5" - metro-core: "npm:0.84.4" - checksum: 10/e59dcc3c691b545ce574383ef22576e8d3e5b8e5e7ea9fbe9e0070d8d36406705c01458c30b4a31ca3b810e43082cd3a1948d389cbb13552f170c336dc651b7e - languageName: node - linkType: hard - "metro-config@npm:0.80.12, metro-config@npm:^0.80.9": version: 0.80.12 resolution: "metro-config@npm:0.80.12" @@ -10294,22 +10088,6 @@ __metadata: languageName: node linkType: hard -"metro-config@npm:0.84.4, metro-config@npm:^0.84.3": - version: 0.84.4 - resolution: "metro-config@npm:0.84.4" - dependencies: - connect: "npm:^3.6.5" - flow-enums-runtime: "npm:^0.0.6" - jest-validate: "npm:^29.7.0" - metro: "npm:0.84.4" - metro-cache: "npm:0.84.4" - metro-core: "npm:0.84.4" - metro-runtime: "npm:0.84.4" - yaml: "npm:^2.6.1" - checksum: 10/54c61d4794dcbe5444e65ef3bb28325449f143afd9972e1093d13871472ee9086094c38daf3735fc688448ab13b60e7800623f3cf5685063f7983956a5f55fcd - languageName: node - linkType: hard - "metro-core@npm:0.80.12": version: 0.80.12 resolution: "metro-core@npm:0.80.12" @@ -10343,17 +10121,6 @@ __metadata: languageName: node linkType: hard -"metro-core@npm:0.84.4, metro-core@npm:^0.84.3": - version: 0.84.4 - resolution: "metro-core@npm:0.84.4" - dependencies: - flow-enums-runtime: "npm:^0.0.6" - lodash.throttle: "npm:^4.1.1" - metro-resolver: "npm:0.84.4" - checksum: 10/9ee8513522277c5fe00a8d1ef6b698763b9fd2bd2cdc90786617eef36896d3e1e778a0fd8aadd42027d5ca222a54056e734a51f5adb321195878f06341692713 - languageName: node - linkType: hard - "metro-file-map@npm:0.80.12": version: 0.80.12 resolution: "metro-file-map@npm:0.80.12" @@ -10411,23 +10178,6 @@ __metadata: languageName: node linkType: hard -"metro-file-map@npm:0.84.4": - version: 0.84.4 - resolution: "metro-file-map@npm:0.84.4" - dependencies: - debug: "npm:^4.4.0" - fb-watchman: "npm:^2.0.0" - flow-enums-runtime: "npm:^0.0.6" - graceful-fs: "npm:^4.2.4" - invariant: "npm:^2.2.4" - jest-worker: "npm:^29.7.0" - micromatch: "npm:^4.0.4" - nullthrows: "npm:^1.1.1" - walker: "npm:^1.0.7" - checksum: 10/ab4d01e5ab78cc78682603b8eaf68e45ccc00fe5e440e4e69d7e6102f79a13e126da3692ae6f3d4b379a8b05b284498fa18d10b1f9447046068e1aa1b658b2db - languageName: node - linkType: hard - "metro-minify-terser@npm:0.80.12": version: 0.80.12 resolution: "metro-minify-terser@npm:0.80.12" @@ -10458,16 +10208,6 @@ __metadata: languageName: node linkType: hard -"metro-minify-terser@npm:0.84.4": - version: 0.84.4 - resolution: "metro-minify-terser@npm:0.84.4" - dependencies: - flow-enums-runtime: "npm:^0.0.6" - terser: "npm:^5.15.0" - checksum: 10/e0893b5672a4ad2bc6e2c492f9994a3eae6e633e49f2e5a52738e80260e37bb5143219ce2c337c22dd16cee850e68b99d1ba4bc378d7cc8e9cd60d636aa051b5 - languageName: node - linkType: hard - "metro-resolver@npm:0.80.12": version: 0.80.12 resolution: "metro-resolver@npm:0.80.12" @@ -10495,15 +10235,6 @@ __metadata: languageName: node linkType: hard -"metro-resolver@npm:0.84.4": - version: 0.84.4 - resolution: "metro-resolver@npm:0.84.4" - dependencies: - flow-enums-runtime: "npm:^0.0.6" - checksum: 10/2234b8820ebebc70ae9688e3f4e4ec031f59bfefe51cd6171f7ce2063d97308663021292dccb2f34d80806d685f7f2583834eb718c8d5465a0385687f14b3996 - languageName: node - linkType: hard - "metro-runtime@npm:0.80.12": version: 0.80.12 resolution: "metro-runtime@npm:0.80.12" @@ -10534,16 +10265,6 @@ __metadata: languageName: node linkType: hard -"metro-runtime@npm:0.84.4, metro-runtime@npm:^0.84.3": - version: 0.84.4 - resolution: "metro-runtime@npm:0.84.4" - dependencies: - "@babel/runtime": "npm:^7.25.0" - flow-enums-runtime: "npm:^0.0.6" - checksum: 10/8c5818fdc67bd8ece9fc16bdcc848e115c76289f41b397efe30e401590dc04e36a4ea5af126682648f3b689ffbee27da20ba27ed261021aa4d222b75a40f353f - languageName: node - linkType: hard - "metro-source-map@npm:0.80.12": version: 0.80.12 resolution: "metro-source-map@npm:0.80.12" @@ -10596,23 +10317,6 @@ __metadata: languageName: node linkType: hard -"metro-source-map@npm:0.84.4, metro-source-map@npm:^0.84.3": - version: 0.84.4 - resolution: "metro-source-map@npm:0.84.4" - dependencies: - "@babel/traverse": "npm:^7.29.0" - "@babel/types": "npm:^7.29.0" - flow-enums-runtime: "npm:^0.0.6" - invariant: "npm:^2.2.4" - metro-symbolicate: "npm:0.84.4" - nullthrows: "npm:^1.1.1" - ob1: "npm:0.84.4" - source-map: "npm:^0.5.6" - vlq: "npm:^1.0.0" - checksum: 10/675a4df8a85ef411a58cb334932bda6f8bd87a8e031f73ac1dfd76cfe89ebbe994c167fc36b66fc550c09e7bef1cfe405c5693ae4c20198db9753b6a7acae4fd - languageName: node - linkType: hard - "metro-symbolicate@npm:0.80.12": version: 0.80.12 resolution: "metro-symbolicate@npm:0.80.12" @@ -10662,22 +10366,6 @@ __metadata: languageName: node linkType: hard -"metro-symbolicate@npm:0.84.4": - version: 0.84.4 - resolution: "metro-symbolicate@npm:0.84.4" - dependencies: - flow-enums-runtime: "npm:^0.0.6" - invariant: "npm:^2.2.4" - metro-source-map: "npm:0.84.4" - nullthrows: "npm:^1.1.1" - source-map: "npm:^0.5.6" - vlq: "npm:^1.0.0" - bin: - metro-symbolicate: src/index.js - checksum: 10/a6aebc3a604aebd1c83dc090249c4f91f2bad25bcdd48c05c5f0cfc56ad223ac2c1ba558123b68ba6c3e97de7515d81b6ba9b048012746395f3eb68a9e90a189 - languageName: node - linkType: hard - "metro-transform-plugins@npm:0.80.12": version: 0.80.12 resolution: "metro-transform-plugins@npm:0.80.12" @@ -10720,20 +10408,6 @@ __metadata: languageName: node linkType: hard -"metro-transform-plugins@npm:0.84.4": - version: 0.84.4 - resolution: "metro-transform-plugins@npm:0.84.4" - dependencies: - "@babel/core": "npm:^7.25.2" - "@babel/generator": "npm:^7.29.1" - "@babel/template": "npm:^7.28.6" - "@babel/traverse": "npm:^7.29.0" - flow-enums-runtime: "npm:^0.0.6" - nullthrows: "npm:^1.1.1" - checksum: 10/ae83306fbb640392205e571ddfb69629ec6d2878d664de85da2150d23458949dba833feef03759d9b15a13c0a50b4191ede41c685d12554c16fb8d56609292d6 - languageName: node - linkType: hard - "metro-transform-worker@npm:0.80.12": version: 0.80.12 resolution: "metro-transform-worker@npm:0.80.12" @@ -10797,27 +10471,6 @@ __metadata: languageName: node linkType: hard -"metro-transform-worker@npm:0.84.4": - version: 0.84.4 - resolution: "metro-transform-worker@npm:0.84.4" - dependencies: - "@babel/core": "npm:^7.25.2" - "@babel/generator": "npm:^7.29.1" - "@babel/parser": "npm:^7.29.0" - "@babel/types": "npm:^7.29.0" - flow-enums-runtime: "npm:^0.0.6" - metro: "npm:0.84.4" - metro-babel-transformer: "npm:0.84.4" - metro-cache: "npm:0.84.4" - metro-cache-key: "npm:0.84.4" - metro-minify-terser: "npm:0.84.4" - metro-source-map: "npm:0.84.4" - metro-transform-plugins: "npm:0.84.4" - nullthrows: "npm:^1.1.1" - checksum: 10/bacccf7a3a051a2216490b221c63f16db97f35845232c0bd32edd211f82befa93b319fd6eb00df47595c24b6d7c3ec1851849773ff532de96c76b931709faa2b - languageName: node - linkType: hard - "metro@npm:0.80.12": version: 0.80.12 resolution: "metro@npm:0.80.12" @@ -10970,55 +10623,6 @@ __metadata: languageName: node linkType: hard -"metro@npm:0.84.4, metro@npm:^0.84.3": - version: 0.84.4 - resolution: "metro@npm:0.84.4" - dependencies: - "@babel/code-frame": "npm:^7.29.0" - "@babel/core": "npm:^7.25.2" - "@babel/generator": "npm:^7.29.1" - "@babel/parser": "npm:^7.29.0" - "@babel/template": "npm:^7.28.6" - "@babel/traverse": "npm:^7.29.0" - "@babel/types": "npm:^7.29.0" - accepts: "npm:^2.0.0" - ci-info: "npm:^2.0.0" - connect: "npm:^3.6.5" - debug: "npm:^4.4.0" - error-stack-parser: "npm:^2.0.6" - flow-enums-runtime: "npm:^0.0.6" - graceful-fs: "npm:^4.2.4" - hermes-parser: "npm:0.35.0" - image-size: "npm:^1.0.2" - invariant: "npm:^2.2.4" - jest-worker: "npm:^29.7.0" - jsc-safe-url: "npm:^0.2.2" - lodash.throttle: "npm:^4.1.1" - metro-babel-transformer: "npm:0.84.4" - metro-cache: "npm:0.84.4" - metro-cache-key: "npm:0.84.4" - metro-config: "npm:0.84.4" - metro-core: "npm:0.84.4" - metro-file-map: "npm:0.84.4" - metro-resolver: "npm:0.84.4" - metro-runtime: "npm:0.84.4" - metro-source-map: "npm:0.84.4" - metro-symbolicate: "npm:0.84.4" - metro-transform-plugins: "npm:0.84.4" - metro-transform-worker: "npm:0.84.4" - mime-types: "npm:^3.0.1" - nullthrows: "npm:^1.1.1" - serialize-error: "npm:^2.1.0" - source-map: "npm:^0.5.6" - throat: "npm:^5.0.0" - ws: "npm:^7.5.10" - yargs: "npm:^17.6.2" - bin: - metro: src/cli.js - checksum: 10/22369963a965398add8e79939852b6a8906565d81bcb2a764255526fc8548417aed2976e315ec44cc432e104ea03afc616879449a0a9e4c292cfe0e9f252649d - languageName: node - linkType: hard - "micromatch@npm:^4.0.4, micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" @@ -11793,15 +11397,6 @@ __metadata: languageName: node linkType: hard -"ob1@npm:0.84.4": - version: 0.84.4 - resolution: "ob1@npm:0.84.4" - dependencies: - flow-enums-runtime: "npm:^0.0.6" - checksum: 10/15621cfa2d6bb196c5046031b3f85259735a245d9d7087f41758be3c31589c464e6eef53d94ec3d680fd8286ef08944782b9112f18d790a99421fbc7e311bb32 - languageName: node - linkType: hard - "object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -12287,13 +11882,6 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^4.0.4": - version: 4.0.4 - resolution: "picomatch@npm:4.0.4" - checksum: 10/f6ef80a3590827ce20378ae110ac78209cc4f74d39236370f1780f957b7ee41c12acde0e4651b90f39983506fd2f5e449994716f516db2e9752924aff8de93ce - languageName: node - linkType: hard - "pify@npm:5.0.0": version: 5.0.0 resolution: "pify@npm:5.0.0" @@ -12978,57 +12566,6 @@ __metadata: languageName: node linkType: hard -"react-native@npm:^0.85.0": - version: 0.85.3 - resolution: "react-native@npm:0.85.3" - dependencies: - "@react-native/assets-registry": "npm:0.85.3" - "@react-native/codegen": "npm:0.85.3" - "@react-native/community-cli-plugin": "npm:0.85.3" - "@react-native/gradle-plugin": "npm:0.85.3" - "@react-native/js-polyfills": "npm:0.85.3" - "@react-native/normalize-colors": "npm:0.85.3" - "@react-native/virtualized-lists": "npm:0.85.3" - abort-controller: "npm:^3.0.0" - anser: "npm:^1.4.9" - ansi-regex: "npm:^5.0.0" - babel-plugin-syntax-hermes-parser: "npm:0.33.3" - base64-js: "npm:^1.5.1" - commander: "npm:^12.0.0" - flow-enums-runtime: "npm:^0.0.6" - hermes-compiler: "npm:250829098.0.10" - invariant: "npm:^2.2.4" - memoize-one: "npm:^5.0.0" - metro-runtime: "npm:^0.84.3" - metro-source-map: "npm:^0.84.3" - nullthrows: "npm:^1.1.1" - pretty-format: "npm:^29.7.0" - promise: "npm:^8.3.0" - react-devtools-core: "npm:^6.1.5" - react-refresh: "npm:^0.14.0" - regenerator-runtime: "npm:^0.13.2" - scheduler: "npm:0.27.0" - semver: "npm:^7.1.3" - stacktrace-parser: "npm:^0.1.10" - tinyglobby: "npm:^0.2.15" - whatwg-fetch: "npm:^3.0.0" - ws: "npm:^7.5.10" - yargs: "npm:^17.6.2" - peerDependencies: - "@react-native/jest-preset": 0.85.3 - "@types/react": ^19.1.1 - react: ^19.2.3 - peerDependenciesMeta: - "@react-native/jest-preset": - optional: true - "@types/react": - optional: true - bin: - react-native: cli.js - checksum: 10/1141ad27311f9bc4fe1111f7132e9fef90f66e6804a8982148bf6c0efee8f9479379d6ab4938ef84c1ef86fb472004cdfd2c0910b6a8e193e4e78a4e2e186bdb - languageName: node - linkType: hard - "react-refresh@npm:^0.14.0, react-refresh@npm:^0.14.2": version: 0.14.2 resolution: "react-refresh@npm:0.14.2" @@ -13099,15 +12636,6 @@ __metadata: languageName: node linkType: hard -"react@npm:18.3.1": - version: 18.3.1 - resolution: "react@npm:18.3.1" - dependencies: - loose-envify: "npm:^1.1.0" - checksum: 10/261137d3f3993eaa2368a83110466fc0e558bc2c7f7ae7ca52d94f03aac945f45146bd85e5f481044db1758a1dbb57879e2fcdd33924e2dde1bdc550ce73f7bf - languageName: node - linkType: hard - "react@npm:19.1.0": version: 19.1.0 resolution: "react@npm:19.1.0" @@ -13551,13 +13079,6 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:0.27.0": - version: 0.27.0 - resolution: "scheduler@npm:0.27.0" - checksum: 10/eab3c3a8373195173e59c147224fc30dabe6dd453f248f5e610e8458512a5a2ee3a06465dc400ebfe6d35c9f5b7f3bb6b2e41c88c86fd177c25a73e7286a1e06 - languageName: node - linkType: hard - "semver@npm:2 || 3 || 4 || 5, semver@npm:^5.6.0": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -14419,16 +13940,6 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.15": - version: 0.2.16 - resolution: "tinyglobby@npm:0.2.16" - dependencies: - fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.4" - checksum: 10/5c2c41b572ada38449e7c86a5fe034f204a1dbba577225a761a14f29f48dc3f2fc0d81a6c56fcc67c5a742cc3aa9fb5e2ca18dbf22b610b0bc0e549b34d5a0f8 - languageName: node - linkType: hard - "tmp@npm:~0.2.1": version: 0.2.5 resolution: "tmp@npm:0.2.5"