Fejlesztőknek
Kezdj bele
Appok/Példa app részletes bemutatása

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.md

2. 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 validate

A pnpm build csomagolja a workereket, és létrehozza a release-t:

built recommendations@1.0.0  sha256-qwcsxz+/Sn1wjxUbCKy7Ah/ROvTpZrZMf/8OTBIGg9Q=  5 workers

A 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 bytes

12. 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.

Utoljára frissítve