diff --git a/packages/iios-support-web/package.json b/packages/iios-support-web/package.json new file mode 100644 index 0000000..d54cadd --- /dev/null +++ b/packages/iios-support-web/package.json @@ -0,0 +1,28 @@ +{ + "name": "@insignia/iios-support-web", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, + "files": ["dist"], + "scripts": { + "build": "tsup", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@insignia/iios-kernel-client": "workspace:*", + "@insignia/iios-message-web": "workspace:*" + }, + "peerDependencies": { + "react": ">=18" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "react": "^19.0.0", + "tsup": "^8.3.5", + "typescript": "^5.7.3" + } +} diff --git a/packages/iios-support-web/src/index.ts b/packages/iios-support-web/src/index.ts new file mode 100644 index 0000000..101aac0 --- /dev/null +++ b/packages/iios-support-web/src/index.ts @@ -0,0 +1,11 @@ +export { + SupportProvider, + useEscalate, + useTickets, + useAssignedTickets, + useCallbackRequest, + useAvailability, +} from './react'; +// Re-export the chat hooks so a support app imports everything from one place. +export { useThread, useMessages } from '@insignia/iios-message-web'; +export type { Ticket, TicketState, CallbackRequest, Message } from '@insignia/iios-kernel-client'; diff --git a/packages/iios-support-web/src/react.tsx b/packages/iios-support-web/src/react.tsx new file mode 100644 index 0000000..8768358 --- /dev/null +++ b/packages/iios-support-web/src/react.tsx @@ -0,0 +1,91 @@ +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { MessageProvider } from '@insignia/iios-message-web'; +import { RestClient, type Ticket, type CallbackRequest, type TicketState } from '@insignia/iios-kernel-client'; + +const SupportContext = createContext(null); + +/** Wraps MessageProvider (chat) and provides a support REST client. */ +export function SupportProvider({ + serviceUrl, + token, + children, +}: { + serviceUrl: string; + token: string; + children: React.ReactNode; +}): React.ReactElement { + const client = useMemo(() => new RestClient({ serviceUrl, token }), [serviceUrl, token]); + return ( + + + {children} + + + ); +} + +function useSupportClient(): RestClient { + const c = useContext(SupportContext); + if (!c) throw new Error('support hooks must be used within '); + return c; +} + +/** Escalate a chat thread to support (creates + queues a ticket). */ +export function useEscalate(): (threadId: string, subject?: string) => Promise { + const client = useSupportClient(); + return (threadId, subject) => client.escalate(threadId, subject); +} + +/** The caller's tickets (scope 'mine') or an agent's assigned tickets ('assigned'). */ +export function useTickets(scope: 'mine' | 'assigned' = 'mine', pollMs = 3000): { + tickets: Ticket[]; + refresh: () => Promise; + createTicket: (subject: string, opts?: { priority?: string; threadId?: string }) => Promise; + patch: (id: string, state: TicketState) => Promise; +} { + const client = useSupportClient(); + const [tickets, setTickets] = useState([]); + const refresh = useCallback(async () => { + try { + setTickets(await client.listTickets(scope)); + } catch { + /* transient */ + } + }, [client, scope]); + useEffect(() => { + void refresh(); + const t = setInterval(() => void refresh(), pollMs); + return () => clearInterval(t); + }, [refresh, pollMs]); + const createTicket = async (subject: string, opts?: { priority?: string; threadId?: string }) => { + const t = await client.createTicket({ subject, ...opts }); + await refresh(); + return t; + }; + const patch = async (id: string, state: TicketState) => { + await client.patchTicket(id, state); + await refresh(); + }; + return { tickets, refresh, createTicket, patch }; +} + +/** Agent convenience: their assigned tickets. */ +export function useAssignedTickets(pollMs = 3000) { + return useTickets('assigned', pollMs); +} + +export function useCallbackRequest(): (input: { + preferChannel: string; + preferredTime?: string; + notes?: string; + ticketId?: string; +}) => Promise { + const client = useSupportClient(); + return (input) => client.requestCallback(input); +} + +/** Agent presence: set AVAILABLE/OFFLINE. */ +export function useAvailability(): (state: string) => Promise { + const client = useSupportClient(); + return (state) => client.setAvailability(state); +} diff --git a/packages/iios-support-web/tsconfig.json b/packages/iios-support-web/tsconfig.json new file mode 100644 index 0000000..4a4d5c4 --- /dev/null +++ b/packages/iios-support-web/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "jsx": "react-jsx", + "types": ["react"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] +} diff --git a/packages/iios-support-web/tsup.config.ts b/packages/iios-support-web/tsup.config.ts new file mode 100644 index 0000000..ea1f3b6 --- /dev/null +++ b/packages/iios-support-web/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + external: ['react', 'react-dom', '@insignia/iios-message-web', '@insignia/iios-kernel-client'], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 134f9fb..ca6fa36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,6 +181,28 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/iios-support-web: + dependencies: + '@insignia/iios-kernel-client': + specifier: workspace:* + version: link:../iios-kernel-client + '@insignia/iios-message-web': + specifier: workspace:* + version: link:../iios-message-web + devDependencies: + '@types/react': + specifier: ^19.0.0 + version: 19.2.17 + react: + specifier: ^19.0.0 + version: 19.2.7 + tsup: + specifier: ^8.3.5 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.16)(typescript@5.9.3) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + packages/iios-testkit: dependencies: '@insignia/iios-contracts': diff --git a/scripts/check-import-boundary.mjs b/scripts/check-import-boundary.mjs index d869cb6..4ceeb20 100644 --- a/scripts/check-import-boundary.mjs +++ b/scripts/check-import-boundary.mjs @@ -20,6 +20,12 @@ const ALLOWED = { '@insignia/iios-service': ['@insignia/iios-contracts', '@insignia/iios-testkit'], '@insignia/iios-message-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'], '@insignia/iios-inbox-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'], + '@insignia/iios-support-web': [ + '@insignia/iios-contracts', + '@insignia/iios-kernel-client', + '@insignia/iios-message-web', + '@insignia/iios-inbox-web', + ], }; const KNOWN = Object.keys(ALLOWED);