Skip to content

Content card containers#540

Open
dsoffiantini wants to merge 87 commits intostagingfrom
content-card-containers
Open

Content card containers#540
dsoffiantini wants to merge 87 commits intostagingfrom
content-card-containers

Conversation

@dsoffiantini
Copy link
Contributor

@dsoffiantini dsoffiantini commented Oct 4, 2025

Background -
We're implementing the Messaging Inbox feature with support for:
1. Container Settings - The SDK reads container payload and displays content according to supported settings configured via AJO Authoring UI (experience.adobe.com).
2. Content Card Templates - Support for displaying content cards with various templates (SmallImage, LargeImage, ImageOnly). See our usage documentation for implementation details.

Key Changes -
Most of the additions are located in the packages/messaging/src/ui/ folder, which contains:
• React components for Content Card rendering (ContentCardContainer, ContentCardView…)
• Supporting UI components (Button, DismissButton, UnreadIcon, EmptyState)
• Hooks for data fetching (useContentCardUI, useContentContainer…)
• Theme system and styling

Description

Related Issue

Motivation and Context

How Has This Been Tested?

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have signed the Adobe Open Source CLA.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@dsoffiantini dsoffiantini force-pushed the content-card-containers branch 8 times, most recently from 81521eb to 00e7269 Compare October 6, 2025 16:25
.cursorignore Outdated
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this accidentally committed ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed .cursorignore file

"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.AEPSampleAppNewArchEnabled"
"bundleIdentifier": "com.adobe.MessagingDemoAppSwiftUI"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change should be reverted before merging

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reverted back to com.AEPSampleAppNewArchEnabled

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we dont need this ..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

earlier we were not adding dist folder to commit. any reason that why have added it to our version control now

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use this to distribute the binaries to the beta testing team while we’re unable to publish to npm.

onPress?.(interactId);
if (actionUrl) {
try {
Linking.openURL(actionUrl);
Copy link
Collaborator

@namArora3112 namArora3112 Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added Linking.canOpenURL() logic to Button.tsx for link validation


const Button: React.FC<ButtonProps> = ({
actionUrl,
id,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The id prop is declared but not utilized anywhere in the component.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed unnecessary id prop from Button.tsx

...props
}) => {
const theme = useTheme();
const handlePress = useCallback(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handlePress callback currently does not accept the GestureResponderEvent emitted by Pressable. As a result, onPress consumers never receive the event, even though the component’s type signature indicates that the event should be forwarded.

This creates an inconsistency between the API contract and the implementation.

Proposed fix:

const handlePress = (event: GestureResponderEvent) => {
  onPress?.(interactId, event);
  if (actionUrl) {
    Linking.openURL(actionUrl).catch(...);
  }
};

This keeps the API consistent and prevents unexpected undefined events in parent components.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added GestureResponderEvent to handlePress callback in Button.tsx

title: string;
onPress?: (interactId?: string, event?: GestureResponderEvent) => void;
interactId?: string;
textStyle?: (TextStyle | undefined) | (TextStyle | undefined)[];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

textStyle?: TextStyle | TextStyle[];
It can be simplified to this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to textStyle?: StyleProp<TextStyle>; to match Reacy Native’s style prop and allow arrays, null/undefined/false, and registered/animated styles without type errors.

import { PropsWithChildren } from "react";
import { StyleSheet, View } from "react-native";

const CenteredView = ({ children }: PropsWithChildren) => (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PropsWithChildren needs a generic type (e.g. {}) — otherwise it becomes any.
So it can be written like this
PropsWithChildren<{}>;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to PropsWithChildren<ViewProps>


const styles = StyleSheet.create({
container: {
flex: 1,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify: this will occupy the entire screen — is that the intent? If so, it seems we should be using or creating a FullScreenCenterView, since the current name is a bit confusing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed CenteredView to be called FullScreenCenterView

}, [actionUrl, interactId, onPress]);

return (
<Pressable onPress={handlePress} style={style} {...props}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An appropriate accessibilityRole should also be defined for this component.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added accessibilityRole = 'button', but also added it as an optional prop if user wants to change it to something else: accessibilityRole?: AccessibilityRole; in Button.tsx


return (
<ContentCardContainerProvider settings={settings}>
<Text accessibilityRole="header" style={[styles.heading, { color: headingColor }]}>{heading.content}</Text>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If heading is undefined, the component will throw when attempting to access heading.content.
Even though heading is marked as a required prop in contentSettings, it may still be missing at runtime.

If you want to add guard rails, you can safely render the heading using the following pattern:

{heading?.content ? (
  <Text
    accessibilityRole="header"
    style={[styles.heading, { color: headingColor }]}
  >
    {heading.content}
  </Text>
) : null}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied the above pattern to heading in ContentCardContainer.tsx as a guard rail

<FlatList
{...props}
data={displayCards}
extraData={refetch}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick question — why are we passing refetch to extraData?
My understanding is that extraData should be used for UI state that affects item rendering, but refetch is just a function and won’t trigger meaningful re-renders.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed extraData={refetch} from the ContentCardContainer, it was being used as a workaround, but not needed anymore

}
CardProps?.listener?.(...args);
}}
style={[
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can also be written like this
style={isHorizontal ? [styles.horizontalCardStyles, { width: Math.floor(windowWidth * 0.75) }] : undefined}

Copy link
Collaborator

@jkartoshka jkartoshka Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewritten to

style={
   isHorizontal 
      ? [styles.horizontalCardStyles, { width: Math.floor(windowWidth * 0.75) }]
      : undefined
}

return (
<ContentCardContainerProvider settings={settings}>
<Text accessibilityRole="header" style={[styles.heading, { color: headingColor }]}>{heading.content}</Text>
<FlatList
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should implement a keyExtractor for this FlatList so it can render more efficiently and avoid unnecessary re-renders.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added keyExtractor={(item: { id: string }) => item.id} to this FlatList


const [dismissedIds, setDismissedIds] = useState(new Set());

const headingColor = useMemo(() => colorScheme === 'dark' ? '#FFFFFF' : '#000000', [colorScheme]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move the color string to constants

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to usage of useTheme colors constant

const { content: contentSettings } = settings;
const { capacity, heading, layout, emptyStateSettings } = contentSettings;

const [dismissedIds, setDismissedIds] = useState(new Set());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is a typescript file we should add proper types for state
For ex:
useState<Set<string | number>>(new Set())

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set());

return (
<ContentCardContainerProvider settings={settings}>
<Text accessibilityRole="header" style={[styles.heading, { color: headingColor }]}>{heading.content}</Text>
<FlatList
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm if this has been manually tested by rotating the device from portrait to landscape, especially for the horizontal FlatList case?
useWindowDimensions should recalculate the width automatically, but I just want to make sure none of the styles break when the orientation changes.

surface,
style,
CardProps,
refetch,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick question: why didn’t we wire refetch to the FlatList onRefresh prop? Is there a reason we handled refresh differently?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed refetch logic from ContentCardContainer as it is not needed anymore

<ContentCardView
template={item}
{...CardProps}
listener={(...args) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid creating a new inline listener inside renderItem for every item. This causes unnecessary re-renders and prevents ContentCardView from taking advantage of memoization.

If ContentCardView passes the card's id when firing the event (e.g., listener(event, item.id)), we can use a single shared callback:

const handleCardEvent = useCallback((event, itemId, ...rest) => {
  if (event === "onDismiss") onDismiss(itemId);
  CardProps?.listener?.(event, itemId, ...rest);
}, [onDismiss, CardProps]);
<ContentCardView
  template={item}
  {...CardProps}
  listener={handleCardEvent} // shared, stable listener
/>

This avoids per-item function creation and improves FlatList performance.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the usage of a new single shared callback:

const handleCardEvent = useCallback(
    (event?: ContentViewEvent, data?: ContentTemplate, nativeEvent?: any) => {
      if (event === 'onDismiss' && data?.id) {
        setDismissedIds((prev) => {
          const next = new Set(prev);
          next.add(data.id as any);
          return next;
        });
      }
      CardProps?.listener?.(event, data, nativeEvent);
    },
    [CardProps]
  );

image={emptyStateSettings?.image?.[colorScheme ?? "light"]?.url ?? ''}
text={
emptyStateSettings?.message?.content ||
"No Content Available"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should localise it or use constants for rendering text

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a constant for "No Content Available"

const EmptyState: React.FC<EmptyStateProps> = ({ image, text }) => {
return (
<CenteredView>
<Image source={{ uri: image }} style={{ width: 120, height: 120, padding: 10 }} resizeMode="contain"/>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quick question: Should we add fallback image or not for network failure cases

cacheung and others added 24 commits January 29, 2026 13:12
Update tutorial doc for content card
update Content card customization guide
Update doc with Templates layout diagrams
Add useContentCardUI hook
update some format and wording
update doc
update doc
update doc format
update contentCardCustomizationGuide file name
* Add unread UI implementation

Add unread UI implementation

* update the unread icon mock example

update the unread icon mock example

* Use useContainerSettigs hook

Use useContainerSettigs hook and update the dist files

* update unreadIcon for error handling

update unreadIcon for error handling

* Update with review comments

Update with review comments

* fix the building errors for sample app and update the sample app

fix the building errors for sample app and update the sample app

* update with review comments

update with review comments

* update for new comments

update for the new comments

* Update test

Update test

* Update metro.config and dist files

Update metro.config and dist files

* Update few UI color in the sample app

Update few UI color in the sample app

* update the code to automatically refetch the content card

update the code to automatically refetch the content card

* Revert "update the code to automatically refetch the content card"

This reverts commit f8e1f1a.

* update read status from native

update read status from native

* Add isRead to contentTemplate constructor

Add isRead to contentTemplate constructor
* adding content card container ui component

* removing console logs

* removing templateType & capacity logic, fixing up styling

* addressing PR feedback

* more styling adjustments

* removing trailing space

* updating metro file

* adding some basic unit tests for content card container + removing duplicate color scheme variable

* removed unnecessary variable

* fixing typing

* addressing more pr feedback

* renaming to CardProps

---------

Co-authored-by: Julia Kartasheva <jkartasheva@adobe.com>
* adding unit tests for content card components

* removing unused value

---------

Co-authored-by: Julia Kartasheva <jkartasheva@adobe.com>
* adding content card container ui component

* removing console logs

* removing templateType & capacity logic, fixing up styling

* addressing PR feedback

* more styling adjustments

* removing trailing space

* updating metro file

* adding some basic unit tests for content card container + removing duplicate color scheme variable

* removed unnecessary variable

* fixing typing

* addressing more pr feedback

* renaming to CardProps

* adding capacity logic to content container

* added chaining to the listener

* adding back old app & track functionality

* adding refetching & extraData

---------

Co-authored-by: Julia Kartasheva <jkartasheva@adobe.com>
fix error message for missing button in the content card
* adding content card container ui component

* removing console logs

* removing templateType & capacity logic, fixing up styling

* addressing PR feedback

* more styling adjustments

* removing trailing space

* updating metro file

* adding some basic unit tests for content card container + removing duplicate color scheme variable

* removed unnecessary variable

* fixing typing

* addressing more pr feedback

* renaming to CardProps

* adding capacity logic to content container

* added chaining to the listener

* adding back old app & track functionality

* adding refetching & extraData

* adding initial changes for test app updates

* fixing up test app for android

* adding staging cards

* adding fixes for messagin error/picture problems in android

---------

Co-authored-by: Julia Kartasheva <jkartasheva@adobe.com>
* adding empty state

* addressing pr feed

* addressing pr feedback

* fixing empty state in android

* adding use callback & moving styling

* changing to inbox for default

---------

Co-authored-by: Julia Kartasheva <jkartasheva@adobe.com>
@jkartoshka jkartoshka force-pushed the content-card-containers branch from 71ae1ef to 723ef9a Compare January 30, 2026 19:15
Julia Kartasheva and others added 4 commits February 23, 2026 10:11
* renaming all instances of container to inbox

* adding unread status

* adding more unit tests for on interaction

* updating documentation

* saving configuration

* adding unread status & renaming inbox

* fixing spacing

* adding sudo

* adding ci fix

* updating enable corepack

* undoing layout.tsx & app.json

---------

Co-authored-by: Julia Kartasheva <jkartasheva@adobe.com>
@dsoffiantini dsoffiantini marked this pull request as ready for review February 25, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants