Developer documentation

Sample plugins

Two fully-worked plugins you can clone, read, and modify — Reviews and Gift Cards.

Reviews plugin

A complete reviews plugin: collects ratings after fulfillment, stores them, surfaces a summary block on product pages, and lets merchants moderate from a custom dashboard tab. Around 380 lines of TypeScript total.

Manifest

definePlugin({
  id: 'com.aeonzap.samples.reviews',
  name: 'Reviews',
  version: '1.0.0',
  scopes: ['orders.read', 'customers.read', 'storefront.blocks.write', 'notifications.send'],
  settings: {
    type: 'object',
    properties: {
      autoPublish: { type: 'boolean', default: false },
      delayHours: { type: 'integer', minimum: 1, maximum: 168, default: 48 },
    },
  },
});

Request review on fulfillment

events: {
  'order.fulfilled': async (ctx, evt) => {
    const { delayHours } = await ctx.settings.get();
    await ctx.scheduler.in({ hours: delayHours }, 'send_request', {
      orderId: evt.order.id,
      email: evt.customer.email,
    });
  },
},
schedules: {
  send_request: async (ctx, payload) => {
    await ctx.notify.send({
      channel: 'email',
      to: payload.email,
      template: 'review-request',
      data: { url: `https://${ctx.store.domain}/r/${payload.orderId}` },
    });
  },
},

Submit handler

routes: {
  'POST /r/:orderId': async (ctx, req) => {
    const order = await ctx.api.orders.get(req.params.orderId);
    const review = {
      id: ctx.id(),
      productId: order.line_items[0].product_id,
      rating: Number(req.body.rating),
      body: String(req.body.body ?? '').slice(0, 2000),
      createdAt: new Date().toISOString(),
      published: (await ctx.settings.get()).autoPublish,
    };
    const list = (await ctx.kv.get(`reviews:${review.productId}`)) ?? [];
    list.unshift(review);
    await ctx.kv.put(`reviews:${review.productId}`, list);
    return { ok: true };
  },
},

Gift Cards plugin

A gift card plugin: sells gift cards as products, generates redeemable codes on order paid, applies them to checkout via a custom discount adapter, and tracks balances per code.

Issue codes on payment

events: {
  'order.paid': async (ctx, evt) => {
    for (const item of evt.order.line_items) {
      if (!item.tags.includes('giftcard')) continue;
      for (let i = 0; i < item.quantity; i++) {
        const code = ctx.code(12);
        await ctx.kv.put(`gc:${code}`, { balance: item.price, currency: evt.order.currency });
        await ctx.notify.send({
          channel: 'email',
          to: evt.order.customer.email,
          template: 'giftcard-issued',
          data: { code, amount: item.price },
        });
      }
    }
  },
},

Discount adapter

adapters: {
  discounts: {
    async resolve(ctx, code, cart) {
      const gc = await ctx.kv.get(`gc:${code}`);
      if (!gc || gc.balance <= 0) return null;
      const apply = Math.min(gc.balance, cart.total);
      return {
        kind: 'fixed',
        amount: apply,
        onApply: async () => {
          gc.balance -= apply;
          await ctx.kv.put(`gc:${code}`, gc);
        },
      };
    },
  },
},

Where to clone

Both plugins live in the public samples repository. Run aeonzap clone reviews or aeonzap clone giftcards to scaffold a working copy locally with a pre-seeded store.