diff --git a/apps/message-demo/package.json b/apps/message-demo/package.json
index 437b8ab..a9099ba 100644
--- a/apps/message-demo/package.json
+++ b/apps/message-demo/package.json
@@ -10,6 +10,7 @@
"dependencies": {
"@insignia/iios-kernel-client": "workspace:*",
"@insignia/iios-message-web": "workspace:*",
+ "@insignia/iios-inbox-web": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/apps/message-demo/src/App.tsx b/apps/message-demo/src/App.tsx
index 13a1472..65569b6 100644
--- a/apps/message-demo/src/App.tsx
+++ b/apps/message-demo/src/App.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { MessageProvider, useThread, useMessages } from '@insignia/iios-message-web';
+import { InboxProvider, useInbox } from '@insignia/iios-inbox-web';
const SERVICE = 'http://localhost:3200';
const APP_ID = 'portal-demo';
@@ -83,12 +84,35 @@ function ChatInner({
);
}
+function InboxSidebar() {
+ const { items, done, snooze } = useInbox({ state: 'OPEN' });
+ return (
+
+
Inbox ({items.length})
+ {items.length === 0 &&
nothing to reply to
}
+ {items.map((i) => (
+
+
{i.title}
+
{i.kind}
+
+
+
+ ))}
+
+ );
+}
+
function Pane(props: { token: string | null; label: string; threadId: string | null; onCreated?: (id: string) => void }) {
if (!props.token) return loading {props.label}…
;
return (
-
-
-
+
+
+
+
+
+
+
+
);
}
diff --git a/packages/iios-service/scripts/smoke-inbox.mjs b/packages/iios-service/scripts/smoke-inbox.mjs
new file mode 100644
index 0000000..384711a
--- /dev/null
+++ b/packages/iios-service/scripts/smoke-inbox.mjs
@@ -0,0 +1,65 @@
+// P3 inbox smoke: Alice sends → Bob's inbox gets a NEEDS_REPLY item → Bob reads
+// → item auto-resolves to DONE. Requires the service running (relay timer on),
+// IIOS_DEV_TOKENS not needed (we sign locally). Run: node scripts/smoke-inbox.mjs
+import 'dotenv/config';
+import { io } from 'socket.io-client';
+import jwt from 'jsonwebtoken';
+
+const SERVICE = process.env.SMOKE_URL ?? 'http://localhost:3200';
+const APP_ID = 'portal-demo';
+const SECRET = JSON.parse(process.env.APP_SECRETS ?? '{"portal-demo":"dev-secret"}')[APP_ID];
+
+const sign = (u) => jwt.sign({ sub: u, name: u, appId: APP_ID, orgId: `org_${APP_ID}` }, SECRET, { algorithm: 'HS256', expiresIn: '1h' });
+const connect = (u) => io(`${SERVICE}/message`, { auth: { token: sign(u) }, transports: ['websocket'], forceNew: true });
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+const assert = (c, m) => {
+ if (!c) { console.error('✗', m); process.exit(1); }
+ console.log('✓', m);
+};
+
+async function inbox(token, state) {
+ const r = await fetch(`${SERVICE}/v1/inbox/items${state ? `?state=${state}` : ''}`, {
+ headers: { authorization: `Bearer ${token}` },
+ });
+ if (!r.ok) throw new Error(`inbox ${r.status}`);
+ return r.json();
+}
+async function pollUntil(fn, ms = 8000) {
+ const end = Date.now() + ms;
+ while (Date.now() < end) {
+ const v = await fn();
+ if (v) return v;
+ await sleep(300);
+ }
+ return null;
+}
+
+const bobToken = sign('bob');
+const alice = connect('alice');
+const bob = connect('bob');
+
+try {
+ const { threadId } = await alice.emitWithAck('open_thread', {});
+ await bob.emitWithAck('open_thread', { threadId }); // Bob joins → participant
+ const sent = await alice.emitWithAck('send_message', { threadId, content: 'hi bob' });
+
+ const open = await pollUntil(async () => {
+ const items = await inbox(bobToken, 'OPEN');
+ return items.find((i) => i.threadId === threadId && i.kind === 'NEEDS_REPLY') ?? null;
+ });
+ assert(open, `Bob's inbox got an OPEN NEEDS_REPLY item (${open?.id})`);
+
+ await bob.emitWithAck('read', { threadId, interactionId: sent.id });
+ const done = await pollUntil(async () => {
+ const items = await inbox(bobToken);
+ const it = items.find((i) => i.id === open.id);
+ return it && it.state === 'DONE' ? it : null;
+ });
+ assert(done, "Bob's item auto-resolved to DONE after reading");
+
+ console.log('\nP3 inbox smoke: PASS');
+} finally {
+ alice.close();
+ bob.close();
+}
+process.exit(0);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9182184..134f9fb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
apps/message-demo:
dependencies:
+ '@insignia/iios-inbox-web':
+ specifier: workspace:*
+ version: link:../../packages/iios-inbox-web
'@insignia/iios-kernel-client':
specifier: workspace:*
version: link:../../packages/iios-kernel-client