> For the complete documentation index, see [llms.txt](https://docs.firework.com/firework-for-developers/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.firework.com/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/shopping-ios.md).

# Shopping (iOS)

FireworkVideoSDK contains a `shopping` property that enables video shopping integration. There are two main points of integration both located on the `FireworkVideoShopping` type.

### **FireworkVideoShoppingDelegate**

Assign `FireworkVideoShoppingDelegate` delegate to receive important shopping events.

```swift
FireworkVideoSDK.shopping.shoppingDelegate = <Your delegate>
```

The shopping lifecycle events provide opportinities to customize the product views, hydrate product information and handle when a user adds a product variant to the cart.

### Shopping configurations

Please refer to [Shopping configurations (iOS)](/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/customization-ios/shopping-configurations-ios.md).

### **Customize** click behaviors for shopping

Please refer to [Customize click behaviors for shopping](https://docs.firework.com/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/customization-ios/customize-click-behaviors-ios#customize-click-behaviors-for-shopping).

### **Global Product Hydration**

The `fireworkShopping(_:updateDetailsForProducts:forVideo:_:)` method will be called when a video will be shown that contains products. It is at this point when the host app will be able to update the associated product information. In `fireworkShopping(_:updateDetailsForProducts:forVideo:_:)`, you could use `ProductHydrating` API to hydrate products. For example, you could update product names, descriptions, and variants.

```swift
func fireworkShopping(
    _ fireworkShopping: FireworkVideoShopping,
    updateDetailsForProducts products: [ProductID],
    forVideo video: VideoDetails,
    _ productHydrator: any ProductHydrating) {
    // Retrieve the most up-to-date product details
    // based on the product IDs from the host app server

    // Introduce a delay to mimic a network request that fetches
    // updated product data after the hydration callback is triggered.
    DispatchQueue.global().asyncAfter(wallDeadline: .now() + 3) {
        // Get product model list
        let productModels = productHydrator.products
        // Call hydration API
        for productID in products {
            productHydrator.hydrateProduct(productID) { productBuilder in
                // Update product info
                productBuilder
                    .name("Latest product name")
                    .description("Latest product description")
                    .isAvailable(true)

                // Set to true to hide the product, or false to keep it visible
                productBuilder.hidden(true)

                // Update product variants.
                // The strategy can be merge or replace.
                // With merge strategy, we will merge these new variants into existing variants. We use variant id to match the variant.
                // With `replace` strategy, we will replace existing variants with these new variants.
                productBuilder.variants(.merge) { variantsBuilder in
                    // Build variant
                    variantsBuilder.variant("variant id1") { variantBuilder in
                        variantBuilder.formattedPrice(100, currencyCode: "USD")
                            .formattedOriginalPrice(120, currencyCode: "USD")
                            .url("Latest variant url1")
                            .imageUrl("Latest variant image url1")
                            .isAvailable(true)
                            .options([
                                "Color": "Latest variant color1",
                                "Size": "Latest variant size1"
                            ])
                        return variantBuilder
                    }
                    
                    // Build variant
                    variantsBuilder.variant("variant id2") { variantBuilder in
                        variantBuilder.name("Latest variant name2")
                            .formattedPrice(110, currencyCode: "USD")
                            .formattedOriginalPrice(130, currencyCode: "USD")
                            .url("Latest variant url2")
                            .imageUrl("Latest variant image url2")
                            .isAvailable(true)
                            .options([
                                "Color": "Latest variant color2",
                                "Size": "Latest variant size2"
                            ])
                        return variantBuilder
                    }
                    return variantsBuilder
                }
                return productBuilder
            }
        }
    }
}
```

**Strategy for hydrating product variants**

1. With `merge` strategy, we will merge these new variants into existing variants. We use variant id to match the variant.
2. With `replace` strategy, we will replace existing variants with these new variants.

### Shopping error reporting

Implement `fireworkShopping(_:didFailWith:)` to be notified when a shopping action cannot be completed. The method has a default empty implementation, so it is optional — implement it only if you want to log or surface these failures.

```swift
func fireworkShopping(
    _ fireworkShopping: FireworkVideoShopping,
    didFailWith error: FireworkVideoShoppingError
) {
    switch error {
    case .missingProductID:
        // The product has no non-empty external identifier.
        print("Shopping failed: missing product ID")
    case .missingProductUnitID:
        // The selected product unit has no non-empty external identifier.
        print("Shopping failed: missing product unit ID")
    @unknown default:
        break
    }
}
```

`FireworkVideoShoppingError` has the following cases:

| Case                   | Description                                                                                                       |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `missingProductID`     | A shopping action requires a non-empty external product identifier, and the product does not have one.            |
| `missingProductUnitID` | A shopping action requires a non-empty external product unit identifier, and the selected unit does not have one. |

{% hint style="info" %}
These errors usually mean the product data is incomplete. Use [Global Product Hydration](#global-product-hydration) to supply the missing external identifiers.
{% endhint %}

### Player Deck one-tap Add to Cart

Player Deck product cards can show a one-tap Add to Cart button that adds the exact product unit shown on the card to your cart, without opening the product detail page. The SDK does not own a cart — you perform the add and report the outcome back, and the SDK animates the button accordingly.

The button is **hidden by default**. To enable it you must do both of the following, **before the Player Deck is created**:

1. Assign `FireworkVideoSDK.shopping.playerDeckAddToCartDelegate`.
2. Set `viewConfiguration.itemView.addToCartButton.isHidden = false`.

```swift
// 1. Assign the delegate
FireworkVideoSDK.shopping.playerDeckAddToCartDelegate = <Your delegate>

// 2. Unhide the button in the player deck configuration
var config = PlayerDeckContentConfiguration()
config.itemView.addToCartButton.isHidden = false

// Apply the changes for PlayerDeckView instance
let playerDeckView = PlayerDeckView()
playerDeckView.viewConfiguration = config

// Apply the changes for PlayerDeckSwiftUIView instance
PlayerDeckSwiftUIView(viewConfiguration: config)
```

Implement `FireworkVideoPlayerDeckAddToCartDelegate` to handle the request:

```swift
extension YourShoppingService: FireworkVideoPlayerDeckAddToCartDelegate {
    func fireworkShopping(
        _ fireworkShopping: FireworkVideoShopping,
        addToCartFromPlayerDeck item: SelectedProductVariant,
        fromVideo video: VideoDetails,
        statusHandler: @escaping (PlayerDeckAddToCartStatus) -> Void
    ) {
        // Add exactly the unit the card represents — do not substitute another unit.
        yourCart.add(productID: item.productID, unitID: item.unitID) { succeeded in
            // The handler can be called from any thread.
            statusHandler(succeeded ? .success : .failure)
        }
    }
}
```

`PlayerDeckAddToCartStatus` has the following cases:

| Case      | Description                                                                                     |
| --------- | ----------------------------------------------------------------------------------------------- |
| `loading` | The request is still in progress. Optional — report it to keep the button in its loading state. |
| `success` | The selected unit was added successfully. Terminal.                                             |
| `failure` | The selected unit could not be added. Terminal.                                                 |

{% hint style="warning" %}
Report `.success` or `.failure` within **10 seconds**. Otherwise the SDK restores the button without presenting a result. Calls made after a terminal result are ignored.
{% endhint %}

`SelectedProductVariant` carries the product context of the tapped card:

| Field       | Type              | Description                                                     |
| ----------- | ----------------- | --------------------------------------------------------------- |
| `productID` | `ProductID`       | The external identifier of the product.                         |
| `unitID`    | `UnitID`          | The external identifier of the exact unit to add to the cart.   |
| `url`       | `URL?`            | The URL associated with the selected unit.                      |
| `product`   | `Product?`        | The full product model.                                         |
| `variant`   | `ProductVariant?` | The selected variant details, including price and availability. |

{% hint style="info" %}
If the tapped card has no external product identifier or no external product unit identifier, the delegate is not called; the failure is reported through [Shopping error reporting](#shopping-error-reporting) instead.
{% endhint %}

{% hint style="info" %}
The delegate is read when a product card is configured. Assigning it after the Player Deck has been created only takes effect on the next product-card configuration update, so assign it during SDK setup. If either the delegate is `nil` or `addToCartButton.isHidden` is `true`, the button is not shown.
{% endhint %}

### Purchase tracking

The host app can record a purchase which will help get a full picture of the user journey flow. In order to do this, call `FireworkVideoSDK.trackPurchase` from your order confirmation (purchase complete) screen. Send every purchase event, without any client-side filtering, so Firework can identify attributed vs non-attributed purchases.

```swift
let orderValue: Decimal = // The final amount paid, including taxes, shipping, and any discounts applied
let shippingPrice: Decimal = // The shipping price
let subtotal: Decimal = // The cost of the items before discounts, taxes, and shipping
let totalDiscounts: Decimal = // The total discounts applied to the order
let lineItems: [LineItem] = [
    LineItem(
        sku: // The ID that can be linked back to the product or product unit.
             // It can be the SKU, barcode, GTIN, MPN, product_unit.external_id,
             // or product.external_id.
        price: // The unit price, including line-level discounts
        quantity: // Quantity
        productName: // The product variant or unit name (optional)
    )
]
FireworkVideoSDK.trackPurchase(
    orderID: "<Order ID of User Purchase>",
    orderValue: orderValue,
    currencyCode: Locale.current.currencyCode ?? "USD",
    countryCode: Locale.current.regionCode,
    shippingPrice: shippingPrice,
    subtotal: subtotal,
    totalDiscounts: totalDiscounts,
    lineItems: lineItems,
    /// Any additional information associated to the purchase.
    /// Reserved keys: order_id, order_value, currency, country, shipping_price,
    /// subtotal, total_discounts, line_items.
    /// Any values passed in the additionalInfo that use a reserved key will be ignored.
    [
        "additionalKey1": "additionalValue1",
        "additionalKey2": "additionalValue2",
        "additionalKey3": "additionalValue3"
    ]
)
```

The parameters are described in the following table:

| Parameter        | Type                | Required | Description                                                                  |
| ---------------- | ------------------- | -------- | ---------------------------------------------------------------------------- |
| `orderID`        | `String`            | Yes      | A unique identifier for the user's order.                                    |
| `orderValue`     | `Decimal`           | Yes      | The final amount paid, including taxes, shipping, and any discounts applied. |
| `currencyCode`   | `String`            | Yes      | The ISO 4217 currency code of the purchase value.                            |
| `countryCode`    | `String?`           | No       | The country code of the purchase.                                            |
| `shippingPrice`  | `Decimal?`          | No       | The shipping price of the order.                                             |
| `subtotal`       | `Decimal`           | Yes      | The cost of the items before discounts, taxes, and shipping.                 |
| `totalDiscounts` | `Decimal?`          | No       | The total discounts applied to the order.                                    |
| `lineItems`      | `[LineItem]`        | Yes      | The purchased items associated with the purchase.                            |
| `additionalInfo` | `[String: String]?` | No       | Any additional information associated to the purchase.                       |

Each `LineItem` has the following fields:

| Field         | Type      | Required | Description                                                                                                                                                 |
| ------------- | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sku`         | `String`  | Yes      | The ID that can be linked back to the product or product unit. It can be the SKU, barcode, GTIN, MPN, `product_unit.external_id`, or `product.external_id`. |
| `price`       | `Decimal` | Yes      | The unit price, including line-level discounts.                                                                                                             |
| `quantity`    | `Int`     | Yes      | The product quantity.                                                                                                                                       |
| `productName` | `String?` | No       | The product variant or unit name.                                                                                                                           |
