# Image support on Image node & Button Reply node (session messages)

**Date:** 2026-07-07
**Status:** Design approved, pending implementation plan

## Goal

Let flow authors send an image together with text in a single non-business-initiated
(session) WhatsApp message, in two places:

1. **Image node** — attach a caption so the image sends with text in one message.
2. **Button Reply node** — attach an image that rides as the interactive message's
   header, alongside the body text and reply buttons, in one message.

Business-initiated (template) nodes are untouched. Text node and List Reply node are
explicitly out of scope (see Non-goals).

## Background / why this is small

The send plumbing already largely exists:

- **Image caption:** `WaNotifications::run()` IMAGE case already passes
  `Arr::get($setting_json, 'text')` as the caption arg
  (`WaNotifications.php:499`), and `sendResource()` adds it to the payload for
  IMAGE/DOCUMENT/VIDEO types (`WaNotifications.php:1095-1098`). The **Video node
  already carries `text`** into `setting_json` this way
  (`JourneyController.php:3238`). The Image node simply never wired it up — its
  build writes only `url_source` + `mime` (`JourneyController.php:3220`), and the
  node UI has no caption field.
- **Button header image:** `sendButtonReply()` builds an `interactive` / `type:
  button` payload with `body` + `action` but no `header`
  (`WaNotifications.php:1261`). There's a leftover `// if media is listed` comment
  at line 1260 anticipating this. WhatsApp's Cloud API supports an optional
  `header` of type `image` on interactive button messages.

## Non-goals

- **Text node:** not modified. The image-with-caption need is served by the Image
  node instead.
- **List Reply node:** not modified. WhatsApp interactive **list** messages only
  accept a **text** header (`WaNotifications.php:1541` hardcodes `type: 'text'`) —
  this is a hard Cloud API rule, so an image cannot ride inside a list message.
  Pairing an image would require a separate preceding message; deferred.
- **Multi-language caption:** the Image node caption is English-only, matching the
  existing en-only image build. Multi-language deferred until needed.

## Changes

### 1. `resources/js/flow/flow.jsx`

**ImageNode** (~line 4404):
- Add a caption field (single input, English), persisted as `node.data.text`,
  following the same state/`setNodes` pattern as the other text fields in the file.
- Empty caption = current behavior.

**ButtonReplyNode** (~line 1315):
- Add an optional image, reusing ImageNode's uploader verbatim: a URL input plus a
  file `<input>` that posts to `/api/v1/upload-blast-media` and stores the returned
  path as `node.data.imageUrl`.
- Empty `imageUrl` = current behavior.

### 2. `app/Http/Controllers/Journey/JourneyController.php`

**Image component build** (~line 3220):
- Add `'text' => $component['settings']['en']['text'] ?? ''` to the `setting_json`
  `en` block.

**Button-reply component build** (~line 3037):
- Carry `imageUrl` into each language's settings when present (alongside the
  existing `text` / `button_reply`).

### 3. `app/Skale/Notifications/Whatsapp/WaNotifications.php`

**IMAGE send:** no change — it already reads `setting_json['text']` as the caption.

**`sendButtonReply()`** (~line 1261):
- Accept the image (read from `setting_json`).
- When an image is present, add `header: { type: "image", image: { link: <url> } }`
  to the interactive payload. Body text and buttons unchanged.
- In the `isBtnReplyToText()` fallback (~line 1240) — triggered when there are more
  than `MAX_BUTTON_REPLY` buttons or a title exceeds the char limit — if an image is
  present, send it as an image-with-caption (reusing the media send path) instead of
  a plain `sendText`, so the image is not silently dropped.

## Media reference

Use the stored image URL as a `link` (WhatsApp fetches it), matching how `imageUrl`
is stored today. If `link` proves unreliable in practice, the existing
upload→`media_id`+reupload helper used by the IMAGE case
(`WaNotifications.php:503-537`) can be reused as a fallback. Which one is required
will be confirmed during implementation.

## Testing

One assertion per branch:

- **Image caption flows end-to-end:** an Image node with a caption produces
  `setting_json['en']['text']`, and the IMAGE send path emits an `image` payload
  with that `caption`.
- **Button Reply header image:** `sendButtonReply()` with an image emits an
  interactive payload containing `header.type == "image"`; without an image it emits
  the current header-less payload.
- **Button→text fallback with image:** with >3 buttons (or an over-long title) and an
  image set, the send path uses the image-with-caption path rather than plain
  `sendText`.

## Backward compatibility

No image / no caption = byte-identical behavior to today for all three files. Only
additive fields.
