Command Palette

Search for a command to run...

App SDK › Example: Reviews App with AI-bindable Surfaces

Example: Reviews App with AI-bindable Surfaces

See a full reviews-style app with ready-made sections and AI-bindable surfaces.

Example: Reviews App with AI-bindable Surfaces

This is the canonical pattern for an app that wants maximum customizer + AI compatibility:

  • ready-made section surfaces
  • composable data_source surfaces
  • composable action surfaces

App-owned surfaces

Ready-made section

import { defineMessages, defineSection, msg } from '@commercit/app-sdk'
import { t } from '@commercit/app-sdk/ui'
import { Badge, Card, CardContent, CardHeader, CardTitle } from '@commercit/app-sdk/ui'

const copy = defineMessages('reviews.widget', {
  badge: 'Verified reviews',
  heading: 'What customers are saying',
  body: 'Installed app section rendered through the shared host runtime.',
})
const badge = msg('reviews.widget.badgeText', 'Verified reviews')

export default defineSection({
  surface: 'inline',
  preferredRegion: 'main',
  messageCatalog: [...Object.values(copy), badge],
  component: function ProductReviewWidget({ config }) {
    return (
      <Card>
        <CardHeader>
          <Badge variant="secondary">{t(badge)}</Badge>
          <CardTitle>{String(config.heading ?? t(copy.heading))}</CardTitle>
        </CardHeader>
        <CardContent>
          <p className="text-sm text-muted-foreground">
            {t(copy.body)}
          </p>
        </CardContent>
      </Card>
    )
  },
})

Data source

import { defineDataSource, docs } from '@commercit/app-sdk'

export default defineDataSource({
  handler: async (params) => {
    const productId = typeof params.product_id === 'string' ? params.product_id : null
    if (!productId) {
      return { average_rating: null, review_count: 0, items: [] }
    }

    const items = await docs.query('reviews', {
      filter: { product_id: productId },
      sort: { created_at: 'desc' },
      limit: 12,
    })

    const ratings = items
      .map((item) => Number((item as Record<string, unknown>).rating))
      .filter((value) => Number.isFinite(value))

    const average =
      ratings.length > 0
        ? ratings.reduce((sum, value) => sum + value, 0) / ratings.length
        : null

    return {
      average_rating: average,
      review_count: items.length,
      items,
    }
  },
})

Action

import { defineAction, docs } from '@commercit/app-sdk'

export default defineAction({
  handler: async (input) => {
    await docs.create('reviews', input)
    return { success: true }
  },
})

AI-bindable custom section

The AI can create a theme/workspace section that binds to the app outputs.

sections/product-review-summary/section.tsx

import { defineMessages, defineSection, t, useBoundAction, useBoundData } from '@commercit/theme-sdk'
import { Button, Card, CardContent, CardHeader, CardTitle, Stack } from '@commercit/theme-sdk/ui'

type ReviewSummary = {
  average_rating?: number
  review_count?: number
}

const copy = defineMessages('productReviewSummary', {
  heading: 'What shoppers think',
  reviewCount: '{count} reviews',
  submitCta: 'Leave a review',
})

export default defineSection({
  messageCatalog: Object.values(copy),
  component: function ProductReviewSummary({ config }) {
    const summary = useBoundData<ReviewSummary>('summary') ?? {}
    const submitReview = useBoundAction('submit_review')

    return (
      <Card>
        <CardHeader>
          <CardTitle>{String(config.heading ?? t(copy.heading))}</CardTitle>
        </CardHeader>
        <CardContent>
          <Stack gap="sm">
            <p className="text-3xl font-semibold">
              {typeof summary.average_rating === 'number' ? summary.average_rating.toFixed(1) : '—'}★
            </p>
            <p className="text-sm text-muted-foreground">
              {t(copy.reviewCount, {
                count: typeof summary.review_count === 'number' ? summary.review_count : 0,
              })}
            </p>
            <div>
              <Button
                onClick={() =>
                  void submitReview({
                    rating: 5,
                    title: 'Amazing',
                  })
                }
              >
                {t(copy.submitCta)}
              </Button>
            </div>
          </Stack>
        </CardContent>
      </Card>
    )
  },
})

The host persists bindings like this:

{
  "_bindings": {
    "data": {
      "summary": {
        "moduleId": "app-version-surface-reviews-summary",
        "kind": "data_source",
        "params": {
          "product_id": "$product.id"
        }
      }
    },
    "actions": {
      "submit_review": {
        "moduleId": "app-version-surface-submit-review",
        "kind": "action"
      }
    }
  }
}

Important rules:

  • bind by moduleId, never by slug
  • use inspect({ what: "surfaces" }) to find valid bindable surfaces
  • use useBoundData(alias) and useBoundAction(alias) inside the section
  • merchants and AI can configure the section
  • merchants and AI cannot edit the installed app surface code itself