@megaads/wm
Version:
To install the library, use npm:
527 lines (414 loc) • 16.5 kB
Markdown
# Customization TeeInBlue
Tài liệu này dành cho lập trình viên mobile cần tích hợp module `customization-teeinblue` từ package `@megaads/wm`.
Module nhận campaign data của TeeInBlue, chuẩn hóa dữ liệu thành các nhóm dễ dùng cho app mobile:
- Danh sách template để user chọn mẫu thiết kế.
- Danh sách option/layer để user nhập text, chọn option, chọn clipart hoặc upload ảnh.
- Dữ liệu preview gồm mockup, print area và design layer.
- Dữ liệu `config` để gửi khi add to cart.
- State để lưu và restore màn hình customization.
## Cài đặt
```sh
npm install @megaads/wm
```
## Khởi tạo service
```ts
import WM from "@megaads/wm";
const service = WM.initCustomizationTeeinblue(campaignData, {
campaignProductIndex: 0,
campaignMockupId: undefined,
mockupId: undefined,
printareaId: undefined,
templateId: savedState?.templateId,
state: savedState,
});
const snapshot = service.getSnapshot();
```
`campaignData` có thể truyền toàn bộ response hoặc phần `result`. Constructor tự xử lý cả hai dạng:
```ts
WM.initCustomizationTeeinblue(apiResponse);
WM.initCustomizationTeeinblue(apiResponse.result);
```
## Init options
| Field | Type | Ý nghĩa |
| --- | --- | --- |
| `campaignProductIndex` | `number` | Index của product trong `campaign_products`. Mặc định là `0`. |
| `campaignMockupId` | `string \| number` | Chọn campaign mockup theo `campaign_mockup.id`. |
| `mockupId` | `string \| number` | Chọn mockup theo `campaign_mockup.mockup_id`. Nên truyền cùng `printareaId` khi dùng variant mockup. |
| `printareaId` | `string \| number` | Chọn artwork theo print area tương ứng. |
| `templateId` | `string \| number` | Template được active ban đầu. |
| `state` | `object` | State đã lưu từ `service.getState()` để restore lựa chọn trước đó. |
Nếu không truyền mockup, service tự chọn theo thứ tự: variant có `campaign_mockup_id`, variant có `mockup_id + printarea_id`, mockup duy nhất, mockup có `mockup_id` hoặc `old_campaign_mockup_id`, mockup có print area, rồi fallback về item đầu tiên.
Về template: nếu campaign chỉ có 1 artwork, service tự chọn template đầu tiên. Nếu có nhiều artwork và app không truyền `templateId`, service **không auto-select** — user phải tự chọn template, và `validation.errors` sẽ có entry `type: 'template'` cho đến khi user chọn.
> Để biết cách render `layerOptions` và `templateOptions` cùng các method update, xem [customization-teeinblue-options.md](./customization-teeinblue-options.md).
## Luồng tích hợp khuyến nghị
1. Fetch campaign data từ API backend/app.
2. Khởi tạo service bằng `WM.initCustomizationTeeinblue(campaignData, options)`.
3. Gọi `getSnapshot()` để render màn hình lần đầu.
4. Khi user thao tác, gọi method thay đổi tương ứng.
5. Sau mỗi thao tác, lấy snapshot mới từ return value hoặc `getSnapshot()` rồi cập nhật UI.
6. Trước khi add to cart, kiểm tra `snapshot.validation.isValid`.
7. Gửi `snapshot.config` trong payload add to cart.
8. Lưu `service.getState()` nếu cần restore khi quay lại màn hình.
Ví dụ tổng quát:
```ts
let snapshot = service.getSnapshot();
function refresh(nextSnapshot?: any) {
snapshot = nextSnapshot ?? service.getSnapshot();
renderCustomization(snapshot);
}
function onSelectTemplate(templateId: string | number) {
refresh(service.selectTemplate(templateId));
}
function onSelectOption(layer: any, optionItem: any) {
refresh(service.changeLayerOptionValue(optionItem, layer));
}
function onChangeText(layer: any, text: string) {
refresh(service.changeInputValue(layer, text));
}
function onUploadPhoto(layer: any, imageUrl: string, file?: unknown) {
refresh(service.changeUploadValue(layer, imageUrl, { file }));
}
function onToggleLayer(layer: any, checked: boolean) {
refresh(service.changeLayerVisibility(layer, checked));
}
function buildCartPayload() {
const latest = service.getSnapshot();
if (!latest.validation.isValid) {
return {
ok: false,
errors: latest.validation.errors,
};
}
return {
ok: true,
customization: latest.config,
customizationState: service.getState(),
};
}
```
## Snapshot
`getSnapshot()` là API chính để mobile render toàn bộ màn hình.
```ts
const snapshot = service.getSnapshot();
```
Snapshot trả về:
| Field | Ý nghĩa |
| --- | --- |
| `campaign` | Campaign data đã clone và normalize. |
| `campaignProduct` | Product đang active trong campaign. |
| `campaignMockup` | Campaign mockup đang active. |
| `mockup` | Dữ liệu preview mockup đã rút gọn cho renderer. |
| `printAreas` | Danh sách vùng in kèm design layers. |
| `artwork` | Artwork đang active. |
| `template` | Template đang active, có `layers` đã được apply value hiện tại. |
| `templateOptions` | Danh sách nhóm template cho UI chọn template. |
| `layerOptions` | Danh sách field personalization để render form. |
| `designLayers` | Danh sách layer text/image để render trong print area. |
| `fonts` | Font cần load cho text layer. |
| `config` | Payload customization để gửi add to cart. |
| `validation` | Kết quả validate required fields. |
## Render template options
`snapshot.templateOptions` là danh sách nhóm template theo artwork.
```ts
type TemplateOptionGroup = {
artworkId: string | number;
label: string;
optionValues: TemplateOptionValue[];
values: TemplateOptionValue[];
};
type TemplateOptionValue = {
id: string | number;
name: string;
title: string;
url: string | null;
thumbnail: string | null;
active: boolean;
artworkId: string | number;
};
```
Render mỗi group như một selector. Khi user chọn template:
```ts
const nextSnapshot = service.selectTemplate(template.id);
```
Lưu ý: chọn template sẽ thay toàn bộ `templateLayers`, vì vậy UI nên refresh từ snapshot mới thay vì giữ reference layer cũ.
## Render layer options
`snapshot.layerOptions` là danh sách control mà user có thể thao tác. Các layer đã được lọc theo `visible`, dependency, `form_type`, `linked`, `group`.
Field quan trọng:
| Field | Ý nghĩa |
| --- | --- |
| `id` | ID layer, dùng lại khi gọi method update. |
| `form_label` | Label hiển thị cho user. |
| `input_type` | Loại control đã normalize. |
| `option_items` | Danh sách item cho option/clipart/grouped clipart. |
| `options` | Alias của `option_items`. |
| `value` | Giá trị logic hiện tại. |
| `show_value` | Giá trị hiển thị/render hiện tại. |
| `active` | Với option item, đánh dấu item đang chọn. |
| `required` | Field bắt buộc nếu `true`. |
| `max_length` | Giới hạn text nếu có. |
| `form_visibility_value` | Trạng thái bật/tắt với layer có visibility control. |
Các `input_type` phổ biến:
| `input_type` | UI mobile nên render |
| --- | --- |
| `text` | Text input. Dùng `max_length` nếu có. |
| `photo` | Upload/select photo. Sau khi upload xong truyền URL vào `changeUploadValue`. |
| `option` | Single choice list/grid. |
| `clipart` | Grid ảnh, chọn trực tiếp `option_items`. |
| `grouped_clipart` | Chọn group trước, sau đó chọn item bên trong `group.options`. |
### Text
```ts
const textLayer = snapshot.layerOptions.find((layer: any) => layer.input_type === "text");
const nextSnapshot = service.changeInputValue(textLayer, "Alex");
```
Với layer có `form_input_textcase: "uppercase"`, service sẽ tự uppercase khi tạo `designLayers`.
### Photo upload
```ts
const photoLayer = snapshot.layerOptions.find((layer: any) => layer.input_type === "photo");
// imageUrl là URL đã upload lên server/storage của app.
const nextSnapshot = service.changeUploadValue(photoLayer, imageUrl, {
file: localFileMetadata,
});
```
Service không upload file hộ mobile app. Mobile cần upload ảnh trước, nhận URL public hoặc URL mà renderer đọc được, rồi truyền URL đó vào `changeUploadValue`.
### Option và clipart
```ts
const optionLayer = snapshot.layerOptions.find((layer: any) => layer.input_type === "option");
const item = optionLayer.option_items[0];
const nextSnapshot = service.changeLayerOptionValue(item, optionLayer);
```
Với `clipart`, cách gọi tương tự:
```ts
const clipartLayer = snapshot.layerOptions.find((layer: any) => layer.input_type === "clipart");
const item = clipartLayer.option_items[0];
service.changeLayerOptionValue(item, clipartLayer);
```
### Grouped clipart
`grouped_clipart` có cấu trúc 2 cấp:
```ts
const groupedLayer = snapshot.layerOptions.find(
(layer: any) => layer.input_type === "grouped_clipart",
);
const group = groupedLayer.option_items[0];
const childItem = group.options[0];
service.changeLayerOptionValue(childItem, group, groupedLayer);
```
Nếu UI chỉ cần chọn group mặc định, có thể dùng:
```ts
service.changeOptionGroupClipartValue(group, groupedLayer);
```
## Render preview
Mobile có 2 hướng render preview:
- Dùng `snapshot.mockup` để render mockup hoàn chỉnh gồm background/mockup image và print area.
- Dùng `snapshot.printAreas` nếu renderer của app tách vùng in riêng.
### Mockup preview
```ts
type MockupPreview = {
id: string | number;
width: number;
height: number;
preview_url: string | null;
preview_thumbnail: string | null;
layers: MockupPreviewLayer[];
};
```
`mockup.layers` gồm:
| `type` | Ý nghĩa |
| --- | --- |
| `image` | Layer ảnh của mockup. Render bằng `src`/`url`. |
| `printarea` | Vùng đặt design. Render `layers` bên trong theo kích thước vùng này. |
Các field tọa độ chính: `top`, `left`, `width`, `height`, `rotate`, `opacity`.
### Design layers
`snapshot.designLayers` và `printarea.layers` chứa layer thật cần vẽ trong vùng in.
Layer ảnh:
```ts
type ImageDesignLayer = {
id: string | number;
type: "image";
src: string;
url: string;
top: number;
left: number;
width: number;
height: number;
rotate: number;
opacity: number;
masked_enable?: boolean;
masked_image?: string | null;
};
```
Layer text:
```ts
type TextDesignLayer = {
id: string | number;
type: "text";
text: string;
defaultText?: string;
top: number;
left: number;
width: number;
height: number;
rotate: number;
color?: string;
align?: string;
typography?: {
family?: string;
variant?: string;
size?: number;
};
typography_type?: "google" | "custom";
custom_font?: {
family?: string;
url?: string;
} | null;
stroke_enabled?: boolean;
stroke_color?: string;
stroke_width?: number;
autoscale_enabled?: boolean;
autoscale_max_width?: number;
};
```
Renderer nên sort layer theo `order` tăng dần nếu cần kiểm soát z-index.
## Load font
`snapshot.fonts` cho biết font nào cần load trước khi render text:
```ts
{
google: {
"Roboto": ["regular", "700"]
},
custom: {
"My Font": {
family: "My Font",
url: "https://cdn.teeinblue.com/path/font.woff2"
}
}
}
```
Với custom font, service đổi đuôi `.otf`/`.ttf` sang `.woff2` nếu layer có `custom_font.url`.
## Validate trước add to cart
```ts
const { validation } = service.getSnapshot();
if (!validation.isValid) {
showErrors(validation.errors);
return;
}
```
Error format:
```ts
type ValidationError =
| {
type: "template";
artworkId: string | number;
message: string;
}
| {
type: "layer";
layerId: string | number;
label: string;
message: string;
};
```
Service validate:
- Template required khi artwork có `template_settings` và có nhiều hơn 1 template.
- Layer required khi layer đang visible, đang thỏa dependency, không phải static/linked, có `form_label`, và chưa có `value` hoặc `upload_image_url`.
## Payload add to cart
Dùng `snapshot.config` hoặc `service.getCustomizationConfig()`.
```ts
const snapshot = service.getSnapshot();
const payload = {
product_id: productId,
variant_id: variantId,
quantity,
customization: snapshot.config,
};
```
`config` có dạng:
```ts
{
disable_make_change: true,
custom_type: {
name: "teeinblue",
},
options: [
{
name: "Pet",
value: "https://cdn.teeinblue.com/...",
optionItem: {
id: "option-id",
name: "Dog",
url: "https://cdn.teeinblue.com/..."
}
}
],
layers: [
{
name: "Name",
type: "text",
value: "Alex",
// Các transform config còn lại của layer được giữ lại.
}
]
}
```
`options` dùng để hiển thị tóm tắt lựa chọn của user. `layers` là dữ liệu chi tiết để backend/order system tái dựng personalization.
## Lưu và restore state
Khi user rời màn hình hoặc cần cache customization:
```ts
const state = service.getState();
```
Restore:
```ts
const service = WM.initCustomizationTeeinblue(campaignData, {
templateId: state.templateId,
state,
});
```
State gồm:
```ts
{
templateId: "template-391321",
layers: {
"layer-1": {
value: "Alex",
show_value: "Alex",
form_visibility_value: true,
selected_name: "Dog",
optionItem: {}
}
}
}
```
Chỉ nên persist object từ `getState()`. Không nên tự build state thủ công nếu không cần thiết.
## Public methods
| Method | Mục đích |
| --- | --- |
| `getSnapshot()` | Lấy toàn bộ data để render UI, preview, config, validation. |
| `getData()` | Alias của `getSnapshot()`. |
| `selectTemplate(templateId)` | Chọn template, rebuild layer/options/preview. |
| `changeLayerOptionValue(optionItem, option, parentLayer?)` | Chọn option, clipart hoặc child item của grouped clipart. |
| `changeOptionValue(optionItem, option, parentLayer?)` | Method gốc cho chọn option. |
| `changeOptionGroupClipartValue(optionItem, option)` | Chọn grouped clipart theo group. |
| `changeInputValue(layerOption, value)` | Update text layer và các linked layer. |
| `changeUploadValue(layerOption, imageUrl, metadata?)` | Update photo/upload layer và các linked layer. |
| `changeLayerVisibility(layerOption, value)` | Bật/tắt layer có `form_visibility`. |
| `validate()` | Validate required template/layer. |
| `getCustomizationConfig()` | Lấy payload add to cart. |
| `getState()` | Lấy state để persist. |
| `getFonts()` | Lấy danh sách Google/custom font cần load. |
| `getPreview()` | Alias của `getMockupPreview()`. |
| `getMockupLayers()` | Lấy raw mockup layers đang active. |
| `getDesignLayers()` | Lấy design layers đang active. |
| `getLayerOptions()` | Lấy personalization controls. |
| `getTemplateOptions()` | Lấy template controls. |
| `getPrintAreas()` | Lấy print areas. |
| `getTemplate()` | Lấy template active kèm layers đã update. |
| `getTemplateLayers()` | Lấy raw template layers đang active. |
| `setTemplateLayers(layers)` | Set lại template layers rồi rebuild. Chỉ dùng cho case nâng cao/debug. |
| `setTemplateOptions(options)` | Override template options. Chỉ dùng cho case nâng cao/debug. |
## Lưu ý tích hợp mobile
- Sau mọi thao tác thay đổi, dùng snapshot mới để render lại vì service có thể rebuild dependency, active state, design layers và config.
- Không giữ reference layer/option quá lâu sau khi gọi `selectTemplate`, vì template layer đã được clone lại.
- URL asset tương đối được chuẩn hóa sang `https://cdn.teeinblue.com/...`. URL `http`, `https`, và `blob` được giữ nguyên.
- Với upload ảnh, app chịu trách nhiệm upload và truyền URL sau upload. Package không xử lý upload file trong public flow này.
- Dependency giữa các layer được xử lý trong service. UI chỉ cần render `snapshot.layerOptions` hiện tại.
- `snapshot.config` nên được lấy lại ngay trước add to cart để tránh gửi payload cũ.
- Nếu render preview bằng canvas/native view, cần quy đổi tọa độ theo ratio giữa kích thước thiết kế (`mockup.width/height` hoặc `printarea.width/height`) và kích thước hiển thị thực tế.