Tham chiếu

Runtime API (pos)

Đối tượng pos trong extension và trang iframe: ngữ cảnh, gọi API, toast, điều hướng, sự kiện.

Xem Markdown

Áp dụng cho api_version: "2026-01".

Mọi extension nhận một đối tượng pos để đọc ngữ cảnh, gọi Open API và ra lệnh cho giao diện POS.

  • Extension trong worker (block, action, hook, trang Worker UI): pos là biến toàn cục do runtime POS nạp sẵn trước khi module chạy.
  • Trang iframe: gọi connect() từ SDK để nhận pos (xem Trang iframe).

Type đầy đủ có trong gói @pancake-pos/ui-extensions (xem SDK & CLI).

Tổng quan

TypeScript
interface Pos {
  apiVersion: string;                 // ví dụ "2026-01"
  target: string;                     // target của extension đang chạy
  extensionId: string;                // id khai trong manifest
  app: { id: string; handle: string; version: string };
  shop: { id: number; name: string; currency: string; country: string; timezone: string };
  user: { id: string; name: string; locale: string };
  data: unknown;                      // dữ liệu ngữ cảnh, schema theo target

  api: PosApi;                        // gọi Open API
  session: { getToken(): Promise<{ token: string; expiresAt: string }> };

  toast: { show(message: string, options?: ToastOptions): void };
  ui: { confirm(options: ConfirmOptions): Promise<boolean> };
  navigate(to: NavigateTarget): void;
  refresh(): void;
  close(): void;

  on(event: PosEvent, handler: (payload: any) => void): () => void;
}

Phạm vi khả dụng

API Block Action headless Action modal/fullscreen Hook, should_render Trang
shop, user, app, data ✓ ✓ ✓ ✓ ✓
api, session ✓ ✓ ✓ ✓ ✓
toast, ui.confirm ✓ ✓ ✓ ✓
navigate ✓ ✓ ✓ ✓
refresh ✓ ✓ ✓
close ✓
on('data-change') ✓ ✓ ✓
page.* ✓ (iframe)

Gọi API ngoài phạm vi sẽ ném lỗi PosRuntimeError với code: 'unavailable'.

pos.data

Dữ liệu ngữ cảnh của vị trí đang chạy, cũng là tham số của hàm export default. Schema từng target xem tại Extension point.

  • Các target gắn với bản ghi POS dùng chung một dạng: pos.data.resource cho biết loại bản ghi (order, purchase…), pos.data.record là bản ghi đã lưu (records ở thanh thao tác hàng loạt), pos.data.draft là nội dung form chi tiết.
  • Trường bị ẩn bởi access.hidden_order_fields không có trong pos.data.
  • Với block, pos.data được cập nhật khi người dùng sửa form; nghe sự kiện data-change để render lại.

pos.api

Gọi Open API bằng app token. SDK tự gắn token, tự gia hạn và thử lại một lần khi token hết hạn.

TypeScript
interface PosApi {
  baseUrl: string;   // gốc Open API, ví dụ "https://pos.pages.fm/api/v1"
  request<T = any>(options: {
    method: 'GET' | 'POST' | 'PUT' | 'DELETE';
    path: string;                          // tương đối với baseUrl, bắt đầu bằng /shops/{shop_id}
    query?: Record<string, unknown>;
    body?: unknown;                        // gửi dạng JSON
    signal?: AbortSignal;
  }): Promise<T>;
  get<T = any>(path: string, options?: { query?: Record<string, unknown>; signal?: AbortSignal }): Promise<T>;
  post<T = any>(path: string, body?: unknown, options?: { query?: Record<string, unknown>; signal?: AbortSignal }): Promise<T>;
  put<T = any>(path: string, body?: unknown, options?: { query?: Record<string, unknown>; signal?: AbortSignal }): Promise<T>;
  delete<T = any>(path: string, options?: { query?: Record<string, unknown>; signal?: AbortSignal }): Promise<T>;
}
  • path phải thuộc shop hiện tại (/shops/${pos.shop.id}/...). Gọi shop khác bị từ chối.
  • Kết quả là JSON nguyên bản của Open API (thường có dạng { success, data, ... }).
  • Method phải nằm trong access.methods của app; không thì ném lỗi trước khi gửi request.

Lỗi

TypeScript
class PosApiError extends Error {
  status: number;            // HTTP status
  code?: number | string;    // error_code của Open API nếu có
  body?: unknown;            // body trả về
}

SDK ném PosApiError khi HTTP status ≥ 400 hoặc body có success: false. Danh sách mã lỗi xem Truy cập dữ liệu › Mã lỗi.

TypeScript
try {
  const { record } = pos.data;   // order.details.action: record là OrderRef
  const res = await pos.api.get(`/shops/${pos.shop.id}/orders/${record.system_id}`);
  render(<OrderInfo order={res.data} />, document.body);
} catch (error) {
  if (error.status === 403) pos.toast.show('Bạn không có quyền xem đơn này', { tone: 'error' });
  else throw error;
}

pos.session.getToken

Lấy app token hiện tại để tự gọi Open API bằng thư viện khác.

TypeScript
const { token, expiresAt } = await pos.session.getToken();
await fetch(`${pos.api.baseUrl}/shops/${pos.shop.id}/orders`, {
  headers: { Authorization: `Bearer ${token}` },
});

Không lưu token lâu dài, không gửi token cho hệ thống bên ngoài, trừ backend của trang tự host. Token hết hạn sau 15 phút; gọi lại getToken() để lấy token mới.

pos.toast.show

TypeScript
type ToastOptions = {
  tone?: 'success' | 'error' | 'info' | 'warning';   // mặc định 'success'
  duration?: number;                                  // giây, mặc định 3, từ 1 đến 10
};

Tối đa 3 toast mỗi 10 giây cho mỗi extension; vượt thì bị bỏ qua.

pos.ui.confirm

Hộp xác nhận do POS vẽ, phủ toàn màn hình.

TypeScript
type ConfirmOptions = {
  title: string;
  content?: string;
  okText?: string;       // mặc định "Đồng ý"
  cancelText?: string;   // mặc định "Huỷ"
  danger?: boolean;      // nút đồng ý màu đỏ
};

if (await pos.ui.confirm({ title: 'Đẩy đơn sang ERP?' })) {
  // ...
}

pos.navigate

Chuyển tới một trang của app.

TypeScript
type NavigateTarget = {
  page: string;                       // id extension app.page
  path?: string;                      // ví dụ "/returns"
  query?: Record<string, string>;
};

pos.navigate({ page: 'home', path: '/returns', query: { order_id: String(pos.data.record.system_id) } });

Nếu đang ở modal, modal đóng trước khi chuyển trang. Nếu đang ở đúng trang đó, POS chỉ đổi URL và phát data-change với path, query mới, không tải lại trang.

pos.refresh

Yêu cầu POS tải lại dữ liệu của màn hình hiện tại (ví dụ đơn hay phiếu nhập đang mở) sau khi app đã cập nhật qua Open API.

  • POS tải lại danh sách đang xem và bản ghi đang mở. Thay đổi chưa lưu trên form của bản ghi đó bị thay bằng dữ liệu mới.
  • Nhiều lần gọi liên tiếp được gộp lại: mỗi loại bản ghi tải lại tối đa 1 lần mỗi 3 giây.

pos.close

Đóng modal/fullscreen của action đang mở.

Sự kiện

TypeScript
const off = pos.on('data-change', (data) => {
  // data có schema giống pos.data
});
off(); // huỷ đăng ký
Sự kiện Phát khi Payload
data-change Dữ liệu ngữ cảnh thay đổi, ví dụ người dùng sửa form chi tiết (gom trong 300 ms) hoặc trang đổi đường dẫn pos.data mới
primary-action Người dùng bấm nút chính của trang iframe không có

Trang iframe

Trang render: "iframe" là một ứng dụng web bình thường, được phục vụ tại gốc domain sandbox của app. Mọi đường dẫn không phải file tĩnh đều trả về index.html, nên router dùng basename là /. Đường dẫn bắt đầu bằng /_pos/ dành riêng cho POS: không dùng làm route hay đường dẫn file.

TypeScript
import { connect } from '@pancake-pos/ui-extensions/page';

const pos = await connect();   // chờ POS gửi ngữ cảnh và token
// pos.data: { path, query }

Ngoài các API ở trên, trang iframe có thêm:

TypeScript
interface PosPage {
  setTitle(title: string): void;
  setPrimaryAction(action: { label: string; disabled?: boolean; loading?: boolean } | null): void;
}

pos.page.setTitle('Đối soát ngày 12/05');
pos.page.setPrimaryAction({ label: 'Xuất Excel' });
pos.on('primary-action', exportExcel);
  • Đồng bộ URL: SDK theo dõi history.pushState, replaceState và popstate trong iframe rồi cập nhật URL của POS. Khi mở link có sẵn, iframe được nạp đúng đường dẫn tương ứng.
  • Chiều cao: iframe chiếm toàn bộ vùng nội dung của POS; trang tự cuộn bên trong.
  • Giao diện: khuyến nghị dùng @pancake-pos/ui (theme và component theo style POS).
  • Lưu trữ: localStorage của trang iframe thuộc riêng domain sandbox của app, không dùng chung với POS hay app khác.

Trang tự host

Khai url thay cho entry khi trang đã chạy trên máy chủ của bạn, ví dụ web ERP sẵn có. POS nhúng thẳng url vào iframe; gói không cần thư mục page/.

JSON
{ "target": "app.page", "id": "home", "title": "ERP", "render": "iframe", "url": "https://erp.example.com/pos" }
  • Kết nối: trang cài @pancake-pos/ui-extensions và gọi connect() như trang trong gói; pos.page, pos.api, pos.toast… dùng như nhau.
  • Đường dẫn: phần đường dẫn của url là gốc của app. /shop/<id>/apps/<handle>/orders/5 trong POS tương ứng https://erp.example.com/pos/orders/5. Trang đi ra ngoài gốc này (ví dụ /login) thì URL của POS giữ nguyên. pos.data.path là đường dẫn thật của trang.
  • Cho phép POS nhúng: đặt header Content-Security-Policy: frame-ancestors https://pos.pancake.vn https://pos.pages.fm để chỉ POS nhúng được trang. Không dựa vào cookie đăng nhập của trang: trình duyệt có thể chặn cookie trong iframe khác domain (Safari chặn mặc định).
  • Gọi API từ máy chủ của bạn: gửi app token (pos.session.getToken()) kèm request về backend, backend gọi Open API bằng token đó (Authorization: Bearer). Token hạn 15 phút và chỉ có quyền của app; không lưu token lâu dài.
  • Duyệt và thay đổi: quản trị viên thấy url khi kích hoạt. Nội dung trang do máy chủ của bạn quyết định và đổi được bất cứ lúc nào mà không qua duyệt version, nên chỉ dùng máy chủ bạn kiểm soát.
  • Chạy thử: pos-app dev --page-url http://localhost:3000/pos thay url bằng máy chủ trên máy bạn; hot reload của Vite hay Next vẫn chạy trong iframe.

Môi trường worker

Extension trong worker chỉ được bảo đảm có:

  • Cú pháp và đối tượng chuẩn của JavaScript (ES2020).
  • fetch, Headers, Request, Response, AbortController, URL, URLSearchParams.
  • setTimeout, clearTimeout, setInterval, clearInterval.
  • console (chỉ xem được ở chế độ phát triển).
  • DOM tối giản do runtime POS cung cấp để Preact hoặc JS thuần dựng element pos-*: document.createElement, document.createTextNode, append, appendChild, insertBefore, removeChild, remove, setAttribute, removeAttribute, textContent, addEventListener, removeEventListener, parentNode, childNodes, firstChild, nextSibling, CustomEvent. document.body là gốc giao diện của extension.

Không dùng: window, document.querySelector, localStorage, sessionStorage, canvas, đo kích thước DOM (getBoundingClientRect…), alert/confirm/prompt, nạp script từ CDN. Các API này có thể không tồn tại hoặc bị khoá bất kỳ lúc nào.

Nhật ký lỗi

  • Chế độ phát triển: lỗi và console.* hiện trong DevTools của trình duyệt.
  • Version đang kích hoạt: lỗi chưa bắt (uncaught), lỗi render, timeout được ghi vào nhật ký lỗi của app, xem trong Nhật ký lỗi của trang quản lý ứng dụng tuỳ biến.