@joinskips/brand 9.0.0-develop.148 → 9.0.0-develop.149
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/chat/chat/SkipsChat.d.ts +36 -0
- package/chat/chat/SkipsChat.js +98 -0
- package/chat/chat/client.d.ts +48 -0
- package/chat/chat/client.js +173 -0
- package/chat/chat/connection.d.ts +4 -0
- package/chat/chat/connection.js +6 -0
- package/chat/chat/generated/skips-app/index.d.ts +1 -0
- package/chat/chat/generated/skips-app/index.js +2 -0
- package/chat/chat/generated/skips-app/types.gen.d.ts +733 -0
- package/chat/chat/generated/skips-app/types.gen.js +2 -0
- package/chat/chat/index.d.ts +3 -0
- package/chat/chat/index.js +2 -0
- package/chat/chat/platform.d.ts +4 -0
- package/chat/chat/platform.js +46 -0
- package/chat/chat/session.d.ts +19 -0
- package/chat/chat/session.js +95 -0
- package/chat/chat/useSkipsChat.d.ts +78 -0
- package/chat/chat/useSkipsChat.js +498 -0
- package/chat/chat/verify.d.ts +35 -0
- package/chat/chat/verify.js +84 -0
- package/chat/helpers/index.d.ts +1 -1
- package/chat/helpers/index.js +1 -1
- package/chat/helpers/message.d.ts +1 -1
- package/chat/helpers/thread.d.ts +12 -1
- package/chat/helpers/thread.js +16 -1
- package/chat/index.d.ts +1 -0
- package/chat/index.js +1 -0
- package/chat/ui/Conversation.d.ts +29 -3
- package/chat/ui/Conversation.js +37 -12
- package/chat/ui/index.d.ts +0 -1
- package/chat/ui/index.js +0 -1
- package/chat/ui/parts.d.ts +1 -1
- package/package.json +9 -1
- package/chat/ui/CheckIn.d.ts +0 -25
- package/chat/ui/CheckIn.js +0 -24
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ImageSourcePropType } from 'react-native';
|
|
2
|
+
import { type PickedFile, type TypingTimes, type WelcomeTypingTimes } from '../helpers/index.ts';
|
|
3
|
+
import type { ChatIcon, ChatWords } from '../ui/parts.ts';
|
|
4
|
+
import { type WelcomeWords } from '../ui/Welcome.tsx';
|
|
5
|
+
import { type SkipsChatOptions } from './useSkipsChat.ts';
|
|
6
|
+
export interface SkipsChatWords extends ChatWords {
|
|
7
|
+
thread: string;
|
|
8
|
+
welcome: WelcomeWords;
|
|
9
|
+
welcomeText: string;
|
|
10
|
+
emailText: string;
|
|
11
|
+
nameText: string;
|
|
12
|
+
codeText: (email: string) => string;
|
|
13
|
+
unreachable: string;
|
|
14
|
+
tryAgain: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SkipsChatWelcome {
|
|
17
|
+
verifyUrl: string;
|
|
18
|
+
onLogIn: (line: string) => void;
|
|
19
|
+
codeNote?: string | undefined;
|
|
20
|
+
resendAfterMs?: number | undefined;
|
|
21
|
+
typing?: WelcomeTypingTimes | undefined;
|
|
22
|
+
}
|
|
23
|
+
export interface SkipsChatProps extends Omit<SkipsChatOptions, 'verifyUrl'> {
|
|
24
|
+
welcome?: SkipsChatWelcome | undefined;
|
|
25
|
+
words: SkipsChatWords;
|
|
26
|
+
Icon: ChatIcon;
|
|
27
|
+
skipMark: ImageSourcePropType;
|
|
28
|
+
skipUserId: number | null;
|
|
29
|
+
typingTimes?: TypingTimes | undefined;
|
|
30
|
+
pickFile?: (() => Promise<PickedFile | null>) | undefined;
|
|
31
|
+
keyboardOffset?: number | undefined;
|
|
32
|
+
bottomInset?: number | undefined;
|
|
33
|
+
draft?: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** The one Skips chat: the welcome for a new visitor, the conversation, Skip's questions and dots, and the box. */
|
|
36
|
+
export declare function SkipsChat({ welcome, words, Icon, skipMark, skipUserId, typingTimes, pickFile, keyboardOffset, bottomInset, draft, ...options }: SkipsChatProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
|
+
import { givenEmail, givenName, RESEND_AFTER_MS, } from "../helpers/index.js";
|
|
4
|
+
import { useCodeEntry, useComposer, useResendCountdown, useStepField, useTypingCue, useWelcomeTyping, } from "../hooks/index.js";
|
|
5
|
+
import { Bubble } from "../ui/Bubble.js";
|
|
6
|
+
import { Conversation, Unsent } from "../ui/Conversation.js";
|
|
7
|
+
import { CodeStep, EmailStep, NameStep, Pill, SkipTyping, WelcomeChoice } from "../ui/Welcome.js";
|
|
8
|
+
import { CODE_LENGTH } from "./verify.js";
|
|
9
|
+
import { useSkipsChat, } from "./useSkipsChat.js";
|
|
10
|
+
/** The one Skips chat: the welcome for a new visitor, the conversation, Skip's questions and dots, and the box. */
|
|
11
|
+
export function SkipsChat({ welcome, words, Icon, skipMark, skipUserId, typingTimes, pickFile, keyboardOffset, bottomInset, draft = '', ...options }) {
|
|
12
|
+
const chat = useSkipsChat({ ...options, verifyUrl: welcome?.verifyUrl });
|
|
13
|
+
const { thread, status, welcome: way } = chat;
|
|
14
|
+
const cue = useTypingCue(thread.messages.filter((message) => !message.mine).map((message) => message.id), chat.skipsTyping, typingTimes);
|
|
15
|
+
const welcomeTyping = useWelcomeTyping(way?.step ?? null, welcome?.typing);
|
|
16
|
+
const composer = useComposer({
|
|
17
|
+
onSend: (text, picked) => {
|
|
18
|
+
chat.stillHere();
|
|
19
|
+
void chat.send(text, picked);
|
|
20
|
+
},
|
|
21
|
+
onTyped: (text) => {
|
|
22
|
+
chat.stillHere();
|
|
23
|
+
chat.typed(text);
|
|
24
|
+
},
|
|
25
|
+
...(pickFile ? { pickFile } : {}),
|
|
26
|
+
initialText: draft,
|
|
27
|
+
});
|
|
28
|
+
// The visitor's first line stays where they wrote it, above the welcome. Once they are through, it is the
|
|
29
|
+
// conversation's first line.
|
|
30
|
+
const opening = way?.step === 'through'
|
|
31
|
+
? thread.messages.find((message) => message.id === way.lineId || message.localId === way.lineId)
|
|
32
|
+
: undefined;
|
|
33
|
+
const retry = (id) => {
|
|
34
|
+
for (const failed of thread.messages)
|
|
35
|
+
if (failed.id === id)
|
|
36
|
+
void chat.retry(failed);
|
|
37
|
+
};
|
|
38
|
+
const hidden = new Set(cue.held);
|
|
39
|
+
if (opening)
|
|
40
|
+
hidden.add(opening.id);
|
|
41
|
+
const load = status === 'starting' && way === null && thread.messages.length === 0 ? 'loading' : 'ready';
|
|
42
|
+
return (_jsx(View, { className: "flex-1",
|
|
43
|
+
// A tap anywhere in the chat is the visitor still being there. Asked on the way down, it takes nothing over.
|
|
44
|
+
onStartShouldSetResponderCapture: () => {
|
|
45
|
+
chat.stillHere();
|
|
46
|
+
return false;
|
|
47
|
+
}, children: _jsx(Conversation, { thread: thread, load: load, feedDown: chat.reconnecting, typists: [], skipUserId: skipUserId, composer: composer, composerClosed: status !== 'ready' || (way !== null && way.step !== 'through'), holding: cue.dots, onRetry: retry, onLoadOlder: () => void chat.loadOlder(), onReload: chat.tryAgain, keyboardOffset: keyboardOffset, bottomInset: bottomInset, words: words, Icon: Icon, skipMark: skipMark, label: words.thread, hidden: hidden, choices: {
|
|
48
|
+
sending: chat.choosing,
|
|
49
|
+
failed: chat.choiceFailed,
|
|
50
|
+
holding: cue.dots,
|
|
51
|
+
words: words.welcome,
|
|
52
|
+
onChoose: (question, answer) => void chat.choose(question, answer),
|
|
53
|
+
}, lead: way !== null && welcome !== undefined ? (_jsx(WelcomeLead, { way: way, opening: opening, welcome: welcome, words: words, Icon: Icon, typing: welcomeTyping, holding: cue.dots, onRetry: retry, chat: chat })) : null, trail: _jsxs(_Fragment, { children: [cue.dots || welcomeTyping !== null ? _jsx(SkipTyping, { label: words.welcome.typing, skipMark: skipMark }) : null, status === 'unreachable' || way?.unreachable === true ? (_jsxs(View, { role: "alert", className: "flex-row items-center gap-3", children: [_jsx(Text, { className: "font-ui text-sm text-error-ink", children: words.unreachable }), status === 'unreachable' ? _jsx(Pill, { label: words.tryAgain, onPress: chat.tryAgain }) : null] })) : null] }) }) }));
|
|
54
|
+
}
|
|
55
|
+
// The welcome, line by line: each step's line, and its field or buttons, show once Skip has typed it.
|
|
56
|
+
function WelcomeLead({ way, opening, welcome, words, Icon, typing, holding, onRetry, chat }) {
|
|
57
|
+
const typed = (step) => typing !== step;
|
|
58
|
+
const skip = (text) => _jsx(SkipLine, { text: text, label: words.skip });
|
|
59
|
+
const asked = way.step === 'code' || way.step === 'name' || (way.step === 'through' && !way.signedIn);
|
|
60
|
+
return (_jsxs(View, { className: "gap-4 pb-4", children: [opening && opening.delivery !== 'sent' ? (_jsx(Unsent, { message: opening, words: words, onRetry: onRetry, Icon: Icon, holding: holding })) : (_jsxs(View, { className: "items-end gap-1 self-end", children: [_jsx(Bubble, { mine: true, text: opening?.text ?? way.line }), _jsx(LineLabel, { text: opening ? `${words.you} · ${words.messageTime(opening.sentAt)}` : words.you })] })), typed('choosing') ? skip(words.welcomeText) : null, way.step === 'choosing' ? (typed('choosing') ? (_jsx(WelcomeChoice, { words: words.welcome, onLogIn: () => {
|
|
61
|
+
welcome.onLogIn(way.line);
|
|
62
|
+
}, onContinueHere: () => {
|
|
63
|
+
chat.continueHere(way.line);
|
|
64
|
+
} })) : null) : !(way.step === 'through' && way.signedIn) && typed('email') ? (skip(words.emailText)) : null, way.step === 'email' && typed('email') ? (_jsx(EmailField, { initial: way.email ?? '', words: words.welcome, onSubmit: (email) => {
|
|
65
|
+
void chat.sendCode(email);
|
|
66
|
+
} })) : null, asked && typed('code') ? (_jsxs(_Fragment, { children: [skip(words.codeText(way.email)), welcome.codeNote === undefined ? null : skip(welcome.codeNote)] })) : null, way.step === 'code' && typed('code') ? (_jsx(CodeField, { step: way, words: words.welcome,
|
|
67
|
+
// No new code has gone out since the one that ran out, so there is no wait to sit through.
|
|
68
|
+
resendAfterMs: way.ranOut ? 0 : (welcome.resendAfterMs ?? RESEND_AFTER_MS), chat: chat }, way.codes)) : null, (way.step === 'name' || (way.step === 'through' && way.named)) && typed('name') ? skip(words.nameText) : null, way.step === 'name' && typed('name') ? _jsx(NameField, { step: way, words: words.welcome, chat: chat }) : null] }));
|
|
69
|
+
}
|
|
70
|
+
function SkipLine({ text, label }) {
|
|
71
|
+
return (_jsxs(View, { className: "max-w-[82%] items-start gap-1 self-start", children: [_jsx(Bubble, { text: text }), _jsx(LineLabel, { text: label })] }));
|
|
72
|
+
}
|
|
73
|
+
// Who said a line of the welcome, as the thread names each run of lines.
|
|
74
|
+
function LineLabel({ text }) {
|
|
75
|
+
return _jsx(Text, { className: "px-1 font-ui text-xs text-ink-secondary", children: text });
|
|
76
|
+
}
|
|
77
|
+
function EmailField({ initial, words, onSubmit }) {
|
|
78
|
+
return _jsx(EmailStep, { words: words, field: useStepField(givenEmail, onSubmit, initial) });
|
|
79
|
+
}
|
|
80
|
+
function NameField({ step, words, chat }) {
|
|
81
|
+
const field = useStepField(givenName, (name) => {
|
|
82
|
+
void chat.giveName(step, name);
|
|
83
|
+
});
|
|
84
|
+
return (_jsx(NameStep, { words: words, field: field, onChangeEmail: () => {
|
|
85
|
+
chat.changeEmail(step);
|
|
86
|
+
} }));
|
|
87
|
+
}
|
|
88
|
+
// Mounted afresh for each code that goes out.
|
|
89
|
+
function CodeField({ step, words, resendAfterMs, chat }) {
|
|
90
|
+
const onSubmit = (code) => {
|
|
91
|
+
void chat.checkCode(step, code);
|
|
92
|
+
};
|
|
93
|
+
return (_jsx(CodeStep, { words: words, length: CODE_LENGTH, noMatch: step.noMatch, ranOut: step.ranOut, entry: useCodeEntry(CODE_LENGTH, onSubmit), secondsLeft: useResendCountdown(resendAfterMs), onSubmit: onSubmit, onResend: () => {
|
|
94
|
+
void chat.sendCode(step.email);
|
|
95
|
+
}, onChangeEmail: () => {
|
|
96
|
+
chat.changeEmail(step);
|
|
97
|
+
} }));
|
|
98
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ChatMessage, type ChoiceItem, type PickedFile, type RawMessage } from '../helpers/index.ts';
|
|
2
|
+
export interface ChatClientOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
inboxIdentifier: string;
|
|
5
|
+
careTeamName: string;
|
|
6
|
+
fetchFn?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
export interface ContactSession {
|
|
9
|
+
sourceId: string;
|
|
10
|
+
pubsubToken: string;
|
|
11
|
+
inboxIdentifier?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Conversation {
|
|
14
|
+
id: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const WEBSITE_CHAT_ATTRIBUTE = "website_chat";
|
|
17
|
+
export declare class ChatApiError extends Error {
|
|
18
|
+
readonly status: number;
|
|
19
|
+
constructor(status: number, detail: string);
|
|
20
|
+
}
|
|
21
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
22
|
+
export declare function isInbox(value: unknown): value is string;
|
|
23
|
+
export declare function contactSessionFrom(value: unknown): ContactSession | null;
|
|
24
|
+
/**
|
|
25
|
+
* The visitor's view of a Skips App message, or null for one they never see. A care team member's photo shows only
|
|
26
|
+
* from a secure web address, so the page never loads one over plain http, and their name stays without it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function visitorView(raw: RawMessage, careTeamName: string): ChatMessage | null;
|
|
29
|
+
export declare class ChatClient {
|
|
30
|
+
private readonly baseUrl;
|
|
31
|
+
private readonly inbox;
|
|
32
|
+
private readonly careTeamName;
|
|
33
|
+
private readonly fetchFn;
|
|
34
|
+
constructor({ baseUrl, inboxIdentifier, careTeamName, fetchFn }: ChatClientOptions);
|
|
35
|
+
private messageFrom;
|
|
36
|
+
private url;
|
|
37
|
+
private inboxOf;
|
|
38
|
+
private request;
|
|
39
|
+
createContact(): Promise<ContactSession>;
|
|
40
|
+
/** `startingPage` is the page the chat was opened on, which the Skips App shows the care team as the conversation's referer. */
|
|
41
|
+
createConversation(contact: ContactSession, startingPage?: string): Promise<Conversation>;
|
|
42
|
+
choose(contact: ContactSession, conversation: Conversation, question: number, answer: ChoiceItem): Promise<ChatMessage>;
|
|
43
|
+
private conversationPath;
|
|
44
|
+
sendMessage(contact: ContactSession, conversation: Conversation, text: string, picked?: PickedFile | null): Promise<ChatMessage>;
|
|
45
|
+
listMessages(contact: ContactSession, conversation: Conversation, before?: number): Promise<ChatMessage[]>;
|
|
46
|
+
typing(contact: ContactSession, conversation: Conversation, on: boolean): Promise<void>;
|
|
47
|
+
seen(contact: ContactSession, conversation: Conversation): Promise<void>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// The Skips App's public Client API, the part of it a visitor's browser may call with
|
|
2
|
+
// no secret: make a contact, open a conversation, send and list messages. The inbox
|
|
3
|
+
// identifier is public by design. Plain fetch, no framework, so a native app can use it.
|
|
4
|
+
import { isRawMessage, toChatMessage, } from "../helpers/index.js";
|
|
5
|
+
// The conversation attribute the chat sets on each conversation it opens, so Skips asks a question with buttons only
|
|
6
|
+
// where they can be pressed.
|
|
7
|
+
export const WEBSITE_CHAT_ATTRIBUTE = 'website_chat';
|
|
8
|
+
export class ChatApiError extends Error {
|
|
9
|
+
status;
|
|
10
|
+
constructor(status, detail) {
|
|
11
|
+
super(`Skips App answered ${String(status)}: ${detail}`);
|
|
12
|
+
this.name = 'ChatApiError';
|
|
13
|
+
this.status = status;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function isRecord(value) {
|
|
17
|
+
return typeof value === 'object' && value !== null;
|
|
18
|
+
}
|
|
19
|
+
// An inbox's identifier as a contact can carry it.
|
|
20
|
+
export function isInbox(value) {
|
|
21
|
+
return typeof value === 'string' && value.length > 0;
|
|
22
|
+
}
|
|
23
|
+
// An inbox that is named and cannot be used is not left out: the contact is in it, so the built-in one is wrong.
|
|
24
|
+
function isContactResponse(value) {
|
|
25
|
+
return (isRecord(value) &&
|
|
26
|
+
typeof value.source_id === 'string' &&
|
|
27
|
+
typeof value.pubsub_token === 'string' &&
|
|
28
|
+
(value.inbox_identifier === undefined || isInbox(value.inbox_identifier)));
|
|
29
|
+
}
|
|
30
|
+
// The contact as the Skips App answers with it, from its own route or by way of the code service.
|
|
31
|
+
export function contactSessionFrom(value) {
|
|
32
|
+
if (!isContactResponse(value))
|
|
33
|
+
return null;
|
|
34
|
+
const contact = { sourceId: value.source_id, pubsubToken: value.pubsub_token };
|
|
35
|
+
return value.inbox_identifier === undefined
|
|
36
|
+
? contact
|
|
37
|
+
: { ...contact, inboxIdentifier: value.inbox_identifier };
|
|
38
|
+
}
|
|
39
|
+
function isConversationResponse(value) {
|
|
40
|
+
return isRecord(value) && typeof value.id === 'number';
|
|
41
|
+
}
|
|
42
|
+
function isSecureAddress(address) {
|
|
43
|
+
try {
|
|
44
|
+
return new URL(address).protocol === 'https:';
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The visitor's view of a Skips App message, or null for one they never see. A care team member's photo shows only
|
|
52
|
+
* from a secure web address, so the page never loads one over plain http, and their name stays without it.
|
|
53
|
+
*/
|
|
54
|
+
export function visitorView(raw, careTeamName) {
|
|
55
|
+
const message = toChatMessage(raw, careTeamName);
|
|
56
|
+
const photo = message?.author?.avatarUrl;
|
|
57
|
+
if (!message?.author || photo === null || photo === undefined || isSecureAddress(photo)) {
|
|
58
|
+
return message;
|
|
59
|
+
}
|
|
60
|
+
return { ...message, author: { ...message.author, avatarUrl: null } };
|
|
61
|
+
}
|
|
62
|
+
export class ChatClient {
|
|
63
|
+
baseUrl;
|
|
64
|
+
inbox;
|
|
65
|
+
careTeamName;
|
|
66
|
+
fetchFn;
|
|
67
|
+
constructor({ baseUrl, inboxIdentifier, careTeamName, fetchFn = fetch }) {
|
|
68
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
69
|
+
this.inbox = inboxIdentifier;
|
|
70
|
+
this.careTeamName = careTeamName;
|
|
71
|
+
this.fetchFn = fetchFn;
|
|
72
|
+
}
|
|
73
|
+
// A message the Skips App answered with, as the visitor sees it. One it stored for the visitor is never hidden.
|
|
74
|
+
messageFrom(body) {
|
|
75
|
+
const message = isRawMessage(body) ? visitorView(body, this.careTeamName) : null;
|
|
76
|
+
if (!message)
|
|
77
|
+
throw new ChatApiError(200, 'unexpected message shape');
|
|
78
|
+
return message;
|
|
79
|
+
}
|
|
80
|
+
url(inbox, path) {
|
|
81
|
+
return `${this.baseUrl}/public/api/v1/inboxes/${inbox}${path}`;
|
|
82
|
+
}
|
|
83
|
+
// A contact's own routes are in the inbox the contact was made in.
|
|
84
|
+
inboxOf(contact) {
|
|
85
|
+
return contact.inboxIdentifier ?? this.inbox;
|
|
86
|
+
}
|
|
87
|
+
async request(inbox, path, init = {}) {
|
|
88
|
+
const headers = new Headers(init.headers);
|
|
89
|
+
// A photo goes as a form, whose boundary fetch writes into the type itself.
|
|
90
|
+
if (!(init.body instanceof FormData))
|
|
91
|
+
headers.set('Content-Type', 'application/json');
|
|
92
|
+
headers.set('Accept', 'application/json');
|
|
93
|
+
// Called detached: window.fetch throws when any other object is its this.
|
|
94
|
+
const { fetchFn } = this;
|
|
95
|
+
const response = await fetchFn(this.url(inbox, path), { ...init, headers });
|
|
96
|
+
const body = await response.json().catch(() => null);
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const detail = isRecord(body) && typeof body.error === 'string' ? body.error : response.statusText;
|
|
99
|
+
throw new ChatApiError(response.status, detail);
|
|
100
|
+
}
|
|
101
|
+
return body;
|
|
102
|
+
}
|
|
103
|
+
async createContact() {
|
|
104
|
+
const contact = contactSessionFrom(await this.request(this.inbox, '/contacts', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
body: JSON.stringify({}),
|
|
107
|
+
}));
|
|
108
|
+
if (!contact)
|
|
109
|
+
throw new ChatApiError(200, 'unexpected contact shape');
|
|
110
|
+
return contact;
|
|
111
|
+
}
|
|
112
|
+
/** `startingPage` is the page the chat was opened on, which the Skips App shows the care team as the conversation's referer. */
|
|
113
|
+
async createConversation(contact, startingPage) {
|
|
114
|
+
// Skips asks a question with buttons only in a conversation the website chat opened.
|
|
115
|
+
const conversation = startingPage
|
|
116
|
+
? { referer_url: startingPage, custom_attributes: { [WEBSITE_CHAT_ATTRIBUTE]: true } }
|
|
117
|
+
: {};
|
|
118
|
+
const body = await this.request(this.inboxOf(contact), `/contacts/${contact.sourceId}/conversations`, { method: 'POST', body: JSON.stringify(conversation) });
|
|
119
|
+
if (!isConversationResponse(body))
|
|
120
|
+
throw new ChatApiError(200, 'unexpected conversation shape');
|
|
121
|
+
return { id: body.id };
|
|
122
|
+
}
|
|
123
|
+
// The visitor's answer to a question from Skips, kept on the question itself, where a reload finds it.
|
|
124
|
+
async choose(contact, conversation, question, answer) {
|
|
125
|
+
const body = await this.request(this.inboxOf(contact), `/contacts/${contact.sourceId}/conversations/${String(conversation.id)}/messages/${String(question)}`, {
|
|
126
|
+
method: 'PATCH',
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
submitted_values: [{ title: answer.title, value: answer.value }],
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
return this.messageFrom(body);
|
|
132
|
+
}
|
|
133
|
+
conversationPath(contact, conversation, path) {
|
|
134
|
+
return `/contacts/${contact.sourceId}/conversations/${String(conversation.id)}${path}`;
|
|
135
|
+
}
|
|
136
|
+
// A line, a photo or PDF, or both. The description types the JSON body only: the form's attachments[] is what
|
|
137
|
+
// the Skips App's Chatwoot takes for files (app/controllers/public/api/v1/inboxes/messages_controller.rb).
|
|
138
|
+
async sendMessage(contact, conversation, text, picked = null) {
|
|
139
|
+
let body = JSON.stringify({ content: text });
|
|
140
|
+
if (picked) {
|
|
141
|
+
if (!picked.file)
|
|
142
|
+
throw new ChatApiError(0, 'picked file has no contents');
|
|
143
|
+
const form = new FormData();
|
|
144
|
+
form.append('content', text);
|
|
145
|
+
form.append('attachments[]', picked.file, picked.name);
|
|
146
|
+
body = form;
|
|
147
|
+
}
|
|
148
|
+
const answer = await this.request(this.inboxOf(contact), this.conversationPath(contact, conversation, '/messages'), { method: 'POST', body });
|
|
149
|
+
return this.messageFrom(answer);
|
|
150
|
+
}
|
|
151
|
+
// The newest page, or the page before a stored message.
|
|
152
|
+
async listMessages(contact, conversation, before) {
|
|
153
|
+
const query = before === undefined ? '' : `?before=${String(before)}`;
|
|
154
|
+
const body = await this.request(this.inboxOf(contact), this.conversationPath(contact, conversation, `/messages${query}`));
|
|
155
|
+
if (!Array.isArray(body))
|
|
156
|
+
throw new ChatApiError(200, 'unexpected messages shape');
|
|
157
|
+
return body.filter(isRawMessage).flatMap((raw) => visitorView(raw, this.careTeamName) ?? []);
|
|
158
|
+
}
|
|
159
|
+
// Whether the visitor is typing, for the care team.
|
|
160
|
+
async typing(contact, conversation, on) {
|
|
161
|
+
const status = { typing_status: on ? 'on' : 'off' };
|
|
162
|
+
await this.request(this.inboxOf(contact), this.conversationPath(contact, conversation, '/toggle_typing'), {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: JSON.stringify(status),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// The visitor has read the conversation up to now.
|
|
168
|
+
async seen(contact, conversation) {
|
|
169
|
+
await this.request(this.inboxOf(contact), this.conversationPath(contact, conversation, '/update_last_seen'), {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ChatApiError, ChatClient, WEBSITE_CHAT_ATTRIBUTE, type ContactSession } from './client.ts';
|
|
2
|
+
export { clearSession, loadReadThrough, loadSession, saveReadThrough, saveSession, SESSION_IDLE_MS, sessionEndsAt, type ChatSession, type SessionStore, } from './session.ts';
|
|
3
|
+
export { platformSocket, platformStore } from './platform.ts';
|
|
4
|
+
export { CODE_LENGTH } from './verify.ts';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The chat's Skips App connection without its screen, for code that runs where React Native does not, as an app's
|
|
2
|
+
// Node tests and a header that only counts replies do.
|
|
3
|
+
export { ChatApiError, ChatClient, WEBSITE_CHAT_ATTRIBUTE } from "./client.js";
|
|
4
|
+
export { clearSession, loadReadThrough, loadSession, saveReadThrough, saveSession, SESSION_IDLE_MS, sessionEndsAt, } from "./session.js";
|
|
5
|
+
export { platformSocket, platformStore } from "./platform.js";
|
|
6
|
+
export { CODE_LENGTH } from "./verify.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { AccountId, AgentBotId, BadRequestError, ClientOptions, ContactSortParam, ConversationId, ConversationUuid, CreateAContactData, CreateAContactError, CreateAContactErrors, CreateAContactResponse, CreateAContactResponses, CreateAConversationData, CreateAConversationError, CreateAConversationErrors, CreateAConversationResponse, CreateAConversationResponses, CreateAMessageData, CreateAMessageError, CreateAMessageErrors, CreateAMessageResponse, CreateAMessageResponses, CustomFilterId, HookId, InboxId, MessageId, Page, PlatformUserId, PortalSlug, PublicContact, PublicContactCreateUpdatePayload, PublicContactIdentifier, PublicContactRecord, PublicConversation, PublicConversationCreatePayload, PublicInbox, PublicInboxIdentifier, PublicMessage, PublicMessageAttachment, PublicMessageCreatePayload, PublicMessageSender, PublicMessageUpdatePayload, ReportMetric, ReportType, RequestError, SourceId, TeamId, ToggleTypingStatusData, ToggleTypingStatusError, ToggleTypingStatusErrors, ToggleTypingStatusResponses, UpdateAMessageData, UpdateAMessageError, UpdateAMessageErrors, UpdateAMessageResponse, UpdateAMessageResponses, WebhookId } from './types.gen';
|