@pocketboss/core 0.1.0 → 0.2.1
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/README.md +205 -26
- package/dist/comments/index.d.ts +2 -0
- package/dist/comments/index.js +2 -0
- package/dist/comments-element.d.ts +37 -0
- package/dist/comments-element.js +468 -0
- package/dist/comments.d.ts +57 -0
- package/dist/comments.js +226 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,41 +1,220 @@
|
|
|
1
1
|
# @pocketboss/core
|
|
2
2
|
|
|
3
|
-
Framework-neutral
|
|
3
|
+
Framework-neutral TypeScript client for PocketBoss Bridge protocol v1. It provides the vanilla JavaScript entry point, typed bridge requests, browser file inputs, and the shared REST client and custom element for order comments.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
The npm packages are not yet released to the public registry. Use the repository workspace quickstart below for a runnable local app. Public installation is available after the package release gates in the [SDK publishing guide](https://github.com/benogroupltd/PocketBoss/blob/main/docs/SDK_PUBLISHING.md) are satisfied.
|
|
6
|
+
|
|
7
|
+
## Runnable vanilla quickstart
|
|
8
|
+
|
|
9
|
+
Requirements: Node.js 24.21.0 and npm 11.19.0. From the repository root:
|
|
10
|
+
|
|
11
|
+
~~~sh
|
|
12
|
+
npm ci
|
|
13
|
+
npm run build:core
|
|
14
|
+
npx vite packages/sdk-core/examples/vanilla --host 127.0.0.1
|
|
15
|
+
~~~
|
|
16
|
+
|
|
17
|
+
Open <http://127.0.0.1:5173>. The fixture reports standalone browser mode. Open <http://127.0.0.1:5173/?mock=1> to exercise its explicit mock handler. The mock is only a development response; it does not demonstrate a native permission or authenticated launch.
|
|
18
|
+
|
|
19
|
+
After a public release, install with <code>npm install @pocketboss/core</code>. Import from <code>@pocketboss/core</code>, <code>@pocketboss/core/comments</code>, or <code>@pocketboss/core/mock</code>. The same package can be used by any JavaScript framework.
|
|
20
|
+
|
|
21
|
+
## Bridge lifecycle
|
|
22
|
+
|
|
23
|
+
Create one client for the Mini App document. Connect after startup, check each launch's capabilities before showing a host action, and close the client when the document is destroyed.
|
|
24
|
+
|
|
25
|
+
~~~ts
|
|
26
|
+
import { BridgeError, PocketBossClient } from '@pocketboss/core';
|
|
7
27
|
|
|
8
28
|
const client = new PocketBossClient();
|
|
9
|
-
|
|
10
|
-
await client.connect();
|
|
11
|
-
|
|
12
|
-
|
|
29
|
+
try {
|
|
30
|
+
const ready = await client.connect();
|
|
31
|
+
if (ready.capabilities.some((item) =>
|
|
32
|
+
item.name === 'runtime.getInfo' && item.version === 1)) {
|
|
33
|
+
const info = await client.getInfo({ timeoutMs: 5000 });
|
|
34
|
+
document.querySelector('#status')!.textContent =
|
|
35
|
+
info.platform + ' ' + info.runtimeVersion;
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const code = error instanceof BridgeError ? error.code : 'INTERNAL';
|
|
39
|
+
document.querySelector('#status')!.textContent = 'Bridge error: ' + code;
|
|
13
40
|
}
|
|
14
|
-
client.close();
|
|
15
|
-
|
|
41
|
+
window.addEventListener('pagehide', () => client.close(), { once: true });
|
|
42
|
+
~~~
|
|
16
43
|
|
|
17
|
-
|
|
44
|
+
<code>isAvailable</code> is false in a normal browser; <code>isMock</code> identifies an explicitly supplied mock transport. <code>connect()</code> rejects with <code>UNAVAILABLE</code> outside a native host, <code>TIMEOUT</code> when the handshake expires, <code>CANCELLED</code> when its signal is aborted, and <code>UNSUPPORTED_PLATFORM</code> when the host protocol is incompatible. A successful handshake returns the launch, app, release, runtime, capability, limits, and expiry context.
|
|
18
45
|
|
|
19
|
-
|
|
20
|
-
import { captureDevicePhoto, pickDeviceFiles } from '@pocketboss/core';
|
|
46
|
+
### Bridge client exports
|
|
21
47
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
48
|
+
| Export | Parameters and return value | Errors and behavior |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| <code>PocketBossClient</code> | <code>new PocketBossClient(transport?: BridgeTransport \| null)</code>; defaults to the browser transport. | A null or missing host produces unavailable state. |
|
|
51
|
+
| <code>isAvailable</code>, <code>isMock</code>, <code>snapshot</code>, <code>getSnapshot()</code> | Read native/mock availability or a <code>BridgeSnapshot</code> containing state, capabilities, and ready message. | Snapshot is empty until ready; getters do not connect. |
|
|
52
|
+
| <code>subscribeState(listener: () => void)</code> | Returns an unsubscribe function. Listener reads the latest snapshot. | No network request. |
|
|
53
|
+
| <code>on(event, listener)</code> | Event name string and <code>(BridgeEvent) => void</code>; returns unsubscribe function. | Unknown event names simply receive no event. |
|
|
54
|
+
| <code>connect(options?: RequestOptions)</code> | Resolves <code>Promise<ReadyMessage></code>; default timeout 15 seconds, clamped to 1–60 seconds. | Rejects with <code>BridgeError</code>; see error codes below. |
|
|
55
|
+
| <code>request(method, payload, options?)</code> | Method is a <code>BridgeMethod</code>; payload and <code>Promise<MethodResponse<M>></code> are inferred from the method map. | Rejects for invalid payload, missing capability, consent/permission denial, expiry, timeout, cancellation, host error, message/pending limits, or unavailable host. |
|
|
56
|
+
| <code>getInfo(options?)</code> | <code>Promise<{platform: 'android' \| 'ios'; runtimeVersion: string; protocolVersion: 1}></code>. | Requires <code>runtime.getInfo@1</code>. |
|
|
57
|
+
| <code>openExternal(url, options?)</code> | URL string; <code>Promise<{opened: true}></code>. | Requires an approved <code>navigation.openExternal@1</code> capability and an approved URL origin. |
|
|
58
|
+
| <code>closeMiniApp(options?)</code> | <code>Promise<{closed: true}></code>; asks the native shell to close this Mini App. | Requires <code>navigation.close@1</code>. This is distinct from local <code>close()</code>. |
|
|
59
|
+
| <code>share(payload, options?)</code> | Optional <code>title</code>, <code>text</code>, <code>url</code>; resolves <code>{presented: true}</code>. | Requires <code>share.open@1</code>; user dismissing the native sheet is not a bridge error. |
|
|
60
|
+
| <code>pickFiles(payload?, options?)</code> | Optional <code>mimeTypes: string[]</code>, <code>multiple: boolean</code>; resolves file handle metadata. | Requires <code>file.pick@1</code>; currently not advertised by native. Use browser <code>pickDeviceFiles</code> for actual file content. |
|
|
61
|
+
| <code>capturePhoto(options?)</code> | Resolves <code>{file: {handle, name, mimeType, sizeBytes, expiresAtMs}}</code>. | Requires <code>camera.capture@1</code>; currently not advertised by native. Use browser <code>captureDevicePhoto</code> for a <code>File</code>. |
|
|
62
|
+
| <code>getAssertion(options?)</code> | Resolves <code>{assertion, expiresAtMs}</code> with a short-lived customer identity assertion. | Requires <code>identity.getAssertion@1</code> and user consent. It does not prove business ownership or staff authority. |
|
|
63
|
+
| <code>getCurrentLocation(accuracy, options?)</code> | Accuracy is <code>'approximate' \| 'precise'</code>; resolves latitude, longitude, accuracy in meters, and capture time. | Requires <code>location.getCurrent@1</code>, approval, and platform permission. Device validation remains a release gate. |
|
|
64
|
+
| <code>subscribeNotifications(options?)</code> | Resolves <code>{subscribed: true}</code>. | Typed but not advertised by the current native host. App-scoped subscription and delivery are not implemented. |
|
|
65
|
+
| <code>sendNotification(payload, options?)</code> | Required title/body and optional path; resolves <code>{queued: true}</code>. | Typed but not advertised by the current native host; do not treat it as a push service. |
|
|
66
|
+
| <code>close()</code> | No parameters; returns <code>void</code>. | Idempotently stops listeners, clears state, and rejects pending work with <code>CANCELLED</code>. It does not log the user out of the Mini App backend. |
|
|
25
67
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
68
|
+
Bridge method request and response contracts are:
|
|
69
|
+
|
|
70
|
+
| Method | Request | Response |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| <code>runtime.getInfo</code> | <code>{}</code> | platform, runtimeVersion, protocolVersion |
|
|
73
|
+
| <code>navigation.openExternal</code> | <code>{url: string}</code> | <code>{opened: true}</code> |
|
|
74
|
+
| <code>navigation.close</code> | <code>{}</code> | <code>{closed: true}</code> |
|
|
75
|
+
| <code>share.open</code> | optional title, text, url | <code>{presented: true}</code> |
|
|
76
|
+
| <code>identity.getAssertion</code> | <code>{}</code> | assertion and expiry in epoch milliseconds |
|
|
77
|
+
| <code>location.getCurrent</code> | accuracy: approximate or precise | latitude, longitude, accuracyMeters, capturedAtMs |
|
|
78
|
+
| <code>file.pick</code> | optional mimeTypes and multiple | file handle metadata array |
|
|
79
|
+
| <code>camera.capture</code> | <code>{}</code> | one file handle and metadata |
|
|
80
|
+
| <code>notifications.subscribe</code> | <code>{}</code> | <code>{subscribed: true}</code> |
|
|
81
|
+
| <code>notifications.send</code> | title, body, optional path | <code>{queued: true}</code> |
|
|
82
|
+
|
|
83
|
+
An SDK method being typed does not mean a host exposes it. Runtime info, external navigation, close, share, and identity have Android/iOS handlers but still depend on release approval and, for identity, consent. Location is implemented but its device validation is pending. File/camera and notifications are not advertised. Payments are not part of this SDK.
|
|
84
|
+
|
|
85
|
+
### Bridge types and error behavior
|
|
86
|
+
|
|
87
|
+
The public type exports are <code>BridgeCapability</code> ({name, version}), <code>BridgeLimits</code> ({maxMessageBytes, maxPendingRequests, defaultTimeoutMs}), <code>ReadyMessage</code> ({protocolVersion, type, launchId, appId, releaseId, runtimeVersion, selectedProtocolVersion, capabilities, limits, serverTimeMs, sessionExpiresAtMs}), <code>BridgeEvent</code> ({protocolVersion, type, launchId, appId, event, sequence, payload}), <code>BridgeSnapshot</code> ({state, isAvailable, isMock, capabilities, ready}), <code>BridgeState</code> (unavailable, idle, connecting, ready, expired, closed), <code>BridgeTransport</code>, <code>BridgeMethod</code>, <code>BridgeMethodMap</code>, <code>MethodRequest<M></code>, <code>MethodResponse<M></code>, and <code>RequestOptions</code> ({signal?, timeoutMs?}). They describe the v1 handshake, events, launch-scoped state, text transport, generated method payloads/results, and cancellation/timeout options. A custom transport implements <code>kind: 'native' | 'mock'</code>, <code>postMessage(serializedMessage)</code>, and <code>subscribe(listener)</code>; application code should normally use the default transport.
|
|
88
|
+
|
|
89
|
+
<code>BridgeError</code> extends <code>Error</code> and exposes <code>code: BridgeErrorCode</code> and optional structured <code>details</code>. The stable codes are <code>UNAVAILABLE</code>, <code>UNSUPPORTED_PLATFORM</code>, <code>NOT_DECLARED</code>, <code>NOT_APPROVED</code>, <code>CONSENT_REQUIRED</code>, <code>DENIED</code>, <code>CANCELLED</code>, <code>TIMEOUT</code>, <code>INVALID_ARGUMENT</code>, <code>RATE_LIMITED</code>, <code>SESSION_EXPIRED</code>, and <code>INTERNAL</code>. Check the code, not message text. Never auto-retry a denied request. A cancelled request may already have reached the native host.
|
|
90
|
+
|
|
91
|
+
### Browser media exports
|
|
92
|
+
|
|
93
|
+
| Export | Parameters and return value | Errors and behavior |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| <code>CaptureFacingMode</code> | Type union <code>'user' \| 'environment'</code>. | Browser and OS may ignore the camera preference. |
|
|
96
|
+
| <code>DeviceFilePickerOptions</code> | Optional <code>accept</code> (browser input accept string), <code>multiple</code>, and <code>capture</code>. | These are HTML input hints, not bridge capabilities or authorization. |
|
|
97
|
+
| <code>pickDeviceFiles(options?)</code> | Returns <code>Promise<File[]></code>; call directly from a click/tap handler. | Cancellation resolves <code>[]</code>; unavailable document/input failure rejects. |
|
|
98
|
+
| <code>captureDevicePhoto({facingMode?})</code> | Returns <code>Promise<File \| null></code>. | Cancellation resolves null; capture is best effort and a chooser may appear. |
|
|
99
|
+
|
|
100
|
+
Files are normal browser <code>File</code> objects. The Mini App backend still validates size, type, authentication, and authorization.
|
|
101
|
+
|
|
102
|
+
## Order comments
|
|
103
|
+
|
|
104
|
+
Comments use PocketBoss REST APIs, independently of the native bridge. The Mini App backend authenticates its owner/staff user, checks active membership and order access, then obtains a short-lived order-scoped session from PocketBoss. The browser receives only that scoped token. Never put a long-lived integration credential in JavaScript or a widget option.
|
|
105
|
+
|
|
106
|
+
The widget's <code>orderId</code> is the canonical PocketBoss order ULID returned after server-side order synchronization, not the Mini App's external order ID. The backend maps the external ID to that ULID before creating a session. <code>workspaceKey</code> is a UI/context key; it is not proof of a business or staff identity.
|
|
107
|
+
|
|
108
|
+
~~~ts
|
|
109
|
+
import { registerOrderCommentsElement } from '@pocketboss/core/comments';
|
|
110
|
+
|
|
111
|
+
const sessions = {
|
|
112
|
+
async create() {
|
|
113
|
+
const response = await fetch('/api/business/order-comments/session', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
credentials: 'same-origin',
|
|
116
|
+
cache: 'no-store',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
'Idempotency-Key': crypto.randomUUID(),
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ externalOrderId: 'shop-order-2048' }),
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) throw new Error('Could not authorize comments.');
|
|
124
|
+
return response.json();
|
|
125
|
+
},
|
|
126
|
+
async revoke(sessionId: string) {
|
|
127
|
+
const response = await fetch('/api/business/order-comments/session', {
|
|
128
|
+
method: 'DELETE',
|
|
129
|
+
credentials: 'same-origin',
|
|
130
|
+
cache: 'no-store',
|
|
131
|
+
headers: { 'Content-Type': 'application/json' },
|
|
132
|
+
body: JSON.stringify({ sessionId }),
|
|
133
|
+
});
|
|
134
|
+
if (!response.ok) throw new Error('Could not revoke comments session.');
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
registerOrderCommentsElement();
|
|
139
|
+
const element = document.querySelector('pb-order-comments')!;
|
|
140
|
+
element.options = {
|
|
141
|
+
apiBaseUrl: 'https://api.example.test/api/v1',
|
|
142
|
+
orderId: pocketBossOrderId,
|
|
143
|
+
workspaceKey: businessId,
|
|
144
|
+
sessions,
|
|
145
|
+
placement: 'inline',
|
|
146
|
+
renderMessage(message) {
|
|
147
|
+
const node = document.createElement('article');
|
|
148
|
+
node.textContent = message.text;
|
|
149
|
+
return node;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
~~~
|
|
153
|
+
|
|
154
|
+
The route names above are the sample Mini App backend's example, not SDK-managed routes. Adapt the callback to your authenticated same-origin server and its actual route.
|
|
155
|
+
|
|
156
|
+
### Comments client exports and results
|
|
157
|
+
|
|
158
|
+
| Export | Parameters and return value | Errors and behavior |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| <code>OrderCommentsSession</code> | {sessionId, token, expiresAt, scopes?}; expiry is an ISO date string. | Client rejects missing/invalid ID, non-<code>pbcs_</code> token, or expired value. |
|
|
161
|
+
| <code>OrderCommentsSessionProvider</code> | <code>create(): Promise<OrderCommentsSession></code>; <code>revoke(sessionId: string): Promise<void></code>. | Create failure becomes <code>COMMENTS_SESSION_EXPIRED</code>; revoke failure is surfaced during close. |
|
|
162
|
+
| <code>OrderCommentsClientOptions</code> | Required <code>apiBaseUrl</code>, PocketBoss <code>orderId</code>, <code>sessions</code>; optional Fetch-compatible <code>fetcher</code> for tests. | Constructor rejects malformed URL or non-ULID order ID. |
|
|
163
|
+
| <code>OrderCommentMessage</code> | messageId, conversationId, orderId, participantSide customer/business, text, deliveryState sent, createdAt ISO string. | Malformed server response rejects with <code>OrderCommentsError</code>. |
|
|
164
|
+
| <code>OrderCommentPage</code> | messages, nextCursor or null, unreadCount. | Malformed page rejects. |
|
|
165
|
+
| <code>OrderCommentsError</code> | Extends <code>Error</code>; exposes <code>status: number \| null</code> and <code>code: string \| null</code>. | Network, HTTP, JSON shape, validation and session failures use this type. |
|
|
166
|
+
| <code>OrderCommentsClient(options)</code> | Owns one order-scoped REST session; lazy session creation; <code>active</code> is a boolean getter. | Does not grant access; backend and PocketBoss authorize every session/request. |
|
|
167
|
+
| <code>messages(cursor?, limit = 50)</code> | Returns <code>Promise<OrderCommentPage></code>; limit is clamped to 1–50. | HTTP and invalid response errors reject. |
|
|
168
|
+
| <code>send(text, clientMessageId?)</code> | Trims text, 1–5000 characters; optional UUID idempotency key; returns <code>Promise<OrderCommentMessage></code>. | Invalid text/key rejects 422; default key requires secure browser randomness. |
|
|
169
|
+
| <code>markRead(messageId)</code> | PocketBoss message ULID; returns <code>Promise<void></code>. | Invalid ID rejects 422; HTTP errors reject. |
|
|
170
|
+
| <code>close()</code> | Returns <code>Promise<void></code>; aborts requests and invokes session revoke. | Rejects with <code>COMMENTS_SESSION_CLOSE_FAILED</code> when a session cannot be revoked. Treat a failed close as protected content still blocked. |
|
|
171
|
+
|
|
172
|
+
On HTTP 401 or 410 the client requests a replacement session and retries once. Other non-2xx responses preserve server <code>status</code> and error <code>code</code>. Network failure rejects with <code>OrderCommentsError</code>. The bearer is memory-only, requests omit ambient credentials and redirects, and no session is written to browser storage.
|
|
173
|
+
|
|
174
|
+
### Comments custom-element exports
|
|
175
|
+
|
|
176
|
+
| Export | Parameters and return value | Errors and behavior |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| <code>OrderCommentsPlacement</code> | <code>inline</code>, <code>modal</code>, <code>bottom-sheet</code>, <code>full-page</code>, <code>headless</code>. | Modal and bottom sheet open on user action; headless supplies only the client. |
|
|
179
|
+
| <code>OrderCommentsThemeMode</code>, <code>OrderCommentsTheme</code> | mode light/dark/auto; optional accent, surface, text, muted, border, customer/business bubble and text colors, fontFamily, spacing, radius. | Presentation only; no effect on authorization. |
|
|
180
|
+
| <code>OrderCommentsElementOptions</code> | Client options plus required <code>workspaceKey</code>, optional placement/theme/title/language and <code>renderMessage(message): HTMLElement</code>. | Constructor/client validation errors surface through the client/element. Render user text with <code>textContent</code>, never HTML interpolation. |
|
|
181
|
+
| <code>registerOrderCommentsElement(tagName = 'pb-order-comments')</code> | Returns <code>void</code>; idempotent and safe during SSR. | No-op when custom elements are unavailable. |
|
|
182
|
+
| <code>mountOrderCommentsElement(element, options)</code> | Mounts typed options and returns <code>PocketBossOrderCommentsElement</code>. | Client/session errors surface from load/open methods. |
|
|
183
|
+
| <code>PocketBossOrderCommentsElement</code> | <code>options</code> setter/getter; <code>commentsClient</code> getter; <code>loadComments(): Promise<void></code>; <code>openCommentsClient(): Promise<OrderCommentsClient></code>. | Reading options before setting them throws; load/open reject for session/API failures. |
|
|
184
|
+
|
|
185
|
+
### Mock exports
|
|
186
|
+
|
|
187
|
+
Import <code>MockBridgeTransport</code>, <code>createMockTransport</code>, <code>MockHandlers</code>, and <code>MockHostOptions</code> from <code>@pocketboss/core/mock</code>. Provide a v1 <code>ready</code> message and typed method handlers. <code>createMockTransport(options)</code> returns a transport with <code>kind: 'mock'</code>; it always reports <code>isMock</code> true and native <code>isAvailable</code> false. Missing handlers reject <code>NOT_APPROVED</code>; unrecognized handler failures map to <code>INTERNAL</code>. Use only in local development and tests.
|
|
188
|
+
|
|
189
|
+
## Authentication, access modes, orders, and security
|
|
190
|
+
|
|
191
|
+
The SDK does not sign users in, select a business workspace, register a business, bind staff identities, synchronize orders, or verify server assertions. Keep these responsibilities in the Mini App backend and PocketBoss APIs:
|
|
192
|
+
|
|
193
|
+
| Access mode | Backend responsibility |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| Guest | Permit only public guest behavior; guest SSO session lookup may return 401. |
|
|
196
|
+
| Personal | Read the same-origin PocketBoss SSO session and use the app-pairwise subject. |
|
|
197
|
+
| Business | Validate the selected workspace and current PocketBoss roles for each protected action. |
|
|
198
|
+
| Mixed | Authorize each route/action from the backend session; never infer a mode from a query string or browser-supplied business ID. |
|
|
199
|
+
|
|
200
|
+
Customer <code>identity.getAssertion</code> is an optional, consented bridge capability. Send the unmodified assertion to your same-origin backend for PocketBoss verification; do not decode it and trust its claims. It does not establish business ownership or staff authority. Business registration needs verified ownership, and staff access needs a verified invitation/identity binding. Order writes use a signed server-to-server integration, an app-scoped customer reference, source versions, idempotency, and a transactional outbox. Keep the PocketBoss order ULID for comments.
|
|
201
|
+
|
|
202
|
+
Call the Mini App backend's logout endpoint and await server revocation when ending or switching an authenticated workspace, then clear the workspace WebView storage according to the host contract. <code>client.close()</code> releases only this SDK client; it does not clear cookies, backend sessions, or native WebView state.
|
|
203
|
+
|
|
204
|
+
The integration private key, signing material, and long-lived PocketBoss credential stay on the server. Use HTTPS, least-privilege scopes, nonce/timestamp replay protection, idempotency, rotation, audit logging, and current membership checks. See [Authentication](https://pocket-boss.beno.co.tz/developer/identity/authentication), [business registration](https://pocket-boss.beno.co.tz/developer/business/registration), [server integration](https://pocket-boss.beno.co.tz/developer/integration/server-api), [orders](https://pocket-boss.beno.co.tz/developer/orders/synchronization), and [comments](https://pocket-boss.beno.co.tz/developer/messaging/order-comments).
|
|
205
|
+
|
|
206
|
+
## Build, test, and package contents
|
|
32
207
|
|
|
33
|
-
|
|
208
|
+
From the repository root:
|
|
34
209
|
|
|
35
|
-
|
|
210
|
+
~~~sh
|
|
211
|
+
npm run build:core
|
|
212
|
+
npm test -w @pocketboss/core
|
|
213
|
+
(cd packages/sdk-core && npm pack --dry-run --json)
|
|
214
|
+
~~~
|
|
36
215
|
|
|
37
|
-
|
|
216
|
+
The package whitelist is <code>dist</code> plus this README; npm adds package metadata. Inspect the dry-run file list after building. It does not publish. The npm manifest currently has no approved license metadata or license file, so this archive is not publication-ready; resolve the project license decision before a release.
|
|
38
217
|
|
|
39
|
-
|
|
218
|
+
## Developer Portal
|
|
40
219
|
|
|
41
|
-
|
|
220
|
+
Continue with the [Vanilla JavaScript guide](https://pocket-boss.beno.co.tz/developer/sdk/vanilla), [bridge reference](https://pocket-boss.beno.co.tz/developer/bridge/capabilities), [quickstart](https://pocket-boss.beno.co.tz/developer/getting-started/quickstart), and the identity, business, orders, and comments guides linked above.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { OrderCommentsClient } from './comments.js';
|
|
2
|
+
import type { OrderCommentMessage, OrderCommentsClientOptions } from './comments.js';
|
|
3
|
+
export type OrderCommentsPlacement = 'inline' | 'modal' | 'bottom-sheet' | 'full-page' | 'headless';
|
|
4
|
+
export type OrderCommentsThemeMode = 'light' | 'dark' | 'auto';
|
|
5
|
+
export interface OrderCommentsTheme {
|
|
6
|
+
mode?: OrderCommentsThemeMode;
|
|
7
|
+
accent?: string;
|
|
8
|
+
surface?: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
muted?: string;
|
|
11
|
+
border?: string;
|
|
12
|
+
customerBubble?: string;
|
|
13
|
+
customerText?: string;
|
|
14
|
+
businessBubble?: string;
|
|
15
|
+
businessText?: string;
|
|
16
|
+
fontFamily?: string;
|
|
17
|
+
spacing?: number;
|
|
18
|
+
radius?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface OrderCommentsElementOptions extends OrderCommentsClientOptions {
|
|
21
|
+
workspaceKey: string;
|
|
22
|
+
placement?: OrderCommentsPlacement;
|
|
23
|
+
theme?: OrderCommentsTheme;
|
|
24
|
+
title?: string;
|
|
25
|
+
language?: string;
|
|
26
|
+
/** Return a DOM node for a message. Use textContent for untrusted message text. */
|
|
27
|
+
renderMessage?: (message: OrderCommentMessage) => HTMLElement;
|
|
28
|
+
}
|
|
29
|
+
export interface PocketBossOrderCommentsElement extends HTMLElement {
|
|
30
|
+
options: OrderCommentsElementOptions;
|
|
31
|
+
readonly commentsClient: OrderCommentsClient | null;
|
|
32
|
+
loadComments(): Promise<void>;
|
|
33
|
+
openCommentsClient(): Promise<OrderCommentsClient>;
|
|
34
|
+
}
|
|
35
|
+
/** Register the framework-neutral comments element. Safe to call during SSR. */
|
|
36
|
+
export declare function registerOrderCommentsElement(tagName?: string): void;
|
|
37
|
+
export declare function mountOrderCommentsElement(element: HTMLElement, options: OrderCommentsElementOptions): PocketBossOrderCommentsElement;
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { OrderCommentsClient, OrderCommentsError, } from './comments.js';
|
|
2
|
+
const defaultTagName = 'pb-order-comments';
|
|
3
|
+
let commentsCloseBarrier = Promise.resolve();
|
|
4
|
+
/** Register the framework-neutral comments element. Safe to call during SSR. */
|
|
5
|
+
export function registerOrderCommentsElement(tagName = defaultTagName) {
|
|
6
|
+
if (typeof window === 'undefined' || !('customElements' in window))
|
|
7
|
+
return;
|
|
8
|
+
if (customElements.get(tagName))
|
|
9
|
+
return;
|
|
10
|
+
class PocketBossOrderComments extends HTMLElement {
|
|
11
|
+
root;
|
|
12
|
+
currentOptions = null;
|
|
13
|
+
currentClient = null;
|
|
14
|
+
pendingClose = Promise.resolve();
|
|
15
|
+
messages = [];
|
|
16
|
+
nextCursor = null;
|
|
17
|
+
unreadCount = 0;
|
|
18
|
+
loading = false;
|
|
19
|
+
sending = false;
|
|
20
|
+
error = null;
|
|
21
|
+
expired = false;
|
|
22
|
+
opened = false;
|
|
23
|
+
draft = '';
|
|
24
|
+
pendingText = null;
|
|
25
|
+
pendingId = null;
|
|
26
|
+
loadSequence = 0;
|
|
27
|
+
lastReadMessageId = null;
|
|
28
|
+
constructor() {
|
|
29
|
+
super();
|
|
30
|
+
this.root = this.attachShadow({ mode: 'open' });
|
|
31
|
+
}
|
|
32
|
+
get options() {
|
|
33
|
+
if (!this.currentOptions)
|
|
34
|
+
throw new Error('Order comments options have not been set.');
|
|
35
|
+
return this.currentOptions;
|
|
36
|
+
}
|
|
37
|
+
set options(value) {
|
|
38
|
+
const old = this.currentOptions;
|
|
39
|
+
this.currentOptions = value;
|
|
40
|
+
if (old && contextKey(old) !== contextKey(value)) {
|
|
41
|
+
this.closeCurrentClient();
|
|
42
|
+
this.resetConversation();
|
|
43
|
+
}
|
|
44
|
+
this.applyTheme(value.theme ?? {});
|
|
45
|
+
this.render();
|
|
46
|
+
if (this.isConnected && isInitiallyOpen(value.placement) && !old)
|
|
47
|
+
void this.load();
|
|
48
|
+
if (this.isConnected && isInitiallyOpen(value.placement) && old && contextKey(old) !== contextKey(value))
|
|
49
|
+
void this.load();
|
|
50
|
+
}
|
|
51
|
+
get commentsClient() {
|
|
52
|
+
return this.currentClient;
|
|
53
|
+
}
|
|
54
|
+
async loadComments() {
|
|
55
|
+
if (this.currentOptions?.placement === 'headless') {
|
|
56
|
+
await this.ensureClient();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (this.currentOptions?.placement === 'modal' || this.currentOptions?.placement === 'bottom-sheet')
|
|
60
|
+
return;
|
|
61
|
+
await this.load();
|
|
62
|
+
}
|
|
63
|
+
openCommentsClient() {
|
|
64
|
+
return this.ensureClient();
|
|
65
|
+
}
|
|
66
|
+
connectedCallback() {
|
|
67
|
+
this.render();
|
|
68
|
+
if (this.currentOptions && isInitiallyOpen(this.currentOptions.placement))
|
|
69
|
+
void this.load();
|
|
70
|
+
}
|
|
71
|
+
disconnectedCallback() {
|
|
72
|
+
this.closeCurrentClient();
|
|
73
|
+
}
|
|
74
|
+
async ensureClient() {
|
|
75
|
+
await commentsCloseBarrier;
|
|
76
|
+
await this.pendingClose;
|
|
77
|
+
if (!this.currentOptions)
|
|
78
|
+
throw new OrderCommentsError('Order comments options have not been set.');
|
|
79
|
+
if (!this.currentClient || !this.currentClient.active) {
|
|
80
|
+
this.currentClient = new OrderCommentsClient(this.currentOptions);
|
|
81
|
+
}
|
|
82
|
+
return this.currentClient;
|
|
83
|
+
}
|
|
84
|
+
closeCurrentClient() {
|
|
85
|
+
const client = this.currentClient;
|
|
86
|
+
this.currentClient = null;
|
|
87
|
+
if (client) {
|
|
88
|
+
this.pendingClose = client.close();
|
|
89
|
+
commentsCloseBarrier = Promise.all([commentsCloseBarrier, this.pendingClose]).then(() => undefined);
|
|
90
|
+
void commentsCloseBarrier.catch(() => { });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
resetConversation() {
|
|
94
|
+
this.messages = [];
|
|
95
|
+
this.nextCursor = null;
|
|
96
|
+
this.unreadCount = 0;
|
|
97
|
+
this.loading = false;
|
|
98
|
+
this.error = null;
|
|
99
|
+
this.expired = false;
|
|
100
|
+
this.pendingId = null;
|
|
101
|
+
this.pendingText = null;
|
|
102
|
+
this.lastReadMessageId = null;
|
|
103
|
+
this.loadSequence++;
|
|
104
|
+
}
|
|
105
|
+
async load(cursor) {
|
|
106
|
+
if (!this.currentOptions || this.loading)
|
|
107
|
+
return;
|
|
108
|
+
const sequence = ++this.loadSequence;
|
|
109
|
+
this.loading = true;
|
|
110
|
+
this.error = null;
|
|
111
|
+
this.expired = false;
|
|
112
|
+
this.render();
|
|
113
|
+
try {
|
|
114
|
+
const page = await (await this.ensureClient()).messages(cursor);
|
|
115
|
+
if (sequence !== this.loadSequence || !this.isConnected)
|
|
116
|
+
return;
|
|
117
|
+
const old = new Set(this.messages.map((message) => message.messageId));
|
|
118
|
+
this.messages = cursor
|
|
119
|
+
? [...this.messages, ...page.messages.filter((message) => !old.has(message.messageId))]
|
|
120
|
+
: page.messages;
|
|
121
|
+
this.nextCursor = page.nextCursor;
|
|
122
|
+
this.unreadCount = page.unreadCount;
|
|
123
|
+
this.markLatestBusinessMessageRead();
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (sequence !== this.loadSequence)
|
|
127
|
+
return;
|
|
128
|
+
this.showError(error);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (sequence === this.loadSequence) {
|
|
132
|
+
this.loading = false;
|
|
133
|
+
this.render();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async send() {
|
|
138
|
+
const text = this.draft.trim();
|
|
139
|
+
if (!text || text.length > 5000 || this.sending)
|
|
140
|
+
return;
|
|
141
|
+
if (this.pendingText !== text || !this.pendingId) {
|
|
142
|
+
this.pendingText = text;
|
|
143
|
+
this.pendingId = secureUuid();
|
|
144
|
+
}
|
|
145
|
+
this.sending = true;
|
|
146
|
+
this.error = null;
|
|
147
|
+
this.render();
|
|
148
|
+
try {
|
|
149
|
+
const message = await (await this.ensureClient()).send(text, this.pendingId);
|
|
150
|
+
if (!this.isConnected)
|
|
151
|
+
return;
|
|
152
|
+
if (!this.messages.some((existing) => existing.messageId === message.messageId))
|
|
153
|
+
this.messages.push(message);
|
|
154
|
+
this.draft = '';
|
|
155
|
+
this.pendingId = null;
|
|
156
|
+
this.pendingText = null;
|
|
157
|
+
this.unreadCount = 0;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
this.showError(error);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
this.sending = false;
|
|
164
|
+
this.render();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
markLatestBusinessMessageRead() {
|
|
168
|
+
const last = [...this.messages].reverse().find((message) => message.participantSide === 'business');
|
|
169
|
+
if (!last || last.messageId === this.lastReadMessageId || !this.currentClient)
|
|
170
|
+
return;
|
|
171
|
+
this.lastReadMessageId = last.messageId;
|
|
172
|
+
void this.currentClient.markRead(last.messageId).catch(() => {
|
|
173
|
+
if (this.lastReadMessageId === last.messageId)
|
|
174
|
+
this.lastReadMessageId = null;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
showError(value) {
|
|
178
|
+
const error = value instanceof OrderCommentsError ? value : null;
|
|
179
|
+
this.expired = error?.status === 401 || error?.status === 410 || error?.code === 'COMMENTS_SESSION_EXPIRED';
|
|
180
|
+
this.error = this.expired
|
|
181
|
+
? 'Comments access expired. Reconnect to renew this secure session.'
|
|
182
|
+
: error?.status === 403 || error?.status === 404
|
|
183
|
+
? 'This order is not available to the current business workspace.'
|
|
184
|
+
: error?.message ?? 'Order comments are unavailable. Retry in a moment.';
|
|
185
|
+
}
|
|
186
|
+
applyTheme(theme) {
|
|
187
|
+
const colors = [
|
|
188
|
+
['accent', theme.accent],
|
|
189
|
+
['surface', theme.surface],
|
|
190
|
+
['text', theme.text],
|
|
191
|
+
['muted', theme.muted],
|
|
192
|
+
['border', theme.border],
|
|
193
|
+
['customer', theme.customerBubble],
|
|
194
|
+
['customer-text', theme.customerText],
|
|
195
|
+
['business', theme.businessBubble],
|
|
196
|
+
['business-text', theme.businessText],
|
|
197
|
+
];
|
|
198
|
+
for (const [name, value] of colors) {
|
|
199
|
+
const safe = safeColor(value);
|
|
200
|
+
if (safe)
|
|
201
|
+
this.style.setProperty(`--pb-comments-${name}`, safe);
|
|
202
|
+
else
|
|
203
|
+
this.style.removeProperty(`--pb-comments-${name}`);
|
|
204
|
+
}
|
|
205
|
+
const font = safeFont(theme.fontFamily);
|
|
206
|
+
if (font)
|
|
207
|
+
this.style.setProperty('--pb-comments-font', font);
|
|
208
|
+
else
|
|
209
|
+
this.style.removeProperty('--pb-comments-font');
|
|
210
|
+
this.style.setProperty('--pb-comments-space', `${safeDimension(theme.spacing, 16, 6, 32)}px`);
|
|
211
|
+
this.style.setProperty('--pb-comments-radius', `${safeDimension(theme.radius, 16, 0, 32)}px`);
|
|
212
|
+
this.root.host.toggleAttribute('data-dark', theme.mode === 'dark' || (theme.mode === 'auto' && prefersDark()));
|
|
213
|
+
}
|
|
214
|
+
render() {
|
|
215
|
+
const options = this.currentOptions;
|
|
216
|
+
if (!options || options.placement === 'headless') {
|
|
217
|
+
this.root.replaceChildren();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const placement = options.placement ?? 'inline';
|
|
221
|
+
const shell = document.createElement('div');
|
|
222
|
+
shell.className = `pb-shell pb-${placement}`;
|
|
223
|
+
if (placement === 'modal' || placement === 'bottom-sheet') {
|
|
224
|
+
const trigger = button(options.title ?? 'Order comments', 'open');
|
|
225
|
+
trigger.className = 'pb-trigger';
|
|
226
|
+
shell.append(trigger);
|
|
227
|
+
if (this.opened) {
|
|
228
|
+
const overlay = document.createElement('div');
|
|
229
|
+
overlay.className = `pb-overlay ${placement === 'bottom-sheet' ? 'pb-sheet-overlay' : ''}`;
|
|
230
|
+
overlay.setAttribute('role', 'presentation');
|
|
231
|
+
overlay.addEventListener('click', (event) => {
|
|
232
|
+
if (event.target === overlay)
|
|
233
|
+
this.closePanel();
|
|
234
|
+
});
|
|
235
|
+
const panel = this.makePanel(options.title ?? 'Order comments', true);
|
|
236
|
+
panel.classList.add(placement === 'bottom-sheet' ? 'pb-sheet' : 'pb-modal');
|
|
237
|
+
overlay.append(panel);
|
|
238
|
+
shell.append(overlay);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
shell.append(this.makePanel(options.title ?? 'Order comments', false));
|
|
243
|
+
}
|
|
244
|
+
this.root.replaceChildren(this.styles(), shell);
|
|
245
|
+
const open = this.root.querySelector('[data-action="open"]');
|
|
246
|
+
open?.addEventListener('click', () => {
|
|
247
|
+
this.opened = true;
|
|
248
|
+
this.render();
|
|
249
|
+
void this.load();
|
|
250
|
+
});
|
|
251
|
+
this.root.querySelector('[data-action="close"]')?.addEventListener('click', () => this.closePanel());
|
|
252
|
+
this.root.querySelector('[data-action="refresh"]')?.addEventListener('click', () => void this.load());
|
|
253
|
+
this.root.querySelector('[data-action="retry"]')?.addEventListener('click', () => void this.load());
|
|
254
|
+
this.root.querySelector('[data-action="more"]')?.addEventListener('click', () => {
|
|
255
|
+
if (this.nextCursor)
|
|
256
|
+
void this.load(this.nextCursor);
|
|
257
|
+
});
|
|
258
|
+
const textarea = this.root.querySelector('textarea');
|
|
259
|
+
if (textarea) {
|
|
260
|
+
textarea.value = this.draft;
|
|
261
|
+
textarea.addEventListener('input', () => {
|
|
262
|
+
this.draft = textarea.value;
|
|
263
|
+
const send = this.root.querySelector('[data-action="send"]');
|
|
264
|
+
if (send)
|
|
265
|
+
send.disabled = this.sending || !this.draft.trim();
|
|
266
|
+
if (this.pendingText !== this.draft.trim()) {
|
|
267
|
+
this.pendingText = null;
|
|
268
|
+
this.pendingId = null;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
textarea.addEventListener('keydown', (event) => {
|
|
272
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
void this.send();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
this.root.querySelector('[data-action="send"]')?.addEventListener('click', () => void this.send());
|
|
279
|
+
if (!this.currentOptions?.renderMessage) {
|
|
280
|
+
this.root.querySelectorAll('[data-message]').forEach((node) => {
|
|
281
|
+
const message = this.messages.find((item) => item.messageId === node.dataset.message);
|
|
282
|
+
if (message)
|
|
283
|
+
node.textContent = message.text;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const list = this.root.querySelector('.pb-messages');
|
|
287
|
+
if (list && this.messages.length && !this.loading)
|
|
288
|
+
list.scrollTop = list.scrollHeight;
|
|
289
|
+
}
|
|
290
|
+
closePanel() {
|
|
291
|
+
this.opened = false;
|
|
292
|
+
this.closeCurrentClient();
|
|
293
|
+
this.resetConversation();
|
|
294
|
+
this.render();
|
|
295
|
+
}
|
|
296
|
+
makePanel(title, closable) {
|
|
297
|
+
const panel = document.createElement('section');
|
|
298
|
+
panel.className = 'pb-panel';
|
|
299
|
+
if (closable)
|
|
300
|
+
panel.setAttribute('role', 'dialog');
|
|
301
|
+
panel.setAttribute('aria-label', title);
|
|
302
|
+
const header = document.createElement('header');
|
|
303
|
+
const heading = document.createElement('strong');
|
|
304
|
+
heading.textContent = title;
|
|
305
|
+
const headerActions = document.createElement('div');
|
|
306
|
+
if (this.unreadCount) {
|
|
307
|
+
const unread = document.createElement('span');
|
|
308
|
+
unread.className = 'pb-unread';
|
|
309
|
+
unread.textContent = `${this.unreadCount} unread`;
|
|
310
|
+
headerActions.append(unread);
|
|
311
|
+
}
|
|
312
|
+
const refresh = button('Refresh', 'refresh');
|
|
313
|
+
refresh.className = 'pb-icon-button';
|
|
314
|
+
refresh.setAttribute('aria-label', 'Refresh comments');
|
|
315
|
+
headerActions.append(refresh);
|
|
316
|
+
if (closable) {
|
|
317
|
+
const close = button('×', 'close');
|
|
318
|
+
close.className = 'pb-icon-button';
|
|
319
|
+
close.setAttribute('aria-label', 'Close comments');
|
|
320
|
+
headerActions.append(close);
|
|
321
|
+
}
|
|
322
|
+
header.append(heading, headerActions);
|
|
323
|
+
panel.append(header);
|
|
324
|
+
if (this.loading && !this.messages.length) {
|
|
325
|
+
const status = document.createElement('div');
|
|
326
|
+
status.className = 'pb-state';
|
|
327
|
+
status.setAttribute('role', 'status');
|
|
328
|
+
status.textContent = 'Loading comments…';
|
|
329
|
+
panel.append(status);
|
|
330
|
+
}
|
|
331
|
+
else if (this.error) {
|
|
332
|
+
const error = document.createElement('div');
|
|
333
|
+
error.className = 'pb-error';
|
|
334
|
+
error.setAttribute('role', 'alert');
|
|
335
|
+
const copy = document.createElement('p');
|
|
336
|
+
copy.textContent = this.error;
|
|
337
|
+
const retry = button(this.expired ? 'Reconnect' : 'Retry', 'retry');
|
|
338
|
+
error.append(copy, retry);
|
|
339
|
+
panel.append(error);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const list = document.createElement('div');
|
|
343
|
+
list.className = 'pb-messages';
|
|
344
|
+
list.setAttribute('aria-live', 'polite');
|
|
345
|
+
list.setAttribute('aria-busy', String(this.loading));
|
|
346
|
+
if (this.nextCursor) {
|
|
347
|
+
const more = button('Load newer comments', 'more');
|
|
348
|
+
more.className = 'pb-more';
|
|
349
|
+
list.append(more);
|
|
350
|
+
}
|
|
351
|
+
if (!this.messages.length) {
|
|
352
|
+
const empty = document.createElement('p');
|
|
353
|
+
empty.className = 'pb-empty';
|
|
354
|
+
empty.textContent = 'No comments yet. Send a message to the business.';
|
|
355
|
+
list.append(empty);
|
|
356
|
+
}
|
|
357
|
+
for (const message of this.messages) {
|
|
358
|
+
const renderMessage = this.currentOptions?.renderMessage;
|
|
359
|
+
if (renderMessage) {
|
|
360
|
+
try {
|
|
361
|
+
const rendered = renderMessage(message);
|
|
362
|
+
if (rendered && rendered.nodeType === 1) {
|
|
363
|
+
rendered.dataset.message ??= message.messageId;
|
|
364
|
+
list.append(rendered);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// A consumer renderer must not take down the comments conversation.
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const bubble = document.createElement('article');
|
|
373
|
+
bubble.className = `pb-bubble ${message.participantSide === 'business' ? 'pb-business' : 'pb-customer'}`;
|
|
374
|
+
const who = document.createElement('strong');
|
|
375
|
+
who.textContent = message.participantSide === 'business' ? 'Business' : 'Customer';
|
|
376
|
+
const text = document.createElement('p');
|
|
377
|
+
text.dataset.message = message.messageId;
|
|
378
|
+
text.textContent = message.text;
|
|
379
|
+
const time = document.createElement('time');
|
|
380
|
+
time.dateTime = message.createdAt;
|
|
381
|
+
time.textContent = new Date(message.createdAt).toLocaleString(this.currentOptions?.language);
|
|
382
|
+
bubble.append(who, text, time);
|
|
383
|
+
list.append(bubble);
|
|
384
|
+
}
|
|
385
|
+
panel.append(list);
|
|
386
|
+
const composer = document.createElement('div');
|
|
387
|
+
composer.className = 'pb-composer';
|
|
388
|
+
const textarea = document.createElement('textarea');
|
|
389
|
+
textarea.setAttribute('maxlength', '5000');
|
|
390
|
+
textarea.setAttribute('aria-label', 'Write a message');
|
|
391
|
+
textarea.placeholder = 'Write a message';
|
|
392
|
+
textarea.rows = 2;
|
|
393
|
+
const send = button(this.sending ? 'Sending…' : 'Send', 'send');
|
|
394
|
+
send.className = 'pb-send';
|
|
395
|
+
send.disabled = this.sending || !this.draft.trim();
|
|
396
|
+
composer.append(textarea, send);
|
|
397
|
+
panel.append(composer);
|
|
398
|
+
}
|
|
399
|
+
return panel;
|
|
400
|
+
}
|
|
401
|
+
styles() {
|
|
402
|
+
const style = document.createElement('style');
|
|
403
|
+
style.textContent = `
|
|
404
|
+
:host{display:block;color:var(--pb-comments-text);font:400 14px/1.45 var(--pb-comments-font);--pb-comments-accent:#1b6856;--pb-comments-surface:#fff;--pb-comments-text:#18312b;--pb-comments-muted:#66756f;--pb-comments-border:#dce6e1;--pb-comments-customer:#1b6856;--pb-comments-customer-text:#fff;--pb-comments-business:#eef4f1;--pb-comments-business-text:#18312b;--pb-comments-font:system-ui,sans-serif;--pb-comments-space:16px;--pb-comments-radius:16px}
|
|
405
|
+
*{box-sizing:border-box}button,textarea{font:inherit}button{cursor:pointer}
|
|
406
|
+
.pb-panel{display:flex;flex-direction:column;min-height:360px;max-height:76vh;background:var(--pb-comments-surface);color:var(--pb-comments-text);border:1px solid var(--pb-comments-border);border-radius:var(--pb-comments-radius);overflow:hidden;box-shadow:0 10px 35px #10282018}
|
|
407
|
+
.pb-inline .pb-panel{width:100%}.pb-full-page .pb-panel{height:100%;min-height:70vh;max-height:none}
|
|
408
|
+
header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:var(--pb-comments-space);border-bottom:1px solid var(--pb-comments-border)}
|
|
409
|
+
header strong{font-size:1rem}header div{display:flex;align-items:center;gap:8px}.pb-icon-button{border:0;background:transparent;color:var(--pb-comments-muted);font-size:1.25rem;padding:5px 8px;border-radius:8px}.pb-icon-button:hover{background:var(--pb-comments-business)}
|
|
410
|
+
.pb-unread{color:var(--pb-comments-accent);font-size:.75rem;font-weight:700}
|
|
411
|
+
.pb-messages{display:flex;flex:1;flex-direction:column;gap:10px;overflow:auto;padding:var(--pb-comments-space);min-height:120px}
|
|
412
|
+
.pb-bubble{width:fit-content;max-width:86%;padding:10px 13px;border-radius:14px;overflow-wrap:anywhere}
|
|
413
|
+
.pb-bubble strong{display:block;margin-bottom:4px;font-size:.72rem;opacity:.72}.pb-bubble p{margin:0;white-space:pre-wrap}.pb-bubble time{display:block;margin-top:6px;font-size:.68rem;opacity:.66}
|
|
414
|
+
.pb-business{align-self:flex-start;background:var(--pb-comments-business);color:var(--pb-comments-business-text)}.pb-customer{align-self:flex-end;background:var(--pb-comments-customer);color:var(--pb-comments-customer-text)}
|
|
415
|
+
.pb-composer{display:flex;align-items:flex-end;gap:8px;padding:10px var(--pb-comments-space);border-top:1px solid var(--pb-comments-border)}
|
|
416
|
+
textarea{flex:1;resize:vertical;min-height:44px;max-height:130px;padding:10px 11px;border:1px solid var(--pb-comments-border);border-radius:10px;background:var(--pb-comments-surface);color:var(--pb-comments-text);outline-color:var(--pb-comments-accent)}
|
|
417
|
+
.pb-send,.pb-trigger,.pb-error button{border:0;border-radius:9px;background:var(--pb-comments-accent);color:#fff;font-weight:700;padding:10px 14px}.pb-send:disabled{opacity:.55;cursor:not-allowed}
|
|
418
|
+
.pb-state,.pb-empty,.pb-error{padding:var(--pb-comments-space);color:var(--pb-comments-muted)}.pb-empty{text-align:center;margin:auto}.pb-error{color:var(--pb-comments-text)}.pb-error p{margin:0 0 12px}.pb-error button{padding:8px 12px}
|
|
419
|
+
.pb-more{align-self:center;border:0;background:transparent;color:var(--pb-comments-accent);font-weight:700;padding:6px 10px}
|
|
420
|
+
.pb-overlay{position:fixed;z-index:2147483000;inset:0;display:grid;place-items:center;padding:20px;background:#10201dcc}.pb-modal{width:min(560px,100%)}
|
|
421
|
+
.pb-sheet-overlay{align-items:end;padding:0}.pb-sheet{width:min(720px,100%);max-height:88vh;border-radius:18px 18px 0 0}.pb-trigger{min-height:42px}
|
|
422
|
+
:host([data-dark]){--pb-comments-surface:#17231f;--pb-comments-text:#eef5f1;--pb-comments-muted:#afbbb5;--pb-comments-border:#35453e;--pb-comments-business:#263a32;--pb-comments-business-text:#eef5f1;--pb-comments-customer:#236b58;--pb-comments-customer-text:#fff}
|
|
423
|
+
`;
|
|
424
|
+
return style;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
customElements.define(tagName, PocketBossOrderComments);
|
|
428
|
+
}
|
|
429
|
+
export function mountOrderCommentsElement(element, options) {
|
|
430
|
+
registerOrderCommentsElement();
|
|
431
|
+
const target = element;
|
|
432
|
+
target.options = options;
|
|
433
|
+
return target;
|
|
434
|
+
}
|
|
435
|
+
function button(label, action) {
|
|
436
|
+
const value = document.createElement('button');
|
|
437
|
+
value.type = 'button';
|
|
438
|
+
value.textContent = label;
|
|
439
|
+
value.dataset.action = action;
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
function contextKey(options) {
|
|
443
|
+
return `${options.apiBaseUrl}\u0000${options.workspaceKey}\u0000${options.orderId}`;
|
|
444
|
+
}
|
|
445
|
+
function isInitiallyOpen(placement) {
|
|
446
|
+
return placement !== 'modal' && placement !== 'bottom-sheet' && placement !== 'headless';
|
|
447
|
+
}
|
|
448
|
+
function secureUuid() {
|
|
449
|
+
if (!globalThis.crypto?.randomUUID)
|
|
450
|
+
throw new OrderCommentsError('Secure randomness is unavailable.');
|
|
451
|
+
return globalThis.crypto.randomUUID();
|
|
452
|
+
}
|
|
453
|
+
function safeColor(value) {
|
|
454
|
+
if (!value || value.length > 64)
|
|
455
|
+
return null;
|
|
456
|
+
return /^(#[0-9a-f]{3,8}|rgba?\([0-9., %]+\)|hsla?\([0-9., %deg]+\))$/i.test(value) ? value : null;
|
|
457
|
+
}
|
|
458
|
+
function safeFont(value) {
|
|
459
|
+
if (!value || value.length > 120 || !/^[a-z0-9 ,"'-]+$/i.test(value))
|
|
460
|
+
return null;
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
function safeDimension(value, fallback, min, max) {
|
|
464
|
+
return typeof value === 'number' && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
|
|
465
|
+
}
|
|
466
|
+
function prefersDark() {
|
|
467
|
+
return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches;
|
|
468
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface OrderCommentsSession {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
token: string;
|
|
4
|
+
expiresAt: string;
|
|
5
|
+
scopes?: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export interface OrderCommentMessage {
|
|
8
|
+
messageId: string;
|
|
9
|
+
conversationId: string;
|
|
10
|
+
orderId: string;
|
|
11
|
+
participantSide: 'customer' | 'business';
|
|
12
|
+
text: string;
|
|
13
|
+
deliveryState: 'sent';
|
|
14
|
+
createdAt: string;
|
|
15
|
+
}
|
|
16
|
+
export interface OrderCommentPage {
|
|
17
|
+
messages: OrderCommentMessage[];
|
|
18
|
+
nextCursor: string | null;
|
|
19
|
+
unreadCount: number;
|
|
20
|
+
}
|
|
21
|
+
export interface OrderCommentsSessionProvider {
|
|
22
|
+
create(): Promise<OrderCommentsSession>;
|
|
23
|
+
revoke(sessionId: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface OrderCommentsClientOptions {
|
|
26
|
+
apiBaseUrl: string;
|
|
27
|
+
orderId: string;
|
|
28
|
+
sessions: OrderCommentsSessionProvider;
|
|
29
|
+
fetcher?: typeof fetch;
|
|
30
|
+
}
|
|
31
|
+
export declare class OrderCommentsError extends Error {
|
|
32
|
+
readonly status: number | null;
|
|
33
|
+
readonly code: string | null;
|
|
34
|
+
constructor(message: string, status?: number | null, code?: string | null);
|
|
35
|
+
}
|
|
36
|
+
/** REST comments transport, independent of the native Mini App bridge. */
|
|
37
|
+
export declare class OrderCommentsClient {
|
|
38
|
+
private readonly apiBaseUrl;
|
|
39
|
+
private readonly orderId;
|
|
40
|
+
private readonly sessions;
|
|
41
|
+
private readonly fetcher;
|
|
42
|
+
private session;
|
|
43
|
+
private openingSession;
|
|
44
|
+
private closePromise;
|
|
45
|
+
private pendingRevocationId;
|
|
46
|
+
private readonly activeRequests;
|
|
47
|
+
private closed;
|
|
48
|
+
constructor(options: OrderCommentsClientOptions);
|
|
49
|
+
get active(): boolean;
|
|
50
|
+
messages(cursor?: string, limit?: number): Promise<OrderCommentPage>;
|
|
51
|
+
send(text: string, clientMessageId?: string): Promise<OrderCommentMessage>;
|
|
52
|
+
markRead(messageId: string): Promise<void>;
|
|
53
|
+
close(): Promise<void>;
|
|
54
|
+
private request;
|
|
55
|
+
private getSession;
|
|
56
|
+
private revokeSession;
|
|
57
|
+
}
|
package/dist/comments.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
export class OrderCommentsError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
constructor(message, status = null, code = null) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = 'OrderCommentsError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** REST comments transport, independent of the native Mini App bridge. */
|
|
12
|
+
export class OrderCommentsClient {
|
|
13
|
+
apiBaseUrl;
|
|
14
|
+
orderId;
|
|
15
|
+
sessions;
|
|
16
|
+
fetcher;
|
|
17
|
+
session = null;
|
|
18
|
+
openingSession = null;
|
|
19
|
+
closePromise = null;
|
|
20
|
+
pendingRevocationId = null;
|
|
21
|
+
activeRequests = new Set();
|
|
22
|
+
closed = false;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
const base = options.apiBaseUrl.replace(/\/+$/, '');
|
|
25
|
+
if (!/^https?:\/\//i.test(base) || base.length > 2048) {
|
|
26
|
+
throw new OrderCommentsError('A valid PocketBoss API base URL is required.');
|
|
27
|
+
}
|
|
28
|
+
if (!/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(options.orderId)) {
|
|
29
|
+
throw new OrderCommentsError('A valid PocketBoss order ID is required.');
|
|
30
|
+
}
|
|
31
|
+
this.apiBaseUrl = base;
|
|
32
|
+
this.orderId = options.orderId;
|
|
33
|
+
this.sessions = options.sessions;
|
|
34
|
+
this.fetcher = options.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
35
|
+
}
|
|
36
|
+
get active() {
|
|
37
|
+
return !this.closed;
|
|
38
|
+
}
|
|
39
|
+
async messages(cursor, limit = 50) {
|
|
40
|
+
const query = new URLSearchParams({ limit: String(Math.min(50, Math.max(1, limit))) });
|
|
41
|
+
if (cursor)
|
|
42
|
+
query.set('cursor', cursor);
|
|
43
|
+
const result = await this.request(`/comments/orders/${encodeURIComponent(this.orderId)}/messages?${query}`);
|
|
44
|
+
const data = record(result.data);
|
|
45
|
+
if (!Array.isArray(data.messages) || !(data.nextCursor === null || typeof data.nextCursor === 'string') || typeof data.unreadCount !== 'number') {
|
|
46
|
+
throw new OrderCommentsError('PocketBoss returned an invalid comments page.');
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
messages: data.messages.map(parseMessage),
|
|
50
|
+
nextCursor: data.nextCursor,
|
|
51
|
+
unreadCount: data.unreadCount,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async send(text, clientMessageId = secureUuid()) {
|
|
55
|
+
const normalizedText = text.trim();
|
|
56
|
+
if (!normalizedText || normalizedText.length > 5000) {
|
|
57
|
+
throw new OrderCommentsError('A message from 1 to 5000 characters is required.', 422, 'VALIDATION_FAILED');
|
|
58
|
+
}
|
|
59
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(clientMessageId)) {
|
|
60
|
+
throw new OrderCommentsError('A secure idempotency key is required.', 422, 'VALIDATION_FAILED');
|
|
61
|
+
}
|
|
62
|
+
const result = await this.request(`/comments/orders/${encodeURIComponent(this.orderId)}/messages`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
body: JSON.stringify({ client_message_id: clientMessageId, text: normalizedText }),
|
|
65
|
+
});
|
|
66
|
+
return parseMessage(result.data);
|
|
67
|
+
}
|
|
68
|
+
async markRead(messageId) {
|
|
69
|
+
if (!/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(messageId)) {
|
|
70
|
+
throw new OrderCommentsError('A valid comments message ID is required.', 422, 'VALIDATION_FAILED');
|
|
71
|
+
}
|
|
72
|
+
await this.request(`/comments/orders/${encodeURIComponent(this.orderId)}/read`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
body: JSON.stringify({ message_id: messageId }),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async close() {
|
|
78
|
+
if (this.closePromise)
|
|
79
|
+
return this.closePromise;
|
|
80
|
+
this.closed = true;
|
|
81
|
+
const active = this.session;
|
|
82
|
+
this.session = null;
|
|
83
|
+
const opening = this.openingSession;
|
|
84
|
+
for (const controller of this.activeRequests)
|
|
85
|
+
controller.abort();
|
|
86
|
+
this.activeRequests.clear();
|
|
87
|
+
this.closePromise = (async () => {
|
|
88
|
+
if (active)
|
|
89
|
+
await this.revokeSession(active.sessionId);
|
|
90
|
+
if (opening) {
|
|
91
|
+
try {
|
|
92
|
+
await opening;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
if (this.pendingRevocationId)
|
|
96
|
+
throw new OrderCommentsError('The comments session could not be revoked during close.', 503, 'COMMENTS_SESSION_CLOSE_FAILED');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (this.pendingRevocationId)
|
|
100
|
+
await this.revokeSession(this.pendingRevocationId);
|
|
101
|
+
})();
|
|
102
|
+
return this.closePromise;
|
|
103
|
+
}
|
|
104
|
+
async request(path, init = {}, retried = false) {
|
|
105
|
+
if (this.closed)
|
|
106
|
+
throw new OrderCommentsError('The order comments session is closed.');
|
|
107
|
+
const session = await this.getSession();
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
this.activeRequests.add(controller);
|
|
110
|
+
const headers = new Headers(init.headers);
|
|
111
|
+
headers.set('Accept', 'application/json');
|
|
112
|
+
headers.set('Authorization', `Bearer ${session.token}`);
|
|
113
|
+
if (init.body !== undefined)
|
|
114
|
+
headers.set('Content-Type', 'application/json');
|
|
115
|
+
let response;
|
|
116
|
+
try {
|
|
117
|
+
response = await this.fetcher(`${this.apiBaseUrl}${path}`, {
|
|
118
|
+
...init,
|
|
119
|
+
cache: 'no-store',
|
|
120
|
+
credentials: 'omit',
|
|
121
|
+
redirect: 'error',
|
|
122
|
+
referrerPolicy: 'no-referrer',
|
|
123
|
+
headers,
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
if (this.closed)
|
|
129
|
+
throw new OrderCommentsError('The order comments session is closed.');
|
|
130
|
+
throw new OrderCommentsError('Could not connect to order comments. Check the connection and retry.');
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
this.activeRequests.delete(controller);
|
|
134
|
+
}
|
|
135
|
+
if ((response.status === 401 || response.status === 410) && !retried) {
|
|
136
|
+
await this.getSession(true);
|
|
137
|
+
return this.request(path, init, true);
|
|
138
|
+
}
|
|
139
|
+
const body = await response.json().catch(() => null);
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const error = isRecord(body) && isRecord(body.error) ? body.error : {};
|
|
142
|
+
throw new OrderCommentsError(typeof error.message === 'string' ? error.message : 'Order comments are unavailable. Retry or sign in again.', response.status, typeof error.code === 'string' ? error.code : null);
|
|
143
|
+
}
|
|
144
|
+
if (!isRecord(body) || !isRecord(body.data)) {
|
|
145
|
+
throw new OrderCommentsError('PocketBoss returned an invalid comments response.');
|
|
146
|
+
}
|
|
147
|
+
return body;
|
|
148
|
+
}
|
|
149
|
+
async getSession(force = false) {
|
|
150
|
+
if (this.closed)
|
|
151
|
+
throw new OrderCommentsError('The order comments session is closed.');
|
|
152
|
+
if (!force && this.session && this.session.expiresAtMs > Date.now() + 5_000)
|
|
153
|
+
return this.session;
|
|
154
|
+
if (this.openingSession)
|
|
155
|
+
return this.openingSession;
|
|
156
|
+
this.openingSession = (async () => {
|
|
157
|
+
const previous = this.session;
|
|
158
|
+
this.session = null;
|
|
159
|
+
if (this.pendingRevocationId)
|
|
160
|
+
await this.revokeSession(this.pendingRevocationId);
|
|
161
|
+
if (previous) {
|
|
162
|
+
try {
|
|
163
|
+
await this.revokeSession(previous.sessionId);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw new OrderCommentsError('The previous comments session could not be closed. Reopen this order after your Mini App session is restored.', 503, 'COMMENTS_SESSION_CLOSE_FAILED');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let value;
|
|
170
|
+
try {
|
|
171
|
+
value = await this.sessions.create();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
throw new OrderCommentsError('Your business session expired. Sign in again to renew comments access.', 401, 'COMMENTS_SESSION_EXPIRED');
|
|
175
|
+
}
|
|
176
|
+
const expiresAtMs = Date.parse(value.expiresAt);
|
|
177
|
+
if (typeof value.sessionId !== 'string' || value.sessionId.length < 16 ||
|
|
178
|
+
typeof value.token !== 'string' || !value.token.startsWith('pbcs_') ||
|
|
179
|
+
!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
|
|
180
|
+
throw new OrderCommentsError('The Mini App backend returned an invalid comments session.');
|
|
181
|
+
}
|
|
182
|
+
const stored = { ...value, expiresAtMs };
|
|
183
|
+
if (this.closed) {
|
|
184
|
+
await this.revokeSession(stored.sessionId);
|
|
185
|
+
throw new OrderCommentsError('The order comments session is closed.');
|
|
186
|
+
}
|
|
187
|
+
this.session = stored;
|
|
188
|
+
return stored;
|
|
189
|
+
})();
|
|
190
|
+
try {
|
|
191
|
+
return await this.openingSession;
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
this.openingSession = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async revokeSession(sessionId) {
|
|
198
|
+
this.pendingRevocationId = sessionId;
|
|
199
|
+
await this.sessions.revoke(sessionId);
|
|
200
|
+
if (this.pendingRevocationId === sessionId)
|
|
201
|
+
this.pendingRevocationId = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function parseMessage(value) {
|
|
205
|
+
if (!isRecord(value) || typeof value.messageId !== 'string' ||
|
|
206
|
+
typeof value.conversationId !== 'string' || typeof value.orderId !== 'string' ||
|
|
207
|
+
(value.participantSide !== 'customer' && value.participantSide !== 'business') ||
|
|
208
|
+
typeof value.text !== 'string' || value.deliveryState !== 'sent' ||
|
|
209
|
+
typeof value.createdAt !== 'string' || !Number.isFinite(Date.parse(value.createdAt))) {
|
|
210
|
+
throw new OrderCommentsError('PocketBoss returned an invalid comment.');
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
function isRecord(value) {
|
|
215
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
216
|
+
}
|
|
217
|
+
function record(value) {
|
|
218
|
+
if (!isRecord(value))
|
|
219
|
+
throw new OrderCommentsError('PocketBoss returned an invalid comments response.');
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
function secureUuid() {
|
|
223
|
+
if (!globalThis.crypto?.randomUUID)
|
|
224
|
+
throw new OrderCommentsError('Secure randomness is unavailable.');
|
|
225
|
+
return globalThis.crypto.randomUUID();
|
|
226
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,8 @@ export { PocketBossClient } from './client.js';
|
|
|
2
2
|
export { BridgeError } from './types.js';
|
|
3
3
|
export { captureDevicePhoto, pickDeviceFiles } from './media.js';
|
|
4
4
|
export type { CaptureFacingMode, DeviceFilePickerOptions } from './media.js';
|
|
5
|
+
export { OrderCommentsClient, OrderCommentsError } from './comments.js';
|
|
6
|
+
export type { OrderCommentMessage, OrderCommentPage, OrderCommentsClientOptions, OrderCommentsSession, OrderCommentsSessionProvider } from './comments.js';
|
|
7
|
+
export { mountOrderCommentsElement, registerOrderCommentsElement } from './comments-element.js';
|
|
8
|
+
export type { OrderCommentsElementOptions, OrderCommentsPlacement, OrderCommentsTheme, OrderCommentsThemeMode, PocketBossOrderCommentsElement } from './comments-element.js';
|
|
5
9
|
export type { BridgeCapability, BridgeErrorCode, BridgeEvent, BridgeLimits, BridgeMethod, BridgeMethodMap, BridgeSnapshot, BridgeState, BridgeTransport, MethodRequest, MethodResponse, ReadyMessage, RequestOptions } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { PocketBossClient } from './client.js';
|
|
2
2
|
export { BridgeError } from './types.js';
|
|
3
3
|
export { captureDevicePhoto, pickDeviceFiles } from './media.js';
|
|
4
|
+
export { OrderCommentsClient, OrderCommentsError } from './comments.js';
|
|
5
|
+
export { mountOrderCommentsElement, registerOrderCommentsElement } from './comments-element.js';
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pocketboss/core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
10
|
+
"./comments": { "types": "./dist/comments/index.d.ts", "default": "./dist/comments/index.js" },
|
|
10
11
|
"./mock": { "types": "./dist/mock.d.ts", "default": "./dist/mock.js" }
|
|
11
12
|
},
|
|
12
13
|
"files": ["dist", "README.md"],
|