Skip to content

0026. Read WooCommerce consent from the connector plugin's user meta

Date: 2026-09-02 Status: Partially superseded by 0041 — reading consent from the connector plugin's user meta remains in effect, but three details below no longer hold. confirmed is reported as null (unknown), not false, because the checkbox cannot say whether an address was confirmed. Consent arrives as its own ConsentData record next to the customer, not inside CustomerData::$consents. There is no last write wins across writers, because a consent mirror has one writer; the stale-write guard stays per mirror, refusing only a consentUpdatedAt strictly before the mirror's, so a flag flipped without moving date_modified_gmt still writes. The timestamp also stays required for settling a two-way conflict between mirrors.

Context

CFX-179 built the consent machinery — ConsentData, ConsentRecorder, CanPullConsent, HasConsentChannels — without a single shop actually producing consent. CFX-93 supplies the first one: WooCommerce, inbound only (the write-back half is out of scope here).

The problem is that WooCommerce has no consent field. A WC customer carries name, email, billing and shipping, order totals — nothing that says whether the person wants email from the shop. Unlike PrestaShop, which at least has a newsletter column, there is nothing on the platform to map.

Three ways to get one:

  1. Derive it from behaviour. Treat "has placed an order" or "has an account" as consent. This is not consent in any legal sense and would manufacture opt-ins out of thin air. Rejected outright.
  2. Read a third-party marketing plugin's storage. Mailchimp for WooCommerce, Klaviyo and others each keep their own opt-in state, in their own tables or meta keys. Conflux would have to detect which one a store runs, follow each plugin's schema changes, and reconcile disagreements between two of them. That is an open-ended integration surface for a single boolean.
  3. Have our own plugin record it. conflux-connector is already installed on every connected store — it is how the connection is established and verified in the first place. It can render a newsletter checkbox and store the answer.

Option 3 is the only one that produces real consent from a source we control. That leaves the question of how Conflux reads it: a dedicated plugin endpoint (/wp-json/conflux-connector/v1/consent), or a WordPress user meta entry surfaced on the standard WC customer payload.

Decision

conflux-connector records the newsletter opt-in as WordPress user meta under the key conflux_newsletter, exposed on the WC REST customer object's meta_data. That key is the contract between plugin and platform; Conflux reads no other consent source for WooCommerce.

WooCommerceTransformer::CONSENT_META_KEY holds it on the Conflux side, and WooCommerceTransformer::mapConsent() is the single place that interprets it:

  • Present → a ConsentData on ConsentChannel::Email. subscribed is the flag's boolean value; confirmed is always false, because a checkbox is a single opt-in and WooCommerce has no confirmation step to report. source is ConsentSource::Integration — the flag itself does not record whether a customer ticked it or a shop admin set it, and inventing User would claim knowledge we do not have.
  • Absent → null, and CustomerData::$consents stays empty. A store without the plugin, or on a version predating the checkbox, says nothing about consent; it does not withdraw it.
  • consentUpdatedAt comes from the customer record's date_modified_gmt, falling back to date_created_gmt, parsed through the run's SourceDateTimes like every other WooCommerce datetime. A record with neither readable yields no consent at all: ConsentRecorder orders writes by that timestamp, so an invented one would decide a conflict it cannot see.

Reading it off meta_data rather than from a dedicated endpoint means consent arrives on the existing bulk-export payload: SystemCapability::Customers already fetches /wp-json/wc/v3/customers, so a full or delta sync carries consent at no extra request and with no second pagination path to keep in step. CanPullConsent::pullConsent() reads the same field from /wp-json/wc/v3/customers/{id} for a single contact, so both paths share one mapper and can never disagree.

WooCommerceIntegration::supportedConsentChannels() returns [ConsentChannel::Email]. One checkbox is one channel; the store cannot express SMS consent and a propagator must not try to push it there.

Consequences

The meta key is now a published interface, not an implementation detail. Renaming it silently breaks consent sync for every store running an older plugin build — and breaks it in the quiet direction, since a missing key reads as "no signal" rather than as an error. Any change to it has to ship as a plugin release first, with the old key still honoured until the fleet has caught up.

confirmed is hard-wired to false for WooCommerce. If a store ever runs double opt-in through the connector, this ADR needs revisiting — the flag alone cannot carry that state, and reporting a confirmed opt-in as unconfirmed is the safe direction to be wrong in.

The date_modified_gmt proxy inherits the caveat ADR-0024's stale-write guard already documents: the timestamp belongs to the customer record, not to the consent field. A plugin that flips the flag without touching the customer record leaves date_modified_gmt unchanged, and the guard will then discard the newer state as stale. The connector must therefore keep the customer record's modification timestamp moving when it writes the flag — which update_user_meta() on the customer's own user does through WooCommerce's own hooks — and the two-timestamp alternative (a companion conflux_newsletter_updated_at meta) stays available if that ever proves insufficient.

Nothing calls pullConsent() yet. It is implemented here because the mapping and the credentials are the same ones bulk export already uses, so implementing both halves together costs one method and keeps a future caller from having to re-derive the field contract.