Példa app részletes bemutatása
A teljes Recommendations példa fájlról fájlra, a manifesttől és szervertől kezdve az öt workeren át a közzétételig, felülvizsgálatig, telepítésig, frissítésig és eltávolításig.
Ezen az oldalon
A Poltura Labs által készített Recommendations a legnagyobb példa app: öt natív worker, egy szerver OAuth-tal, session checkkel, műveletekkel és webhookokkal, valamint app-tárhellyel. Ez az oldal abban a sorrendben mutatja be a felépítését, ahogyan megírnád. Az oldalon található minden code blockot a dokumentáció build-folyamata emel ki közvetlenül a példa forráskódjából, így a valódi, aktuális kódot látod.
A példa még nem nyilvános (lásd: Példák), ezért a kódot itt teljes terjedelmében közöljük. Ha kisebb lépéssel kezdenél, kövesd először az Építsd meg az első appodat útmutatót.
1. A projekt
A poltura app create parancs egy ennél kisebb projektet hoz létre; ez a bemutató a kész példát követi végig.
Scaffold a new app project.
USAGE
$ poltura app create [FLAGS] <dir>
ARGUMENTS
dir Directory to create the project in (must be empty).
FLAGS
--template <template> Project template (choices: "hosted", "native", default:
"native").
-h, --help Show help.packages/app-sdk/examples/complete-app/
poltura.app.json
package.json
src/server.ts
src/elements.ts
src/workers/admin-page.tsx
src/workers/admin-product.tsx
src/workers/storefront-product.tsx
src/workers/storefront-footer.tsx
src/workers/app-page.tsx
src/styles.css
src/locales/en.json
src/locales/hu.json
README.md2. A manifest
A poltura.app.json deklarálja az öt workert, azokat az oldalakat, amelyeken keresztül kettő elérhetővé válik,
két műveletet, két webhookot és az app által igényelt jogosultságokat. Minden mező részletes leírását megtalálod
a Manifest oldalon.
{
"schemaVersion": 1,
"apiVersion": "v1",
"handle": "recommendations",
"releaseVersion": "1.0.0",
"displayName": "Recommendations by Poltura Labs",
"description": "Product recommendations for admin settings, product panels, storefront blocks, footer capture, and an app-owned page.",
"translations": [
{
"locale": "hu-HU",
"displayName": "Ajánlások a Poltura Labs-tól",
"description": "Termékajánlók az adminhoz, a termékoldalhoz, a lábléchez és egy alkalmazásoldalhoz."
}
],
"supportUrl": "https://recommendations.apps.example.com/support",
"privacyUrl": "https://recommendations.apps.example.com/privacy",
"homepageUrl": "https://recommendations.apps.example.com/",
"backendOrigin": "https://recommendations.apps.example.com",
"redirectUrls": ["https://recommendations.apps.example.com/oauth/callback"],
"permissions": {
"required": [
{
"id": "settings.read",
"purpose": "Read store locale, currency, and public capabilities"
},
{
"id": "products.read",
"purpose": "List products and collections used as recommendation sources"
},
{
"id": "orders.read",
"purpose": "Read new orders that refresh recommendation weights"
}
],
"optional": []
},
"webhooks": [
{
"topic": "order.created",
"destination": "https://recommendations.apps.example.com/webhooks/orders"
},
{
"topic": "product.updated",
"destination": "https://recommendations.apps.example.com/webhooks/products"
}
],
"settings": [
{
"id": "highlight_mode",
"type": "enum",
"label": "Highlight mode",
"options": ["quiet", "featured"],
"default": "quiet"
}
],
"actions": [
{
"id": "save_featured",
"purpose": "Save featured product ids for this installation",
"surfaces": ["admin"]
},
{
"id": "recommend",
"purpose": "Return a product recommendation for the current shopper",
"surfaces": ["storefront"]
}
],
"destinations": [
{
"id": "newsletter_capture",
"url": "https://recommendations.apps.example.com/capture",
"purpose": "Receive optional newsletter captures from the footer worker"
}
],
"adminPages": [
{
"id": "recommendations",
"path": "workers/admin-page.js",
"title": "Recommendations",
"translations": [
{
"locale": "hu-HU",
"title": "Ajánlások"
}
],
"icon": "star"
}
],
"storefrontPages": [
{
"id": "recs_page",
"path": "workers/app-page.js",
"title": "Recommendations",
"slug": "recommendations",
"translations": [
{
"locale": "hu-HU",
"title": "Ajánlások"
}
],
"icon": "star"
}
],
"elements": "src/elements.ts",
"workers": [
{
"target": "admin.page",
"entry": "src/workers/admin-page.tsx",
"stylesheet": "src/styles.css",
"element": "recommendations-panel"
},
{
"target": "admin.product",
"entry": "src/workers/admin-product.tsx",
"stylesheet": "src/styles.css"
},
{
"target": "storefront.product",
"entry": "src/workers/storefront-product.tsx",
"stylesheet": "src/styles.css",
"element": "recommendations"
},
{
"target": "storefront.layout.footer",
"entry": "src/workers/storefront-footer.tsx",
"stylesheet": "src/styles.css",
"element": "newsletter-capture"
},
{
"target": "storefront.app-page",
"entry": "src/workers/app-page.tsx",
"stylesheet": "src/styles.css",
"element": "recommendations-page"
}
]
}3. A szerver
A src/server.ts egy egyszerű Node HTTP-szerver a 8787-es porton. A /connect elindítja egy bolt összekapcsolását,
a /oauth/callback pedig befejezi azt. A további részletekért olvasd el a Hitelesítés oldalt.
A szervered az App API-t maga az app nevében hívja meg a gépi felhasználó tokenjével és a bolti telepítéssel
a Poltura-Installation headerben; lásd: Hitelesítés.
if (sessions === null) {
send(res, 503, 'Session verification is not configured.');
return;
}
const header = requestHeaders(req).get('authorization') ?? '';
const token = header.startsWith('Bearer ') ? header.slice('Bearer '.length) : '';
const assertion = await sessions.verify(token);
send(
res,
200,
JSON.stringify({ installationId: assertion.installationId, storeId: assertion.storeId }),
'application/json; charset=utf-8',
);Minden kérés a /actions/<id> endpointra egy workertől érkező action call. Az actions.handle ellenőrzi a Poltura aláírását,
és futtatja a save_featured vagy a recommend műveletet:
const raw = await readBody(req);
const output = await actions.handle(raw, requestHeaders(req));
send(res, 200, JSON.stringify(output), 'application/json; charset=utf-8');Futtasd a pnpm start paranccsal. A kereskedő számára semmi sem változik, amíg össze nem kapcsolja a boltját.
A szerver feliratkozik az order.created és product.updated eseményekre. Mindegyik kezelő ellenőrzi a nyers törzs
aláírását, mielőtt beolvasná azt:
const raw = await readBody(req);
const event = verifier.verify(raw, requestHeaders(req));
if (event.topic === PublicTopics.ORDER_CREATED) {
await saveLastOrder(client(), event.data.orderId, 0, event.eventId);
}
send(res, 200, event.topic);const raw = await readBody(req);
const event = verifier.verify(raw, requestHeaders(req));
send(res, 200, event.topic);A kiemelt termékek az app-tárhelyben tárolódnak (telepítésenként egy kis key-value store-ban) az App API-n keresztül. Az írási művelet megadja a felülírt verziót, és idempotenciakulcsot tartalmaz, így az újrapróbált kérések csak egyszer érvényesülnek:
export async function saveFeatured(
client: PolturaClient,
productIds: readonly string[],
expectedVersion: number,
idempotencyKey: string,
): Promise<StorageRecord> {
const value = FeaturedProducts.parse({ productIds: [...productIds] });
return client.storage.put(STORAGE_NAMESPACE, FEATURED_KEY, value, {
expectedVersion,
idempotencyKey,
});
}Az app telepítése után a boltban leadott új rendelés vagy egy termékmódosítás webhookot küld ezekre a route-okra.
5. Adminoldal
Az admin.page worker az app saját oldala az adminfelületen. Beolvassa a beállításait, lehetővé teszi a kereskedő számára,
hogy a platform választójával termékeket jelöljön ki, és elmenti azokat a save_featured műveleten keresztül.
export function AdminPage(): ReactNode {
const copy = copyFor(useLocale());
const settings = useSettings();
const navigate = useNavigate();
const toast = useToast();
const pick = useResourcePicker();
const saveFeatured = useAction('save_featured');
const heading = String(settings.heading ?? copy.heading);
const [picked, setPicked] = useState('');
return (
<Card className='offer-panel rounded-theme bg-surface text-foreground p-4'>
<Stack>
<Heading>{heading}</Heading>
<Text>{copy.adminIntro}</Text>
<TextField label={copy.featuredLabel} value={picked} onChange={setPicked} />
<Inline>
<Button
label={copy.pickProducts}
onPress={async () => {
const ids = await pick('product', true);
if (ids === null) return;
setPicked(ids.join(','));
await toast('success', copy.pickedToast);
}}
/>
<Button
label={copy.save}
onPress={async () => {
const productIds = picked
.split(',')
.map((value) => value.trim())
.filter((value) => value.length > 0);
await saveFeatured({
productIds,
expectedVersion: 0,
idempotencyKey: crypto.randomUUID(),
});
await toast('success', copy.savedToast);
}}
/>
<Button
label={copy.openPage}
onPress={() => navigate({ kind: 'app_page', pageId: RECOMMENDATIONS_PAGE })}
/>
</Inline>
</Stack>
</Card>
);
}A kereskedő az Alkalmazások, majd az Ajánlások menüpont alatt találja meg.
6. Termékpanel
Az admin.product worker egy panelt ad hozzá minden termékoldalhoz az adminfelületen. Csak azt a terméket olvassa be,
amelyet a kereskedő éppen megnyitott, a gombja pedig megnyitja az app adminoldalát.
export function AdminProduct(): ReactNode {
const copy = copyFor(useLocale());
const selected = useSelectedEntity();
const navigate = useNavigate();
if (selected.status !== 'ready' || selected.entity.kind !== 'product') {
return <Text>{copy.unavailable}</Text>;
}
return (
<Card className='rounded-theme bg-surface text-foreground p-4'>
<Stack>
<Heading>{selected.entity.product.title}</Heading>
<Button
label={copy.openSettings}
onPress={() => navigate({ kind: 'app_page', pageId: RECOMMENDATIONS_PAGE })}
/>
</Stack>
</Card>
);
}7. Webáruház-blokk
A storefront.product worker egy blokk, amelyet a kereskedők a product template-re helyezhetnek el a témaszerkesztőben. Beolvassa
az éppen megtekintett terméket, a recommend műveleten keresztül ajánlást kér a szervertől, és kosárba helyezi azt.
export function StorefrontProduct(): ReactNode {
const copy = copyFor(useLocale());
const settings = useSettings();
const theme = useTheme();
const cart = useCart();
const toast = useToast();
const host = useStorefrontContext();
const recommend = useAction('recommend');
const heading = String(settings.heading ?? copy.alsoLike);
const storeName = theme?.settings?.store_name;
const [contextLabel, setContextLabel] = useState(copy.alsoLike);
useEffect(() => {
void host.read().then(async (context) => {
if (context.kind === 'product') setContextLabel(context.product.title);
const suggestion = await recommend({
productId: context.kind === 'product' ? context.product.id : null,
});
const title = z.object({ title: z.string().min(1) }).safeParse(suggestion);
if (title.success) setContextLabel(title.data.title);
});
}, [host, recommend]);
return (
<Card className='offer-panel rounded-theme bg-surface text-foreground p-4'>
<Stack>
<Heading className='font-display'>{heading}</Heading>
<Text>{z.string().min(1).safeParse(storeName).data ?? contextLabel}</Text>
<Button
label={copy.addToCart}
className='bg-primary text-on-primary'
onPress={async () => {
const parsed = VariantId.safeParse(settings.variant_id);
if (!parsed.success) {
await toast.show({ tone: 'warning', message: copy.cartRejected });
return;
}
const reply = await cart.add({ variantId: parsed.data, quantity: 1 });
if (reply.status === 'ok') {
await toast.show({ tone: 'success', message: copy.added });
return;
}
await toast.show({ tone: 'warning', message: copy.cartRejected });
}}
/>
</Stack>
</Card>
);
}A vásárlók ott látják a blokkot, ahová a kereskedő elhelyezte, egy Kosárba gombbal.
8. Lábléc-zóna
A storefront.layout.footer worker a téma láblécében helyezkedik el egy hírlevél-űrlappal. A böngészőben ellenőrzi
a címet, és egy toast értesítést jelenít meg; a példa nem továbbítja a címet sehová.
export function StorefrontFooter(): ReactNode {
const copy = copyFor(useLocale());
const toast = useToast();
const [email, setEmail] = useState('');
const [error, setError] = useState('');
return (
<Card className='rounded-theme bg-muted text-foreground p-4'>
<Stack>
<Text>{copy.footerHelp}</Text>
<TextField
label={copy.emailLabel}
value={email}
error={error}
onChange={(value) => {
setEmail(value);
setError('');
}}
/>
<Button
label={copy.subscribe}
onPress={async () => {
const parsed = Email.safeParse(email);
if (!parsed.success) {
setError(copy.invalidEmail);
await toast.show({ tone: 'warning', message: copy.invalidEmail });
return;
}
setError('');
await toast.show({ tone: 'success', message: copy.subscribed });
}}
/>
</Stack>
</Card>
);
}9. Webáruház-oldal
A storefront.app-page worker a bolt egy teljes oldala az /apps/recommendations címen, mivel a storefrontPages
a recommendations slugot adja meg hozzá.
export function AppPage(): ReactNode {
const copy = copyFor(useLocale());
const settings = useSettings();
const navigate = useNavigate();
const heading = String(settings.heading ?? copy.heading);
return (
<Card className='rounded-theme bg-surface text-foreground p-4'>
<Stack>
<Heading>{heading}</Heading>
<Text>{copy.pageIntro}</Text>
<Button label={copy.shopProducts} onPress={() => navigate.to('/products')} />
</Stack>
</Card>
);
}10. Stílusok
Mind az öt worker a src/styles.css fájlt használja. Ez egy színt és egy utility class-t ad hozzá a Poltura témarétegéhez:
@theme {
--color-offer-highlight: #fef3c7;
}
@utility offer-panel {
border-width: 1px;
}Az inheritTheme alapértelmezés szerint be van kapcsolva, így a workerek átveszik az adminfelület vagy a bolt színeit,
betűtípusait és térközeit. Lásd: Stílusok.
11. Tesztelés, build és ellenőrzés
pnpm test:unit
pnpm build
pnpm validateA pnpm build csomagolja a workereket, és létrehozza a release-t:
built recommendations@1.0.0 sha256-qwcsxz+/Sn1wjxUbCKy7Ah/ROvTpZrZMf/8OTBIGg9Q= 5 workersA pnpm validate egy ideiglenes mappába buildel, és összeveti az eredményt a release formátummal:
built recommendations@1.0.0 sha256-qwcsxz+/Sn1wjxUbCKy7Ah/ROvTpZrZMf/8OTBIGg9Q= 5 workers
ok recommendations@1.0.0
manifest.json 7765 bytes12. Közzététel, felülvizsgálat, telepítés
poltura app publish --api "$POLTURA_API" --app <app id> --token "$POLTURA_APP_TOKEN"Az app azonosítója és a fejlesztői token (pltd_…, vagy személyes pltu_…) az adminfelület Fejlesztői alkalmazások
menüpontjából származik. Add hozzá a --admin-origin <admin origin> flaget, ha a felülvizsgálati linket is ki szeretnéd
íratni. A CLI kiírja a published <releaseId> üzenetet és a felülvizsgálati linket. A Poltura egyik felülvizsgálója jóváhagyja
az éles kiadást. A bolt tulajdonosa a Poltura ID-ban telepíti a https://id.poltura.com/install?app=<app id> címen,
és a Telepített appok alatt látja (Hitelesítés).
13. Frissítés új jogosultsággal
Adj hozzá egy jogosultsági kérést a permissions.optional vagy a permissions.required listához (összesen legfeljebb 32).
Növeld a releaseVersion értékét. Tegyél közzé egy új éles release-t. Független felülvizsgálat következik. A telepítés
azonnal átáll az új release-re (app.updated); az új kötelező jogosultságok addig várakoznak, amíg egy adminisztrátor jóvá
nem hagyja őket a Poltura ID-ban, és az App API addig 403 insufficient_scope hibát ad rájuk (a jóváhagyást az
installation.permissions_changed esemény követi).
14. Eltávolítás
A kereskedő a Kapcsolt alkalmazások menüpontban távolítja el az appot. A telepítés removing, majd removed állapotba
kerül, te pedig megkapod az app.suspended, majd az app.uninstalled eseményt. Egy későbbi telepítés üres app-tárhellyel
indul. A saját szervereiden lévő adatok ott maradnak: töröld őket az app.uninstalled eseménykor, ahogy az adatvédelmi
irányelveid előírják.