@retion-miniapp/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/dist/chunk-C2AZ33LI.cjs +175 -0
- package/dist/chunk-C2AZ33LI.cjs.map +1 -0
- package/dist/chunk-PVXYU6XK.js +165 -0
- package/dist/chunk-PVXYU6XK.js.map +1 -0
- package/dist/index.cjs +132 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +131 -0
- package/dist/index.d.ts +131 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/types-8g8OpZsj.d.cts +65 -0
- package/dist/types-8g8OpZsj.d.ts +65 -0
- package/dist/vue/index.cjs +19 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +13 -0
- package/dist/vue/index.d.ts +13 -0
- package/dist/vue/index.js +17 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +97 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Định dạng theo [Keep a Changelog](https://keepachangelog.com/vi/1.1.0/), version theo [SemVer](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Bridge giao thức v1 với host React Native (request/response theo id, timeout, event).
|
|
9
|
+
- API: `getRouteParams`, `onContextChange`, `getPageInfo`, `getAccessToken`, `getUserInfo`, `sendMessage`, `insertText`, `closeApp`, `openWebview`, `showToast`, `getSystemInfo`, `isInHost`, `setMockHandler`.
|
|
10
|
+
- `MiniAppError` + `ErrorCode`.
|
|
11
|
+
- Vue composable `useRouteParams` (`@retion-miniapp/sdk/vue`).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Retion
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @retion-miniapp/sdk
|
|
2
|
+
|
|
3
|
+
SDK cho web mini app chạy trong app Retion. Bridge với app được ẩn hoàn toàn — chỉ cần import hàm.
|
|
4
|
+
|
|
5
|
+
- Chạy được ở **cả web lẫn app**: ngoài app, SDK không lỗi, `getRouteParams()` đọc query URL, API cần app reject `NOT_IN_HOST`.
|
|
6
|
+
- Mọi API trả `Promise`, hoặc dùng callback `{ success, fail }`.
|
|
7
|
+
- TypeScript, ESM + CJS, tree-shake theo từng hàm, có composable Vue.
|
|
8
|
+
|
|
9
|
+
## Cài đặt
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @retion-miniapp/sdk
|
|
13
|
+
npm i -D @retion-miniapp/cli # build + deploy
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Dùng nhanh
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { getRouteParams, getAccessToken, insertText, closeApp, isInHost } from '@retion-miniapp/sdk';
|
|
20
|
+
|
|
21
|
+
const { page_id, client_id } = getRouteParams();
|
|
22
|
+
|
|
23
|
+
const token = await getAccessToken();
|
|
24
|
+
const order = await fetch(`https://api.your-app.com/orders?client=${client_id}`, {
|
|
25
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
26
|
+
}).then((r) => r.json());
|
|
27
|
+
|
|
28
|
+
if (isInHost()) {
|
|
29
|
+
await insertText({ text: `Đơn #${order.id}: ${order.total}đ` });
|
|
30
|
+
await closeApp();
|
|
31
|
+
} else {
|
|
32
|
+
router.back(); // chạy trên web: giữ hành vi cũ
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Mini app theo page
|
|
37
|
+
|
|
38
|
+
Mini app được cài theo **page** (kênh). Mỗi page có token, quyền, dữ liệu riêng — app chạy **1 instance mini app cho mỗi page**:
|
|
39
|
+
|
|
40
|
+
- Đổi conversation **trong cùng page** → `onContextChange` (nếu `context_update: true`), hoặc reload với query mới.
|
|
41
|
+
- Đổi **page** → instance khác, mini app khởi động lại với `page_id` mới. Không cần tự xử lý.
|
|
42
|
+
- `getAccessToken()` luôn trả token của page hiện tại — gọi lại mỗi khi cần, không lưu lâu.
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
| API | Trả về | Mô tả |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `getRouteParams()` | `RouteParams` | `page_id, client_id, org_id, message_id, locale, is_page_admin, ...` (đồng bộ) |
|
|
49
|
+
| `onContextChange(cb)` | `() => void` | Đổi conversation trong cùng page mà không reload |
|
|
50
|
+
| `getPageInfo()` | `PageInfo` | `{ page_id, name, avatar, platform }` |
|
|
51
|
+
| `getAccessToken()` | `string` | Token của page hiện tại |
|
|
52
|
+
| `getUserInfo()` | `UserInfo` | Nhân viên đang đăng nhập |
|
|
53
|
+
| `sendMessage({ text })` | `void` | Gửi tin vào conversation |
|
|
54
|
+
| `insertText({ text })` | `void` | Chèn text vào ô nhập chat |
|
|
55
|
+
| `closeApp()` | `void` | Đóng mini app |
|
|
56
|
+
| `openWebview({ url })` | `void` | Mở link http(s) |
|
|
57
|
+
| `showToast({ message })` | `void` | Toast native |
|
|
58
|
+
| `getSystemInfo()` | `SystemInfo` | `platform, appVersion, sdkVersion, inHost, theme` — ngoài app không lỗi |
|
|
59
|
+
| `isInHost()` | `boolean` | Đang chạy trong app Retion có hỗ trợ SDK |
|
|
60
|
+
|
|
61
|
+
### Lỗi
|
|
62
|
+
|
|
63
|
+
Mọi lỗi là `MiniAppError` với `code`:
|
|
64
|
+
|
|
65
|
+
| code | Khi nào |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `NOT_IN_HOST` | Chạy ngoài app (web / màn iframe cũ của app) và không có mock |
|
|
68
|
+
| `TIMEOUT` | Host không phản hồi (15s) |
|
|
69
|
+
| `NOT_SUPPORTED` | App version hiện tại chưa hỗ trợ API |
|
|
70
|
+
| `INVALID_PARAMS` | Tham số sai (url không phải http(s), text rỗng, ...) |
|
|
71
|
+
| `USER_CANCELLED` | User huỷ thao tác |
|
|
72
|
+
| `HOST_ERROR` | Lỗi khác phía app |
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import { closeApp, MiniAppError, ErrorCode } from '@retion-miniapp/sdk';
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await closeApp();
|
|
79
|
+
} catch (e) {
|
|
80
|
+
if (e instanceof MiniAppError && e.code === ErrorCode.NOT_IN_HOST) { /* đang chạy trên web */ }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Vue
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import { useRouteParams } from '@retion-miniapp/sdk/vue';
|
|
88
|
+
|
|
89
|
+
const params = useRouteParams(); // ShallowRef<RouteParams>, tự cập nhật khi đổi conversation
|
|
90
|
+
watch(() => params.value.client_id, fetchData, { immediate: true });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Dev trên trình duyệt
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import { setMockHandler } from '@retion-miniapp/sdk';
|
|
97
|
+
|
|
98
|
+
if (import.meta.env.DEV) {
|
|
99
|
+
setMockHandler((method, params) => {
|
|
100
|
+
if (method === 'getAccessToken') return 'dev-token';
|
|
101
|
+
if (method === 'getPageInfo') return { page_id: 'p1', name: 'Page test', platform: 'facebook' };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Build cho app
|
|
107
|
+
|
|
108
|
+
`vite.config`: `base: './'` (chạy offline từ file local). Sau đó dùng [`@retion-miniapp/cli`](https://www.npmjs.com/package/@retion-miniapp/cli):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx retion-miniapp init
|
|
112
|
+
npx retion-miniapp deploy
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Giao thức (cho team app)
|
|
116
|
+
|
|
117
|
+
Xem chú thích đầu [`src/bridge.ts`](./src/bridge.ts). Host phải inject `window.__RETION_MINIAPP_HOST__ = { protocol: 1 }` trước khi trang chạy — thiếu marker, SDK coi như chạy ngoài app.
|
|
118
|
+
|
|
119
|
+
## Phát triển
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm i
|
|
123
|
+
npm run check # typecheck + lint + test + build + publint
|
|
124
|
+
npm run release:patch # (hoặc minor/major) — xem mục Release
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Release
|
|
128
|
+
|
|
129
|
+
1. Ghi thay đổi vào `CHANGELOG.md` dưới `## Unreleased`.
|
|
130
|
+
2. `npm run release:patch` → chạy `check` → tăng version → cập nhật CHANGELOG → commit + tag `vX.Y.Z` → push.
|
|
131
|
+
3. GitHub Actions (`release.yml`) thấy tag → publish lên npm (cần secret `NPM_TOKEN`).
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var ErrorCode = {
|
|
5
|
+
/** Không chạy trong app Retion (vd mở trên trình duyệt) và không có mock handler. */
|
|
6
|
+
NOT_IN_HOST: "NOT_IN_HOST",
|
|
7
|
+
/** Host không phản hồi trong thời gian chờ. */
|
|
8
|
+
TIMEOUT: "TIMEOUT",
|
|
9
|
+
/** Host (phiên bản app hiện tại) chưa hỗ trợ API này. */
|
|
10
|
+
NOT_SUPPORTED: "NOT_SUPPORTED",
|
|
11
|
+
/** Tham số truyền vào không hợp lệ. */
|
|
12
|
+
INVALID_PARAMS: "INVALID_PARAMS",
|
|
13
|
+
/** User từ chối / huỷ thao tác. */
|
|
14
|
+
USER_CANCELLED: "USER_CANCELLED",
|
|
15
|
+
/** Lỗi khác phía host. */
|
|
16
|
+
HOST_ERROR: "HOST_ERROR"
|
|
17
|
+
};
|
|
18
|
+
var MiniAppError = class _MiniAppError extends Error {
|
|
19
|
+
constructor(code, message, method) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "MiniAppError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.method = method;
|
|
24
|
+
}
|
|
25
|
+
/** Chuẩn hoá lỗi từ host / mock (object thuần, Error, string) về MiniAppError. */
|
|
26
|
+
static from(error, method) {
|
|
27
|
+
if (error instanceof _MiniAppError) {
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
if (error && typeof error === "object") {
|
|
31
|
+
const { code, message } = error;
|
|
32
|
+
return new _MiniAppError(
|
|
33
|
+
typeof code === "string" ? code : ErrorCode.HOST_ERROR,
|
|
34
|
+
typeof message === "string" ? message : "Unknown error",
|
|
35
|
+
method
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return new _MiniAppError(ErrorCode.HOST_ERROR, String(error), method);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/bridge.ts
|
|
43
|
+
var PROTOCOL_VERSION = 1;
|
|
44
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
45
|
+
var CONTEXT_EVENT = "context";
|
|
46
|
+
var hasWindow = typeof window !== "undefined";
|
|
47
|
+
var readInitialContext = () => {
|
|
48
|
+
var _a;
|
|
49
|
+
if (!hasWindow) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
const query = Object.fromEntries(new URLSearchParams(window.location.search));
|
|
53
|
+
return { ...query, ...(_a = window.__RETION_MINIAPP_CONTEXT__) != null ? _a : {} };
|
|
54
|
+
};
|
|
55
|
+
var createState = () => {
|
|
56
|
+
const state = {
|
|
57
|
+
protocol: PROTOCOL_VERSION,
|
|
58
|
+
seq: 0,
|
|
59
|
+
pending: /* @__PURE__ */ new Map(),
|
|
60
|
+
listeners: /* @__PURE__ */ new Map(),
|
|
61
|
+
mock: null,
|
|
62
|
+
context: readInitialContext(),
|
|
63
|
+
receive: (message) => {
|
|
64
|
+
if (!message || typeof message !== "object") {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if ("event" in message) {
|
|
68
|
+
if (message.event === CONTEXT_EVENT) {
|
|
69
|
+
const prev = state.context;
|
|
70
|
+
state.context = { ...prev, ...message.data };
|
|
71
|
+
emit(state, CONTEXT_EVENT, { next: { ...state.context }, prev });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
emit(state, message.event, message.data);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const pending = state.pending.get(message.id);
|
|
78
|
+
if (!pending) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
state.pending.delete(message.id);
|
|
82
|
+
clearTimeout(pending.timer);
|
|
83
|
+
if (message.error) {
|
|
84
|
+
pending.reject(MiniAppError.from(message.error, pending.method));
|
|
85
|
+
} else {
|
|
86
|
+
pending.resolve(message.result);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return state;
|
|
91
|
+
};
|
|
92
|
+
var emit = (state, event, data) => {
|
|
93
|
+
var _a;
|
|
94
|
+
(_a = state.listeners.get(event)) == null ? void 0 : _a.forEach((listener) => {
|
|
95
|
+
try {
|
|
96
|
+
listener(data);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`[retion-miniapp] listener "${event}" l\u1ED7i`, error);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
var fallback_state = null;
|
|
103
|
+
var getState = () => {
|
|
104
|
+
var _a;
|
|
105
|
+
if (!hasWindow) {
|
|
106
|
+
return fallback_state != null ? fallback_state : fallback_state = createState();
|
|
107
|
+
}
|
|
108
|
+
return (_a = window.__RETION_MINIAPP__) != null ? _a : window.__RETION_MINIAPP__ = createState();
|
|
109
|
+
};
|
|
110
|
+
var isInHost = () => {
|
|
111
|
+
var _a, _b;
|
|
112
|
+
return hasWindow && typeof ((_a = window.__RETION_MINIAPP_HOST__) == null ? void 0 : _a.protocol) === "number" && typeof ((_b = window.ReactNativeWebView) == null ? void 0 : _b.postMessage) === "function";
|
|
113
|
+
};
|
|
114
|
+
var post = (message) => {
|
|
115
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(message));
|
|
116
|
+
};
|
|
117
|
+
var call = (method, params, timeout_ms = DEFAULT_TIMEOUT_MS) => {
|
|
118
|
+
const state = getState();
|
|
119
|
+
if (!isInHost()) {
|
|
120
|
+
if (!state.mock) {
|
|
121
|
+
return Promise.reject(
|
|
122
|
+
new MiniAppError(ErrorCode.NOT_IN_HOST, `${method} ch\u1EC9 ch\u1EA1y trong app Retion`, method)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const mock = state.mock;
|
|
126
|
+
return new Promise((resolve) => resolve(mock(method, params))).catch((error) => {
|
|
127
|
+
throw MiniAppError.from(error, method);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const id = `${Date.now().toString(36)}-${++state.seq}`;
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
state.pending.delete(id);
|
|
134
|
+
reject(new MiniAppError(ErrorCode.TIMEOUT, `${method}: host kh\xF4ng ph\u1EA3n h\u1ED3i sau ${timeout_ms}ms`, method));
|
|
135
|
+
}, timeout_ms);
|
|
136
|
+
state.pending.set(id, { method, resolve, reject, timer });
|
|
137
|
+
post({ __retion: PROTOCOL_VERSION, id, method, params });
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
var notify = (method, params) => {
|
|
141
|
+
if (isInHost()) {
|
|
142
|
+
post({ __retion: PROTOCOL_VERSION, method, params });
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var on = (event, listener) => {
|
|
146
|
+
const { listeners } = getState();
|
|
147
|
+
let set = listeners.get(event);
|
|
148
|
+
if (!set) {
|
|
149
|
+
set = /* @__PURE__ */ new Set();
|
|
150
|
+
listeners.set(event, set);
|
|
151
|
+
}
|
|
152
|
+
set.add(listener);
|
|
153
|
+
return () => {
|
|
154
|
+
set.delete(listener);
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// src/apis/route.ts
|
|
159
|
+
var getRouteParams = () => ({ ...getState().context });
|
|
160
|
+
var onContextChange = (listener) => on(CONTEXT_EVENT, (data) => {
|
|
161
|
+
const { next, prev } = data;
|
|
162
|
+
listener(next, prev);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
exports.ErrorCode = ErrorCode;
|
|
166
|
+
exports.MiniAppError = MiniAppError;
|
|
167
|
+
exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
168
|
+
exports.call = call;
|
|
169
|
+
exports.getRouteParams = getRouteParams;
|
|
170
|
+
exports.getState = getState;
|
|
171
|
+
exports.isInHost = isInHost;
|
|
172
|
+
exports.notify = notify;
|
|
173
|
+
exports.onContextChange = onContextChange;
|
|
174
|
+
//# sourceMappingURL=chunk-C2AZ33LI.cjs.map
|
|
175
|
+
//# sourceMappingURL=chunk-C2AZ33LI.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/bridge.ts","../src/apis/route.ts"],"names":[],"mappings":"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","file":"chunk-C2AZ33LI.cjs","sourcesContent":["export const ErrorCode = {\n /** Không chạy trong app Retion (vd mở trên trình duyệt) và không có mock handler. */\n NOT_IN_HOST: 'NOT_IN_HOST',\n /** Host không phản hồi trong thời gian chờ. */\n TIMEOUT: 'TIMEOUT',\n /** Host (phiên bản app hiện tại) chưa hỗ trợ API này. */\n NOT_SUPPORTED: 'NOT_SUPPORTED',\n /** Tham số truyền vào không hợp lệ. */\n INVALID_PARAMS: 'INVALID_PARAMS',\n /** User từ chối / huỷ thao tác. */\n USER_CANCELLED: 'USER_CANCELLED',\n /** Lỗi khác phía host. */\n HOST_ERROR: 'HOST_ERROR',\n} as const;\n\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode] | (string & {});\n\nexport class MiniAppError extends Error {\n readonly code: ErrorCode;\n readonly method?: string;\n\n constructor(code: ErrorCode, message: string, method?: string) {\n super(message);\n this.name = 'MiniAppError';\n this.code = code;\n this.method = method;\n }\n\n /** Chuẩn hoá lỗi từ host / mock (object thuần, Error, string) về MiniAppError. */\n static from(error: unknown, method?: string): MiniAppError {\n if (error instanceof MiniAppError) {\n return error;\n }\n if (error && typeof error === 'object') {\n const { code, message } = error as { code?: unknown; message?: unknown };\n return new MiniAppError(\n typeof code === 'string' ? code : ErrorCode.HOST_ERROR,\n typeof message === 'string' ? message : 'Unknown error',\n method,\n );\n }\n return new MiniAppError(ErrorCode.HOST_ERROR, String(error), method);\n }\n}\n","/**\n * Bridge web mini app ↔ host (app Retion). Nội bộ SDK — dev mini app không import trực tiếp.\n *\n * Giao thức v1\n * web → host : ReactNativeWebView.postMessage(JSON.stringify(RequestMessage))\n * host → web : window.__RETION_MINIAPP__.receive(ResponseMessage | EventMessage)\n * host inject trước khi trang chạy:\n * window.__RETION_MINIAPP_HOST__ = { protocol } ← đánh dấu host hỗ trợ SDK\n * window.__RETION_MINIAPP_CONTEXT__ = RouteParams ← context ban đầu\n *\n * Chỉ có ReactNativeWebView KHÔNG đủ để coi là host: màn iframe cũ của app cũng là RN WebView\n * nhưng không hiểu giao thức này (gọi API sẽ treo tới timeout). Thiếu marker → NOT_IN_HOST ngay.\n *\n * State đặt trên window (singleton): nhiều bản SDK trong cùng trang (trùng version trong\n * node_modules) vẫn dùng chung 1 bridge thay vì ghi đè hàm receive của nhau.\n */\nimport { ErrorCode, MiniAppError } from './errors';\nimport type { MockHandler, RouteParams } from './types';\n\nexport const PROTOCOL_VERSION = 1;\nexport const DEFAULT_TIMEOUT_MS = 15_000;\nexport const CONTEXT_EVENT = 'context';\n\nexport interface RequestMessage {\n __retion: typeof PROTOCOL_VERSION;\n /** Có id → host phải trả ResponseMessage. Không có → lệnh 1 chiều. */\n id?: string;\n method: string;\n params?: unknown;\n}\n\nexport type ResponseMessage =\n | { id: string; result?: unknown; error?: undefined }\n | { id: string; error: { code: string; message: string } };\n\nexport interface EventMessage {\n event: string;\n data?: unknown;\n}\n\nexport interface HostInfo {\n /** Phiên bản giao thức host hỗ trợ. */\n protocol: number;\n}\n\ntype Listener = (data: unknown) => void;\n\ninterface Pending {\n method: string;\n resolve: (value: unknown) => void;\n reject: (error: MiniAppError) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\ninterface BridgeState {\n protocol: number;\n seq: number;\n pending: Map<string, Pending>;\n listeners: Map<string, Set<Listener>>;\n mock: MockHandler | null;\n context: RouteParams;\n receive: (message: ResponseMessage | EventMessage) => void;\n}\n\ndeclare global {\n interface Window {\n ReactNativeWebView?: { postMessage: (message: string) => void };\n __RETION_MINIAPP__?: BridgeState;\n __RETION_MINIAPP_CONTEXT__?: RouteParams;\n __RETION_MINIAPP_HOST__?: HostInfo;\n }\n}\n\nconst hasWindow = typeof window !== 'undefined';\n\nconst readInitialContext = (): RouteParams => {\n if (!hasWindow) {\n return {};\n }\n const query = Object.fromEntries(new URLSearchParams(window.location.search));\n return { ...query, ...(window.__RETION_MINIAPP_CONTEXT__ ?? {}) };\n};\n\nconst createState = (): BridgeState => {\n const state: BridgeState = {\n protocol: PROTOCOL_VERSION,\n seq: 0,\n pending: new Map(),\n listeners: new Map(),\n mock: null,\n context: readInitialContext(),\n receive: (message) => {\n if (!message || typeof message !== 'object') {\n return;\n }\n if ('event' in message) {\n if (message.event === CONTEXT_EVENT) {\n // Cập nhật context TRƯỚC khi emit → getRouteParams() luôn đúng, kể cả không ai lắng nghe.\n const prev = state.context;\n state.context = { ...prev, ...(message.data as RouteParams) };\n emit(state, CONTEXT_EVENT, { next: { ...state.context }, prev });\n return;\n }\n emit(state, message.event, message.data);\n return;\n }\n const pending = state.pending.get(message.id);\n if (!pending) {\n return;\n }\n state.pending.delete(message.id);\n clearTimeout(pending.timer);\n if (message.error) {\n pending.reject(MiniAppError.from(message.error, pending.method));\n } else {\n pending.resolve(message.result);\n }\n },\n };\n return state;\n};\n\nconst emit = (state: BridgeState, event: string, data: unknown) => {\n state.listeners.get(event)?.forEach((listener) => {\n try {\n listener(data);\n } catch (error) {\n console.error(`[retion-miniapp] listener \"${event}\" lỗi`, error);\n }\n });\n};\n\nlet fallback_state: BridgeState | null = null;\n\n/** @internal */\nexport const getState = (): BridgeState => {\n if (!hasWindow) {\n return (fallback_state ??= createState());\n }\n return (window.__RETION_MINIAPP__ ??= createState());\n};\n\n/** Đang chạy trong app Retion có hỗ trợ SDK (host đã inject marker + kênh postMessage). */\nexport const isInHost = (): boolean =>\n hasWindow &&\n typeof window.__RETION_MINIAPP_HOST__?.protocol === 'number' &&\n typeof window.ReactNativeWebView?.postMessage === 'function';\n\nconst post = (message: RequestMessage) => {\n window.ReactNativeWebView!.postMessage(JSON.stringify(message));\n};\n\n/** Gửi lệnh lên host và chờ kết quả. Ngoài app → mock handler (nếu có) hoặc reject NOT_IN_HOST. */\nexport const call = <T>(method: string, params?: unknown, timeout_ms = DEFAULT_TIMEOUT_MS): Promise<T> => {\n const state = getState();\n\n if (!isInHost()) {\n if (!state.mock) {\n return Promise.reject(\n new MiniAppError(ErrorCode.NOT_IN_HOST, `${method} chỉ chạy trong app Retion`, method),\n );\n }\n const mock = state.mock;\n return new Promise<T>((resolve) => resolve(mock(method, params) as T)).catch((error: unknown) => {\n throw MiniAppError.from(error, method);\n });\n }\n\n const id = `${Date.now().toString(36)}-${++state.seq}`;\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n state.pending.delete(id);\n reject(new MiniAppError(ErrorCode.TIMEOUT, `${method}: host không phản hồi sau ${timeout_ms}ms`, method));\n }, timeout_ms);\n state.pending.set(id, { method, resolve: resolve as (v: unknown) => void, reject, timer });\n post({ __retion: PROTOCOL_VERSION, id, method, params });\n });\n};\n\n/** Lệnh 1 chiều (không chờ kết quả). Ngoài app: bỏ qua. */\nexport const notify = (method: string, params?: unknown): void => {\n if (isInHost()) {\n post({ __retion: PROTOCOL_VERSION, method, params });\n }\n};\n\n/** Lắng nghe event host đẩy xuống. Trả về hàm huỷ. */\nexport const on = (event: string, listener: Listener): (() => void) => {\n const { listeners } = getState();\n let set = listeners.get(event);\n if (!set) {\n set = new Set();\n listeners.set(event, set);\n }\n set.add(listener);\n return () => {\n set.delete(listener);\n };\n};\n","import { CONTEXT_EVENT, getState, on } from '../bridge';\nimport type { RouteParams } from '../types';\n\ntype ContextListener = (next: RouteParams, prev: RouteParams) => void;\n\n/**\n * Param lúc mở mini app: query URL + context host inject (host ưu tiên).\n * @example\n * const { client_id, org_id } = getRouteParams();\n */\nexport const getRouteParams = (): RouteParams => ({ ...getState().context });\n\n/**\n * Host đổi conversation mà KHÔNG reload mini app — chỉ xảy ra khi `miniapp.config.json`\n * có `context_update: true`. Nếu không, host reload với query mới và getRouteParams() vẫn đúng.\n * @returns hàm huỷ đăng ký\n */\nexport const onContextChange = (listener: ContextListener): (() => void) =>\n on(CONTEXT_EVENT, (data) => {\n const { next, prev } = data as { next: RouteParams; prev: RouteParams };\n listener(next, prev);\n });\n"]}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ErrorCode = {
|
|
3
|
+
/** Không chạy trong app Retion (vd mở trên trình duyệt) và không có mock handler. */
|
|
4
|
+
NOT_IN_HOST: "NOT_IN_HOST",
|
|
5
|
+
/** Host không phản hồi trong thời gian chờ. */
|
|
6
|
+
TIMEOUT: "TIMEOUT",
|
|
7
|
+
/** Host (phiên bản app hiện tại) chưa hỗ trợ API này. */
|
|
8
|
+
NOT_SUPPORTED: "NOT_SUPPORTED",
|
|
9
|
+
/** Tham số truyền vào không hợp lệ. */
|
|
10
|
+
INVALID_PARAMS: "INVALID_PARAMS",
|
|
11
|
+
/** User từ chối / huỷ thao tác. */
|
|
12
|
+
USER_CANCELLED: "USER_CANCELLED",
|
|
13
|
+
/** Lỗi khác phía host. */
|
|
14
|
+
HOST_ERROR: "HOST_ERROR"
|
|
15
|
+
};
|
|
16
|
+
var MiniAppError = class _MiniAppError extends Error {
|
|
17
|
+
constructor(code, message, method) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "MiniAppError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.method = method;
|
|
22
|
+
}
|
|
23
|
+
/** Chuẩn hoá lỗi từ host / mock (object thuần, Error, string) về MiniAppError. */
|
|
24
|
+
static from(error, method) {
|
|
25
|
+
if (error instanceof _MiniAppError) {
|
|
26
|
+
return error;
|
|
27
|
+
}
|
|
28
|
+
if (error && typeof error === "object") {
|
|
29
|
+
const { code, message } = error;
|
|
30
|
+
return new _MiniAppError(
|
|
31
|
+
typeof code === "string" ? code : ErrorCode.HOST_ERROR,
|
|
32
|
+
typeof message === "string" ? message : "Unknown error",
|
|
33
|
+
method
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return new _MiniAppError(ErrorCode.HOST_ERROR, String(error), method);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/bridge.ts
|
|
41
|
+
var PROTOCOL_VERSION = 1;
|
|
42
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
43
|
+
var CONTEXT_EVENT = "context";
|
|
44
|
+
var hasWindow = typeof window !== "undefined";
|
|
45
|
+
var readInitialContext = () => {
|
|
46
|
+
var _a;
|
|
47
|
+
if (!hasWindow) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
const query = Object.fromEntries(new URLSearchParams(window.location.search));
|
|
51
|
+
return { ...query, ...(_a = window.__RETION_MINIAPP_CONTEXT__) != null ? _a : {} };
|
|
52
|
+
};
|
|
53
|
+
var createState = () => {
|
|
54
|
+
const state = {
|
|
55
|
+
protocol: PROTOCOL_VERSION,
|
|
56
|
+
seq: 0,
|
|
57
|
+
pending: /* @__PURE__ */ new Map(),
|
|
58
|
+
listeners: /* @__PURE__ */ new Map(),
|
|
59
|
+
mock: null,
|
|
60
|
+
context: readInitialContext(),
|
|
61
|
+
receive: (message) => {
|
|
62
|
+
if (!message || typeof message !== "object") {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if ("event" in message) {
|
|
66
|
+
if (message.event === CONTEXT_EVENT) {
|
|
67
|
+
const prev = state.context;
|
|
68
|
+
state.context = { ...prev, ...message.data };
|
|
69
|
+
emit(state, CONTEXT_EVENT, { next: { ...state.context }, prev });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
emit(state, message.event, message.data);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const pending = state.pending.get(message.id);
|
|
76
|
+
if (!pending) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
state.pending.delete(message.id);
|
|
80
|
+
clearTimeout(pending.timer);
|
|
81
|
+
if (message.error) {
|
|
82
|
+
pending.reject(MiniAppError.from(message.error, pending.method));
|
|
83
|
+
} else {
|
|
84
|
+
pending.resolve(message.result);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return state;
|
|
89
|
+
};
|
|
90
|
+
var emit = (state, event, data) => {
|
|
91
|
+
var _a;
|
|
92
|
+
(_a = state.listeners.get(event)) == null ? void 0 : _a.forEach((listener) => {
|
|
93
|
+
try {
|
|
94
|
+
listener(data);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`[retion-miniapp] listener "${event}" l\u1ED7i`, error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
var fallback_state = null;
|
|
101
|
+
var getState = () => {
|
|
102
|
+
var _a;
|
|
103
|
+
if (!hasWindow) {
|
|
104
|
+
return fallback_state != null ? fallback_state : fallback_state = createState();
|
|
105
|
+
}
|
|
106
|
+
return (_a = window.__RETION_MINIAPP__) != null ? _a : window.__RETION_MINIAPP__ = createState();
|
|
107
|
+
};
|
|
108
|
+
var isInHost = () => {
|
|
109
|
+
var _a, _b;
|
|
110
|
+
return hasWindow && typeof ((_a = window.__RETION_MINIAPP_HOST__) == null ? void 0 : _a.protocol) === "number" && typeof ((_b = window.ReactNativeWebView) == null ? void 0 : _b.postMessage) === "function";
|
|
111
|
+
};
|
|
112
|
+
var post = (message) => {
|
|
113
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(message));
|
|
114
|
+
};
|
|
115
|
+
var call = (method, params, timeout_ms = DEFAULT_TIMEOUT_MS) => {
|
|
116
|
+
const state = getState();
|
|
117
|
+
if (!isInHost()) {
|
|
118
|
+
if (!state.mock) {
|
|
119
|
+
return Promise.reject(
|
|
120
|
+
new MiniAppError(ErrorCode.NOT_IN_HOST, `${method} ch\u1EC9 ch\u1EA1y trong app Retion`, method)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const mock = state.mock;
|
|
124
|
+
return new Promise((resolve) => resolve(mock(method, params))).catch((error) => {
|
|
125
|
+
throw MiniAppError.from(error, method);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const id = `${Date.now().toString(36)}-${++state.seq}`;
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
state.pending.delete(id);
|
|
132
|
+
reject(new MiniAppError(ErrorCode.TIMEOUT, `${method}: host kh\xF4ng ph\u1EA3n h\u1ED3i sau ${timeout_ms}ms`, method));
|
|
133
|
+
}, timeout_ms);
|
|
134
|
+
state.pending.set(id, { method, resolve, reject, timer });
|
|
135
|
+
post({ __retion: PROTOCOL_VERSION, id, method, params });
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
var notify = (method, params) => {
|
|
139
|
+
if (isInHost()) {
|
|
140
|
+
post({ __retion: PROTOCOL_VERSION, method, params });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var on = (event, listener) => {
|
|
144
|
+
const { listeners } = getState();
|
|
145
|
+
let set = listeners.get(event);
|
|
146
|
+
if (!set) {
|
|
147
|
+
set = /* @__PURE__ */ new Set();
|
|
148
|
+
listeners.set(event, set);
|
|
149
|
+
}
|
|
150
|
+
set.add(listener);
|
|
151
|
+
return () => {
|
|
152
|
+
set.delete(listener);
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// src/apis/route.ts
|
|
157
|
+
var getRouteParams = () => ({ ...getState().context });
|
|
158
|
+
var onContextChange = (listener) => on(CONTEXT_EVENT, (data) => {
|
|
159
|
+
const { next, prev } = data;
|
|
160
|
+
listener(next, prev);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
export { ErrorCode, MiniAppError, PROTOCOL_VERSION, call, getRouteParams, getState, isInHost, notify, onContextChange };
|
|
164
|
+
//# sourceMappingURL=chunk-PVXYU6XK.js.map
|
|
165
|
+
//# sourceMappingURL=chunk-PVXYU6XK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/bridge.ts","../src/apis/route.ts"],"names":[],"mappings":"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","file":"chunk-PVXYU6XK.js","sourcesContent":["export const ErrorCode = {\n /** Không chạy trong app Retion (vd mở trên trình duyệt) và không có mock handler. */\n NOT_IN_HOST: 'NOT_IN_HOST',\n /** Host không phản hồi trong thời gian chờ. */\n TIMEOUT: 'TIMEOUT',\n /** Host (phiên bản app hiện tại) chưa hỗ trợ API này. */\n NOT_SUPPORTED: 'NOT_SUPPORTED',\n /** Tham số truyền vào không hợp lệ. */\n INVALID_PARAMS: 'INVALID_PARAMS',\n /** User từ chối / huỷ thao tác. */\n USER_CANCELLED: 'USER_CANCELLED',\n /** Lỗi khác phía host. */\n HOST_ERROR: 'HOST_ERROR',\n} as const;\n\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode] | (string & {});\n\nexport class MiniAppError extends Error {\n readonly code: ErrorCode;\n readonly method?: string;\n\n constructor(code: ErrorCode, message: string, method?: string) {\n super(message);\n this.name = 'MiniAppError';\n this.code = code;\n this.method = method;\n }\n\n /** Chuẩn hoá lỗi từ host / mock (object thuần, Error, string) về MiniAppError. */\n static from(error: unknown, method?: string): MiniAppError {\n if (error instanceof MiniAppError) {\n return error;\n }\n if (error && typeof error === 'object') {\n const { code, message } = error as { code?: unknown; message?: unknown };\n return new MiniAppError(\n typeof code === 'string' ? code : ErrorCode.HOST_ERROR,\n typeof message === 'string' ? message : 'Unknown error',\n method,\n );\n }\n return new MiniAppError(ErrorCode.HOST_ERROR, String(error), method);\n }\n}\n","/**\n * Bridge web mini app ↔ host (app Retion). Nội bộ SDK — dev mini app không import trực tiếp.\n *\n * Giao thức v1\n * web → host : ReactNativeWebView.postMessage(JSON.stringify(RequestMessage))\n * host → web : window.__RETION_MINIAPP__.receive(ResponseMessage | EventMessage)\n * host inject trước khi trang chạy:\n * window.__RETION_MINIAPP_HOST__ = { protocol } ← đánh dấu host hỗ trợ SDK\n * window.__RETION_MINIAPP_CONTEXT__ = RouteParams ← context ban đầu\n *\n * Chỉ có ReactNativeWebView KHÔNG đủ để coi là host: màn iframe cũ của app cũng là RN WebView\n * nhưng không hiểu giao thức này (gọi API sẽ treo tới timeout). Thiếu marker → NOT_IN_HOST ngay.\n *\n * State đặt trên window (singleton): nhiều bản SDK trong cùng trang (trùng version trong\n * node_modules) vẫn dùng chung 1 bridge thay vì ghi đè hàm receive của nhau.\n */\nimport { ErrorCode, MiniAppError } from './errors';\nimport type { MockHandler, RouteParams } from './types';\n\nexport const PROTOCOL_VERSION = 1;\nexport const DEFAULT_TIMEOUT_MS = 15_000;\nexport const CONTEXT_EVENT = 'context';\n\nexport interface RequestMessage {\n __retion: typeof PROTOCOL_VERSION;\n /** Có id → host phải trả ResponseMessage. Không có → lệnh 1 chiều. */\n id?: string;\n method: string;\n params?: unknown;\n}\n\nexport type ResponseMessage =\n | { id: string; result?: unknown; error?: undefined }\n | { id: string; error: { code: string; message: string } };\n\nexport interface EventMessage {\n event: string;\n data?: unknown;\n}\n\nexport interface HostInfo {\n /** Phiên bản giao thức host hỗ trợ. */\n protocol: number;\n}\n\ntype Listener = (data: unknown) => void;\n\ninterface Pending {\n method: string;\n resolve: (value: unknown) => void;\n reject: (error: MiniAppError) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\ninterface BridgeState {\n protocol: number;\n seq: number;\n pending: Map<string, Pending>;\n listeners: Map<string, Set<Listener>>;\n mock: MockHandler | null;\n context: RouteParams;\n receive: (message: ResponseMessage | EventMessage) => void;\n}\n\ndeclare global {\n interface Window {\n ReactNativeWebView?: { postMessage: (message: string) => void };\n __RETION_MINIAPP__?: BridgeState;\n __RETION_MINIAPP_CONTEXT__?: RouteParams;\n __RETION_MINIAPP_HOST__?: HostInfo;\n }\n}\n\nconst hasWindow = typeof window !== 'undefined';\n\nconst readInitialContext = (): RouteParams => {\n if (!hasWindow) {\n return {};\n }\n const query = Object.fromEntries(new URLSearchParams(window.location.search));\n return { ...query, ...(window.__RETION_MINIAPP_CONTEXT__ ?? {}) };\n};\n\nconst createState = (): BridgeState => {\n const state: BridgeState = {\n protocol: PROTOCOL_VERSION,\n seq: 0,\n pending: new Map(),\n listeners: new Map(),\n mock: null,\n context: readInitialContext(),\n receive: (message) => {\n if (!message || typeof message !== 'object') {\n return;\n }\n if ('event' in message) {\n if (message.event === CONTEXT_EVENT) {\n // Cập nhật context TRƯỚC khi emit → getRouteParams() luôn đúng, kể cả không ai lắng nghe.\n const prev = state.context;\n state.context = { ...prev, ...(message.data as RouteParams) };\n emit(state, CONTEXT_EVENT, { next: { ...state.context }, prev });\n return;\n }\n emit(state, message.event, message.data);\n return;\n }\n const pending = state.pending.get(message.id);\n if (!pending) {\n return;\n }\n state.pending.delete(message.id);\n clearTimeout(pending.timer);\n if (message.error) {\n pending.reject(MiniAppError.from(message.error, pending.method));\n } else {\n pending.resolve(message.result);\n }\n },\n };\n return state;\n};\n\nconst emit = (state: BridgeState, event: string, data: unknown) => {\n state.listeners.get(event)?.forEach((listener) => {\n try {\n listener(data);\n } catch (error) {\n console.error(`[retion-miniapp] listener \"${event}\" lỗi`, error);\n }\n });\n};\n\nlet fallback_state: BridgeState | null = null;\n\n/** @internal */\nexport const getState = (): BridgeState => {\n if (!hasWindow) {\n return (fallback_state ??= createState());\n }\n return (window.__RETION_MINIAPP__ ??= createState());\n};\n\n/** Đang chạy trong app Retion có hỗ trợ SDK (host đã inject marker + kênh postMessage). */\nexport const isInHost = (): boolean =>\n hasWindow &&\n typeof window.__RETION_MINIAPP_HOST__?.protocol === 'number' &&\n typeof window.ReactNativeWebView?.postMessage === 'function';\n\nconst post = (message: RequestMessage) => {\n window.ReactNativeWebView!.postMessage(JSON.stringify(message));\n};\n\n/** Gửi lệnh lên host và chờ kết quả. Ngoài app → mock handler (nếu có) hoặc reject NOT_IN_HOST. */\nexport const call = <T>(method: string, params?: unknown, timeout_ms = DEFAULT_TIMEOUT_MS): Promise<T> => {\n const state = getState();\n\n if (!isInHost()) {\n if (!state.mock) {\n return Promise.reject(\n new MiniAppError(ErrorCode.NOT_IN_HOST, `${method} chỉ chạy trong app Retion`, method),\n );\n }\n const mock = state.mock;\n return new Promise<T>((resolve) => resolve(mock(method, params) as T)).catch((error: unknown) => {\n throw MiniAppError.from(error, method);\n });\n }\n\n const id = `${Date.now().toString(36)}-${++state.seq}`;\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n state.pending.delete(id);\n reject(new MiniAppError(ErrorCode.TIMEOUT, `${method}: host không phản hồi sau ${timeout_ms}ms`, method));\n }, timeout_ms);\n state.pending.set(id, { method, resolve: resolve as (v: unknown) => void, reject, timer });\n post({ __retion: PROTOCOL_VERSION, id, method, params });\n });\n};\n\n/** Lệnh 1 chiều (không chờ kết quả). Ngoài app: bỏ qua. */\nexport const notify = (method: string, params?: unknown): void => {\n if (isInHost()) {\n post({ __retion: PROTOCOL_VERSION, method, params });\n }\n};\n\n/** Lắng nghe event host đẩy xuống. Trả về hàm huỷ. */\nexport const on = (event: string, listener: Listener): (() => void) => {\n const { listeners } = getState();\n let set = listeners.get(event);\n if (!set) {\n set = new Set();\n listeners.set(event, set);\n }\n set.add(listener);\n return () => {\n set.delete(listener);\n };\n};\n","import { CONTEXT_EVENT, getState, on } from '../bridge';\nimport type { RouteParams } from '../types';\n\ntype ContextListener = (next: RouteParams, prev: RouteParams) => void;\n\n/**\n * Param lúc mở mini app: query URL + context host inject (host ưu tiên).\n * @example\n * const { client_id, org_id } = getRouteParams();\n */\nexport const getRouteParams = (): RouteParams => ({ ...getState().context });\n\n/**\n * Host đổi conversation mà KHÔNG reload mini app — chỉ xảy ra khi `miniapp.config.json`\n * có `context_update: true`. Nếu không, host reload với query mới và getRouteParams() vẫn đúng.\n * @returns hàm huỷ đăng ký\n */\nexport const onContextChange = (listener: ContextListener): (() => void) =>\n on(CONTEXT_EVENT, (data) => {\n const { next, prev } = data as { next: RouteParams; prev: RouteParams };\n listener(next, prev);\n });\n"]}
|