@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,46 @@
|
|
|
1
|
+
// The platform's WebSocket behind the small shape the live feed expects (openCable). Browsers and phones both have
|
|
2
|
+
// one.
|
|
3
|
+
export function platformSocket(url) {
|
|
4
|
+
const socket = new WebSocket(url);
|
|
5
|
+
const adapter = {
|
|
6
|
+
send: (data) => {
|
|
7
|
+
socket.send(data);
|
|
8
|
+
},
|
|
9
|
+
close: () => {
|
|
10
|
+
socket.close();
|
|
11
|
+
},
|
|
12
|
+
onopen: null,
|
|
13
|
+
onmessage: null,
|
|
14
|
+
onclose: null,
|
|
15
|
+
onerror: null,
|
|
16
|
+
};
|
|
17
|
+
socket.onerror = (event) => {
|
|
18
|
+
adapter.onerror?.(event);
|
|
19
|
+
};
|
|
20
|
+
socket.onopen = (event) => {
|
|
21
|
+
adapter.onopen?.(event);
|
|
22
|
+
};
|
|
23
|
+
socket.onmessage = (event) => {
|
|
24
|
+
adapter.onmessage?.({ data: event.data });
|
|
25
|
+
};
|
|
26
|
+
socket.onclose = (event) => {
|
|
27
|
+
adapter.onclose?.(event);
|
|
28
|
+
};
|
|
29
|
+
return adapter;
|
|
30
|
+
}
|
|
31
|
+
// The tab's sessionStorage on the web, which ends a visitor's chat with its tab. A phone has none, so there the
|
|
32
|
+
// chat keeps it for as long as the app runs.
|
|
33
|
+
export function platformStore() {
|
|
34
|
+
if (typeof sessionStorage !== 'undefined')
|
|
35
|
+
return sessionStorage;
|
|
36
|
+
const kept = new Map();
|
|
37
|
+
return {
|
|
38
|
+
getItem: (key) => kept.get(key) ?? null,
|
|
39
|
+
setItem: (key, value) => {
|
|
40
|
+
kept.set(key, value);
|
|
41
|
+
},
|
|
42
|
+
removeItem: (key) => {
|
|
43
|
+
kept.delete(key);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ContactSession, type Conversation } from './client.ts';
|
|
2
|
+
export interface ChatSession {
|
|
3
|
+
contact: ContactSession;
|
|
4
|
+
conversation: Conversation;
|
|
5
|
+
}
|
|
6
|
+
export interface SessionStore {
|
|
7
|
+
getItem(key: string): string | null;
|
|
8
|
+
setItem(key: string, value: string): void;
|
|
9
|
+
removeItem(key: string): void;
|
|
10
|
+
}
|
|
11
|
+
export declare function sessionKey(inboxIdentifier: string): string;
|
|
12
|
+
export declare const SESSION_IDLE_MS: number;
|
|
13
|
+
export declare function loadSession(store: SessionStore, inboxIdentifier: string, now: number): ChatSession | null;
|
|
14
|
+
export declare function sessionEndsAt(store: SessionStore, inboxIdentifier: string, now: number): number | null;
|
|
15
|
+
export declare function saveSession(store: SessionStore, inboxIdentifier: string, session: ChatSession, now: number): void;
|
|
16
|
+
export declare function clearSession(store: SessionStore, inboxIdentifier: string): void;
|
|
17
|
+
export declare function readKey(inboxIdentifier: string): string;
|
|
18
|
+
export declare function loadReadThrough(store: SessionStore, inboxIdentifier: string): number;
|
|
19
|
+
export declare function saveReadThrough(store: SessionStore, inboxIdentifier: string, id: number): void;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// What the browser keeps so a refresh returns to the same conversation: the contact
|
|
2
|
+
// and the conversation, under one key per inbox. The store is injected (sessionStorage
|
|
3
|
+
// on the web, so a chat ends with its tab), a chat left idle too long is forgotten, and
|
|
4
|
+
// anything unreadable counts as no session. The code a visitor was emailed is never kept.
|
|
5
|
+
import { isInbox, isRecord } from "./client.js";
|
|
6
|
+
export function sessionKey(inboxIdentifier) {
|
|
7
|
+
return `skips-chat:${inboxIdentifier}`;
|
|
8
|
+
}
|
|
9
|
+
function isSession(value) {
|
|
10
|
+
return (isRecord(value) &&
|
|
11
|
+
isRecord(value.contact) &&
|
|
12
|
+
typeof value.contact.sourceId === 'string' &&
|
|
13
|
+
typeof value.contact.pubsubToken === 'string' &&
|
|
14
|
+
(value.contact.inboxIdentifier === undefined || isInbox(value.contact.inboxIdentifier)) &&
|
|
15
|
+
isRecord(value.conversation) &&
|
|
16
|
+
typeof value.conversation.id === 'number');
|
|
17
|
+
}
|
|
18
|
+
// How long a chat stays signed in after it was last used. A visitor who comes back later
|
|
19
|
+
// than this is asked for a new code.
|
|
20
|
+
export const SESSION_IDLE_MS = 30 * 60 * 1000;
|
|
21
|
+
function isKept(value) {
|
|
22
|
+
return isRecord(value) && typeof value.usedAt === 'number' && isSession(value);
|
|
23
|
+
}
|
|
24
|
+
// The session this store holds with when it was last used, if that was within the idle limit. One
|
|
25
|
+
// left idle longer, or left by a version that kept no time with it, is removed so that what opens a
|
|
26
|
+
// conversation does not stay behind.
|
|
27
|
+
function loadKept(store, inboxIdentifier, now) {
|
|
28
|
+
try {
|
|
29
|
+
const key = sessionKey(inboxIdentifier);
|
|
30
|
+
const raw = store.getItem(key);
|
|
31
|
+
if (raw === null)
|
|
32
|
+
return null;
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (isKept(parsed) && now - parsed.usedAt <= SESSION_IDLE_MS)
|
|
35
|
+
return parsed;
|
|
36
|
+
clearSession(store, inboxIdentifier);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function loadSession(store, inboxIdentifier, now) {
|
|
44
|
+
const kept = loadKept(store, inboxIdentifier, now);
|
|
45
|
+
return kept && { contact: kept.contact, conversation: kept.conversation };
|
|
46
|
+
}
|
|
47
|
+
// When the stored session's idle limit runs out, or null when there is no session to run out.
|
|
48
|
+
export function sessionEndsAt(store, inboxIdentifier, now) {
|
|
49
|
+
const kept = loadKept(store, inboxIdentifier, now);
|
|
50
|
+
return kept && kept.usedAt + SESSION_IDLE_MS;
|
|
51
|
+
}
|
|
52
|
+
// Keeps the session as used at `now`. Saving it again starts its idle limit again.
|
|
53
|
+
export function saveSession(store, inboxIdentifier, session, now) {
|
|
54
|
+
try {
|
|
55
|
+
const kept = { ...session, usedAt: now };
|
|
56
|
+
store.setItem(sessionKey(inboxIdentifier), JSON.stringify(kept));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Storage can be blocked; the conversation then lasts one page view.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function clearSession(store, inboxIdentifier) {
|
|
63
|
+
try {
|
|
64
|
+
store.removeItem(sessionKey(inboxIdentifier));
|
|
65
|
+
store.removeItem(readKey(inboxIdentifier));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Nothing to clear if it could not be stored.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function readKey(inboxIdentifier) {
|
|
72
|
+
return `skips-chat-read:${inboxIdentifier}`;
|
|
73
|
+
}
|
|
74
|
+
// The newest message the visitor has had on their screen, by the Skips App's id (they only grow). The
|
|
75
|
+
// header's chat button counts the replies after it as unread, on this page or the next one.
|
|
76
|
+
export function loadReadThrough(store, inboxIdentifier) {
|
|
77
|
+
try {
|
|
78
|
+
const stored = Number(store.getItem(readKey(inboxIdentifier)));
|
|
79
|
+
return Number.isSafeInteger(stored) ? stored : 0;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Only ever moves forward: an older list arriving late does not make a reply unread again.
|
|
86
|
+
export function saveReadThrough(store, inboxIdentifier, id) {
|
|
87
|
+
if (id <= loadReadThrough(store, inboxIdentifier))
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
store.setItem(readKey(inboxIdentifier), String(id));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Storage can be blocked; the count then starts from the first reply of the next page.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type ChatMessage, type ChoiceItem, type PickedFile, type Socket, type Thread, type ThreadMessage } from '../helpers/index.ts';
|
|
2
|
+
import type { SkipsTyping } from '../hooks/index.ts';
|
|
3
|
+
import { type ContactSession } from './client.ts';
|
|
4
|
+
import { type SessionStore } from './session.ts';
|
|
5
|
+
export type ChatStatus = 'starting' | 'ready' | 'unreachable';
|
|
6
|
+
export interface SkipsChatOptions {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
inboxIdentifier: string;
|
|
9
|
+
careTeamName: string;
|
|
10
|
+
pageAddress?: string | undefined;
|
|
11
|
+
fetchFn?: typeof fetch | undefined;
|
|
12
|
+
createSocket?: ((url: string) => Socket) | undefined;
|
|
13
|
+
store?: SessionStore | undefined;
|
|
14
|
+
now?: (() => number) | undefined;
|
|
15
|
+
opening?: string | undefined;
|
|
16
|
+
verifyUrl?: string | undefined;
|
|
17
|
+
idleMs?: number | undefined;
|
|
18
|
+
onSignedOut?: (() => void) | undefined;
|
|
19
|
+
member?: SkipsChatMember | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface SkipsChatMember {
|
|
22
|
+
contact: ContactSession;
|
|
23
|
+
conversationId: number | null;
|
|
24
|
+
onStarted?: ((conversationId: number) => void) | undefined;
|
|
25
|
+
onSent?: ((conversationId: number, message: ChatMessage) => void) | undefined;
|
|
26
|
+
}
|
|
27
|
+
interface WelcomeBase {
|
|
28
|
+
line: string;
|
|
29
|
+
unreachable: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface WelcomeAsking extends WelcomeBase {
|
|
32
|
+
step: 'choosing' | 'email';
|
|
33
|
+
email?: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
export interface WelcomeCode extends WelcomeBase {
|
|
36
|
+
step: 'code';
|
|
37
|
+
email: string;
|
|
38
|
+
noMatch: boolean;
|
|
39
|
+
ranOut: boolean;
|
|
40
|
+
codes: number;
|
|
41
|
+
}
|
|
42
|
+
export interface WelcomeName extends WelcomeBase {
|
|
43
|
+
step: 'name';
|
|
44
|
+
email: string;
|
|
45
|
+
code: string;
|
|
46
|
+
codes: number;
|
|
47
|
+
}
|
|
48
|
+
interface WelcomeThrough extends WelcomeBase {
|
|
49
|
+
step: 'through';
|
|
50
|
+
email: string;
|
|
51
|
+
named: boolean;
|
|
52
|
+
signedIn: boolean;
|
|
53
|
+
lineId: number;
|
|
54
|
+
}
|
|
55
|
+
export type Welcome = WelcomeAsking | WelcomeCode | WelcomeName | WelcomeThrough;
|
|
56
|
+
export interface ChatState {
|
|
57
|
+
thread: Thread;
|
|
58
|
+
status: ChatStatus;
|
|
59
|
+
send: (text: string, picked?: PickedFile | null) => Promise<void>;
|
|
60
|
+
retry: (failed: ThreadMessage) => Promise<void>;
|
|
61
|
+
tryAgain: () => void;
|
|
62
|
+
reconnecting: boolean;
|
|
63
|
+
loadOlder: () => Promise<void>;
|
|
64
|
+
typed: (text: string) => void;
|
|
65
|
+
stillHere: () => void;
|
|
66
|
+
welcome: Welcome | null;
|
|
67
|
+
continueHere: (line: string) => void;
|
|
68
|
+
sendCode: (email: string) => Promise<void>;
|
|
69
|
+
checkCode: (step: WelcomeCode, code: string) => Promise<void>;
|
|
70
|
+
giveName: (step: WelcomeName, name: string) => Promise<void>;
|
|
71
|
+
changeEmail: (step: WelcomeCode | WelcomeName) => void;
|
|
72
|
+
skipsTyping: SkipsTyping;
|
|
73
|
+
choose: (question: ThreadMessage, answer: ChoiceItem) => Promise<void>;
|
|
74
|
+
choosing: string | null;
|
|
75
|
+
choiceFailed: number | null;
|
|
76
|
+
}
|
|
77
|
+
export declare function useSkipsChat({ baseUrl, inboxIdentifier, careTeamName, pageAddress, fetchFn, createSocket, store: givenStore, opening, verifyUrl, idleMs, ...changing }: SkipsChatOptions): ChatState;
|
|
78
|
+
export {};
|