Conversation
Staging to main for 3.8.1 release
Previously the SDK sent only the ECID to the conversation and feedback endpoints, dropping any additional identities the customer set via Identity.updateIdentities (e.g. hashedEmail, CRMID, custom namespaces). Now the full EdgeIdentity identityMap is carried verbatim: - ConciergeState stores the whole identityMap; ECID is derived from it and kept as the readiness signal (updateExperienceCloudId -> updateIdentity). - The request/feedback bodies serialize the map via JSONObject (escaping arbitrary keys/values), replacing the hardcoded ECID-only block. - Namespaces are forwarded as-is (no interpretation/relabeling); priority and identity-graph rules are configured server-side in AEP. - Log namespace names only, never id values (PII). Adds unit tests for the full-map, ECID-only (no regression), and empty-map cases, plus auth-token coexistence, and documents the capability in the implementation guide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JSONObject.toString() returns null rather than throwing on an unserializable map, so the previous runCatching/getOrElse fallback was never exercised (and would have emitted "identityMap": null). Replace it with an explicit null-coalescing fallback to an empty object, and add tests for the serialization-failure and empty-identityMap branches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Back merge main to dev after 3.8.1 release
The `clear resets state to initial values` test populated identityMap but never verified it was nulled after clear(). Add the missing assertion so a future selective-reset refactor that forgets identityMap is caught directly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Matches the updated design doc: accept/reject is now the only confirmation signal, and the localMessage field is accepted/decoded (rendering deferred). Still to be implemented: - Real forward to Brand Concierge — ConciergeChatService has no extra-XDM parameter yet; NotImplementedDataHandoffForwarder remains a stub - localMessage chat rendering — accepted and decoded, but not yet appended to the chat transcript (needs a ConciergeStateRepository-style bridge into ConciergeChatViewModel) - Ordering when a checkout-result turn and an ordinary chat message arrive around the same time — both currently share ConciergeChatService's single request slot
Wrap the existing data-handoff event/response mechanism in a typed Concierge.sendDataHandoff(routingHint, xdmFields, localMessage, completion) call so host apps no longer build/dispatch the Event or parse the response themselves. The old ConciergeDataHandoffEvent public surface is now internal wire-format plumbing behind this API.
Report ConciergeDataHandoffRejectReason (a typed enum) from Concierge.sendDataHandoff's completion instead of a raw wire-format String, and route data-handoff request/response events through the standard EventSource.REQUEST_CONTENT/RESPONSE_CONTENT (matching Edge, EdgeIdentity, and EdgeConsent) instead of a dedicated custom source, disambiguating by event name in the shared request-content listener.
…feature/concierge-identitymap-passthrough
The Identities section re-documented the Edge Identity updateIdentities API, which belongs to the identity extension's docs. Replace the inline sample with a link to the official Edge Identity API reference so there is a single source of truth; keep only the Brand Concierge-specific behavior (verbatim forwarding, ECID auto-included, server-side rules). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
[MAR-2833] Add sendDataHandoff API
Forward accepted data-handoff events asynchronously through the Brand Concierge conversation service without rendering backend responses in chat. Merge caller-provided XDM fields into the outbound request while preserving the SDK-managed identity map, and isolate forwarding failures from the event accept/reject response. Document Android data-handoff integration requirements and add coverage for forwarding, XDM serialization, unavailable surfaces, and failure handling.
… fixes Address PR #172 review comments and two follow-up review passes on the Brand Concierge forwarding path in ConciergeChatViewModel: - Release the data-handoff exclusivity reservation immediately on both session deactivation and delivery timeout, instead of leaking it until a stale request drains through the queue (was causing spurious CHAT_IN_PROGRESS rejections on retry). - Add a catch-all around the shared chat/handoff processing coroutine so a synchronous exception from the conversation service can no longer permanently kill it. - Clean up UI state (placeholder + Processing) on every cancellation path, not just timeouts. - Reject reserved XDM keys (e.g. identityMap) at the network-client boundary as defense-in-depth beyond the existing event-level check. - Warn when a second chat session overwrites an already-active data handoff forwarder instead of silently dropping it. - Remove dead code: an unreachable ERROR-state branch that was also triple-wrapping the error message, a redundant CAS retry loop, and a concurrent Set standing in for what's always a single in-flight handoff. - Extract a shared helper for the repeated "rethrow cancellation, otherwise report" error-handling shape.
Data-handoff failures (timeout, service error, or an error frame) no longer inject a generic error bubble into the chat transcript - the outcome is reported only through the sendDataHandoff completion callback, matching how the feature is surfaced everywhere else. Ordinary user-typed chat still renders its error bubble unchanged (streamConversation gains a renderErrorsInChat flag). Placeholder cleanup now lives entirely inside streamConversation, where the turn's placeholder provably exists (including on timeout/deactivation cancellation), so processDataHandoffRequest's catch blocks only reset state. This fixes a case where a failed handoff with no placeholder could remove a prior turn's assistant message. Adds coverage for the end-to-end handler->forwarder->view-model wiring, the accept/reject response event's inResponseToEvent correlation, silent handoff failure/timeout/error-frame paths, and removal targeting (a failed or timed-out handoff must not delete a prior turn).
…assthrough Concierge identityMap passthrough
The upstream/dev merge (PR #168) added full identityMap passthrough via identityMapJson(), but only wired it into the feedback request body. createXdmObject (used by chat and data-handoff requests) still built its own bespoke ECID-only identityMap inline, so it never picked up the passthrough behavior — breaking 4 ConciergeConversationServiceClientTest cases after the merge. createXdmObject now reuses identityMapJson(state), the same helper the feedback path already relies on.
ConciergeConversationSession now owns the request queue, the shared CHAT_IN_PROGRESS gate, the ConversationService, and the observable messages/state flows. ConciergeChatViewModel becomes a renderer: it delegates those flows and routes user actions into the session, keeping only UI concerns - input state, welcome card, feedback, STT, links, and UI tracking. Because the session outlives the composable and the ViewModel, a data handoff fired while the chat is hidden or closed is now accepted, forwarded, buffered into the transcript, and painted when the chat is next shown. The NO_ACTIVE_SESSION rejection is gone from the enqueue path; CHAT_IN_PROGRESS, EMPTY_RESPONSE, DELIVERY_FAILED and DELIVERY_TIMEOUT are unchanged. ActiveConciergeDataHandoffForwarder and the composable's activate/deactivate wiring are deleted in favor of SessionDataHandoffForwarder. Supporting changes: - ChatScreenState no longer carries the feedback dialog. It is per-renderer state on the ViewModel, so a thumbs-up on one chat surface no longer opens a dialog on another. - conversationId follows the backend instead of pinning to the first value seen, which would otherwise tag later turns and their feedback with a conversation the 30-minute session roll had already ended. - The session exposes shutdown() for privately owned instances (the debug demo, tests). It drains queued handoffs so their callers are never left without a completion, and refuses to tear down the process session. The shared session is deliberately left running in onCleared. - buildCardElementDict moves to a shared util. Public API is unchanged: Concierge.sendDataHandoff, ConciergeDataHandoffCallback and ConciergeDataHandoffRejectReason are untouched.
Reverts 3dc674d ("move the conversation onto a process-lifetime session") back to a session-scoped ConciergeChatViewModel, restoring the NO_ACTIVE_SESSION rejection. A process-lifetime transcript survives logout/consent withdrawal with no reset hook, merges turns across surfaces, and grows unbounded - the reviewer's stated privacy concern. Buffering a handoff fired while chat is hidden remains a product option, but belongs in its own PR. On top of the revert: - Data handoff failures now render a generic error bubble like any other failed turn, instead of failing silently in the transcript. - A failed/cancelled/timed-out turn rolls back everything it added - not just the last message - via truncateMessagesTo(turnStartIndex), so a multi-message turn (card + CTA) doesn't leave partial content on screen after the caller is told it failed. Unified into a single atomic transcript update and applied identically to chat and handoff turns. - DataHandoffDeliveryResult.Failed carries an internal-only message with the underlying failure detail, logged for debugging but never exposed on the public ConciergeDataHandoffCallback (a fun interface; adding a parameter there would be a breaking change). - A missing or blank routingHint is accepted as an empty service query instead of rejected, since xdmFields alone can carry enough routing context - matching iOS. MISSING_ROUTING_HINT is removed outright. A present-but-null routingHint is still rejected as INVALID_ROUTING_HINT_TYPE, distinct from an absent key. Also fixes two bugs the wholesale revert would otherwise have reintroduced, both called out by name in 3dc674d's own commit message as deliberate fixes bundled into that commit: - An open feedback dialog no longer closes when an unrelated turn transitions to Processing/Error/Idle. - conversationId now follows the backend instead of pinning to the first value seen, so a session rolled over after 30 idle minutes doesn't tag later turns and feedback with a stale id. Adds test coverage for buildCardElementDict's known-key allowlist, lost when the revert deleted its dedicated test file.
Once a handoff is accepted into the chat, a service error, an empty response, and a delivery timeout all now roll the turn back and render the same generic error message a failed chat turn renders. Previously only DELIVERY_FAILED painted a bubble, so a timed-out or empty handoff left localMessage sitting in the transcript with no reply and no error - the host had to render its own UI for two of the three failure modes but not the third. Matches adobe/aepsdk-concierge-ios#164. Deactivating the session mid-flight still rolls back silently, since there is no longer a chat surface to show an error on. Also in this pass: - Add the shared-processor catch-all the PR description already claimed. A throwing host completion callback could kill the single processor coroutine and strand the queue forever. - Mark the chat busy synchronously when a handoff reserves the queue, closing the window where a chat message could silently queue behind an in-flight handoff for up to the delivery timeout. - Cancel the timeout job when the request is already complete. - Document which reject reasons render in chat so host apps do not double-render an error.
…and stop treating an empty response as an error enqueueDataHandoff marks the chat busy when it reserves the slot, but processDataHandoffRequest's two early returns - an already-completed request, and a session that went inactive - returned without clearing it, and deactivateDataHandoffSession does not touch _state either. Dismissing the chat after enqueuing a handoff but before the processor dequeued it therefore left the composer disabled for the rest of the ViewModel's life, including after reopening the chat. Separately, routing EMPTY_RESPONSE through handleConversationError overshot iOS. There the empty-response branch renders its own copy and deliberately dispatches no errorOccurred, because nothing failed - the request was delivered and answered, there was just nothing to show. Only the stream-error branch tracks, which a timeout also unwinds through. Split renderTurnMessage out of handleConversationError so the empty case can render without the error telemetry, and give it the same distinct copy iOS uses.
Render a handoff's localMessage as an agent message rather than a user message. App code fires the handoff after a real-world event, so attributing it to the user misreads the transcript and makes the recommendations that follow look like a user request. Split the single 90s handoff cap into a 10s first-chunk cap and a 60s turn ceiling. A flat cap cannot both fail a wedged backend quickly and give a slow-but-healthy response room to finish. The first-chunk cap is disarmed by the first streamed chunk. Drop the network read timeout to 15s so it sits below the turn ceiling instead of above it. Default routingHint to an empty string for callers whose XDM fields alone determine routing.
closeConcierge() tore down the data handoff session, but there is no matching re-activation in openConcierge() — activation lives only in the DisposableEffect inside the ConciergeChat composable. In dialog mode this was harmless: closing removes the chat from composition, so onDispose deactivates anyway. In direct-Compose and ConciergeChatView embedding the chat stays composed across the call, so the effect never re-runs and every later handoff was rejected with NO_ACTIVE_SESSION for the life of the ViewModel, with no recovery path. Make the DisposableEffect the sole owner of the session lifetime, with onCleared() as the backstop. That is the only signal that is correct in every integration mode, since it tracks whether a chat surface is actually rendered rather than whether the dialog flag is set.
Keep handoff failures out of the transcript while preserving local messages. Start the first-response timeout after request preparation and terminate streams immediately on error frames. Update tests and documentation for the new behavior.
The mock conversation service always succeeded, so the demo could not show the behaviour this work is actually about: a handoff that fails after it was accepted renders no error UI, leaving localMessage on screen with nothing after it. The existing invalid-payload preset only covers pre-acceptance validation, which never rendered anything in the first place. The mock now branches on routing hint via MockDataHandoffRoutingHints: demo-stream-error emits an ERROR frame (DELIVERY_FAILED), demo-empty-response completes with nothing renderable (EMPTY_RESPONSE), and demo-silent-service stays quiet past the first-chunk cap (DELIVERY_TIMEOUT). Each has a demo preset that sends a localMessage, so the silent-failure case is what shows on screen. The presets are mockOnly and drop out of the button list when the mock switch is off, since the live service cannot be asked for those outcomes. Also drops the reject-reason table's "Renders in chat" column, which now reads No for all 14 rows and no longer distinguishes anything - the paragraph below the table states the rule once - and sorts two coroutine imports.
Make the auth-token saturation test wait for submission, not a clock The test filled the executor pool and its bounded queue with 20 blocked resolveToken() calls, then asserted a 21st was rejected fast. It waited deterministically only for the 4 pool threads and gave the 16 queued submissions a flat 200ms. Under load one of those submits could land late, leaving a queue slot free: the measured call was accepted instead of rejected and waited out its own 5s timeout - still null, so the null assertion passed and only the fail-fast bound broke. Wait for every saturating caller to report TIMED_WAITING instead. A caller parks in Future.get(timeout) immediately after submit() returns, so that state means its task has landed; an untimed park, which is what queue-lock contention shows, no longer opens the gate early. Bounded at 5s and asserted, so a real hang still fails with a clear message.
…vice [MAR-2835] Forward data handoff to BC service
The recording waveform was drawn inside the mic IconButton, whose content is clipped to a circle, so the outer bars were cropped. Draw the waveform as a sibling of the button instead. Taps still reach the button. Shape the bars to follow a fixed height profile (3, 7.88, 6, 10.5, 7.5, 3), resampled when barCount differs, and change the default bar count from 5 to 6. Size the waveform gradient to each bar rather than the whole canvas, so short bars show the full gradient instead of only mid-tones. Add unit tests for the bar height profile and an instrumented test that checks each bar is drawn with the full gradient.
Updating version to 3.9.0
Carousel cards are sized to the tallest card, but Card's built-in content wrapper dropped that height, so CTAs on shorter cards sat partway down the card. Replace the outer Card with a Surface, which passes the card height through to its content, and add a weighted spacer so the CTA row sits at the bottom. Content taller than the card still scrolls. Add a UI test checking that CTAs line up with the card's bottom padding when content is short.
…-on-listening-waveform [MAR-2991] Fix clipped listening waveform and match bar shape
Move the price column out of the text column so it sits after the weighted spacer, keeping the price and CTAs together at the bottom of the card. Add tests for: - the price anchoring to the bottom when there are no CTAs - the price anchoring to the bottom when there is no "was" price - prices and CTAs lining up across cards with different subtitles - CTAs lining up when only one card has a "was" price
…ard-cta [MAR-3033] Anchor extended product card CTAs to the bottom of the card
Use the configured input outline gradient while the field is focused, applying the focus outline width. Fall back to the solid focus color when no renderable gradient is configured. Draw the gradient after the Surface content so the background doesn't obscure the border, and add an instrumentation test verifying the focused gradient renders across the border.
Render input border gradients when focused
- Add configurable description limits to extended product cards. The new `--product-card-description-max-lines` option defaults to two lines and supports unclamped descriptions with `none` or invalid values. Carousels reserve CTA space across cards to keep prices aligned. - Update the product card demo with 2-, 4-, and 6-line descriptions and a longer sample to demonstrate ellipsis. - Add config and UI tests, and document the theme option.
Add tests for: - product-card-description-max-lines keeping existing layout values - descriptionMaxLines defaulting to 2 and being set by the theme - the reserved CTA placeholder being hidden from accessibility and not tappable - the reserved CTA placeholder still taking up space on the card Fix productCarousel_reservesCtaSlotAcrossExtendedCards, which failed because the carousel's hidden sizing copy of each card also matched the price text. The test now reads only the visible card.
- Align `--product-card-description-max-lines` parsing with iOS by ignoring invalid values, clamp directly configured nonpositive line counts to prevent Compose failures, and memoize carousel CTA-slot detection. - Update the style guide and add regression coverage for mapper behavior and invalid direct theme values.
…pport [MAR-2887] Add support for taller product card descriptions
cdhoffmann
approved these changes
Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Related Issue
MAR-2833 / MAR-2835, MAR-2887, MAR-2991, MAR-3033