npm package: @personizely/shopify-hydrogen
Description: Personizely adapter for Shopify Hydrogen headless storefronts - Simple React Provider integration
Author: Personizely
Latest version: 1.0.4

# @personizely/shopify-hydrogen

Personizely adapter for Shopify Hydrogen headless storefronts. Simple React Provider integration for personalization, popups, and A/B testing.

## Features

- ✅ **Plug & Play** - Minimal configuration required
- ✅ **Server-Side Script Loading** - Prevents A/B test flicker
- ✅ **Fully Reactive** - Integrates seamlessly with Hydrogen's cart
- ✅ **TypeScript** - Fully typed for great DX
- ✅ **Auto Page Detection** - Automatically detects product/collection pages
- ✅ **Cart & Checkout Triggers** - Easy widget triggering

## Installation

```bash
npm install @personizely/shopify-hydrogen
```

## Quick Start

### 1. Set Up Your Root Loader

In your `app/root.tsx`, add Personizely configuration to the loader:

```tsx
export async function loader(args: Route.LoaderArgs) {
  const { env } = args.context

  return {
    // ... other data
    personizely: {
      storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
      shopDomain: env.PUBLIC_STORE_DOMAIN,
    }
  }
}
```

### 2. Wrap Your App with PersonizelyProvider

In your `app/root.tsx` default export:

```tsx
import { PersonizelyProvider } from '@personizely/shopify-hydrogen'

export default function App() {
  const data = useRouteLoaderData<RootLoader>('root')

  if (!data) {
    return <Outlet />
  }

  return (
    <PersonizelyProvider
      websiteApiKey="your-api-key"
      storefrontAccessToken={data.personizely.storefrontAccessToken}
      shopDomain={data.personizely.shopDomain}
      locale="en-US"
    >
      <Outlet />
    </PersonizelyProvider>
  )
}
```

**That's it!** The Provider:
- ✅ Automatically integrates with Hydrogen's cart from the root loader
- ✅ Sets up the config before loading the script
- ✅ Loads the Personizely snippet dynamically
- ✅ Provides hooks for triggering widgets

### 3. Trigger Cart Add Events

In your product form component:

```tsx
import { useCartAdd } from '@personizely/shopify-hydrogen'

export function ProductForm({ product, selectedVariant }) {
  const triggerCartAdd = useCartAdd()

  const handleAddToCart = async () => {
    // Trigger Personizely widget (e.g., upsell popup)
    const widgetShown = await triggerCartAdd({
      id: product.id,
      variantId: selectedVariant.id,
      price: parseFloat(selectedVariant.price.amount),
      quantity: 1,
      handle: product.handle,
    })

    if (!widgetShown) {
      // Add to cart normally if no widget was shown
      // Your cart add logic here
    }
  }

  return (
    <button onClick={handleAddToCart}>
      Add to Cart
    </button>
  )
}
```

### 4. Trigger Checkout Events

In your cart or checkout button component:

```tsx
import { useCheckout } from '@personizely/shopify-hydrogen'

export function CheckoutButton({ checkoutUrl }) {
  const triggerCheckout = useCheckout()

  const handleCheckout = async () => {
    // Trigger Personizely widget (e.g., exit intent offer)
    const widgetShown = await triggerCheckout()

    if (!widgetShown) {
      // Proceed to checkout if no widget was shown
      window.location.href = checkoutUrl
    }
  }

  return (
    <button onClick={handleCheckout}>
      Proceed to Checkout
    </button>
  )
}
```

### 5. Set Page Context

Use `usePageContext` to inform Personizely about the current page for better targeting:

```tsx
import { usePageContext } from '@personizely/shopify-hydrogen'

export default function ProductRoute() {
  const { product } = useLoaderData<typeof loader>()

  // Set page context on product pages
  usePageContext({
    pageType: 'product',
    product: {
      id: extractId(product.id), // Numeric ID
      handle: product.handle,
      tags: product.tags
    }
  })

  return <ProductPage product={product} />
}
```

**When to use `usePageContext`:**
- **Product pages** - Set `pageType: 'product'` with product data
- **Collection pages** - Set `pageType: 'collection'` with collection data
- **Home page** - Set `pageType: 'home'`
- **Cart page** - Set `pageType: 'cart'`

**Example for collection page:**
```tsx
usePageContext({
  pageType: 'collection',
  collection: {
    id: extractId(collection.id)
  }
})
```

## API Reference

### Components

#### `<PersonizelyProvider>`

React context provider for Personizely integration.

**Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `websiteApiKey` | `string` | ✅ | Your Personizely website API key |
| `storefrontAccessToken` | `string` | ✅ | Shopify Storefront API access token |
| `shopDomain` | `string` | ✅ | Your shop domain (e.g., `'myshop.myshopify.com'`) |
| `locale` | `string` | ❌ | Shop locale (default: `'en-US'`) |
| `currency` | `object` | ❌ | Currency configuration (see below) |
| `market` | `string` | ❌ | Market handle for international stores |
| `apiVersion` | `string` | ❌ | Storefront API version (default: `'2024-01'`) |
| `methods` | `object` | ❌ | Custom method overrides (see below) |
| `customer` | `Customer \| null` | ❌ | Customer object for personalization |

**Currency Configuration:**
```tsx
currency={{
  rate: 1.2,           // Conversion rate from base currency
  active: 'EUR',       // Current currency code
  base: 'USD'          // Base currency code
}}
```

**Methods Configuration:**
```tsx
methods={{
  // Custom money formatting
  formatMoney: (amount, includeDecimals) => {
    // amount is in cents
    // Return formatted string (e.g., "$19.99" or "$20")
    return includeDecimals
      ? `$${(amount / 100).toFixed(2)}`
      : `$${Math.round(amount / 100)}`
  },

  // Custom product URL builder
  builtProductPath: ({ handle }) => {
    // Return product URL path
    return `/products/${handle}`
  }
}}
```

**Complete Example:**
```tsx
<PersonizelyProvider
  websiteApiKey="your-api-key"
  storefrontAccessToken={data.personizely.storefrontAccessToken}
  shopDomain={data.personizely.shopDomain}
  locale="en-US"
  currency={{
    rate: 0.85,
    active: 'EUR',
    base: 'USD'
  }}
  market="europe"
  methods={{
    formatMoney: (amount, includeDecimals) => {
      const value = amount / 100
      return includeDecimals
        ? `€${value.toFixed(2).replace('.', ',')}`
        : `€${Math.round(value)}`
    },
    builtProductPath: ({ handle }) => `/shop/${handle}`
  }}
  customer={customer}
>
  {children}
</PersonizelyProvider>
```

### Hooks

#### `useCartAdd()`

Returns a function to trigger cart add widgets (e.g., upsells, cross-sells).

**Returns:** `(product: CartAddProduct) => Promise<boolean>`
- Returns `true` if a widget was shown
- Returns `false` if no widget was shown

**CartAddProduct Type:**
```tsx
{
  id: string | number              // Product ID
  variantId: string | number       // Variant ID
  price?: string | number          // Price in dollars (will be converted to cents)
  quantity?: number                // Quantity (default: 1)
  handle?: string                  // Product handle
  properties?: Record<string, string> // Custom properties
}
```

**Example:**
```tsx
const triggerCartAdd = useCartAdd()

const widgetShown = await triggerCartAdd({
  id: 'gid://shopify/Product/123',
  variantId: 'gid://shopify/ProductVariant/456',
  price: 19.99,  // In dollars
  quantity: 1,
  handle: 'awesome-product',
  properties: {
    gift_wrap: 'yes'
  }
})
```

#### `useCheckout()`

Returns a function to trigger checkout widgets (e.g., exit intent offers, discount popups).

**Returns:** `() => Promise<boolean>`
- Returns `true` if a widget was shown
- Returns `false` if no widget was shown

**Example:**
```tsx
const triggerCheckout = useCheckout()

const widgetShown = await triggerCheckout()
if (!widgetShown) {
  // Proceed with checkout
  window.location.href = checkoutUrl
}
```

#### `usePageContext(context)`

Sets the current page context for better widget targeting. Call this hook in your route components.

**Parameters:**
- `context: Partial<PageContext>` - Page context object

**PageContext Type:**
```tsx
{
  pageType?: string | number       // Page type: 'product' | 'collection' | 'home' | 'cart' | custom
  product?: {                      // Product data (for product pages)
    id: number                     // Numeric product ID
    handle: string                 // Product handle
    tags?: string[]                // Product tags
  } | null
  collection?: {                   // Collection data (for collection pages)
    id: number                     // Numeric collection ID
  } | null
}
```

**Examples:**

Product page:
```tsx
import { usePageContext } from '@personizely/shopify-hydrogen'

export default function ProductRoute() {
  const { product } = useLoaderData<typeof loader>()

  usePageContext({
    pageType: 'product',
    product: {
      id: extractNumericId(product.id),  // Extract numeric ID from GID
      handle: product.handle,
      tags: product.tags
    }
  })

  return <ProductPage product={product} />
}
```

Collection page:
```tsx
usePageContext({
  pageType: 'collection',
  collection: {
    id: extractNumericId(collection.id)
  }
})
```

Home page:
```tsx
usePageContext({
  pageType: 'home'
})
```

Cart page:
```tsx
usePageContext({
  pageType: 'cart'
})
```

#### `usePersonizely()`

Returns the Personizely context with cart and adapter instances.

**Returns:** `{ cart: Cart, adapter: Adapter, websiteApiKey: string }`

**Example:**
```tsx
const { cart, adapter, websiteApiKey } = usePersonizely()

// Access cart methods
const cartTotal = cart.getValue()
const itemCount = cart.getSize()

// Access adapter methods
const formattedPrice = adapter.formatMoney(1999, true) // "$19.99"
const productUrl = adapter.buildProductPath({ handle: 'my-product' })

// Get customer data (returns what was set via the customer prop)
const customer = adapter.getCustomer()
if (customer) {
  console.log('Customer:', customer.displayName, customer.tags)
}
```

## Advanced Usage

### Multi-Currency Support

```tsx
// Define currency outside component for stable reference
const CURRENCY = {
  rate: 0.85,      // EUR/USD rate
  active: 'EUR',   // Display currency
  base: 'USD'      // Base currency
}

export default function App() {
  return (
    <PersonizelyProvider
      websiteApiKey="your-api-key"
      storefrontAccessToken={token}
      shopDomain={domain}
      currency={CURRENCY}
      methods={{
        formatMoney: (amount, includeDecimals) => {
          const value = amount / 100
          return includeDecimals
            ? `€${value.toFixed(2).replace('.', ',')}`
            : `€${Math.round(value)}`
        }
      }}
    >
      {children}
    </PersonizelyProvider>
  )
}
```

### International Markets

```tsx
// Define market outside component for stable reference
const MARKET = 'europe'

export default function App() {
  return (
    <PersonizelyProvider
      websiteApiKey="your-api-key"
      storefrontAccessToken={token}
      shopDomain={domain}
      market={MARKET}
      locale="de-DE"
    >
      {children}
    </PersonizelyProvider>
  )
}
```

### Authenticated Customers

Pass customer data directly to enable customer-specific personalization:

```tsx
export default function App() {
  const data = useRouteLoaderData<RootLoader>('root')

  // Get customer data from your loader or Hydrogen's customer account
  const customer = data.customer ? {
    id: data.customer.id,
    email: data.customer.email,
    firstName: data.customer.firstName,
    lastName: data.customer.lastName,
    displayName: data.customer.displayName,
    hasAccount: true,
    tags: data.customer.tags || []
  } : null

  return (
    <PersonizelyProvider
      websiteApiKey="your-api-key"
      storefrontAccessToken={data.personizely.storefrontAccessToken}
      shopDomain={data.personizely.shopDomain}
      customer={customer}
    >
      {children}
    </PersonizelyProvider>
  )
}
```

**Customer Type:**
```tsx
type Customer = {
  id: string
  email?: string | null
  phone?: string | null
  firstName?: string | null
  lastName?: string | null
  displayName: string
  hasAccount: boolean
  tags: string[]
}
```

### Custom Product URLs

```tsx
<PersonizelyProvider
  websiteApiKey="your-api-key"
  storefrontAccessToken={token}
  shopDomain={domain}
  methods={{
    builtProductPath: ({ handle }) => {
      // Custom URL structure
      return `/shop/products/${handle}`
    }
  }}
>
  {children}
</PersonizelyProvider>
```

## TypeScript

This package is fully typed. Import types as needed:

```tsx
import type {
  CartAddProduct,
  PageContext,
  PersonizelyConfigMethods,
  Product,
  Customer,
} from '@personizely/shopify-hydrogen'
```

## How It Works

1. **Provider Setup**: `PersonizelyProvider` sets up `window.__PLY_HYDROGEN_CONFIG__` with cart and adapter instances
2. **Script Loading**: Provider dynamically loads the Personizely snippet after config is ready
3. **Cart Integration**: Automatically integrates with Hydrogen's cart from the root loader
4. **Page Context**: Use `usePageContext()` in route components to set page type and data
5. **Event Triggers**: `useCartAdd` and `useCheckout` hooks trigger widgets at key moments

## Compatibility

- **Shopify Hydrogen**: v2.x
- **React Router**: v7.x
- **React**: v18.x
- **TypeScript**: v5.x

## Getting Your API Keys

1. **Personizely Website API Key**: Get this from your [Personizely dashboard](https://app.personizely.net/settings)
2. **Shopify Storefront Access Token**: Create a [custom app in Shopify Admin](https://shopify.dev/docs/api/usage/authentication#getting-started-with-authenticated-access) with Storefront API access

## Support

- 📧 Email: support@personizely.net
- 📚 Docs: https://help.personizely.net
- 💬 Chat: Available in your Personizely dashboard

## License

MIT