flutter_salesforce_personalization 2.0.0
flutter_salesforce_personalization: ^2.0.0 copied to clipboard
A Flutter plugin that wraps the native iOS/Android Salesforce Personalization SDKs to deliver personalized UI components (Hero Banner, Recommendations) in Flutter apps.
Changelog #
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
2.0.0 #
The first release since 1.0.0. It renames the public out-of-the-box type
surface — a breaking change, which is what drives this major version bump —
and adds a first-class engagement-tracking API on ComponentContext.
Added #
Added engagement tracking on ComponentContext so custom component authors
no longer inspect engagementPayloads directly:
-
context.trackEngagement(action)— single-element components (Banner). Never deduped; fires every call. -
context.trackEngagementPerItem(index, action)— list components (Recommendations). Never deduped; fires every call. Renamed fromtrackEngagementForItem(no behavior change beyond the name). -
context.trackEngagementViewOnce([action = 'View'])/context.trackEngagementViewOncePerItem(index, [action = 'View'])— once-per-serving reporting.These dedup — use them only for
Viewor another action that is genuinely once-per-serving. Never use them forClickor any repeatable action: every call after the first for a given serving is silently dropped.Both guarantee
actionfires at most once perComponentContextinstance, even across widget unmount/remount recycles that reuse that same instance. Because that dedup keys on the context object's own identity (notpersonalizationId), the primitive alone is safe to call straight frombuild()— including aStatelessWidget's — with no lifecycle wiring, but it re-fires on a freshComponentContextinstance (e.g. fromcontroller.refresh(), or a production → preview transition that re-fetches) even if the new instance happens to carry the samepersonalizationId— instance identity andpersonalizationIdare independent signals, and every successful fetch mints a new instance regardless of whether the id changed.Layered on top, within a living widget
State, the OOTB widgets (HeroBanner,RecsCarousel) and the recommended custom-component pattern pair this primitive with a per-indexpersonalizationId"memo":Viewfires once perpersonalizationId, not just once per instance — a same-PID refetch does not refire, a new PID does. A fully disposed-and-remountedContentZone(e.g. an outer virtualized list dropping it offscreen and rebuilding it) starts a newStatewith no memo of its own — that's a genuine new serving and correctly refires; this is the intended, narrowed contract.HeroBannercallscontext.trackEngagementViewOnce()on mount and again only whenpersonalizationIdchanges; eachRecsCarouselcard callscontext.trackEngagementViewOncePerItem(index)inbuild(), gated by the carouselState's PID memo. Seedoc/VIEW_ONCE_TRACKING.mdfor the full treatment, including the PID-memo pattern and the same-PID re-serve edge case (a re-serve at an already-memoed PID does not refire, even if an earlier serving at that PID lacked usable payloads and the later one now has them — this is intended behavior).The example app's custom components demonstrate the pattern.
MiniRecsComponent(_MiniRecsList) is aStatefulWidgetholding the same per-index PID memo asRecsCarousel— the primitive + memo exemplar for build-time tracking.CoverflowRecommendationstracksViewfromonPageChanged(plus the initial page frominitState, sincePageView.builderbuilds cached, currently-offscreen neighbors andonPageChangeddoesn't fire for page 0), gated by the same PID-memo pattern — the visibility-driven exemplar. Both fireClickon every tap via the publictrackEngagementPerItem.
All four methods resolve the payload for the given action (matched
case-insensitively, whitespace-trimmed), apply a consistent logging policy,
and are fire-and-forget — they never throw and are a safe no-op when no
matching payload exists. Added an optional componentName field to
ComponentContext used to tag diagnostic log messages. The well-known
action names EngagementActions.view and EngagementActions.click are
exported for discoverability; the track methods still accept any string.
Engagement diagnostic logging is driven by cause rather than content source:
missing or wrong-shape payloads log at DEBUG (expected in mock/preview and
can fire per frame); an action not found on a correctly-shaped payload logs
at WARN (actionable misconfiguration). The OOTB Banner and Recommendations
widgets route all tracking through the ComponentContext methods above. The
one-time-per-fetch shape-mismatch validation log (distinct from the
per-track DEBUG path above) is WARN, matching the existing count-mismatch
log. The OOTB widgets' diagnostic log tag is the backend-resolved
componentName (e.g. Salesforce_Banner, Salesforce_Recommendations).
Changed #
Renamed the out-of-the-box (OOTB) component classes and their associated
model, style, and event types to carry a Salesforce prefix, aligning the
public type surface with the backend component API names
(Salesforce_Banner / Salesforce_Recommendations). Any app importing the
old names must update its imports, constructor calls, and type annotations.
| Old | New |
|---|---|
BannerComponent |
SalesforceBanner |
Recommendations |
SalesforceRecommendations |
BannerModel |
SalesforceBannerModel |
RecommendationsModel |
SalesforceRecommendationsModel |
RecommendationItem |
SalesforceRecommendationItem |
RecommendationTapEvent |
SalesforceRecommendationTapEvent |
BannerStyle |
SalesforceBannerStyle |
RecommendationsStyle |
SalesforceRecommendationsStyle |
SectionHeaderStyle |
SalesforceSectionHeaderStyle |
RecsCardStyle |
SalesforceRecommendationCardStyle |
Not affected: the backend component-name string constants
('Salesforce_Banner' / 'Salesforce_Recommendations') and internal log
tags are unchanged, so no backend or mock-content configuration changes are
required.
- Upgraded the bundled native Personalization SDK from 2.x to 3.x. The
plugin's native dependency moved a full major: iOS
Salesforce-Personalization~> 2.0→~> 3.0, Androidcom.salesforce.personalization:sdk2.+→3.+. Host apps pick these up transitively (they are not declared directly), but must be able to resolve the 3.x artifacts — the same Salesforce GitHub Pages Maven repos on Android and CocoaPods on iOS; see doc/Android.md and doc/iOS.md. Both constraints deliberately float within the 3.x major (~> 3.0,3.+) rather than pinning an exact release, so host apps receive native patch/minor fixes without waiting on a plugin release; the plugin is validated against the current 3.x line at publish time. Apps that require byte-reproducible builds should pin the exact resolved native version in their own lockfile (Podfile.lock/ Gradle dependency locking), which is where build reproducibility belongs — not in a library's dependency range. - Lowered the toolchain floor to the verified minimums, reducing version
conflicts for host apps. Dart
^3.3.0(was^3.5.0), Flutter>=3.19.0(was>=3.24.0), Kotlin Gradle plugin2.3.0(was2.4.0), Gradle wrapper8.13(was8.14), and Swift5.7(was5.9). No source changes were needed; these were empirically confirmed to build green on both platforms.compileSdk 37, AGP8.13.2, and iOS15.0remain hard floors imposed by the native Personalization SDK 3.x.
Removed #
Removed the low-level engagement export that 1.0.0 briefly exposed.
EngagementPayload, the EngagementPayloads* types, and the direct
EngagementPayloads.trackEngagement(payload) entry point are no longer part
of the public API. Custom component authors track engagement through the
ComponentContext methods added above, using the ComponentContext the zone
hands to build(model, context) (do not construct or cache your own).
Migration: replace EngagementPayloads.trackEngagement(payload) with
context.trackEngagement(action) (single element) or
context.trackEngagementPerItem(index, action) (list); for once-per-serving
View, use context.trackEngagementViewOnce() /
context.trackEngagementViewOncePerItem(index).
Fixed #
ContentZone widget-lifecycle correctness for dynamic reconfiguration:
allowedComponentschanges are now honored on rebuild —didUpdateWidgetrebuilds the allowed-name list and component lookup and diffs by value, so a changed component set is picked up in place (no remount), while an unchanged one never triggers a redundant refetch.- A same-name component-instance swap re-renders in place from the retained
serving JSON, reusing the same
ComponentContextinstance — no network refetch and no engagementViewrefire. A swap whose validator rejects or throws surfaces the fallback (recordinglastRefreshError) without a fetch, and the previously resolved serving is preserved so a later compatible swap recovers in place. - Controller unbinding is ownership-aware — a zone clears only the controller binding it actually installed, so swapping a shared controller no longer tears down another zone's binding.
1.0.0 — Initial public release #
First public release of the Salesforce Personalization Flutter plugin — a Flutter wrapper over the native iOS and Android Personalization SDKs for rendering personalized content and tracking engagement.
Features #
- ContentZone — widget that fetches personalized content from the native
SDK and renders the matching component, with
loadingandfallbackbuilders, a configurabletimeoutMs, and optionalDecisionsRequestContextfor anchor/attribute biasing. - Out-of-the-box components —
BannerComponentandRecommendations, each with optionalstyleoverrides and tap handlers. - Custom components — implement
Component<T extends ComponentModel>to render your own backend transformers, or override an OOTB component by reusing its name. - Event tracking — single
PersonalizationModule.track(...)entry point with sealed event types:CustomEvent,CartEvent,OrderEvent, andCatalogObjectEvent. - Engagement tracking — OOTB
BannerComponentandRecommendationsauto-trackView(on mount and on everypersonalizationIdchange) andClick(on tap) for all content sources. At the time of this release, custom-component authors could callEngagementPayloads.trackEngagement(payload)directly; that low-level API was later removed from the public export surface (see 2.0.0 above) in favor ofComponentContext.trackEngagement/trackEngagementPerItem. - Identity — profile ID, profile attributes (upsert / clear), and party identification, plus read APIs for current state.
- Consent —
setConsent/isConsentOptIn. - Preview — QR-code / deep-link preview mode via
handlePreviewUrl. - Pull-to-refresh —
ContentZoneControllerfor programmatic refresh, with silent and loading-state modes. - Offline / design-time —
MockDataContentZone<T>renders caller-supplied mock content with no network or native SDK.
Configuration #
The host app initializes the native SDK; the plugin does not expose a Dart-side
configure(...). Provide the CDP appId, endpoint, and cdnUrl to the
native layer — the example app reads them from AndroidManifest.xml and
Info.plist, but any configuration strategy works. See the
README and doc/GETTING_STARTED.md.
Platform support #
- Flutter 3.24+ / Dart 3.5+ — superseded in 2.0.0, which lowered this floor to Flutter 3.19+ / Dart 3.3+ (the current requirement; see README and example/README.md).
- iOS 15.0+
- Android API 26+
On Android, the plugin degrades gracefully when the native SDK is absent from the classpath rather than crashing the host app.