# Product Hydration

## Introduction

Product hydration lets you update product information at runtime before it is displayed to the user. This is useful for syncing prices, availability, descriptions, and variants with your own backend.

The Firework SDK supports two hydration modes:

| Mode            | Trigger                                           | Scope                                           | Listener                            |
| --------------- | ------------------------------------------------- | ----------------------------------------------- | ----------------------------------- |
| **Video-level** | A video or ivestream with products starts playing | Products from a single video/livestream         | `setOnProductHydrationListener`     |
| **Feed-level**  | A page of feed data is loaded                     | Products aggregated from all videos in the page | `setOnFeedProductHydrationListener` |

Both modes use the same `ProductHydrator` API to modify products. The sections below explain each mode and the full hydration API.

***

## Hydration Level

### Feed-Level Product Hydration

> **Note**: Feed-level hydration currently only supports **short videos**. Livestream content is not supported.

Feed-level hydration batches all products from a loaded page of feed data into a **single callback**, instead of issuing one callback per video. Products are aggregated across all videos in the page and deduplicated by product ID.

This is the recommended approach for feed views (e.g. `FwPlayerDeckView`) where multiple videos are loaded at once, as it reduces the number of hydration requests from N (one per video) to 1 (one per page).

#### Setup

Register the listener via `FireworkSdk.shopping.setOnFeedProductHydrationListener`:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        FireworkSdk.shopping.setOnFeedProductHydrationListener { products, hydrator ->
            // Fetch latest product data from your backend
            val updatedProducts = myApi.fetchProducts(products.mapNotNull { it.id })

            for (product in products) {
                val productId = product.id ?: continue
                val updated = updatedProducts[productId] ?: continue
                hydrator.hydrate(productId) {
                    name(updated.name)
                    description(updated.description)
                    isAvailable(updated.inStock)
                    for (unit in product.units) {
                        val unitId = unit.id ?: continue
                        variant(unitId) {
                            price(updated.variants[unitId]?.price ?: 0.0)
                        }
                    }
                    this
                }
            }
            hydrator.completeHydration()
        }
    }

    override fun onDestroy() {
        FireworkSdk.shopping.setOnFeedProductHydrationListener(null)
        super.onDestroy()
    }
}
```

{% endtab %}
{% endtabs %}

#### Callback Parameters

| Parameter  | Type              | Description                                                               |
| ---------- | ----------------- | ------------------------------------------------------------------------- |
| `products` | `List<Product>`   | Aggregated and deduplicated products from all videos in the loaded page.  |
| `hydrator` | `ProductHydrator` | Builder used to modify product fields. Same API as video-level hydration. |

Unlike video-level hydration, the callback does **not** include a `videoInfo` parameter because the products span multiple videos.

:exclamation: You **must** call `hydrator.completeHydration()` when finished. Without this call, updated values will not be applied.

#### Behavior

* **Applies to all view types**: Feed-level hydration is a global listener. Once set, it takes effect for all SDK view components (`FwPlayerDeckView`, `FwVideoFeedView`, `FwStoryBlockView`).
* **Automatic per-item disabling (DeckView inline only)**: When a feed-level listener is set, per-item hydration is automatically disabled **only** for `FwPlayerDeckView` inline cards. It does **not** disable per-item hydration for fullscreen playback (whether launched from `FwPlayerDeckView` or `FwVideoFeedView`) or `FwStoryBlockView`.
* **Coexistence**: Setting both `setOnFeedProductHydrationListener` and `setOnProductHydrationListener` at the same time is technically supported. If both are set, feed-level handles DeckView inline hydration while video-level handles fullscreen and StoryBlock. However, this is **not recommended** — see Best Practices below.
* **Timeout**: The SDK waits up to 10 seconds for `completeHydration()` to be called. If the timeout is exceeded or an exception occurs, the original (non-hydrated) products are displayed.
* **Pagination**: Each page load triggers a separate hydration callback. Products that were already hydrated in a previous page are not requested again.

***

### Video-Level Product Hydration

Video-level hydration is triggered each time a video with associated products starts playing. The SDK calls your listener with the products for that specific video.

Register the listener via `FireworkSdk.shopping.setOnProductHydrationListener`:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        FireworkSdk.shopping.setOnProductHydrationListener { products, hydrator, videoInfo ->
            for (product in products) {
                product.id?.let { productId ->
                    hydrator.hydrate(productId) {
                        name("Modified product name")
                        description("Modified product description.")
                        isAvailable(false)
                    }
                }
            }
            hydrator.completeHydration()
        }
    }

    override fun onDestroy() {
        FireworkSdk.shopping.setOnProductHydrationListener(null)
        super.onDestroy()
    }
}
```

{% endtab %}
{% endtabs %}

#### Callback Parameters

| Parameter   | Type              | Description                                               |
| ----------- | ----------------- | --------------------------------------------------------- |
| `products`  | `List<Product>`   | Products associated with the video that is about to play. |
| `hydrator`  | `ProductHydrator` | Builder used to modify product fields.                    |
| `videoInfo` | `VideoInfo`       | Information about the video that requested hydration.     |

:exclamation: You **must** call `hydrator.completeHydration()` when finished. Without this call, updated values will not be applied.

:exclamation: All code examples below only show the `hydrate {}` block for brevity. Refer to the example above for the full listener setup pattern. Remember to always call `hydrator.completeHydration()` after all hydrations are done.

### Best Practices

For the simplest and most predictable integration, set **only one** hydration listener — either feed-level or video-level — rather than both.

| Recommendation                                            | When to use                                                                                               |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| **Feed-level only** (`setOnFeedProductHydrationListener`) | Your app uses `FwPlayerDeckView` and you want to reduce hydration requests from N-per-page to 1-per-page. |
| **Video-level only** (`setOnProductHydrationListener`)    | You need per-video `videoInfo` in the callback, or your app primarily uses video and also livestream.     |

Setting a single listener avoids confusion about which listener handles which context, simplifies debugging, and ensures consistent hydration logic across all surfaces.

***

## Product Hydration

Use `ProductBuilder` methods inside the `hydrate {}` block to modify product-level properties. These methods apply to both video-level and feed-level hydration.

#### Basic Attributes

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    name("Updated Product Name")
    description("Updated product description.")
    subtitle("Limited Edition")
    currency("USD")
    isAvailable(true)
}
```

{% endtab %}
{% endtabs %}

| Method                              | Description                                                          |
| ----------------------------------- | -------------------------------------------------------------------- |
| `name(name: String)`                | Sets or updates the product name.                                    |
| `description(description: String)`  | Sets or updates the product description.                             |
| `subtitle(subtitle: String?)`       | Sets or updates the product subtitle. Pass `null` to clear.          |
| `currency(currency: String)`        | Sets the product-level currency code string (e.g. `"USD"`, `"EUR"`). |
| `isAvailable(isAvailable: Boolean)` | Sets the availability status of the product.                         |

#### Main Product Image

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    mainProductImage("https://example.com/images/product-hero.jpg")
}
```

{% endtab %}
{% endtabs %}

| Method                          | Description                                                                                                                       |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `mainProductImage(url: String)` | Sets or updates the main product image URL. If a main image already exists, updates its URL; otherwise creates a new image entry. |

#### Display Control

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    isDisabled(false)
    hidePrice(true)
    hidden(null)
}
```

{% endtab %}
{% endtabs %}

| Method                            | Description                                                                                  |
| --------------------------------- | -------------------------------------------------------------------------------------------- |
| `isDisabled(isDisabled: Boolean)` | Sets the disabled status of the product. A disabled product is treated as unavailable.       |
| `hidePrice(hidePrice: Boolean)`   | Controls whether the product price is hidden from display.                                   |
| `hidden(hidden: Boolean?)`        | Sets whether the product should be hidden entirely. Pass `null` to use the default behavior. |

#### CTA (Call-To-Action) Customization

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    customCTATitle("Buy Now")
    customCTATitleTranslation("Comprar Ahora")
    customCTAUrl("https://example.com/checkout/product-123")
    customCTATarget("_blank")
    hidePrimaryCTA(false)
}
```

{% endtab %}
{% endtabs %}

| Method                                      | Description                                                                        |
| ------------------------------------------- | ---------------------------------------------------------------------------------- |
| `customCTATitle(title: String?)`            | Sets a custom CTA button title. Pass `null` to clear.                              |
| `customCTATitleTranslation(title: String?)` | Sets a translated CTA title for localization. Pass `null` to clear.                |
| `customCTAUrl(url: String?)`                | Sets the URL the CTA button links to. Pass `null` to clear.                        |
| `customCTATarget(target: String?)`          | Sets the CTA target identifier. Pass `null` to clear.                              |
| `hidePrimaryCTA(hide: Boolean?)`            | Controls whether the primary CTA button is hidden. Pass `null` to use the default. |

***

## Variant Management

### Add a new variant

Appends a new variant to the existing list of product variants. Image management is handled automatically.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    addVariant(
        ProductUnit(
            id = "variant_blue_l",
            name = "Blue / L",
            price = Money(amount = 19.0, CurrencyCode.USD),
            originalPrice = Money(amount = 29.0, CurrencyCode.USD),
            image = ProductImage("https://example.com/images/blue-l.jpg"),
            url = "https://example.com/products/variant_blue_l",
            options = listOf(
                ProductUnitOption(name = "color", value = "Blue"),
                ProductUnitOption(name = "size", value = "L"),
            ),
        ),
    )
}
```

{% endtab %}
{% endtabs %}

### Remove a variant

Removes a variant by its unique identifier. If the variant has an associated image, the image is also removed from the product's image lists.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    removeVariant("variant_blue_l")
}
```

{% endtab %}
{% endtabs %}

### Clear all existing variants

Removes all variants from the product, including their associated images.

:exclamation: You **must** add at least one variant after clearing all existing variants.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    clearVariants()
    allowedVariantOptions(listOf("color", "size"))
    addVariant(
        ProductUnit(
            id = "variant_red_m",
            name = "Red / M",
            price = Money(amount = 19.0, CurrencyCode.USD),
            originalPrice = Money(amount = 29.0, CurrencyCode.USD),
            image = ProductImage("https://example.com/images/red-m.jpg"),
            url = "https://example.com/products/variant_red_m",
            options = listOf(
                ProductUnitOption(name = "color", value = "Red"),
                ProductUnitOption(name = "size", value = "M"),
            ),
        ),
    )
}
```

{% endtab %}
{% endtabs %}

### Replace existing variants with new variants

Replaces the entire set of variants at once. This clears existing variants internally and sets the new list, which is useful for refreshing variant offerings.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    allowedVariantOptions(listOf("color", "size"))
    val variant = ProductUnit(
        id = "variant_blue_l",
        name = "Blue / L",
        price = Money(amount = 19.0, CurrencyCode.USD),
        originalPrice = Money(amount = 29.0, CurrencyCode.USD),
        image = ProductImage("https://example.com/images/blue-l.jpg"),
        url = "https://example.com/products/variant_blue_l",
        options = listOf(
            ProductUnitOption(name = "color", value = "Blue"),
            ProductUnitOption(name = "size", value = "L"),
        ),
    )
    replaceVariants(listOf(variant))
}
```

{% endtab %}
{% endtabs %}

### Modify existing variants

Locates a variant by its unique identifier and applies modifications through a `ProductVariantBuilder` block. If no variant with the given ID is found, the call is a no-op.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    product.units.forEach { unit ->
        unit.id?.let { variantId ->
            variant(variantId) {
                name("Updated Variant Name")
                price(30.0)
                originalPrice(40.0)
                currency(CurrencyCode.USD)
                url("https://example.com/products/updated-variant")
                imageUrl("https://example.com/images/updated.jpg")
                isAvailable(true)
                unitOptions(
                    listOf(
                        ProductUnitOption(name = "color", value = "Blue"),
                        ProductUnitOption(name = "size", value = "L"),
                    ),
                )
            }
        }
    }
    this
}
```

{% endtab %}
{% endtabs %}

***

## Important Notes

#### `allowedVariantOptions`

Sets the list of attribute names that are permissible for product variants. This ensures that all product variants conform to the expected attributes such as color, size, material, etc.

:exclamation: Call this method **before** adding new variants when using `clearVariants()` or `replaceVariants()` to ensure compatibility.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    allowedVariantOptions(listOf("color", "size"))
    // Now add variants that have "color" and "size" options
    addVariant(...)
}
```

{% endtab %}
{% endtabs %}

#### `completeHydration()`

Must be called after all `hydrate()` calls are finished. Without this call, the SDK will not apply any hydrated values.

***

## API Reference

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
interface ProductHydrator {

    fun hydrate(
        productId: String,
        builderBlock: ProductBuilder.() -> ProductBuilder,
    )

    fun completeHydration(): List<Product>

    class ProductBuilder {
        // -- Basic Attributes --
        fun name(name: String): ProductBuilder
        fun description(description: String): ProductBuilder
        fun subtitle(subtitle: String?): ProductBuilder
        fun currency(currency: String): ProductBuilder
        fun isAvailable(isAvailable: Boolean): ProductBuilder

        // -- Main Product Image --
        fun mainProductImage(url: String): ProductBuilder

        // -- Display Control --
        fun isDisabled(isDisabled: Boolean): ProductBuilder
        fun hidePrice(hidePrice: Boolean): ProductBuilder
        fun hidden(hidden: Boolean?): ProductBuilder

        // -- CTA Customization --
        fun customCTATitle(title: String?): ProductBuilder
        fun customCTATitleTranslation(title: String?): ProductBuilder
        fun hidePrimaryCTA(hide: Boolean?): ProductBuilder
        fun customCTATarget(target: String?): ProductBuilder
        fun customCTAUrl(url: String?): ProductBuilder

        // -- Variant Management --
        fun addVariant(variant: ProductUnit): ProductBuilder
        fun removeVariant(id: String): ProductBuilder
        fun clearVariants(): ProductBuilder
        fun replaceVariants(variants: List<ProductUnit>): ProductBuilder
        fun allowedVariantOptions(options: List<String>): ProductBuilder
        fun variant(
            id: String,
            builderBlock: ProductVariantBuilder.() -> ProductVariantBuilder,
        ): ProductBuilder
    }

    class ProductVariantBuilder {
        fun name(name: String): ProductVariantBuilder
        fun price(price: Double): ProductVariantBuilder
        fun originalPrice(price: Double): ProductVariantBuilder
        fun currency(currencyCode: CurrencyCode): ProductVariantBuilder
        fun url(url: String): ProductVariantBuilder
        fun imageUrl(url: String): ProductVariantBuilder
        fun isAvailable(isAvailable: Boolean): ProductVariantBuilder
        fun unitOptions(options: List<ProductUnitOption>): ProductVariantBuilder
    }
}
```

{% endtab %}
{% endtabs %}
