npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@retion-miniapp/sdk

v0.14.0

Published

Retion Mini App SDK — kết nối web mini app với app Retion

Readme

@retion-miniapp/sdk

SDK cho web mini app chạy trong app Retion.

npm i @retion-miniapp/sdk

| API | Dùng để | |---|---| | getRouteParams() | Biết mini app đang mở ở tổ chức / page / khách nào | | getToken(type) | Lấy token của page đang mở để gọi API | | onContextChange(cb) | Bắt buộc — chuyển sang page / conversation khác | | closeApp() | Đóng mini app | | isInHost() | Đang chạy trong app hay trên trình duyệt | | onAppPaused(cb) / onAppResumed(cb) | Mini app bị đóng (chạy ngầm) / mở lại | | requestPayment(req) | Thanh toán qua cổng của app (Tap to Pay…) | | printImage({ image }) | In hoá đơn ra máy in của app | | openScreen({ screen, params }) | Mở 1 màn hình của app | | openLink(url) | Mở link bằng hệ điều hành (deeplink app khác, tel:, mailto:, https) | | haptic(type) | Rung phản hồi (bấm nút, thanh toán xong…) | | showToast(message, options?) | Hiện toast trên app, nổi trên mini app và camera | | getBiometricAvailability() / authenticateBiometric(req) | Kiểm tra Face ID / Touch ID và trả nguyên kết quả xác thực native | | startSpeech(options) / stopSpeech() / onSpeech(event, cb) | Bắt đầu/dừng nhận dạng và nhận văn bản tạm thời/cuối cùng | | scanCode(options?) / startScanner(options?) / stopScanner() / onScanner(event, cb) | Quét mã một lần hoặc liên tiếp kiểu quầy thu ngân | | getCurrentLocation(options?) | Xin quyền và lấy vị trí thiết bị một lần | | uploadFiles(options?) / getUploadCapabilities() | Chọn ảnh / video / tài liệu bằng UI của app, app nén + upload rồi trả link |

1 mini app = 1 trang, load 1 lần. Mở mini app ở page / conversation khác, app không nạp lại trang mà gọi onContextChange — thiếu nó web sẽ hiện dữ liệu page cũ. Đổi tổ chức thì app nạp lại trang.


Ví dụ

import { getRouteParams, getToken, onContextChange } from '@retion-miniapp/sdk';

async function load() {
  const { org_id, page_id } = getRouteParams();
  const token = await getToken('access');
  // gọi API của bạn với org_id, page_id, token
}

load();                  // lúc mở
onContextChange(load);   // chuyển conversation

getRouteParams()

Lấy param của mini app (như query trên URL): tổ chức, page, khách của conversation đang mở.

const { org_id, page_id, client_id } = getRouteParams();

| Field | Kiểu | Ý nghĩa | |---|---|---| | org_id | string | Tổ chức đang dùng app | | page_id | string | Page (kênh) của conversation | | client_id | string | Khách hàng của conversation | | message_id | string? | Tin nhắn mở mini app (nếu mở từ 1 tin cụ thể) | | comment_id | string? | Bình luận mở mini app (nếu mở từ bình luận) | | locale | string | Ngôn ngữ app: vi, en, id, lo, km, th, tl, zh | | is_page_admin | 1 \| 0 | 1 nếu người dùng là admin của page |

Ngoài app (trình duyệt / iframe cũ): đọc từ query URL — mọi giá trị là chuỗi (is_page_admin là "1" / "0").


getToken(type)

Lấy token để gọi API. Luôn trả token của page đang mở. Gọi mỗi lần cần — không lưu lại (token có thể đổi khi chuyển page hoặc hết hạn).

const token = await getToken('access');

| type | Token | Cần quyền | |---|---|---| | 'access' | Token của mini app cho page hiện tại | Không | | 'partner' | Token đối tác của page | partner_token | | 'chatbox' | Token đăng nhập của người dùng app — toàn quyền như người dùng | chatbox_token |

  • Quyền do admin bật trên Console (app → Quyền lấy token) — không khai trong code được. Chưa bật → lỗi PERMISSION_DENIED.
  • Ngoài app: đọc ?access_token= / ?partner_token= / ?chatbox_token= trên URL; không có → lỗi NOT_IN_HOST.
  • getAccessToken() = getToken('access').

onContextChange(callback)

Gọi khi mini app đang mở sẵn được mở lại ở conversation khác (khác page hoặc khác khách, cùng tổ chức). App không nạp lại trang, nên web phải tự tải lại dữ liệu ở đây. Bắt buộc với mọi mini app.

onContextChange(() => load());

Callback nhận 2 tham số, dùng khi cần so sánh:

onContextChange((next, prev) => {
  if (next.page_id !== prev.page_id) {
    // đổi page → xoá dữ liệu riêng của page cũ trước khi load
  }
  load();
});

| Tham số | Kiểu | Ý nghĩa | |---|---|---| | next | RouteParams | Conversation mới — giống getRouteParams() gọi lúc này | | prev | RouteParams | Conversation trước đó |

Khi nào được gọi / không được gọi

| Tình huống | App làm gì | Callback | |---|---|---| | Mở lại ở conversation khác, cùng tổ chức | Giữ nguyên trang, gửi thông tin mới | ✅ Gọi | | Mở lại ở đúng conversation cũ | Giữ nguyên trang | ❌ Không gọi | | Mở lại ở tổ chức khác | Nạp lại trang | ❌ Không gọi (code chạy lại từ đầu) | | Mở lần đầu / trang chưa load xong | Nạp trang | ❌ Không gọi (code chạy lại từ đầu) |

Lưu ý

  • Trong callback, getRouteParams() và getToken() đã trả thông tin + token của conversation mới.
  • App tạm che mini app cho tới khi callback chạy xong → đổi giao diện sang trạng thái loading ngay đầu callback (trước lệnh await đầu tiên) để người dùng không thấy dữ liệu cũ.
  • Trả về hàm huỷ lắng nghe: const off = onContextChange(load); off();

closeApp()

Đóng mini app, quay về màn chat. Mini app vẫn chạy ngầm để mở lại tức thì.

await closeApp();

Ngoài app → lỗi NOT_IN_HOST.


isInHost()

true nếu đang chạy trong app Retion; false nếu trên trình duyệt / iframe cũ. Dùng khi 1 source chạy cả 2 nơi.

if (isInHost()) await closeApp();
else history.back();

onAppPaused(callback) / onAppResumed(callback)

Mini app bị đóng (vẫn chạy ngầm) / được mở lại. Dừng việc chạy nền khi bị đóng để đỡ tốn pin.

onAppPaused(() => clearInterval(timer));   // polling, video, animation...
onAppResumed(() => load());                // làm mới dữ liệu nếu cần

Trả về hàm huỷ lắng nghe.


Chức năng native của app

Thanh toán, in, mở màn hình — app tự cài phần xử lý. Kết quả trả thẳng về chỗ await.

| Kết quả | Web nhận | |---|---| | Thành công | Giá trị trả về của await | | App không hỗ trợ | Lỗi NOT_SUPPORTED — ẩn nút hoặc báo người dùng | | Người dùng huỷ | Lỗi CANCELLED | | Đang thanh toán / in mà gọi lần nữa (bấm 2 lần) | Lỗi BUSY — lượt đầu vẫn chạy bình thường |

requestPayment(request)

import { requestPayment, MiniAppError, ErrorCode } from '@retion-miniapp/sdk';

try {
  const { transaction_id, provider } = await requestPayment({
    amount: 12.5,          // theo đơn vị tiền tệ
    currency: 'CAD',
    order_id: order.id,
  });
  await markOrderPaid(order.id, { transaction_id, provider });
} catch (e) {
  if (e instanceof MiniAppError && e.code === ErrorCode.CANCELLED) {return;} // người dùng huỷ
  showError(e);
}

| Field | Kiểu | | |---|---|---| | amount | number | Số tiền theo đơn vị tiền tệ (12.5 = 12,50) | | currency | string | ISO 4217: CAD, USD, VND… | | order_id | string? | Mã đơn của bạn — gắn vào giao dịch để đối soát | | note | string? | Ghi chú giao dịch | | metadata | object? | Dữ liệu thêm app cần |

Kết quả: { transaction_id, provider, ... }. Chờ tối đa 10 phút (người dùng chạm thẻ), không bị timeout 15s.

Luôn đối soát qua backend theo order_id. Web có thể bị đóng / hết thời gian chờ trong khi giao dịch phía app vẫn thành công — đừng chỉ dựa vào kết quả await để quyết định đơn đã trả hay chưa.

printImage({ image })

In ảnh hoá đơn (base64, có hoặc không có data:image/...;base64,). App tự hiện chọn máy in. Resolve khi in xong.

await printImage({ image: canvas.toDataURL('image/png') });

openScreen({ screen, params })

Mở 1 màn hình của app — app đóng mini app rồi chuyển màn. Tên màn hình do app định nghĩa (xem tài liệu của app); màn không tồn tại → NOT_SUPPORTED.

await openScreen({ screen: 'customer', params: { customer_id } });

openLink(url)

Nhờ app mở link bằng hệ điều hành: deeplink sang app khác (retion://…, zalo://…), tel:, mailto:, hoặc https (trình duyệt ngoài / Universal Link mở thẳng app đích). Dùng thay window.open — WebView trong app không tự mở link ra ngoài, gọi window.open sẽ nạp link ngay trong mini app.

import { isInHost, openLink } from '@retion-miniapp/sdk';

if (isInHost()) {
  await openLink('https://retion.ai/chat').catch(() => {/* không mở được: chưa cài app đích… */});
} else {
  window.open('https://retion.ai/chat', '_blank');
}

| Lỗi | Khi nào | |---|---| | INVALID_PARAMS | url sai định dạng hoặc scheme bị chặn (javascript:, file:, data:, about:, blob:) | | NOT_SUPPORTED | Không ứng dụng nào mở được link hoặc tính năng chưa được hỗ trợ |

haptic(type)

Rung phản hồi — gọi lúc nào cũng được, không chờ kết quả, không throw. Ngoài app / app chưa hỗ trợ → bỏ qua. Tên type y hệt react-native-haptic-feedback, dùng hằng HapticType (không cần gõ chuỗi). Bỏ trống → selection.

import { haptic, HapticType } from '@retion-miniapp/sdk';

haptic(HapticType.impactLight);         // bấm nút
haptic(HapticType.notificationSuccess); // thanh toán xong

| HapticType | Dùng khi | |---|---| | selection | Đổi tab, picker, toggle | | impactLight / impactMedium / impactHeavy | Bấm nút, kéo thả | | rigid / soft | Va chạm cứng / mềm | | notificationSuccess / notificationWarning / notificationError | Kết quả thao tác |


Face ID / Touch ID / Android biometrics

Kiểm tra thiết bị hỗ trợ rồi yêu cầu xác thực:

import { getBiometricAvailability, authenticateBiometric } from '@retion-miniapp/sdk';

async function authenticate() {
  try {
    const { available } = await getBiometricAvailability();
    if (!available) return;

    const result = await authenticateBiometric({
      reason: 'Xác nhận danh tính',
      cancelButtonText: 'Huỷ',
    });
    if (!result.success) {
      console.log(result.error);
      return;
    }
    // Tiếp tục thao tác sau khi xác thực thành công.
  } catch (error) {
    console.error(error);
  }
}

| API | Kết quả | | --- | --- | | getBiometricAvailability() | { available, biometryType?, error? } — khả dụng và loại sinh trắc học | | authenticateBiometric({ reason, cancelButtonText? }) | { success, error? } — kết quả xác thực |

reason là nội dung hộp xác thực, dài 1–200 ký tự. cancelButtonText là nhãn nút huỷ, không bắt buộc, tối đa 80 ký tự.

Kiểm tra result.success trước khi tiếp tục. Người dùng huỷ có thể trả { success: false, error: 'User cancellation' }; lỗi khác có thể đi vào catch. Loại xác thực do thiết bị quyết định: FaceID, TouchID hoặc Biometrics trên Android.


Nhận dạng giọng nói

Đăng ký nhận văn bản trước khi bắt đầu nghe:

import { startSpeech, stopSpeech, onSpeech } from '@retion-miniapp/sdk';

const offPartial = onSpeech('speech.partial', result => console.log(result.text));
const offFinal = onSpeech('speech.final', result => console.log(result.text));
const offError = onSpeech('speech.error', error => console.error(error.code, error.message));
const offEnd = onSpeech('speech.end', () => console.log('Đã dừng nghe'));

// Gọi khi bấm nút bắt đầu.
async function startListening() {
  try { await startSpeech({ lang: 'vi-VN' }); }
  catch (error) { console.error(error); }
}

// Gọi khi bấm nút dừng; vẫn giữ listener để nhận câu cuối.
async function stopListening() {
  try { await stopSpeech(); }
  catch (error) { console.error(error); }
}

// Khi huỷ component: dừng nếu không còn cần nghe và dọn listener.
async function dispose() {
  try { await stopSpeech(); }
  finally { offPartial(); offFinal(); offError(); offEnd(); }
}

| API / event | Kết quả | | --- | --- | | startSpeech(options?) | Bắt đầu nghe; trả Promise<void>, văn bản đến qua event | | stopSpeech() | Yêu cầu dừng; trả Promise<void>, không phải câu cuối | | speech.partial | { text, isFinal, results } — văn bản tạm thời | | speech.final | { text, isFinal, results } — văn bản đã chốt | | speech.error | { code, error, message } — lỗi khi đang nghe | | speech.end | Phiên nghe đã kết thúc |

Truyền lang để chọn ngôn ngữ, ví dụ vi-VN, en-US. Mặc định là vi-VN, không tự đoán ngôn ngữ đang nói. Muốn đổi ngôn ngữ, dừng phiên hiện tại rồi bắt đầu lại.

Các tuỳ chọn khác: continuous (mặc định true), interimResults (true), addsPunctuation (true), requiresOnDeviceRecognition (false). Khả năng nhận dạng phụ thuộc thiết bị và dịch vụ đang dùng.

Giữ listener sau khi gọi dừng để nhận câu cuối nếu có, và chờ speech.end. Nếu cần lưu câu cuối khi rời màn hình, đợi event kết thúc trước khi dọn listener. Huỷ listener không tự dừng nghe.

Lỗi bắt đầu/dừng đi vào catch; lỗi trong phiên đến qua speech.error. BUSY nghĩa là đang có phiên khác, PERMISSION_DENIED là chưa được cấp quyền, NOT_SUPPORTED là tính năng chưa được hỗ trợ. Xem thêm xử lý lỗi.


showToast(message, options?)

Web gọi lúc nào cũng được, không cần await; ngoài app thì bỏ qua. Toast mới thay toast cũ. message tối đa 200 ký tự. Cần @retion-miniapp/react-native 0.13.0+.

import { showToast, ToastType } from '@retion-miniapp/sdk';

showToast('Đã thêm vào giỏ', { type: ToastType.success });
showToast('Không tìm thấy sản phẩm', { type: ToastType.error });

ToastType: info (mặc định), success, error.


Quét mã QR / barcode

scanCode phủ camera kín vùng mini app và tự đóng khi có mã. startScanner hiện camera ở phía trên, web vẫn dùng được ở phần dưới (hoặc phủ kín với ScannerLayout.full), và mở đến khi người dùng bấm Đóng hoặc web gọi stopScanner(). Camera có sẵn zoom, đèn pin và nút quét từ ảnh, kết quả từ ảnh trả về như quét camera.

import { scanCode, startScanner, stopScanner, onScanner, CodeType, ScannerEvent, ScannerEndReason, ScannerLayout } from '@retion-miniapp/sdk';

// Quét một lần: có mã thì tự đóng camera
try {
  const { value, type } = await scanCode({ types: [CodeType.qr] });
} catch (error) {
  // error.code === 'CANCELLED' khi người dùng đóng camera
}

// Quét liên tiếp: đăng ký event trước khi mở camera
const off_result = onScanner(ScannerEvent.result, ({ value, type }) => addToCart(value));
const off_end = onScanner(ScannerEvent.end, ({ reason, message }) => {
  off_result();
  off_end();
  if (reason === ScannerEndReason.error) console.error(message);
});
await startScanner({ types: CodeType.all, cooldown_ms: 1500 });
// Đóng từ web nếu cần; người dùng cũng có nút Đóng trên camera
await stopScanner();

// Camera nằm đè (absolute): web giữ nguyên kích thước, tự chừa chỗ dưới camera.
// Mini app nằm trong iframe cùng origin thì đọc / nghe ở window.top
window.addEventListener('retionscannerheight', () => setCameraTop(window.__retionScannerHeight ?? 0));
await startScanner({ layout: ScannerLayout.top, absolute: true });

| Hằng | Giá trị | | --- | --- | | CodeType | qr, ean13, ean8, upcA, upcE, code128, code39, code93, codabar, itf, pdf417, aztec, dataMatrix; CodeType.all là mảng gồm tất cả, cũng là mặc định khi bỏ trống types | | ScannerEvent | result: { value, type }; end: { reason, message? } | | ScannerEndReason | user: người dùng đóng; web: web gọi stopScanner(); closed: mini app đóng/ẩn, reload hoặc app xuống nền; error: lỗi camera, có message |

| Tuỳ chọn | Mặc định | Ý nghĩa | | --- | --- | --- | | types | CodeType.all | Loại mã cần quét; quét ít loại hơn thì nhanh và ít nhận nhầm hơn | | layout | ScannerLayout.top | Chỉ startScanner: camera ở trên web (top) hoặc phủ kín (full) | | absolute | false | Chỉ startScanner với top: camera nằm đè lên web thay vì đẩy web xuống, web không co / giãn khi mở, đóng camera. Web tự chừa chỗ theo window.__retionScannerHeight (px, 0 khi đã đóng), đổi thì bắn event retionscannerheight | | hint | Đưa mã vào khung để quét | Chữ hướng dẫn trên camera, 1–80 ký tự; truyền theo ngôn ngữ của mini app | | cooldown_ms | 1500 | Chỉ startScanner, 0–60000 ms: cùng một mã phải rời khung đủ thời gian này mới được nhận lại, nên giữ nguyên mã trước camera không bị đếm thêm. Mã khác nhận ngay |

| Mã lỗi | Ý nghĩa | | --- | --- | | CANCELLED | Đóng camera trước khi có mã (scanCode), hoặc mini app đóng lúc đang xin quyền | | PERMISSION_DENIED | Chưa cấp quyền camera; hướng dẫn bật lại trong Cài đặt | | BUSY | Đang có phiên quét khác; cả máy chỉ có một phiên | | NOT_SUPPORTED | Binary thiếu module camera, hoặc thiết bị không có camera sau | | INVALID_PARAMS | types hoặc cooldown_ms không hợp lệ | | HOST_ERROR | Lỗi camera trong lúc scanCode |


Vị trí hiện tại

Xin quyền (nếu chưa có) và trả một vị trí. Không theo dõi liên tục, không lấy vị trí nền.

import { getCurrentLocation } from '@retion-miniapp/sdk';

async function locate() {
  try {
    const { coords, timestamp } = await getCurrentLocation({ timeout: 15000, maximumAge: 0 });
    console.log(coords.latitude, coords.longitude, coords.accuracy, timestamp);
  } catch (error) {
    // PERMISSION_DENIED | POSITION_UNAVAILABLE | TIMEOUT | BUSY | NOT_SUPPORTED
    console.error(error.code, error.message);
  }
}

| Tuỳ chọn | Mặc định | Ghi chú | | --- | --- | --- | | enableHighAccuracy | false | Yêu cầu GPS chính xác cao | | timeout | 15000 | 1–60000 ms | | maximumAge | 0 | 0–86400000 ms; cho phép dùng vị trí đã lưu |

Kết quả: { coords: { latitude, longitude, accuracy, altitude, altitudeAccuracy, heading, speed }, timestamp }. Android chấp nhận cả quyền vị trí gần đúng (accuracy khi đó lớn hơn). Cần app chủ dùng @retion-miniapp/react-native 0.9.0+; bản cũ trả NOT_SUPPORTED.

Upload file

Chọn file bằng giao diện của app (thư viện ảnh, camera, trình chọn tài liệu…), app nén và upload rồi trả link. Web chỉ nhận kết quả cuối, không nhận file gốc hay đường dẫn trong máy. Cần app chủ dùng @retion-miniapp/react-native 0.10.0+; bản cũ trả NOT_SUPPORTED.

import { uploadFiles, getUploadCapabilities } from '@retion-miniapp/sdk';

// Hỏi app hỗ trợ nguồn nào để ẩn / hiện nút (vd chỉ hiện nút "Tài liệu" khi có 'document').
const { sources } = await getUploadCapabilities(); // ['library', 'camera', 'document']

const controller = new AbortController();
try {
  const { files, failed } = await uploadFiles({
    source: 'library',
    accept: ['image/*'],
    max_count: 5,
    max_size: 20 * 1024 * 1024,
    process: { image: { max_width: 1920, max_height: 1920, quality: 0.8 } },
    onProgress: ({ percent }) => console.log(percent),
    signal: controller.signal, // controller.abort() để huỷ
  });
  console.log(files.map(file => file.url));
  if (failed.length) console.warn('Một số file lỗi', failed);
} catch (error) {
  if (error.code !== 'CANCELLED') console.error(error.code, error.message);
}

Ví dụ khác:

// Tài liệu (PDF, Word, Excel) từ Files / iCloud / Drive
await uploadFiles({ source: 'document', accept: ['application/pdf', '.doc', '.docx', '.xls', '.xlsx'], max_count: 3 });

// Quay video từ camera, nén mức thấp
await uploadFiles({ source: 'camera', accept: ['video/*'], process: { video: { max_duration: 30, quality: 'low' } } });

// Upload lên server riêng của mini app thay vì máy chủ của app
await uploadFiles({
  upload: {
    target: 'url',
    url: 'https://api.example.com/upload',
    headers: { Authorization: `Bearer ${token}` },
    form_data: { folder: 'avatars' },
    field_name: 'file',
    url_field: 'data.url', // server trả { data: { url: '...' } }
  },
});

uploadFiles(options?): Promise<UploadFilesResult>

UploadFilesOptions (tất cả đều tuỳ chọn)

| Prop | Type | Mặc định | Mô tả | | --- | --- | --- | --- | | source | string | 'library' | Nguồn lấy file. Thường có 'library' (thư viện ảnh / video), 'camera' (chụp / quay mới), 'document' (mọi loại file). App có thể đăng ký thêm; xem getUploadCapabilities(). Tên: chữ thường, số, _, -, tối đa 32 ký tự | | accept | string[] | [] (mọi file) | Giống <input accept>: mime ('image/*', 'application/pdf') hoặc đuôi file ('.docx'). Tối đa 50 mục. File không khớp nằm trong failed với mã INVALID_FILE | | max_count | number | 1 | Số file tối đa được chọn, số nguyên 1–20. Camera luôn chụp 1 | | max_size | number | không giới hạn | Byte tối đa mỗi file, sau khi nén. File vượt nằm trong failed với mã FILE_TOO_LARGE | | process | UploadProcessOptions | {} | Nén trước khi upload (bảng dưới). App hỗ trợ tới đâu áp dụng tới đó; tài liệu không bị nén | | upload | UploadTarget \| UploadUrlTarget | { target: 'host' } | Upload lên đâu (bảng dưới) | | concurrency | number | 3 | Số file upload song song, số nguyên 1–5 | | onProgress | (progress: UploadProgress) => void | — | Tiến trình gộp cả lượt, tối đa ~5 lần/giây. Chỉ để hiển thị | | signal | AbortSignal | — | abort() dừng các upload đang chạy, Promise reject CANCELLED. Không đóng được trình chọn file đang mở |

process

| Prop | Type | Mặc định | Mô tả | | --- | --- | --- | --- | | image.max_width | number | giữ nguyên | Chiều rộng tối đa (px), số nguyên 1–10000. Giữ tỉ lệ | | image.max_height | number | giữ nguyên | Chiều cao tối đa (px), số nguyên 1–10000. Giữ tỉ lệ | | image.quality | number | 0.8 | Chất lượng JPEG, trong khoảng (0, 1] | | video.max_duration | number | không giới hạn | Thời lượng tối đa khi quay (giây), số nguyên 1–3600 | | video.quality | 'low' \| 'medium' \| 'high' | 'medium' | Mức nén video |

Ảnh HEIC trên iOS được chuyển sang JPEG.

upload: máy chủ của app (UploadTarget)

| Prop | Type | Mặc định | Mô tả | | --- | --- | --- | --- | | target | string | 'host' | Tên đích app đăng ký. 'host' = máy chủ của app; app tự gắn token | | params | Record<string, unknown> | {} | Tham số thêm cho đích (vd thư mục). Xem tài liệu của app |

upload: server của mini app (UploadUrlTarget). App gửi multipart/form-data bằng POST.

| Prop | Type | Mặc định | Mô tả | | --- | --- | --- | --- | | target | 'url' | — | Bắt buộc | | url | string | — | Bắt buộc, chỉ https:// | | headers | Record<string, string> | {} | Header gửi kèm (vd Authorization). Tối đa 20 key | | form_data | Record<string, string> | {} | Field text gửi kèm trong form. Tối đa 20 key | | field_name | string | 'file' | Tên field chứa file | | url_field | string | 'url' | Đường dẫn tới link trong JSON server trả, dạng a.b.c. Thiếu link → UPLOAD_FAILED |

Server phải trả HTTP 2xx. Mỗi file là một request riêng.

UploadProgress

| Prop | Type | Mô tả | | --- | --- | --- | | loaded | number | Byte đã gửi (cả lượt) | | total | number | Tổng byte (cả lượt); có thể là 0 khi chưa biết | | percent | number | 0–100 | | done_count | number | Số file đã xong (thành công hoặc lỗi) | | count | number | Số file sẽ upload |

UploadFilesResult

| Prop | Type | Mô tả | | --- | --- | --- | | files | UploadedFile[] | File upload thành công, đúng thứ tự đã chọn | | failed | { name: string; code: string; message: string }[] | File lỗi riêng lẻ (INVALID_FILE, FILE_TOO_LARGE, UPLOAD_FAILED…). Các file khác vẫn thành công |

UploadedFile

| Prop | Type | Mô tả | | --- | --- | --- | | url | string | Link file đã upload | | name | string | Tên file | | mime_type | string | Vd image/jpeg | | size | number? | Byte (sau nén) | | width / height | number? | Kích thước ảnh / video (px), nếu có | | duration | number? | Thời lượng video (giây), nếu có | | response | unknown? | JSON server trả (target: 'url') hoặc dữ liệu thêm app trả |

Khi nào reject

| code | Khi nào | | --- | --- | | CANCELLED | Người dùng đóng trình chọn mà không chọn gì, gọi signal.abort(), hoặc mini app bị đóng / reload | | PERMISSION_DENIED | Người dùng từ chối quyền ảnh / camera | | NOT_SUPPORTED | App chưa hỗ trợ source / upload.target này, hoặc app phiên bản cũ | | BUSY | Đang mở một trình chọn file khác. Các lượt đang upload không chặn lượt mới | | INVALID_PARAMS | Tham số sai kiểu / ngoài khoảng (thông báo nêu rõ prop) | | UPLOAD_FAILED / FILE_TOO_LARGE / INVALID_FILE | Mọi file đều lỗi. Mã và thông báo lấy từ file đầu tiên | | NOT_IN_HOST | Chạy ngoài app |

Thời gian chờ tối đa 30 phút (gồm lúc chọn file và upload).

getUploadCapabilities(): Promise<UploadCapabilities>

| Prop | Type | Mô tả | | --- | --- | --- | | sources | string[] | Nguồn app hỗ trợ, vd ['library', 'camera', 'document'] | | file_types | Record<string, UploadFileType[]> | Loại file theo nguồn, dùng để hiện lựa chọn thay vì tự ghi danh sách. Nguồn app tự đăng ký không có mục này; app dưới 0.10.3 không trả field này | | targets | string[] | Đích upload app hỗ trợ. Luôn có 'url'; thường có 'host' | | max_count | number | max_count lớn nhất cho phép (20) |

UploadFileType

| Prop | Type | Mô tả | | --- | --- | --- | | type | string | Mã loại, vd 'image', 'pdf' | | label | string | Tên hiển thị (tiếng Việt) | | accept | string[] | Truyền thẳng vào uploadFiles({ accept }); [] = mọi file |

const { file_types } = await getUploadCapabilities();
const [first] = file_types?.library ?? [];
await uploadFiles({ source: 'library', accept: first?.accept });

App bản cũ reject NOT_SUPPORTED.


Lỗi

Mọi lỗi là MiniAppError, có code (loại lỗi) và method (hàm gây lỗi).

import { getToken, MiniAppError, ErrorCode } from '@retion-miniapp/sdk';

try {
  await getToken('chatbox');
} catch (e) {
  if (e instanceof MiniAppError && e.code === ErrorCode.PERMISSION_DENIED) {
    // chưa được cấp quyền chatbox_token
  }
}

| code | Khi nào | |---|---| | NOT_IN_HOST | Chạy ngoài app mà gọi hàm cần app | | PERMISSION_DENIED | Chưa được cấp quyền lấy token đó, hoặc người dùng từ chối quyền vị trí | | POSITION_UNAVAILABLE | Tắt dịch vụ định vị / không có tín hiệu | | TIMEOUT | App không phản hồi sau 15s (thanh toán / in: 10 phút, upload: 30 phút), hoặc hết timeout khi lấy vị trí | | NOT_SUPPORTED | Phiên bản app hiện tại chưa hỗ trợ hàm này | | CANCELLED | Người dùng huỷ (thanh toán, chọn máy in, chọn file…), hoặc mini app đóng khi đang lấy vị trí / upload | | BUSY | Đang thanh toán / in / lấy vị trí / mở trình chọn file mà gọi lần nữa | | INVALID_PARAMS | Tham số truyền vào sai kiểu / ngoài khoảng | | UPLOAD_FAILED | Upload lỗi (mạng, server trả lỗi, response không có link) | | FILE_TOO_LARGE | File vượt max_size | | INVALID_FILE | File không khớp accept | | HOST_ERROR | Lỗi khác phía app (vd page chưa có token) |


TypeScript

Kèm sẵn type. Export từ package:

  • Ngữ cảnh / token: RouteParams, TokenType
  • Native: PaymentRequest, PaymentResult, PrintImageRequest, OpenScreenRequest, HapticType, ToastType, ShowToastOptions
  • Sinh trắc học: BiometryType, BiometricAvailability, BiometricRequest, BiometricResult
  • Giọng nói: SpeechOptions, SpeechResult, SpeechError, SpeechEvents
  • Quét mã: CodeType, ScannerEvent, ScannerEndReason, ScannerLayout, ScanCodeOptions, StartScannerOptions, ScanResult, ScannerEnd, ScannerEvents
  • Vị trí: LocationOptions, LocationResult
  • Upload: UploadFilesOptions, UploadProcessOptions, UploadFilesResult, UploadedFile, UploadProgress, UploadTarget, UploadUrlTarget, UploadCapabilities, UploadFileType
  • Lỗi: MiniAppError, ErrorCode
import type { RouteParams, TokenType, UploadFilesOptions, UploadedFile } from '@retion-miniapp/sdk';

Build + deploy lên app: @retion-miniapp/cli.

License

MIT