From 35ff6b195120b26faf2fb933e0a6872d4ce454ee Mon Sep 17 00:00:00 2001 From: Spencer Smith Date: Tue, 24 Feb 2026 15:05:29 -0700 Subject: [PATCH 1/5] Pass inbox items during subscribe ruleset callback --- .../createPersonalizationDetails.js | 2 + .../RulesEngine/createDecisionProvider.js | 7 +- packages/core/src/constants/schema.js | 1 + .../createPersonalizationDetails.spec.js | 8 ++ .../topLevel/cartViewDecisions.spec.js | 2 + .../topLevel/mergedMetricDecisions.spec.js | 1 + .../topLevel/mixedPropositions.spec.js | 1 + ...eDecisionsWithDomActionSchemaItems.spec.js | 1 + .../topLevel/pageWideScopeDecisions.spec.js | 1 + ...cisionsWithoutDomActionSchemaItems.spec.js | 1 + .../topLevel/productsViewDecisions.spec.js | 1 + .../redirectPageWideScopeDecision.spec.js | 1 + .../topLevel/scopesFoo1Foo2Decisions.spec.js | 1 + .../createDecisionProvider.spec.js | 9 +- .../ContentCardsDemo/ContentCards.css | 57 +++++++++--- .../ContentCardsDemo/ContentCards.jsx | 88 +++++++++++++------ 16 files changed, 139 insertions(+), 43 deletions(-) diff --git a/packages/core/src/components/Personalization/createPersonalizationDetails.js b/packages/core/src/components/Personalization/createPersonalizationDetails.js index ca3d6c843..94f7a0ccc 100644 --- a/packages/core/src/components/Personalization/createPersonalizationDetails.js +++ b/packages/core/src/components/Personalization/createPersonalizationDetails.js @@ -21,6 +21,7 @@ import { REDIRECT_ITEM, RULESET_ITEM, MESSAGE_CONTENT_CARD, + INBOX_ITEM, } from "../../constants/schema.js"; import { buildPageSurface, @@ -102,6 +103,7 @@ export default ({ RULESET_ITEM, MESSAGE_IN_APP, MESSAGE_CONTENT_CARD, + INBOX_ITEM, ]; if (scopes.includes(PAGE_WIDE_SCOPE)) { diff --git a/packages/core/src/components/RulesEngine/createDecisionProvider.js b/packages/core/src/components/RulesEngine/createDecisionProvider.js index 709240d1c..5bf66b7e9 100644 --- a/packages/core/src/components/RulesEngine/createDecisionProvider.js +++ b/packages/core/src/components/RulesEngine/createDecisionProvider.js @@ -14,6 +14,7 @@ import { getActivityId } from "./utils/index.js"; export default ({ eventRegistry }) => { const payloadsBasedOnActivityId = {}; + const passthroughPayloads = []; const addPayload = (payload) => { const activityId = getActivityId(payload); @@ -28,6 +29,8 @@ export default ({ eventRegistry }) => { if (evaluableRulesetPayload.isEvaluable) { payloadsBasedOnActivityId[activityId] = evaluableRulesetPayload; + } else if (Array.isArray(payload.items) && payload.items.length > 0) { + passthroughPayloads.push(payload); } }; @@ -36,9 +39,11 @@ export default ({ eventRegistry }) => { payloadsBasedOnActivityId, ).sort(({ rank: rankA }, { rank: rankB }) => rankA - rankB); - return sortedPayloadsBasedOnActivityId + const evaluatedPayloads = sortedPayloadsBasedOnActivityId .map((payload) => payload.evaluate(context)) .filter((payload) => payload.items.length > 0); + + return [...evaluatedPayloads, ...passthroughPayloads]; }; const addPayloads = (personalizationPayloads) => { diff --git a/packages/core/src/constants/schema.js b/packages/core/src/constants/schema.js index 2df5c5546..f559a89f5 100644 --- a/packages/core/src/constants/schema.js +++ b/packages/core/src/constants/schema.js @@ -18,6 +18,7 @@ export const HTML_CONTENT_ITEM = export const JSON_CONTENT_ITEM = "https://ns.adobe.com/personalization/json-content-item"; export const RULESET_ITEM = "https://ns.adobe.com/personalization/ruleset-item"; +export const INBOX_ITEM = "https://ns.adobe.com/personalization/inbox-item"; export const REDIRECT_ITEM = "https://ns.adobe.com/personalization/redirect-item"; diff --git a/packages/core/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js b/packages/core/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js index 21bbad23c..f13879824 100644 --- a/packages/core/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js +++ b/packages/core/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js @@ -22,6 +22,7 @@ import { REDIRECT_ITEM, RULESET_ITEM, MESSAGE_CONTENT_CARD, + INBOX_ITEM, } from "../../../../../src/constants/schema.js"; describe("Personalization::createPersonalizationDetails", () => { @@ -162,6 +163,7 @@ describe("Personalization::createPersonalizationDetails", () => { RULESET_ITEM, MESSAGE_IN_APP, MESSAGE_CONTENT_CARD, + INBOX_ITEM, DOM_ACTION, ], decisionScopes: expectedDecisionScopes, @@ -201,6 +203,7 @@ describe("Personalization::createPersonalizationDetails", () => { RULESET_ITEM, MESSAGE_IN_APP, MESSAGE_CONTENT_CARD, + INBOX_ITEM, DOM_ACTION, ], decisionScopes: expectedDecisionScopes, @@ -240,6 +243,7 @@ describe("Personalization::createPersonalizationDetails", () => { RULESET_ITEM, MESSAGE_IN_APP, MESSAGE_CONTENT_CARD, + INBOX_ITEM, DOM_ACTION, ], decisionScopes: expectedDecisionScopes, @@ -279,6 +283,7 @@ describe("Personalization::createPersonalizationDetails", () => { RULESET_ITEM, MESSAGE_IN_APP, MESSAGE_CONTENT_CARD, + INBOX_ITEM, ], decisionScopes: expectedDecisionScopes, surfaces: [], @@ -319,6 +324,7 @@ describe("Personalization::createPersonalizationDetails", () => { RULESET_ITEM, MESSAGE_IN_APP, MESSAGE_CONTENT_CARD, + INBOX_ITEM, ], decisionScopes: expectedDecisionScopes, surfaces: ["web://test1.com/"], @@ -360,6 +366,7 @@ describe("Personalization::createPersonalizationDetails", () => { RULESET_ITEM, MESSAGE_IN_APP, MESSAGE_CONTENT_CARD, + INBOX_ITEM, ], decisionScopes: expectedDecisionScopes, surfaces: ["web://test1.com/"], @@ -487,6 +494,7 @@ describe("Personalization::createPersonalizationDetails", () => { RULESET_ITEM, MESSAGE_IN_APP, MESSAGE_CONTENT_CARD, + INBOX_ITEM, DOM_ACTION, ], decisionScopes: expectedDecisionScopes, diff --git a/packages/core/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js b/packages/core/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js index c0795fa41..c6b9773e7 100644 --- a/packages/core/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js +++ b/packages/core/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js @@ -37,6 +37,7 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/ruleset-item", "https://ns.adobe.com/personalization/message/in-app", "https://ns.adobe.com/personalization/message/content-card", + "https://ns.adobe.com/personalization/inbox-item", "https://ns.adobe.com/personalization/dom-action", ], decisionScopes: ["__view__"], @@ -172,6 +173,7 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/ruleset-item", "https://ns.adobe.com/personalization/message/in-app", "https://ns.adobe.com/personalization/message/content-card", + "https://ns.adobe.com/personalization/inbox-item", "https://ns.adobe.com/personalization/dom-action", ], decisionScopes: ["__view__"], diff --git a/packages/core/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js b/packages/core/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js index b1e631360..53ed30605 100644 --- a/packages/core/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js +++ b/packages/core/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js @@ -35,6 +35,7 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/ruleset-item", "https://ns.adobe.com/personalization/message/in-app", "https://ns.adobe.com/personalization/message/content-card", + "https://ns.adobe.com/personalization/inbox-item", "https://ns.adobe.com/personalization/dom-action", ], decisionScopes: ["__view__"], diff --git a/packages/core/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js b/packages/core/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js index 95c0ffb2a..5eb0eebdf 100644 --- a/packages/core/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js +++ b/packages/core/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js @@ -38,6 +38,7 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/ruleset-item", "https://ns.adobe.com/personalization/message/in-app", "https://ns.adobe.com/personalization/message/content-card", + "https://ns.adobe.com/personalization/inbox-item", "https://ns.adobe.com/personalization/dom-action", ], decisionScopes: ["__view__"], diff --git a/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js b/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js index 5093dbd5f..cdac0e8ea 100644 --- a/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js +++ b/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js @@ -36,6 +36,7 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/ruleset-item", "https://ns.adobe.com/personalization/message/in-app", "https://ns.adobe.com/personalization/message/content-card", + "https://ns.adobe.com/personalization/inbox-item", "https://ns.adobe.com/personalization/dom-action", ], decisionScopes: ["__view__"], diff --git a/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js b/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js index a2623925b..4c22ad806 100644 --- a/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js +++ b/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js @@ -37,6 +37,7 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/ruleset-item", "https://ns.adobe.com/personalization/message/in-app", "https://ns.adobe.com/personalization/message/content-card", + "https://ns.adobe.com/personalization/inbox-item", "https://ns.adobe.com/personalization/dom-action", ], decisionScopes: ["__view__"], diff --git a/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js b/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js index e4c644292..a3d75e436 100644 --- a/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js +++ b/packages/core/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js @@ -37,6 +37,7 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/ruleset-item", "https://ns.adobe.com/personalization/message/in-app", "https://ns.adobe.com/personalization/message/content-card", + "https://ns.adobe.com/personalization/inbox-item", "https://ns.adobe.com/personalization/dom-action", ], decisionScopes: ["__view__"], diff --git a/packages/core/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js b/packages/core/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js index 666dafa24..b287b866a 100644 --- a/packages/core/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js +++ b/packages/core/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js @@ -35,6 +35,7 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/ruleset-item", "https://ns.adobe.com/personalization/message/in-app", "https://ns.adobe.com/personalization/message/content-card", + "https://ns.adobe.com/personalization/inbox-item", "https://ns.adobe.com/personalization/dom-action", ], decisionScopes: ["__view__"], diff --git a/packages/core/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js b/packages/core/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js index 8f9bddc91..f9bc166e3 100644 --- a/packages/core/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js +++ b/packages/core/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js @@ -35,6 +35,7 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/ruleset-item", "https://ns.adobe.com/personalization/message/in-app", "https://ns.adobe.com/personalization/message/content-card", + "https://ns.adobe.com/personalization/inbox-item", "https://ns.adobe.com/personalization/dom-action", ], decisionScopes: ["__view__"], diff --git a/packages/core/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js b/packages/core/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js index 44d91ab9c..b82436332 100644 --- a/packages/core/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js +++ b/packages/core/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js @@ -35,6 +35,7 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/ruleset-item", "https://ns.adobe.com/personalization/message/in-app", "https://ns.adobe.com/personalization/message/content-card", + "https://ns.adobe.com/personalization/inbox-item", "https://ns.adobe.com/personalization/dom-action", ], decisionScopes: ["__view__"], diff --git a/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js b/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js index 88c6d22d0..504aa4f82 100644 --- a/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js +++ b/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js @@ -444,8 +444,8 @@ describe("RulesEngine:createDecisionProvider", () => { }, ]); }); - it("ignores payloads that aren't json-ruleset type", () => { - decisionProvider.addPayload({ + it("passes through payloads that have items but no ruleset items (e.g. inbox-only or TGT dom-action)", () => { + const passthroughPayload = { id: "AT:eyJhY3Rpdml0eUlkIjoiMTQxMDY0IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", scope: "__view__", scopeDetails: { @@ -480,7 +480,8 @@ describe("RulesEngine:createDecisionProvider", () => { }, }, ], - }); - expect(decisionProvider.evaluate()).toEqual([]); + }; + decisionProvider.addPayload(passthroughPayload); + expect(decisionProvider.evaluate()).toEqual([passthroughPayload]); }); }); diff --git a/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.css b/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.css index 3fbd057da..b56d1e4f2 100644 --- a/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.css +++ b/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.css @@ -1,15 +1,52 @@ -/* Card.css */ +/* Content Cards container and cards */ +.content-cards-container { + margin: 24px 0; + max-width: 1000px; + border: 2px dashed #6b7280; + border-radius: 12px; + padding: 24px; + background-color: #f9fafb; +} + +.content-cards-container__title { + margin: 0 0 8px 0; + font-size: 1.125rem; + font-weight: 600; + color: #111827; +} + +.content-cards-container__description { + margin: 0 0 20px 0; + font-size: 0.875rem; + color: #6b7280; +} + +.content-cards-container__empty { + padding: 32px; + text-align: center; + color: #9ca3af; + font-size: 0.875rem; + border-radius: 8px; + background-color: #f3f4f6; +} + +.content-cards-container__list { + display: flex; + flex-direction: column; + gap: 16px; +} + .pretty-card { - color: black; - border: 1px solid #ccc; - border-radius: 8px; - padding: 16px; - margin: 16px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - background-color: lightblue; - transition: transform 0.2s; + color: black; + border: 1px solid #ccc; + border-radius: 8px; + padding: 16px; + margin: 0; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + background-color: lightblue; + transition: transform 0.2s; } .pretty-card:hover { - transform: scale(1.05); + transform: scale(1.05); } diff --git a/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.jsx b/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.jsx index 14b93fa98..5a4f2950e 100644 --- a/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.jsx +++ b/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.jsx @@ -542,6 +542,13 @@ const prettyDate = (value) => { return output; }; +const getContentString = (value) => { + if (value == null) return ""; + if (typeof value === "string") return value; + if (typeof value === "object" && value.content != null) return value.content; + return String(value); +}; + const createContentCard = (proposition, item) => { const { data = {} } = item; const { @@ -552,7 +559,7 @@ const createContentCard = (proposition, item) => { displayedDate, } = data; - return { + const flat = { ...content, meta, qualifiedDate, @@ -560,6 +567,13 @@ const createContentCard = (proposition, item) => { publishedDate, getProposition: () => proposition, }; + if (flat.title != null && typeof flat.title === "object") { + flat.title = getContentString(flat.title); + } + if (flat.body != null && typeof flat.body === "object") { + flat.body = getContentString(flat.body); + } + return flat; }; const extractContentCards = (propositions) => @@ -749,35 +763,53 @@ export default function ContentCards() { Reset -
-

Content Cards

-
- {contentCards.map((item, index) => ( -
onClickedContentCard([item])} - > - -

{item.title}

-

- {item.imageUrl && Item Image} -

-

{item.body}

-

Published: {prettyDate(item.publishedDate)}

-

Qualified: {prettyDate(item.qualifiedDate)}

-

Displayed: {prettyDate(item.displayedDate)}

+
+

Content Cards

+

+ Cards returned from subscribeRulesetItems (content-card + schema) appear below. Use Response Source and the action buttons to + trigger or reset data. +

+
+ {contentCards.length === 0 ? ( +
+ No content cards yet. Select a response source and load the page + (or trigger an event) to see cards here.
- ))} + ) : ( + contentCards.map((item, index) => ( +
onClickedContentCard([item])} + > + +

{item.title}

+

+ {item.imageUrl && ( + Item Image + )} +

+

{item.body}

+

Published: {prettyDate(item.publishedDate)}

+

Qualified: {prettyDate(item.qualifiedDate)}

+

Displayed: {prettyDate(item.displayedDate)}

+
+ )) + )}
-
+
); } From 8715fd3f661f90e84a47c030535fa64c9af0d388 Mon Sep 17 00:00:00 2001 From: Spencer Smith Date: Tue, 24 Feb 2026 15:09:38 -0700 Subject: [PATCH 2/5] Add changeset file --- .changeset/every-ravens-look.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/every-ravens-look.md diff --git a/.changeset/every-ravens-look.md b/.changeset/every-ravens-look.md new file mode 100644 index 000000000..a204bdc12 --- /dev/null +++ b/.changeset/every-ravens-look.md @@ -0,0 +1,5 @@ +--- +"@adobe/alloy": patch +--- + +Passes inbox item after user subscribes to ruleset items From df31cc26aebc33764bbaaf155c9690d9fc278815 Mon Sep 17 00:00:00 2001 From: Spencer Smith Date: Wed, 25 Feb 2026 15:22:07 -0700 Subject: [PATCH 3/5] Only let inbox-item payloads passthrough decision provider; Add sandbox demo for message inbox --- .../RulesEngine/createDecisionProvider.js | 7 +- .../createDecisionProvider.spec.js | 54 ++- sandboxes/browser/src/App.jsx | 5 + .../ContentCardsDemo/ContentCards.css | 41 +- .../ContentCardsDemo/ContentCards.jsx | 88 ++-- .../MessageInboxDemo/MessageInbox.css | 256 ++++++++++ .../MessageInboxDemo/MessageInbox.jsx | 448 ++++++++++++++++++ 7 files changed, 780 insertions(+), 119 deletions(-) create mode 100644 sandboxes/browser/src/components/MessageInboxDemo/MessageInbox.css create mode 100644 sandboxes/browser/src/components/MessageInboxDemo/MessageInbox.jsx diff --git a/packages/core/src/components/RulesEngine/createDecisionProvider.js b/packages/core/src/components/RulesEngine/createDecisionProvider.js index 5bf66b7e9..ed735972e 100644 --- a/packages/core/src/components/RulesEngine/createDecisionProvider.js +++ b/packages/core/src/components/RulesEngine/createDecisionProvider.js @@ -9,9 +9,14 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +import { INBOX_ITEM } from "../../constants/schema.js"; import createEvaluableRulesetPayload from "./createEvaluableRulesetPayload.js"; import { getActivityId } from "./utils/index.js"; +const hasInboxItem = (payload) => + Array.isArray(payload.items) && + payload.items.some((item) => item.schema === INBOX_ITEM); + export default ({ eventRegistry }) => { const payloadsBasedOnActivityId = {}; const passthroughPayloads = []; @@ -29,7 +34,7 @@ export default ({ eventRegistry }) => { if (evaluableRulesetPayload.isEvaluable) { payloadsBasedOnActivityId[activityId] = evaluableRulesetPayload; - } else if (Array.isArray(payload.items) && payload.items.length > 0) { + } else if (hasInboxItem(payload)) { passthroughPayloads.push(payload); } }; diff --git a/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js b/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js index 504aa4f82..ba0becd2f 100644 --- a/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js +++ b/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js @@ -444,27 +444,43 @@ describe("RulesEngine:createDecisionProvider", () => { }, ]); }); - it("passes through payloads that have items but no ruleset items (e.g. inbox-only or TGT dom-action)", () => { - const passthroughPayload = { + it("passes through payloads that contain inbox items but are not evaluable rulesets", () => { + const inboxPayload = { + id: "66e05490-5e91-45c4-8eee-339784032940", + scope: "mobileapp://com.app/trendingnow", + scopeDetails: { + decisionProvider: "AJO", + activity: { id: "99db8aff4-82af-460e-8524-73e1441afdfa#id" }, + correlationID: "za380bc9-aea0-486e-85f4-5904cc53124d-0", + }, + items: [ + { + id: "569d1166-d3e0-4aea-b9a7-6de8ebdf3aec", + schema: "https://ns.adobe.com/personalization/inbox-item", + data: { + content: { + heading: { content: "Trending Now Inbox" }, + layout: { orientation: "horizontal" }, + capacity: 10, + }, + }, + }, + ], + }; + decisionProvider.addPayload(inboxPayload); + expect(decisionProvider.evaluate()).toEqual([inboxPayload]); + }); + + it("does not pass through payloads that have items but no inbox items (e.g. TGT dom-action only)", () => { + const tgtPayload = { id: "AT:eyJhY3Rpdml0eUlkIjoiMTQxMDY0IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", scope: "__view__", scopeDetails: { decisionProvider: "TGT", - activity: { - id: "141064", - }, - experience: { - id: "0", - }, - strategies: [ - { - algorithmID: "0", - trafficType: "0", - }, - ], - characteristics: { - eventToken: "abc", - }, + activity: { id: "141064" }, + experience: { id: "0" }, + strategies: [{ algorithmID: "0", trafficType: "0" }], + characteristics: { eventToken: "abc" }, correlationID: "141064:0:0:0", }, items: [ @@ -481,7 +497,7 @@ describe("RulesEngine:createDecisionProvider", () => { }, ], }; - decisionProvider.addPayload(passthroughPayload); - expect(decisionProvider.evaluate()).toEqual([passthroughPayload]); + decisionProvider.addPayload(tgtPayload); + expect(decisionProvider.evaluate()).toEqual([]); }); }); diff --git a/sandboxes/browser/src/App.jsx b/sandboxes/browser/src/App.jsx index 648c33b59..0f82bd7cf 100755 --- a/sandboxes/browser/src/App.jsx +++ b/sandboxes/browser/src/App.jsx @@ -32,6 +32,7 @@ import AlloyVersion from "./components/AlloyVersion"; import ConfigOverrides from "./ConfigOverrides"; import InAppMessages from "./components/InAppMessagesDemo/InAppMessages"; import ContentCards from "./components/ContentCardsDemo/ContentCards"; +import MessageInbox from "./components/MessageInboxDemo/MessageInbox"; import PushNotifications from "./PushNotifications"; import ReferrerTest from "./ReferrerTest"; import Advertising from "./Advertising"; @@ -108,6 +109,9 @@ const BasicExample = () => {
  • Content Cards
  • +
  • + Message Inbox +
  • Push Notifications
  • @@ -149,6 +153,7 @@ const BasicExample = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.css b/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.css index b56d1e4f2..0c91c1d2a 100644 --- a/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.css +++ b/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.css @@ -1,47 +1,10 @@ -/* Content Cards container and cards */ -.content-cards-container { - margin: 24px 0; - max-width: 1000px; - border: 2px dashed #6b7280; - border-radius: 12px; - padding: 24px; - background-color: #f9fafb; -} - -.content-cards-container__title { - margin: 0 0 8px 0; - font-size: 1.125rem; - font-weight: 600; - color: #111827; -} - -.content-cards-container__description { - margin: 0 0 20px 0; - font-size: 0.875rem; - color: #6b7280; -} - -.content-cards-container__empty { - padding: 32px; - text-align: center; - color: #9ca3af; - font-size: 0.875rem; - border-radius: 8px; - background-color: #f3f4f6; -} - -.content-cards-container__list { - display: flex; - flex-direction: column; - gap: 16px; -} - +/* Card.css */ .pretty-card { color: black; border: 1px solid #ccc; border-radius: 8px; padding: 16px; - margin: 0; + margin: 16px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); background-color: lightblue; transition: transform 0.2s; diff --git a/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.jsx b/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.jsx index 5a4f2950e..14b93fa98 100644 --- a/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.jsx +++ b/sandboxes/browser/src/components/ContentCardsDemo/ContentCards.jsx @@ -542,13 +542,6 @@ const prettyDate = (value) => { return output; }; -const getContentString = (value) => { - if (value == null) return ""; - if (typeof value === "string") return value; - if (typeof value === "object" && value.content != null) return value.content; - return String(value); -}; - const createContentCard = (proposition, item) => { const { data = {} } = item; const { @@ -559,7 +552,7 @@ const createContentCard = (proposition, item) => { displayedDate, } = data; - const flat = { + return { ...content, meta, qualifiedDate, @@ -567,13 +560,6 @@ const createContentCard = (proposition, item) => { publishedDate, getProposition: () => proposition, }; - if (flat.title != null && typeof flat.title === "object") { - flat.title = getContentString(flat.title); - } - if (flat.body != null && typeof flat.body === "object") { - flat.body = getContentString(flat.body); - } - return flat; }; const extractContentCards = (propositions) => @@ -763,53 +749,35 @@ export default function ContentCards() { Reset
    -
    -

    Content Cards

    -

    - Cards returned from subscribeRulesetItems (content-card - schema) appear below. Use Response Source and the action buttons to - trigger or reset data. -

    -
    - {contentCards.length === 0 ? ( -
    - No content cards yet. Select a response source and load the page - (or trigger an event) to see cards here. -
    - ) : ( - contentCards.map((item, index) => ( -
    onClickedContentCard([item])} +
    +

    Content Cards

    +
    + {contentCards.map((item, index) => ( +
    onClickedContentCard([item])} + > + -

    {item.title}

    -

    - {item.imageUrl && ( - Item Image - )} -

    -

    {item.body}

    -

    Published: {prettyDate(item.publishedDate)}

    -

    Qualified: {prettyDate(item.qualifiedDate)}

    -

    Displayed: {prettyDate(item.displayedDate)}

    -
    - )) - )} + dismiss + +

    {item.title}

    +

    + {item.imageUrl && Item Image} +

    +

    {item.body}

    +

    Published: {prettyDate(item.publishedDate)}

    +

    Qualified: {prettyDate(item.qualifiedDate)}

    +

    Displayed: {prettyDate(item.displayedDate)}

    +
    + ))}
    -
    + ); } diff --git a/sandboxes/browser/src/components/MessageInboxDemo/MessageInbox.css b/sandboxes/browser/src/components/MessageInboxDemo/MessageInbox.css new file mode 100644 index 000000000..2eb0705e3 --- /dev/null +++ b/sandboxes/browser/src/components/MessageInboxDemo/MessageInbox.css @@ -0,0 +1,256 @@ +/* Message Inbox page */ +.message-inbox-page { + padding: 24px; + max-width: 640px; +} + +.message-inbox-page__title { + margin: 0 0 8px 0; + font-size: 1.5rem; + font-weight: 600; +} + +.message-inbox-page__intro { + margin: 0 0 24px 0; + color: #6b7280; + font-size: 0.875rem; +} + +/* Inbox trigger button */ +.message-inbox-trigger { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + font-size: 1rem; + font-weight: 500; + color: #fff; + background-color: #2563eb; + border: none; + border-radius: 8px; + cursor: pointer; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.message-inbox-trigger:hover { + background-color: #1d4ed8; +} + +.message-inbox-trigger__badge { + min-width: 20px; + padding: 2px 6px; + font-size: 0.75rem; + font-weight: 600; + color: #2563eb; + background-color: #fff; + border-radius: 10px; +} + +/* Toasts */ +.message-inbox-toasts { + position: fixed; + top: 24px; + right: 24px; + z-index: 999; + display: flex; + flex-direction: column; + gap: 10px; + max-width: 320px; + pointer-events: none; +} + +.message-inbox-toast { + pointer-events: auto; + padding: 12px 16px; + background-color: #fff; + border: 1px solid #e5e7eb; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + cursor: pointer; + transition: opacity 0.2s, transform 0.2s; + animation: message-inbox-toast-in 0.25s ease-out; +} + +.message-inbox-toast:hover { + opacity: 0.95; +} + +.message-inbox-toast__title { + margin: 0 0 4px 0; + font-size: 0.875rem; + font-weight: 600; + color: #111827; +} + +.message-inbox-toast__body { + margin: 0; + font-size: 0.8125rem; + color: #6b7280; + line-height: 1.35; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +@keyframes message-inbox-toast-in { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* Sidebar overlay and panel */ +.message-inbox-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.4); + z-index: 1000; +} + +.message-inbox-sidebar { + position: fixed; + top: 0; + right: 0; + width: 380px; + max-width: 100%; + height: 100%; + background-color: #fff; + box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15); + z-index: 1001; + display: flex; + flex-direction: column; +} + +.message-inbox-sidebar__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px; + border-bottom: 1px solid #e5e7eb; + flex-shrink: 0; +} + +.message-inbox-sidebar__heading { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: #111827; +} + +.message-inbox-sidebar__close { + padding: 6px; + font-size: 1.25rem; + line-height: 1; + color: #6b7280; + background: none; + border: none; + cursor: pointer; + border-radius: 4px; +} + +.message-inbox-sidebar__close:hover { + color: #111; + background-color: #f3f4f6; +} + +.message-inbox-sidebar__list { + flex: 1; + overflow-y: auto; + padding: 16px; +} + +.message-inbox-sidebar__empty { + padding: 32px 16px; + text-align: center; + color: #9ca3af; + font-size: 0.875rem; +} + +/* Inbox message card (from content-card) */ +.inbox-message { + padding: 14px 16px; + margin-bottom: 12px; + background-color: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.15s; +} + +.inbox-message:hover { + background-color: #f3f4f6; +} + +.inbox-message--read { + opacity: 0.75; +} + +.inbox-message--read .inbox-message__title { + font-weight: 500; +} + +.inbox-message__title { + margin: 0 0 4px 0; + font-size: 0.9375rem; + font-weight: 600; + color: #111827; +} + +.inbox-message__body { + margin: 0; + font-size: 0.8125rem; + color: #6b7280; + line-height: 1.4; +} + +.inbox-message__meta { + margin-top: 8px; + font-size: 0.75rem; + color: #9ca3af; +} + +/* Send messages section */ +.send-messages { + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid #e5e7eb; +} + +.send-messages__title { + margin: 0 0 12px 0; + font-size: 1rem; + font-weight: 600; +} + +.send-messages__hint { + margin: 0 0 16px 0; + font-size: 0.8125rem; + color: #6b7280; +} + +.send-messages__buttons { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.send-messages__btn { + padding: 8px 16px; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + background-color: #fff; + border: 1px solid #d1d5db; + border-radius: 6px; + cursor: pointer; +} + +.send-messages__btn:hover { + background-color: #f9fafb; + border-color: #9ca3af; +} diff --git a/sandboxes/browser/src/components/MessageInboxDemo/MessageInbox.jsx b/sandboxes/browser/src/components/MessageInboxDemo/MessageInbox.jsx new file mode 100644 index 000000000..30152a6d8 --- /dev/null +++ b/sandboxes/browser/src/components/MessageInboxDemo/MessageInbox.jsx @@ -0,0 +1,448 @@ +/* +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 in the License. +*/ +import { useEffect, useState, useRef, useCallback } from "react"; +import ContentSecurityPolicy from "../ContentSecurityPolicy"; +import "./MessageInbox.css"; +import { getAlloyTestConfigs } from "../utils"; +import useAlloy from "../../helpers/useAlloy"; + +const configKey = localStorage.getItem("iam-configKey") || "stage"; +const config = getAlloyTestConfigs(); +const { datastreamId, orgId, edgeDomain } = config[configKey]; + +const SURFACE = "web://aepdemo.com/messageInbox"; +const CONTENT_CARD_SCHEMA = + "https://ns.adobe.com/personalization/message/content-card"; +const INBOX_ITEM_SCHEMA = "https://ns.adobe.com/personalization/inbox-item"; +const RULESET_ITEM_SCHEMA = "https://ns.adobe.com/personalization/ruleset-item"; + +const getContentString = (value) => { + if (value == null) return ""; + if (typeof value === "string") return value; + if (typeof value === "object" && value.content != null) return value.content; + return String(value); +}; + +const createContentCard = (proposition, item) => { + const { data = {}, id: itemId } = item; + const { + content = {}, + meta = {}, + publishedDate, + qualifiedDate, + displayedDate, + } = data; + const flat = { + id: itemId ?? proposition?.id, + ...content, + meta, + qualifiedDate, + displayedDate, + publishedDate, + getProposition: () => proposition, + }; + if (flat.title != null && typeof flat.title === "object") { + flat.title = getContentString(flat.title); + } + if (flat.body != null && typeof flat.body === "object") { + flat.body = getContentString(flat.body); + } + return flat; +}; + +const extractContentCards = (propositions) => + propositions.reduce((all, prop) => { + const { items = [] } = prop; + return [ + ...all, + ...items + .filter((item) => item.schema === CONTENT_CARD_SCHEMA) + .map((item) => createContentCard(prop, item)), + ]; + }, []); + +const extractInboxConfig = (propositions) => { + for (const prop of propositions) { + const { items = [] } = prop; + const inboxItem = items.find((item) => item.schema === INBOX_ITEM_SCHEMA); + if (inboxItem?.data?.content) return inboxItem.data.content; + } + return null; +}; + +const buildInboxItemProposition = (overrides = {}) => ({ + id: "inbox-config-1", + scope: SURFACE, + scopeDetails: { + decisionProvider: "AJO", + correlationID: "inbox-demo-0", + activity: { id: "inbox-demo#config" }, + }, + items: [ + { + id: "inbox-item-1", + schema: INBOX_ITEM_SCHEMA, + data: { + content: { + heading: { content: "Message Inbox" }, + layout: { orientation: "vertical" }, + capacity: 20, + isUnreadEnabled: true, + emptyStateSettings: { + message: { + content: "No messages yet. Send one using the buttons below.", + }, + }, + ...overrides, + }, + }, + }, + ], +}); + +const buildContentCardProposition = ({ id, title, body }) => { + const ts = Date.now(); + const cardId = `card-${id}-${ts}`; + return { + id: `msg-${id}-${ts}`, + scope: SURFACE, + scopeDetails: { + decisionProvider: "AJO", + correlationID: `msg-${id}-${ts}`, + activity: { id: `msg-${id}-${ts}#activity` }, + }, + items: [ + { + id: `ruleset-${id}-${ts}`, + schema: RULESET_ITEM_SCHEMA, + data: { + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + key: "~type", + matcher: "eq", + values: ["always"], + }, + type: "matcher", + }, + ], + logic: "and", + }, + type: "group", + }, + consequences: [ + { + type: "schema", + detail: { + schema: CONTENT_CARD_SCHEMA, + id: cardId, + data: { + content: { + title: { content: title }, + body: { content: body }, + }, + contentType: "application/json", + publishedDate: Date.now(), + expiryDate: Math.floor(Date.now() / 1000) + 86400 * 30, + meta: { surface: SURFACE }, + }, + }, + id: cardId, + }, + ], + }, + ], + }, + }, + ], + }; +}; + +const buildHandle = (propositions) => ({ + requestId: `mock-${Date.now()}`, + handle: [ + { + type: "personalization:decisions", + eventIndex: 0, + payload: propositions, + }, + ], +}); + +const INITIAL_INBOX_RESPONSE = buildHandle([buildInboxItemProposition()]); + +const TOAST_DURATION_MS = 4000; + +const MESSAGE_TEMPLATES = [ + { + id: "welcome", + title: "Welcome", + body: "Thanks for visiting. Here’s a quick tip: open the Message Inbox to see notifications from this demo.", + }, + { + id: "promo", + title: "Limited-time offer", + body: "Get 20% off your next order. Use code DEMO20 at checkout.", + }, + { + id: "alert", + title: "Reminder", + body: "Your session will expire in 15 minutes. Save your work if needed.", + }, + { + id: "update", + title: "Update", + body: "We’ve added a new Message Inbox demo. Check it out from the sidebar.", + }, +]; + +export default function MessageInbox() { + const [messages, setMessages] = useState([]); + const [toasts, setToasts] = useState([]); + const [inboxConfig, setInboxConfig] = useState(null); + const [inboxOpen, setInboxOpen] = useState(false); + const unsubscribeRef = useRef(null); + const toastTimeoutsRef = useRef({}); + const dismissToastRef = useRef(() => {}); + const messagesRef = useRef([]); + messagesRef.current = messages; + + useAlloy({ + configurations: { + alloy: { + defaultConsent: "in", + datastreamId, + orgId, + edgeDomain, + thirdPartyCookiesEnabled: false, + targetMigrationEnabled: false, + personalizationStorageEnabled: true, + debugEnabled: true, + }, + }, + }); + + useEffect(() => { + if (!window.alloy) return; + + const sub = window.alloy("subscribeRulesetItems", { + surfaces: [SURFACE], + schemas: [CONTENT_CARD_SCHEMA, INBOX_ITEM_SCHEMA], + callback: (result, collectEvent) => { + const { propositions = [] } = result; + const newCards = extractContentCards(propositions); + const configFromPayload = extractInboxConfig(propositions); + + const prev = messagesRef.current; + const existingIds = new Set(prev.map((m) => m.id).filter(Boolean)); + const toAdd = newCards + .filter((m) => m.id == null || !existingIds.has(m.id)) + .map((m) => ({ ...m, read: false })); + + if (toAdd.length > 0) { + setMessages((p) => [...p, ...toAdd]); + setToasts((t) => [...t, ...toAdd]); + toAdd.forEach((card) => { + const timeoutId = setTimeout(() => { + dismissToastRef.current(card.id); + }, TOAST_DURATION_MS); + toastTimeoutsRef.current[card.id] = timeoutId; + }); + } + if (configFromPayload) setInboxConfig(configFromPayload); + collectEvent("display", propositions); + }, + }); + + unsubscribeRef.current = sub; + + window.alloy("applyResponse", { + renderDecisions: true, + responseBody: INITIAL_INBOX_RESPONSE, + personalization: { + decisionContext: { "~type": "always" }, + }, + }); + + return () => { + sub.then((r) => r?.unsubscribe?.()); + Object.values(toastTimeoutsRef.current).forEach(clearTimeout); + toastTimeoutsRef.current = {}; + }; + }, []); + + const dismissToast = useCallback((messageId) => { + const timeoutId = toastTimeoutsRef.current[messageId]; + if (timeoutId) clearTimeout(timeoutId); + delete toastTimeoutsRef.current[messageId]; + setToasts((prev) => prev.filter((t) => t.id !== messageId)); + }, []); + + dismissToastRef.current = dismissToast; + + const sendMessage = (template) => { + if (!window.alloy) return; + const proposition = buildContentCardProposition(template); + window.alloy("applyResponse", { + renderDecisions: true, + responseBody: buildHandle([proposition]), + personalization: { + decisionContext: { "~type": "always" }, + surfaces: [SURFACE], + }, + }); + }; + + const markMessageRead = (msgId) => { + setMessages((prev) => + prev.map((m) => (m.id === msgId ? { ...m, read: true } : m)), + ); + }; + + const unreadCount = messages.filter((m) => !m.read).length; + + const heading = + inboxConfig?.heading != null + ? getContentString(inboxConfig.heading) + : "Message Inbox"; + const emptyMessage = + inboxConfig?.emptyStateSettings?.message != null + ? getContentString(inboxConfig.emptyStateSettings.message) + : "No messages yet. Send one using the buttons below."; + + return ( +
    + +

    Message Inbox

    +

    + This page demonstrates a "message inbox" fed by content cards + and configured by an inbox-item. Open the inbox to see messages from + this session; use the buttons below to add mock messages. +

    + + + +
    + {toasts.map((toast) => ( +
    { + markMessageRead(toast.id); + dismissToast(toast.id); + }} + > +

    {toast.title}

    +

    {toast.body}

    +
    + ))} +
    + +
    +

    Send a message

    +

    + Each button adds a new message to the inbox for this session (like + notifications that would come from the SDK when content cards are + returned). +

    +
    + {MESSAGE_TEMPLATES.map((t) => ( + + ))} +
    +
    + + {inboxOpen && ( + <> +
    setInboxOpen(false)} + onKeyDown={(e) => e.key === "Escape" && setInboxOpen(false)} + /> + + + )} +
    + ); +} From 17db6d0e570555988cbcf351934c1ad5bc6ad117 Mon Sep 17 00:00:00 2001 From: Spencer Smith Date: Thu, 26 Feb 2026 21:39:52 -0700 Subject: [PATCH 4/5] Allow payloads with inbox item to be evaluated rather than passed through in decision provider --- .../RulesEngine/createDecisionProvider.js | 12 +----- .../createEvaluableRulesetPayload.js | 33 ++++++++++++++- .../createDecisionProvider.spec.js | 25 +++++++++-- .../createEvaluableRulesetPayload.spec.js | 41 +++++++++++++++++++ 4 files changed, 95 insertions(+), 16 deletions(-) diff --git a/packages/core/src/components/RulesEngine/createDecisionProvider.js b/packages/core/src/components/RulesEngine/createDecisionProvider.js index ed735972e..709240d1c 100644 --- a/packages/core/src/components/RulesEngine/createDecisionProvider.js +++ b/packages/core/src/components/RulesEngine/createDecisionProvider.js @@ -9,17 +9,11 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { INBOX_ITEM } from "../../constants/schema.js"; import createEvaluableRulesetPayload from "./createEvaluableRulesetPayload.js"; import { getActivityId } from "./utils/index.js"; -const hasInboxItem = (payload) => - Array.isArray(payload.items) && - payload.items.some((item) => item.schema === INBOX_ITEM); - export default ({ eventRegistry }) => { const payloadsBasedOnActivityId = {}; - const passthroughPayloads = []; const addPayload = (payload) => { const activityId = getActivityId(payload); @@ -34,8 +28,6 @@ export default ({ eventRegistry }) => { if (evaluableRulesetPayload.isEvaluable) { payloadsBasedOnActivityId[activityId] = evaluableRulesetPayload; - } else if (hasInboxItem(payload)) { - passthroughPayloads.push(payload); } }; @@ -44,11 +36,9 @@ export default ({ eventRegistry }) => { payloadsBasedOnActivityId, ).sort(({ rank: rankA }, { rank: rankB }) => rankA - rankB); - const evaluatedPayloads = sortedPayloadsBasedOnActivityId + return sortedPayloadsBasedOnActivityId .map((payload) => payload.evaluate(context)) .filter((payload) => payload.items.length > 0); - - return [...evaluatedPayloads, ...passthroughPayloads]; }; const addPayloads = (personalizationPayloads) => { diff --git a/packages/core/src/components/RulesEngine/createEvaluableRulesetPayload.js b/packages/core/src/components/RulesEngine/createEvaluableRulesetPayload.js index c233eca11..21a133b4e 100644 --- a/packages/core/src/components/RulesEngine/createEvaluableRulesetPayload.js +++ b/packages/core/src/components/RulesEngine/createEvaluableRulesetPayload.js @@ -10,7 +10,11 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ import RulesEngine from "@adobe/aep-rules-engine"; -import { JSON_CONTENT_ITEM, RULESET_ITEM } from "../../constants/schema.js"; +import { + INBOX_ITEM, + JSON_CONTENT_ITEM, + RULESET_ITEM, +} from "../../constants/schema.js"; import { DISPLAY } from "../../constants/eventType.js"; import { PropositionEventType } from "../../constants/propositionEventType.js"; import { generateEventHash, getActivityId } from "./utils/index.js"; @@ -45,6 +49,8 @@ const isRulesetItem = (item) => { } }; +const isInboxItem = (item) => item?.schema === INBOX_ITEM; + export default (payload, eventRegistry) => { const consequenceAdapter = createConsequenceAdapter(); const activityId = getActivityId(payload); @@ -66,10 +72,33 @@ export default (payload, eventRegistry) => { ); }; + const inboxItems = Array.isArray(payload.items) + ? payload.items.filter(isInboxItem) + : []; + const evaluate = (context) => { const displayEvent = eventRegistry.getEvent(DISPLAY, activityId); const displayedDate = displayEvent?.timestamps[0]; + if (items.length === 0 && inboxItems.length > 0) { + const event = eventRegistry.addEvent({ + eventType: PropositionEventType.TRIGGER, + eventId: activityId, + }); + const qualifiedDate = event.timestamps[0]; + return { + ...payload, + items: inboxItems.map((item) => ({ + ...item, + data: { + ...item.data, + qualifiedDate, + displayedDate, + }, + })), + }; + } + const qualifyingItems = flattenArray( items.map((item) => item.execute(context)), ) @@ -101,6 +130,6 @@ export default (payload, eventRegistry) => { return { rank: payload?.scopeDetails?.rank || Infinity, evaluate, - isEvaluable: items.length > 0, + isEvaluable: items.length > 0 || inboxItems.length > 0, }; }; diff --git a/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js b/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js index ba0becd2f..d9a4f666f 100644 --- a/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js +++ b/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js @@ -444,7 +444,7 @@ describe("RulesEngine:createDecisionProvider", () => { }, ]); }); - it("passes through payloads that contain inbox items but are not evaluable rulesets", () => { + it("returns payloads that contain inbox items as evaluable propositions (no rules engine)", () => { const inboxPayload = { id: "66e05490-5e91-45c4-8eee-339784032940", scope: "mobileapp://com.app/trendingnow", @@ -468,10 +468,29 @@ describe("RulesEngine:createDecisionProvider", () => { ], }; decisionProvider.addPayload(inboxPayload); - expect(decisionProvider.evaluate()).toEqual([inboxPayload]); + const result = decisionProvider.evaluate(); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + id: inboxPayload.id, + scope: inboxPayload.scope, + items: [ + { + id: "569d1166-d3e0-4aea-b9a7-6de8ebdf3aec", + schema: "https://ns.adobe.com/personalization/inbox-item", + data: { + content: { + heading: { content: "Trending Now Inbox" }, + layout: { orientation: "horizontal" }, + capacity: 10, + }, + qualifiedDate: expect.any(Number), + }, + }, + ], + }); }); - it("does not pass through payloads that have items but no inbox items (e.g. TGT dom-action only)", () => { + it("does not add payloads that have items but no inbox or ruleset items (e.g. TGT dom-action only)", () => { const tgtPayload = { id: "AT:eyJhY3Rpdml0eUlkIjoiMTQxMDY0IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", scope: "__view__", diff --git a/packages/core/test/unit/specs/components/RulesEngine/createEvaluableRulesetPayload.spec.js b/packages/core/test/unit/specs/components/RulesEngine/createEvaluableRulesetPayload.spec.js index fb906b258..cf7a17049 100644 --- a/packages/core/test/unit/specs/components/RulesEngine/createEvaluableRulesetPayload.spec.js +++ b/packages/core/test/unit/specs/components/RulesEngine/createEvaluableRulesetPayload.spec.js @@ -361,4 +361,45 @@ describe("RulesEngine:createEvaluableRulesetPayload", () => { scope: "web://mywebsite.com", }); }); + + it("returns payloads with only inbox items as evaluable, evaluate() returns those items with qualifiedDate/displayedDate", () => { + const payload = { + id: "66e05490-5e91-45c4-8eee-339784032940", + scopeDetails: { + decisionProvider: "AJO", + activity: { id: "99db8aff4-82af-460e-8524-73e1441afdfa#id" }, + }, + items: [ + { + id: "569d1166-d3e0-4aea-b9a7-6de8ebdf3aec", + schema: "https://ns.adobe.com/personalization/inbox-item", + data: { + content: { + heading: { content: "Trending Now Inbox" }, + capacity: 10, + }, + }, + }, + ], + }; + const evaluable = createEvaluableRulesetPayload(payload, eventRegistry); + expect(evaluable.isEvaluable).toBe(true); + const result = evaluable.evaluate({}); + expect(result).toMatchObject({ + id: payload.id, + items: [ + { + id: "569d1166-d3e0-4aea-b9a7-6de8ebdf3aec", + schema: "https://ns.adobe.com/personalization/inbox-item", + data: { + content: { + heading: { content: "Trending Now Inbox" }, + capacity: 10, + }, + qualifiedDate: expect.any(Number), + }, + }, + ], + }); + }); }); From 0c57d9df5c3e8efd6f73f0bc1ed8c83a8721c394 Mon Sep 17 00:00:00 2001 From: Spencer Smith Date: Fri, 27 Feb 2026 09:52:43 -0700 Subject: [PATCH 5/5] Rewrite: Extract inbox items at response handler rather than as rules --- .../createEvaluableRulesetPayload.js | 33 +--------- .../RulesEngine/createOnResponseHandler.js | 19 +++++- .../createDecisionProvider.spec.js | 24 +------- .../createEvaluableRulesetPayload.spec.js | 41 ------------- .../createOnResponseHandler.spec.js | 61 +++++++++++++++++++ 5 files changed, 81 insertions(+), 97 deletions(-) diff --git a/packages/core/src/components/RulesEngine/createEvaluableRulesetPayload.js b/packages/core/src/components/RulesEngine/createEvaluableRulesetPayload.js index 21a133b4e..c233eca11 100644 --- a/packages/core/src/components/RulesEngine/createEvaluableRulesetPayload.js +++ b/packages/core/src/components/RulesEngine/createEvaluableRulesetPayload.js @@ -10,11 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ import RulesEngine from "@adobe/aep-rules-engine"; -import { - INBOX_ITEM, - JSON_CONTENT_ITEM, - RULESET_ITEM, -} from "../../constants/schema.js"; +import { JSON_CONTENT_ITEM, RULESET_ITEM } from "../../constants/schema.js"; import { DISPLAY } from "../../constants/eventType.js"; import { PropositionEventType } from "../../constants/propositionEventType.js"; import { generateEventHash, getActivityId } from "./utils/index.js"; @@ -49,8 +45,6 @@ const isRulesetItem = (item) => { } }; -const isInboxItem = (item) => item?.schema === INBOX_ITEM; - export default (payload, eventRegistry) => { const consequenceAdapter = createConsequenceAdapter(); const activityId = getActivityId(payload); @@ -72,33 +66,10 @@ export default (payload, eventRegistry) => { ); }; - const inboxItems = Array.isArray(payload.items) - ? payload.items.filter(isInboxItem) - : []; - const evaluate = (context) => { const displayEvent = eventRegistry.getEvent(DISPLAY, activityId); const displayedDate = displayEvent?.timestamps[0]; - if (items.length === 0 && inboxItems.length > 0) { - const event = eventRegistry.addEvent({ - eventType: PropositionEventType.TRIGGER, - eventId: activityId, - }); - const qualifiedDate = event.timestamps[0]; - return { - ...payload, - items: inboxItems.map((item) => ({ - ...item, - data: { - ...item.data, - qualifiedDate, - displayedDate, - }, - })), - }; - } - const qualifyingItems = flattenArray( items.map((item) => item.execute(context)), ) @@ -130,6 +101,6 @@ export default (payload, eventRegistry) => { return { rank: payload?.scopeDetails?.rank || Infinity, evaluate, - isEvaluable: items.length > 0 || inboxItems.length > 0, + isEvaluable: items.length > 0, }; }; diff --git a/packages/core/src/components/RulesEngine/createOnResponseHandler.js b/packages/core/src/components/RulesEngine/createOnResponseHandler.js index 6fbfbac84..bd928107c 100644 --- a/packages/core/src/components/RulesEngine/createOnResponseHandler.js +++ b/packages/core/src/components/RulesEngine/createOnResponseHandler.js @@ -10,9 +10,18 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ +import { INBOX_ITEM } from "../../constants/schema.js"; import { PERSONALIZATION_DECISIONS_HANDLE } from "../../constants/decisionProvider.js"; import flattenObject from "../../utils/flattenObject.js"; +const extractInboxPropositions = (payloads) => + payloads.filter( + (payload) => + Array.isArray(payload.items) && + payload.items.length > 0 && + payload.items.every((item) => item.schema === INBOX_ITEM), + ); + export default ({ renderDecisions, decisionProvider, @@ -27,16 +36,20 @@ export default ({ }; return ({ response }) => { - decisionProvider.addPayloads( - response.getPayloadsByType(PERSONALIZATION_DECISIONS_HANDLE), + const personalizationPayloads = response.getPayloadsByType( + PERSONALIZATION_DECISIONS_HANDLE, ); + decisionProvider.addPayloads(personalizationPayloads); // only evaluate events that include a personalization query if (!event.hasQuery()) { return { propositions: [] }; } - const propositions = decisionProvider.evaluate(context); + const inboxPropositions = extractInboxPropositions(personalizationPayloads); + + const evaluatedPropositions = decisionProvider.evaluate(context); + const propositions = [...evaluatedPropositions, ...inboxPropositions]; return applyResponse({ renderDecisions, diff --git a/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js b/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js index d9a4f666f..e7a346922 100644 --- a/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js +++ b/packages/core/test/unit/specs/components/RulesEngine/createDecisionProvider.spec.js @@ -444,14 +444,13 @@ describe("RulesEngine:createDecisionProvider", () => { }, ]); }); - it("returns payloads that contain inbox items as evaluable propositions (no rules engine)", () => { + it("does not add payloads that contain only inbox items (handled in response handler)", () => { const inboxPayload = { id: "66e05490-5e91-45c4-8eee-339784032940", scope: "mobileapp://com.app/trendingnow", scopeDetails: { decisionProvider: "AJO", activity: { id: "99db8aff4-82af-460e-8524-73e1441afdfa#id" }, - correlationID: "za380bc9-aea0-486e-85f4-5904cc53124d-0", }, items: [ { @@ -468,26 +467,7 @@ describe("RulesEngine:createDecisionProvider", () => { ], }; decisionProvider.addPayload(inboxPayload); - const result = decisionProvider.evaluate(); - expect(result).toHaveLength(1); - expect(result[0]).toMatchObject({ - id: inboxPayload.id, - scope: inboxPayload.scope, - items: [ - { - id: "569d1166-d3e0-4aea-b9a7-6de8ebdf3aec", - schema: "https://ns.adobe.com/personalization/inbox-item", - data: { - content: { - heading: { content: "Trending Now Inbox" }, - layout: { orientation: "horizontal" }, - capacity: 10, - }, - qualifiedDate: expect.any(Number), - }, - }, - ], - }); + expect(decisionProvider.evaluate()).toEqual([]); }); it("does not add payloads that have items but no inbox or ruleset items (e.g. TGT dom-action only)", () => { diff --git a/packages/core/test/unit/specs/components/RulesEngine/createEvaluableRulesetPayload.spec.js b/packages/core/test/unit/specs/components/RulesEngine/createEvaluableRulesetPayload.spec.js index cf7a17049..fb906b258 100644 --- a/packages/core/test/unit/specs/components/RulesEngine/createEvaluableRulesetPayload.spec.js +++ b/packages/core/test/unit/specs/components/RulesEngine/createEvaluableRulesetPayload.spec.js @@ -361,45 +361,4 @@ describe("RulesEngine:createEvaluableRulesetPayload", () => { scope: "web://mywebsite.com", }); }); - - it("returns payloads with only inbox items as evaluable, evaluate() returns those items with qualifiedDate/displayedDate", () => { - const payload = { - id: "66e05490-5e91-45c4-8eee-339784032940", - scopeDetails: { - decisionProvider: "AJO", - activity: { id: "99db8aff4-82af-460e-8524-73e1441afdfa#id" }, - }, - items: [ - { - id: "569d1166-d3e0-4aea-b9a7-6de8ebdf3aec", - schema: "https://ns.adobe.com/personalization/inbox-item", - data: { - content: { - heading: { content: "Trending Now Inbox" }, - capacity: 10, - }, - }, - }, - ], - }; - const evaluable = createEvaluableRulesetPayload(payload, eventRegistry); - expect(evaluable.isEvaluable).toBe(true); - const result = evaluable.evaluate({}); - expect(result).toMatchObject({ - id: payload.id, - items: [ - { - id: "569d1166-d3e0-4aea-b9a7-6de8ebdf3aec", - schema: "https://ns.adobe.com/personalization/inbox-item", - data: { - content: { - heading: { content: "Trending Now Inbox" }, - capacity: 10, - }, - qualifiedDate: expect.any(Number), - }, - }, - ], - }); - }); }); diff --git a/packages/core/test/unit/specs/components/RulesEngine/createOnResponseHandler.spec.js b/packages/core/test/unit/specs/components/RulesEngine/createOnResponseHandler.spec.js index d434d18fe..ff5093b7b 100644 --- a/packages/core/test/unit/specs/components/RulesEngine/createOnResponseHandler.spec.js +++ b/packages/core/test/unit/specs/components/RulesEngine/createOnResponseHandler.spec.js @@ -396,4 +396,65 @@ describe("RulesEngine:createOnResponseHandler", () => { identityMap: undefined, }); }); + + it("converts inbox-only payloads to propositions and passes them to applyResponse with evaluated propositions", () => { + const event = { + getViewName: () => undefined, + getUserIdentityMap: () => undefined, + hasQuery: () => true, + getContent: () => ({ query: {}, xdm: {}, data: {} }), + }; + const responseHandler = createOnResponseHandler({ + renderDecisions: true, + decisionProvider, + applyResponse, + event, + personalization: {}, + decisionContext: {}, + }); + const inboxPayload = { + id: "66e05490-5e91-45c4-8eee-339784032940", + scope: "mobileapp://com.app/trendingnow", + scopeDetails: { + decisionProvider: "AJO", + activity: { id: "99db8aff4-82af-460e-8524-73e1441afdfa#id" }, + }, + items: [ + { + id: "569d1166-d3e0-4aea-b9a7-6de8ebdf3aec", + schema: "https://ns.adobe.com/personalization/inbox-item", + data: { + content: { + heading: { content: "Trending Now Inbox" }, + capacity: 10, + }, + }, + }, + ], + }; + const response = { + getPayloadsByType: () => [inboxPayload], + }; + responseHandler({ response }); + expect(lifecycle.onDecision).toHaveBeenCalledWith( + expect.objectContaining({ + propositions: [ + expect.objectContaining({ + id: inboxPayload.id, + items: [ + expect.objectContaining({ + schema: "https://ns.adobe.com/personalization/inbox-item", + data: expect.objectContaining({ + content: { + heading: { content: "Trending Now Inbox" }, + capacity: 10, + }, + }), + }), + ], + }), + ], + }), + ); + }); });