diff --git a/.circleci/config.yml b/.circleci/config.yml index 0d1b27ee..bb7ad532 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,35 +10,44 @@ jobs: unit-test: docker: - image: cimg/node:22.12 + resource_class: large steps: - - checkout + - checkout - - restore_cache: - keys: + - restore_cache: + keys: - yarn-cache-{{ checksum "yarn.lock" }} + - yarn-cache- - - run: - name: Install Node.js and Yarn Dependencies - command: yarn install + - run: + name: Enable Corepack + command: sudo corepack enable - - run: - name: Build the Project - command: yarn run build + - run: + name: Install Node.js and Yarn Dependencies + command: yarn install --immutable --network-timeout 300000 + no_output_timeout: 20m + environment: + NODE_OPTIONS: "--max-old-space-size=4096" - - run: - name: Expo Prebuild - command: cd apps/AEPSampleAppNewArchEnabled && npx expo prebuild + - run: + name: Build the Project + command: yarn run build - - save_cache: - key: yarn-cache-{{ checksum "yarn.lock" }} - paths: - - ./node_modules - - ./apps/AEPSampleAppNewArchEnabled/node_modules + - run: + name: Expo Prebuild + command: cd apps/AEPSampleAppNewArchEnabled && npx expo prebuild - - run: - name: Unit Tests - command: yarn test --watchAll=false --runInBand + - save_cache: + paths: + - .yarn/cache + - .yarn/unplugged + key: yarn-cache-{{ checksum "yarn.lock" }} + + - run: + name: Unit Tests + command: yarn test --watchAll=false --runInBand build-sample-app-android: executor: @@ -47,90 +56,105 @@ jobs: resource_class: large steps: - - checkout + - checkout + + - run: + name: Enable Corepack + command: sudo corepack enable - - run: - name: Install Node.js and Yarn Dependencies - command: yarn install + - run: + name: Install Node.js and Yarn Dependencies + command: yarn install --network-timeout 300000 + no_output_timeout: 20m + environment: + NODE_OPTIONS: "--max-old-space-size=4096" - - run: - name: Build the Project - command: yarn run build + - run: + name: Build the Project + command: yarn run build - - run: - name: Expo Prebuild - command: cd apps/AEPSampleAppNewArchEnabled && npx expo prebuild + - run: + name: Expo Prebuild + command: cd apps/AEPSampleAppNewArchEnabled && npx expo prebuild - - run: - name: Building Android Sample App - command: yarn sampleappnewarchenabled:android:build - environment: - _JAVA_OPTIONS: "-Xmx4096M -XX:MaxMetaspaceSize=512m" + - run: + name: Building Android Sample App + command: yarn sampleappnewarchenabled:android:build + environment: + _JAVA_OPTIONS: "-Xmx4096M -XX:MaxMetaspaceSize=512m" build-sample-app-ios: macos: - xcode: 16.2.0 - resource_class: m2pro.medium + xcode: 16.4.0 + resource_class: m4pro.medium steps: - - checkout - - - restore_cache: - keys: - - ios-yarn-cache-{{ checksum "yarn.lock" }} - - - run: - name: Set Xcode Path - command: sudo xcode-select -s /Applications/Xcode.app - - - node/install: - node-version: '22.12.0' - - - run: - name: Install Node.js and Yarn Dependencies - command: yarn install - - - run: - name: Build the Project - command: yarn run build - - - run: - name: Expo Prebuild - command: cd apps/AEPSampleAppNewArchEnabled && npx expo prebuild - - - save_cache: - key: ios-yarn-cache-{{ checksum "yarn.lock" }} - paths: - - ./node_modules - - ./apps/AEPSampleAppNewArchEnabled/node_modules - - - run: - name: Install Cocoapods - command: sudo gem install cocoapods - - - run: - name: Install Pods - command: yarn sampleappnewarchenabled:ios:pod:install - - - run: - name: Building iOS Sample App - command: | - cd apps/AEPSampleAppNewArchEnabled/ios - xcodebuild \ - -workspace AEPSampleAppNewArchEnabled.xcworkspace \ - -scheme AEPSampleAppNewArchEnabled \ - -sdk iphonesimulator \ - -destination "generic/platform=iOS Simulator" \ - clean build + - checkout + + - restore_cache: + keys: + - yarn-cache-{{ checksum "yarn.lock" }} + - yarn-cache- + + - run: + name: Set Xcode Path + command: sudo xcode-select -s /Applications/Xcode.app + + - node/install: + node-version: '22.12.0' + + - run: + name: Enable Corepack + command: sudo corepack enable + + - run: + name: Install Node.js and Yarn Dependencies + command: 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 + + - run: + name: Expo Prebuild + command: cd apps/AEPSampleAppNewArchEnabled && npx expo prebuild + + - save_cache: + paths: + - .yarn/cache + - .yarn/unplugged + key: yarn-cache-{{ checksum "yarn.lock" }} + + - run: + name: Install Cocoapods + command: sudo gem install cocoapods + + - run: + name: Install Pods + command: yarn sampleappnewarchenabled:ios:pod:install + + - run: + name: Building iOS Sample App + command: | + cd apps/AEPSampleAppNewArchEnabled/ios + xcodebuild \ + -workspace AEPSampleAppNewArchEnabled.xcworkspace \ + -scheme AEPSampleAppNewArchEnabled \ + -sdk iphonesimulator \ + -destination "generic/platform=iOS Simulator" \ + clean build workflows: version: 2.1 ci-workflow: jobs: - - unit-test - - build-sample-app-ios - # - build-sample-app-android - # Disable the Android build job because of the error below: - # Execution failed for task ':react-native-reanimated:configureCMakeDebug[arm64-v8a]'. - # > [CXX1210] /root/project/apps/AEPSampleApp/node_modules/react-native-reanimated/android/CMakeLists.txt debug|arm64-v8a : No compatible library found - # The Android build job will be enabled once the issue is resolved. + - unit-test + - build-sample-app-ios + # - build-sample-app-android + # Disable the Android build job because of the error below: + # Execution failed for task ':react-native-reanimated:configureCMakeDebug[arm64-v8a]'. + # > [CXX1210] /root/project/apps/AEPSampleApp/node_modules/react-native-reanimated/android/CMakeLists.txt debug|arm64-v8a : No compatible library found + # The Android build job will be enabled once the issue is resolved. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..025dd8c8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..af3ad128 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +/.yarn/** linguist-vendored +/.yarn/releases/* binary +/.yarn/plugins/**/* binary +/.pnp.* binary linguist-generated diff --git a/.gitignore b/.gitignore index 48e6d3b4..77fb31a4 100644 --- a/.gitignore +++ b/.gitignore @@ -152,3 +152,6 @@ out/ # ignore ds store .DS_Store + +# temporary allow messaging compiled code +!packages/messaging/dist/ \ No newline at end of file diff --git a/.yarnrc.yml b/.yarnrc.yml index 3186f3f0..de80f2ba 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1,2 @@ nodeLinker: node-modules +nmHoistingLimits: workspaces diff --git a/apps/AEPSampleApp/package.json b/apps/AEPSampleApp/package.json index 8a6f58d5..dda62205 100644 --- a/apps/AEPSampleApp/package.json +++ b/apps/AEPSampleApp/package.json @@ -2,11 +2,6 @@ "name": "aepsampleapp", "version": "2.0.0", "private": true, - "workspaces": { - "nohoist": [ - "**/*" - ] - }, "scripts": { "android": "react-native run-android", "ios": "react-native run-ios", @@ -70,6 +65,7 @@ "node": ">=20" }, "installConfig": { - "hoistingLimits": "dependencies" + "hoistingLimits": "workspaces", + "selfReferences": false } } diff --git a/apps/AEPSampleAppNewArchEnabled/app.json b/apps/AEPSampleAppNewArchEnabled/app.json index bdd5d190..4bd57c84 100644 --- a/apps/AEPSampleAppNewArchEnabled/app.json +++ b/apps/AEPSampleAppNewArchEnabled/app.json @@ -49,4 +49,4 @@ "typedRoutes": true } } -} +} \ No newline at end of file diff --git a/apps/AEPSampleAppNewArchEnabled/app/InboxView.tsx b/apps/AEPSampleAppNewArchEnabled/app/InboxView.tsx new file mode 100644 index 00000000..0e98223c --- /dev/null +++ b/apps/AEPSampleAppNewArchEnabled/app/InboxView.tsx @@ -0,0 +1,479 @@ +/* +Copyright 2025 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 { MobileCore } from "@adobe/react-native-aepcore"; +import { + InboxSettings, + Inbox, + ContentCardView, + ThemeProvider, + useContentCardUI, + useInbox +} from "@adobe/react-native-aepmessaging"; +import React, { memo, useCallback, useEffect, useState } from "react"; +import { + Appearance, + ColorSchemeName, + FlatList, + Modal, + Platform, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { Colors } from "../constants/Colors"; +import { useColorScheme } from "../hooks/useColorScheme"; +import { mockSettings, MockSurface } from "../mocks/contentCards/inbox/mockSettings"; +import { + DemoItem, + IMAGE_ONLY_TEMPLATES, + LARGE_IMAGE_TEMPLATES, + SMALL_IMAGE_TEMPLATES, +} from "../mocks/contentCards/templates/demoitems"; + +const VIEW_OPTIONS = [ + "Remote", + "Vertical", + "Horizontal", + "Inbox with Styling", + "Empty", + "Custom Card View", + "Templates" +] as const; +type ViewOption = (typeof VIEW_OPTIONS)[number]; + +const THEME_OPTIONS: Array<{ + label: string; + value: ColorSchemeName; +}> = [ + { label: "Light", value: "light" }, + { label: "Dark", value: "dark" }, + { label: "System", value: null }, + ]; + +const TEMPLATE_OPTIONS: Array<{ + label: string; + value: string; + items: DemoItem[]; +}> = [ + { label: "Small Image", value: "SmallImage", items: SMALL_IMAGE_TEMPLATES }, + { label: "Large Image", value: "LargeImage", items: LARGE_IMAGE_TEMPLATES }, + { label: "Image Only", value: "ImageOnly", items: IMAGE_ONLY_TEMPLATES }, + ]; +type TemplateOption = typeof TEMPLATE_OPTIONS[number]['value']; + +const ITEMS_BY_VIEW = Object.fromEntries( + TEMPLATE_OPTIONS.map(o => [o.value, o.items]) +) as Record; + +const StyledText = ({ text }: { text: string }) => { + return {text}; +}; + +const Switcher = ({ title, options, selected, onChange, colors, colorScheme }: { + title: string; + options: { label: string; value: string }[]; + selected: string; + onChange: (value: string) => void; + colors: any; + colorScheme: ColorSchemeName; +}) => ( + + {title} + + {options.map(({ label, value }) => ( + onChange(value)} + > + + {label} + + + ))} + + +); + +const Header = ({ + isLoading, + onTrackAction, + selectedView, + setSelectedView, + selectedTemplate, + onTemplateChange +}: { + isLoading: boolean; + onTrackAction: () => void; + selectedView: ViewOption; + setSelectedView: (view: ViewOption) => void; + selectedTemplate: TemplateOption; + onTemplateChange: (template: TemplateOption) => void; +}) => { + const [showPicker, setShowPicker] = useState(false); + const [selectedTheme, setSelectedTheme] = useState("System"); + const [trackInput, setTrackInput] = useState(""); + const colorScheme = useColorScheme(); + + const handleThemeChange = useCallback( + (theme: string, value: ColorSchemeName) => { + setSelectedTheme(theme); + Appearance.setColorScheme(value); + }, [] + ); + + // Track action and refresh content cards + const handleTrackAction = useCallback(async () => { + if (!trackInput.trim()) { + return; + } + + MobileCore.trackAction(trackInput); + await onTrackAction(); + setTrackInput(''); + }, [trackInput, onTrackAction]); + + const colors = colorScheme === "dark" ? Colors.dark : Colors.light; + + return ( + + {/* View Picker */} + + Select View Type + setShowPicker(true)} + > + {selectedView} + + + + {/* Theme Switcher */} + ({ label, value: label }))} + selected={selectedTheme} + onChange={(label) => handleThemeChange(label, THEME_OPTIONS.find(o => o.label === label)!.value)} + colors={colors} + colorScheme={colorScheme} + /> + + {selectedView == 'Templates' ? ( + /* Template Switcher */ + ({ label, value }))} + selected={selectedTemplate} + onChange={(val) => onTemplateChange(val as TemplateOption)} + colors={colors} + colorScheme={colorScheme} + />) + + /* Track Action Input */ + : ( + + Track Action + + + + + {isLoading ? 'Loading...' : 'Track'} + + + + + )} + + {/* View Picker Modal */} + + setShowPicker(false)}> + + {VIEW_OPTIONS.map((option) => ( + { + setSelectedView(option); + setShowPicker(false); + }} + > + {option} + + ))} + setShowPicker(false)}> + Cancel + + + + + + ); +}; + +const MemoHeader = memo(Header); + +const InboxView = () => { + const colorScheme = useColorScheme(); + const [selectedView, setSelectedView] = useState('Remote'); + const [selectedTemplate, setSelectedTemplate] = useState('SmallImage'); + + const surface = + Platform.OS === "android" + ? "rn/android/remote_image" + : "rn/cards"; + const { isLoading, refetch } = useContentCardUI(surface); + const { + settings, + error, + isLoading: isLoadingInbox, + refetch: refetchInbox + } = useInbox(surface); + + const items = selectedView === 'Templates' ? ITEMS_BY_VIEW[selectedTemplate] : undefined; + + useEffect(() => { + MobileCore.trackAction("small_image1"); + }, []); + + if (selectedView === 'Remote') { + return ( + <> + + + + ); + } + + else if (selectedView !== 'Templates') { + function getMocks(view: ViewOption): MockSurface { + return `mock/${view.toLowerCase().replaceAll(' ', '-')}` as MockSurface; + } + + const settings = mockSettings[ + getMocks(selectedView) + ] as { surfaceSettings: InboxSettings; inboxStyle?: any; CardProps?: any }; + + return ( + <> + + + + ); + } + + const renderContentCard = (item: any, isRemote: boolean) => { + const cardView = ; + + if (!isRemote) { + return ( + + + {item.customThemes ? ( + + {cardView} + + ) : ( + cardView + )} + + ); + } + return cardView; + }; + + return ( + item.key} + renderItem={({ item }: any) => + renderContentCard(item, false) + } + ListHeaderComponent={ + + } + /> + ); +}; +export default InboxView; + +const SPACING = { s: 10, m: 20, l: 24 }; + +const styles = StyleSheet.create({ + infoText: { + color: "darkgray", + fontSize: 18, + paddingTop: SPACING.s, + paddingBottom: SPACING.s, + }, + themeSwitcher: { + width: "100%", + borderRadius: SPACING.s, + padding: 4, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + section: { + marginHorizontal: SPACING.s, + marginBottom: SPACING.s, + }, + themeOption: { + flex: 1, + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 8, + marginHorizontal: 1, + alignItems: "center", + justifyContent: "center", + shadowOffset: { width: 0, height: 1 }, + shadowRadius: 2, + }, + themeOptionSelected: { + shadowColor: "#000", + shadowOpacity: 0.1, + elevation: 2, + }, + themeOptionUnselected: { + backgroundColor: "transparent", + shadowColor: "transparent", + shadowOpacity: 0, + elevation: 0, + }, + textLabel: { + fontSize: 14, + }, + buttonNeutral: { + height: 40, + borderWidth: 1, + borderRadius: 5, + justifyContent: "center", + paddingHorizontal: SPACING.s, + }, + buttonPrimary: { + backgroundColor: "#007AFF", + paddingHorizontal: 16, + paddingVertical: SPACING.s, + borderRadius: 8, + }, + modalOverlay: { + flex: 1, + justifyContent: "center", + alignItems: "center", + backgroundColor: "rgba(0,0,0,0.5)", + }, + modalCard: { + borderRadius: 10, + padding: SPACING.m, + backgroundColor: "white", + width: "80%", + }, + modalOption: { + paddingVertical: SPACING.s, + borderBottomWidth: 1, + borderBottomColor: "#eee", + }, + modalCancel: { + paddingVertical: SPACING.s, + marginTop: SPACING.s, + }, + modalCancelText: { + color: "#FF3B30", + }, + panel: { + borderRadius: SPACING.s, + borderWidth: 1, + padding: 15, + }, + titleText: { + fontWeight: "600", + marginBottom: 5 + }, + textCenter: { + textAlign: "center", + }, + rowCenter: { + flexDirection: "row", + alignItems: "center", + }, + trackInput: { + flex: 1, + height: 40, + borderWidth: 1, + borderRadius: 8, + paddingHorizontal: SPACING.s, + marginRight: SPACING.s, + }, + trackButtonText: { + color: "white", + fontWeight: "600", + }, +}); \ No newline at end of file diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx index c70e5d4b..5fcf5af9 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx @@ -11,26 +11,19 @@ governing permissions and limitations under the License. */ import React from 'react'; -import {Button, Text, View, ScrollView} from 'react-native'; -import {MobileCore} from '@adobe/react-native-aepcore'; +import { Button, Text, View, ScrollView } from 'react-native'; +import { MobileCore } from '@adobe/react-native-aepcore'; import { - Messaging, - PersonalizationSchema, - MessagingEdgeEventType, - PropositionItem, - Message, - ContentCard, - HTMLProposition, - JSONPropositionItem -} from '@adobe/react-native-aepmessaging' -import { MessagingProposition } from '@adobe/react-native-aepmessaging'; + Messaging, + PersonalizationSchema, + MessagingEdgeEventType +} from '@adobe/react-native-aepmessaging'; import styles from '../styles/styles'; import { useRouter } from 'expo-router'; const SURFACES = ['android-cbe-preview', 'cbe/json', 'android-cc']; const SURFACES_WITH_CONTENT_CARDS = ['android-cc']; - const messagingExtensionVersion = async () => { const version = await Messaging.extensionVersion(); console.log(`AdobeExperienceSDK: Messaging version: ${version}`); @@ -43,28 +36,27 @@ const refreshInAppMessages = () => { const setMessagingDelegate = () => { Messaging.setMessagingDelegate({ - onDismiss: msg => console.log('dismissed!', msg), - onShow: msg => { + onDismiss: (msg) => console.log('dismissed!', msg), + onShow: (msg) => { console.log('show', msg); msg.handleJavascriptMessage('myInappCallback', (content: string) => { console.log('Received webview content in onShow:', content); }); - msg.evaluateJavascript("(function() {console.log('my test'); return 'some result';})();", (result: string) => { - console.log('Result:', result); - }); }, shouldShowMessage: () => true, shouldSaveMessage: () => true, - urlLoaded: (url, message) => console.log(url, message), + urlLoaded: (url, message) => console.log(url, message) }); console.log('messaging delegate set'); }; + const getPropositionsForSurfaces = async () => { const messages = await Messaging.getPropositionsForSurfaces(SURFACES); console.log('getPropositionsForSurfaces', JSON.stringify(messages)); }; + const trackAction = async () => { - MobileCore.trackAction('iamjs', {full: true}); + MobileCore.trackAction('iamjs', { full: true }); }; const updatePropositionsForSurfaces = async () => { @@ -84,47 +76,60 @@ const getLatestMessage = async () => { // this method can be used to track click interactions with content cards const trackContentCardInteraction = async () => { - const messages = await Messaging.getPropositionsForSurfaces(SURFACES_WITH_CONTENT_CARDS); - - for (const surface of SURFACES_WITH_CONTENT_CARDS) { + const messages = await Messaging.getPropositionsForSurfaces( + SURFACES_WITH_CONTENT_CARDS + ); + + for (const surface of SURFACES_WITH_CONTENT_CARDS) { const propositions = messages[surface] || []; for (const proposition of propositions) { for (const propositionItem of proposition.items) { if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) { // Cast to ContentCard for the legacy tracking method - Messaging.trackContentCardInteraction(proposition, propositionItem as any); - console.log('trackContentCardInteraction', proposition, propositionItem); + Messaging.trackContentCardInteraction( + proposition, + propositionItem as any + ); + console.log( + 'trackContentCardInteraction', + proposition, + propositionItem + ); } } } } -} +}; // this method can be used to track display interactions with content cards const trackContentCardDisplay = async () => { - const messages = await Messaging.getPropositionsForSurfaces(SURFACES_WITH_CONTENT_CARDS); + const messages = await Messaging.getPropositionsForSurfaces( + SURFACES_WITH_CONTENT_CARDS + ); - for (const surface of SURFACES_WITH_CONTENT_CARDS) { + for (const surface of SURFACES_WITH_CONTENT_CARDS) { const propositions = messages[surface] || []; for (const proposition of propositions) { for (const propositionItem of proposition.items) { if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) { // Cast to ContentCard for the legacy tracking method - Messaging.trackContentCardDisplay(proposition, propositionItem as any); + Messaging.trackContentCardDisplay( + proposition, + propositionItem as any + ); console.log('trackContentCardDisplay', proposition, propositionItem); } } } } -} - +}; // Method demonstrating unified tracking using PropositionItem methods const unifiedTrackingExample = async () => { - const messages = await Messaging.getPropositionsForSurfaces(SURFACES); - for (const surface of SURFACES) { + const messages = await Messaging.getPropositionsForSurfaces(SURFACES); + for (const surface of SURFACES) { const propositions = messages[surface] || []; for (const proposition of propositions) { @@ -134,19 +139,20 @@ const unifiedTrackingExample = async () => { } } } -} - - +}; function MessagingView() { const router = useRouter(); return ( - +