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
sectionsurfaces - composable
data_sourcesurfaces - composable
actionsurfaces
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)anduseBoundAction(alias)inside the section - merchants and AI can configure the section
- merchants and AI cannot edit the installed app surface code itself