@appsupportkit/support-sdk 0.2.0 → 0.4.0
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/lib/commonjs/api.js +61 -12
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/BottomSheet.js +1 -1
- package/lib/commonjs/components/BottomSheet.js.map +1 -1
- package/lib/commonjs/components/LauncherButton.js +4 -4
- package/lib/commonjs/components/LauncherButton.js.map +1 -1
- package/lib/commonjs/components/SupportButton.js +192 -66
- package/lib/commonjs/components/SupportButton.js.map +1 -1
- package/lib/commonjs/components/icons.js +117 -110
- package/lib/commonjs/components/icons.js.map +1 -1
- package/lib/commonjs/components/primitives.js +3 -154
- package/lib/commonjs/components/primitives.js.map +1 -1
- package/lib/commonjs/components/screens/ChatScreen.js +487 -0
- package/lib/commonjs/components/screens/ChatScreen.js.map +1 -0
- package/lib/commonjs/components/screens/LanguageScreen.js +150 -0
- package/lib/commonjs/components/screens/LanguageScreen.js.map +1 -0
- package/lib/commonjs/i18n/index.js +101 -0
- package/lib/commonjs/i18n/index.js.map +1 -0
- package/lib/commonjs/i18n/languages.js +95 -0
- package/lib/commonjs/i18n/languages.js.map +1 -0
- package/lib/commonjs/i18n/strings/de.js +75 -0
- package/lib/commonjs/i18n/strings/de.js.map +1 -0
- package/lib/commonjs/i18n/strings/en.js +91 -0
- package/lib/commonjs/i18n/strings/en.js.map +1 -0
- package/lib/commonjs/i18n/strings/es.js +75 -0
- package/lib/commonjs/i18n/strings/es.js.map +1 -0
- package/lib/commonjs/i18n/strings/fr.js +75 -0
- package/lib/commonjs/i18n/strings/fr.js.map +1 -0
- package/lib/commonjs/i18n/strings/hi.js +75 -0
- package/lib/commonjs/i18n/strings/hi.js.map +1 -0
- package/lib/commonjs/i18n/strings/it.js +75 -0
- package/lib/commonjs/i18n/strings/it.js.map +1 -0
- package/lib/commonjs/i18n/strings/ja.js +75 -0
- package/lib/commonjs/i18n/strings/ja.js.map +1 -0
- package/lib/commonjs/i18n/strings/ko.js +75 -0
- package/lib/commonjs/i18n/strings/ko.js.map +1 -0
- package/lib/commonjs/i18n/strings/nl.js +75 -0
- package/lib/commonjs/i18n/strings/nl.js.map +1 -0
- package/lib/commonjs/i18n/strings/pl.js +75 -0
- package/lib/commonjs/i18n/strings/pl.js.map +1 -0
- package/lib/commonjs/i18n/strings/pt.js +75 -0
- package/lib/commonjs/i18n/strings/pt.js.map +1 -0
- package/lib/commonjs/i18n/strings/ru.js +75 -0
- package/lib/commonjs/i18n/strings/ru.js.map +1 -0
- package/lib/commonjs/i18n/strings/tr.js +75 -0
- package/lib/commonjs/i18n/strings/tr.js.map +1 -0
- package/lib/commonjs/i18n/strings/zh.js +75 -0
- package/lib/commonjs/i18n/strings/zh.js.map +1 -0
- package/lib/commonjs/index.js +38 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/push.js +78 -3
- package/lib/commonjs/push.js.map +1 -1
- package/lib/commonjs/review.js +54 -0
- package/lib/commonjs/review.js.map +1 -0
- package/lib/commonjs/theme.js +142 -23
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/commonjs/types.js +32 -3
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/useSupportThreads.js +41 -24
- package/lib/commonjs/useSupportThreads.js.map +1 -1
- package/lib/commonjs/utils.js +25 -30
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/commonjs/version.js +13 -0
- package/lib/commonjs/version.js.map +1 -0
- package/lib/module/api.js +57 -10
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/BottomSheet.js +1 -1
- package/lib/module/components/BottomSheet.js.map +1 -1
- package/lib/module/components/LauncherButton.js +5 -5
- package/lib/module/components/LauncherButton.js.map +1 -1
- package/lib/module/components/SupportButton.js +199 -73
- package/lib/module/components/SupportButton.js.map +1 -1
- package/lib/module/components/icons.js +115 -107
- package/lib/module/components/icons.js.map +1 -1
- package/lib/module/components/primitives.js +5 -147
- package/lib/module/components/primitives.js.map +1 -1
- package/lib/module/components/screens/ChatScreen.js +485 -0
- package/lib/module/components/screens/ChatScreen.js.map +1 -0
- package/lib/module/components/screens/LanguageScreen.js +147 -0
- package/lib/module/components/screens/LanguageScreen.js.map +1 -0
- package/lib/module/i18n/index.js +56 -0
- package/lib/module/i18n/index.js.map +1 -0
- package/lib/module/i18n/languages.js +88 -0
- package/lib/module/i18n/languages.js.map +1 -0
- package/lib/module/i18n/strings/de.js +71 -0
- package/lib/module/i18n/strings/de.js.map +1 -0
- package/lib/module/i18n/strings/en.js +87 -0
- package/lib/module/i18n/strings/en.js.map +1 -0
- package/lib/module/i18n/strings/es.js +71 -0
- package/lib/module/i18n/strings/es.js.map +1 -0
- package/lib/module/i18n/strings/fr.js +71 -0
- package/lib/module/i18n/strings/fr.js.map +1 -0
- package/lib/module/i18n/strings/hi.js +71 -0
- package/lib/module/i18n/strings/hi.js.map +1 -0
- package/lib/module/i18n/strings/it.js +71 -0
- package/lib/module/i18n/strings/it.js.map +1 -0
- package/lib/module/i18n/strings/ja.js +71 -0
- package/lib/module/i18n/strings/ja.js.map +1 -0
- package/lib/module/i18n/strings/ko.js +71 -0
- package/lib/module/i18n/strings/ko.js.map +1 -0
- package/lib/module/i18n/strings/nl.js +71 -0
- package/lib/module/i18n/strings/nl.js.map +1 -0
- package/lib/module/i18n/strings/pl.js +71 -0
- package/lib/module/i18n/strings/pl.js.map +1 -0
- package/lib/module/i18n/strings/pt.js +71 -0
- package/lib/module/i18n/strings/pt.js.map +1 -0
- package/lib/module/i18n/strings/ru.js +71 -0
- package/lib/module/i18n/strings/ru.js.map +1 -0
- package/lib/module/i18n/strings/tr.js +71 -0
- package/lib/module/i18n/strings/tr.js.map +1 -0
- package/lib/module/i18n/strings/zh.js +71 -0
- package/lib/module/i18n/strings/zh.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/push.js +77 -3
- package/lib/module/push.js.map +1 -1
- package/lib/module/review.js +48 -0
- package/lib/module/review.js.map +1 -0
- package/lib/module/theme.js +141 -23
- package/lib/module/theme.js.map +1 -1
- package/lib/module/types.js +32 -3
- package/lib/module/types.js.map +1 -1
- package/lib/module/useSupportThreads.js +41 -24
- package/lib/module/useSupportThreads.js.map +1 -1
- package/lib/module/utils.js +24 -29
- package/lib/module/utils.js.map +1 -1
- package/lib/module/version.js +9 -0
- package/lib/module/version.js.map +1 -0
- package/lib/typescript/api.d.ts +40 -10
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/SupportButton.d.ts +2 -2
- package/lib/typescript/components/SupportButton.d.ts.map +1 -1
- package/lib/typescript/components/icons.d.ts +10 -14
- package/lib/typescript/components/icons.d.ts.map +1 -1
- package/lib/typescript/components/primitives.d.ts +3 -30
- package/lib/typescript/components/primitives.d.ts.map +1 -1
- package/lib/typescript/components/screens/ChatScreen.d.ts +21 -0
- package/lib/typescript/components/screens/ChatScreen.d.ts.map +1 -0
- package/lib/typescript/components/screens/LanguageScreen.d.ts +30 -0
- package/lib/typescript/components/screens/LanguageScreen.d.ts.map +1 -0
- package/lib/typescript/i18n/index.d.ts +15 -0
- package/lib/typescript/i18n/index.d.ts.map +1 -0
- package/lib/typescript/i18n/languages.d.ts +36 -0
- package/lib/typescript/i18n/languages.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/de.d.ts +3 -0
- package/lib/typescript/i18n/strings/de.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/en.d.ts +86 -0
- package/lib/typescript/i18n/strings/en.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/es.d.ts +3 -0
- package/lib/typescript/i18n/strings/es.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/fr.d.ts +3 -0
- package/lib/typescript/i18n/strings/fr.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/hi.d.ts +3 -0
- package/lib/typescript/i18n/strings/hi.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/it.d.ts +3 -0
- package/lib/typescript/i18n/strings/it.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ja.d.ts +3 -0
- package/lib/typescript/i18n/strings/ja.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ko.d.ts +3 -0
- package/lib/typescript/i18n/strings/ko.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/nl.d.ts +3 -0
- package/lib/typescript/i18n/strings/nl.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/pl.d.ts +3 -0
- package/lib/typescript/i18n/strings/pl.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/pt.d.ts +3 -0
- package/lib/typescript/i18n/strings/pt.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ru.d.ts +3 -0
- package/lib/typescript/i18n/strings/ru.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/tr.d.ts +3 -0
- package/lib/typescript/i18n/strings/tr.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/zh.d.ts +3 -0
- package/lib/typescript/i18n/strings/zh.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +4 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/push.d.ts +14 -2
- package/lib/typescript/push.d.ts.map +1 -1
- package/lib/typescript/review.d.ts +15 -0
- package/lib/typescript/review.d.ts.map +1 -0
- package/lib/typescript/theme.d.ts +23 -5
- package/lib/typescript/theme.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +86 -13
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/useSupportThreads.d.ts +20 -4
- package/lib/typescript/useSupportThreads.d.ts.map +1 -1
- package/lib/typescript/utils.d.ts +8 -6
- package/lib/typescript/utils.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +7 -0
- package/lib/typescript/version.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/api.ts +75 -23
- package/src/components/BottomSheet.tsx +1 -1
- package/src/components/LauncherButton.tsx +4 -4
- package/src/components/SupportButton.tsx +244 -86
- package/src/components/icons.tsx +96 -101
- package/src/components/primitives.tsx +9 -161
- package/src/components/screens/ChatScreen.tsx +541 -0
- package/src/components/screens/LanguageScreen.tsx +172 -0
- package/src/i18n/index.ts +81 -0
- package/src/i18n/languages.ts +106 -0
- package/src/i18n/strings/de.ts +81 -0
- package/src/i18n/strings/en.ts +97 -0
- package/src/i18n/strings/es.ts +80 -0
- package/src/i18n/strings/fr.ts +81 -0
- package/src/i18n/strings/hi.ts +80 -0
- package/src/i18n/strings/it.ts +80 -0
- package/src/i18n/strings/ja.ts +80 -0
- package/src/i18n/strings/ko.ts +79 -0
- package/src/i18n/strings/nl.ts +80 -0
- package/src/i18n/strings/pl.ts +80 -0
- package/src/i18n/strings/pt.ts +80 -0
- package/src/i18n/strings/ru.ts +80 -0
- package/src/i18n/strings/tr.ts +80 -0
- package/src/i18n/strings/zh.ts +78 -0
- package/src/index.tsx +13 -0
- package/src/push.ts +100 -2
- package/src/review.ts +54 -0
- package/src/theme.ts +182 -28
- package/src/types.ts +85 -13
- package/src/useSupportThreads.ts +61 -28
- package/src/utils.ts +27 -29
- package/src/version.ts +6 -0
- package/lib/commonjs/components/screens/ComposeScreen.js +0 -228
- package/lib/commonjs/components/screens/ComposeScreen.js.map +0 -1
- package/lib/commonjs/components/screens/HomeScreen.js +0 -286
- package/lib/commonjs/components/screens/HomeScreen.js.map +0 -1
- package/lib/commonjs/components/screens/ThreadScreen.js +0 -327
- package/lib/commonjs/components/screens/ThreadScreen.js.map +0 -1
- package/lib/module/components/screens/ComposeScreen.js +0 -224
- package/lib/module/components/screens/ComposeScreen.js.map +0 -1
- package/lib/module/components/screens/HomeScreen.js +0 -282
- package/lib/module/components/screens/HomeScreen.js.map +0 -1
- package/lib/module/components/screens/ThreadScreen.js +0 -325
- package/lib/module/components/screens/ThreadScreen.js.map +0 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts +0 -10
- package/lib/typescript/components/screens/ComposeScreen.d.ts.map +0 -1
- package/lib/typescript/components/screens/HomeScreen.d.ts +0 -14
- package/lib/typescript/components/screens/HomeScreen.d.ts.map +0 -1
- package/lib/typescript/components/screens/ThreadScreen.d.ts +0 -10
- package/lib/typescript/components/screens/ThreadScreen.d.ts.map +0 -1
- package/src/components/screens/ComposeScreen.tsx +0 -233
- package/src/components/screens/HomeScreen.tsx +0 -287
- package/src/components/screens/ThreadScreen.tsx +0 -397
package/src/types.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { Language } from './i18n/languages';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* 'support' is the single chat (0.4+). The other three are the typed requests
|
|
6
|
+
* older builds file; their conversations still show in the chat.
|
|
7
|
+
*/
|
|
8
|
+
export type QueryType = 'feedback' | 'feature_request' | 'bug_report' | 'support';
|
|
9
|
+
/** Shown to the support team, which can filter and message users by it. */
|
|
10
|
+
export type UserPlan = 'free' | 'trial' | 'pro';
|
|
4
11
|
export type QueryStatus = 'open' | 'resolved';
|
|
5
12
|
export type MessageSender = 'user' | 'admin';
|
|
6
13
|
export type ThemeName = 'dark' | 'light';
|
|
14
|
+
/** What the host app asks for. "system" follows the app's light/dark appearance. */
|
|
15
|
+
export type ThemePreference = ThemeName | 'system';
|
|
16
|
+
/**
|
|
17
|
+
* A brand color: one hex for both modes, or a pair. A single color is
|
|
18
|
+
* lightened in dark mode (darkened in light) only as far as it takes to
|
|
19
|
+
* stay visible; a pair is used exactly as given.
|
|
20
|
+
*/
|
|
21
|
+
export type AccentColor = string | { light: string; dark: string };
|
|
7
22
|
export type PushPermissionStatus = 'undetermined' | 'granted' | 'denied';
|
|
8
23
|
|
|
9
24
|
/**
|
|
@@ -17,8 +32,17 @@ export type LauncherVariant = 'solid' | 'soft' | 'minimal';
|
|
|
17
32
|
export interface SupportUser {
|
|
18
33
|
/** Display name shown to the support team. */
|
|
19
34
|
name: string;
|
|
20
|
-
/**
|
|
21
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The user's plan: "free", "trial" (in a free trial) or "pro" (paying).
|
|
37
|
+
* With RevenueCat: "trial" when the active entitlement's periodType is
|
|
38
|
+
* TRIAL, "pro" when it is active otherwise, else "free".
|
|
39
|
+
*/
|
|
40
|
+
plan?: UserPlan;
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated Pass `plan`, which can also say "trial". Still accepted:
|
|
43
|
+
* true reads as "pro", false as "free". One of the two is required.
|
|
44
|
+
*/
|
|
45
|
+
isPaid?: boolean;
|
|
22
46
|
/** RevenueCat app user id — identifies the user's threads. */
|
|
23
47
|
revenuecatId: string;
|
|
24
48
|
/** Host app version, e.g. "2.4.1". */
|
|
@@ -38,14 +62,19 @@ export interface SupportButtonProps {
|
|
|
38
62
|
* Edge Functions.
|
|
39
63
|
*/
|
|
40
64
|
supabaseUrl: string;
|
|
41
|
-
/** Visual theme. Matches the admin dashboard tokens. Default "dark". */
|
|
42
|
-
theme?: ThemeName;
|
|
43
65
|
/**
|
|
44
|
-
*
|
|
45
|
-
* the
|
|
46
|
-
*
|
|
66
|
+
* Visual theme. Default "system": follows the host app's light/dark
|
|
67
|
+
* appearance (the device setting, unless the app pins one) and switches
|
|
68
|
+
* live when it changes. Pass "light" or "dark" to pin it.
|
|
69
|
+
*/
|
|
70
|
+
theme?: ThemePreference;
|
|
71
|
+
/**
|
|
72
|
+
* Brand color replacing the default accent across the launcher, buttons,
|
|
73
|
+
* and the user's own message bubbles: a hex like "#FF5A1F", or
|
|
74
|
+
* `{ light, dark }` for a color per mode. The pressed shade, the 12% wash,
|
|
75
|
+
* a text-safe variant, and a legible foreground are derived from it.
|
|
47
76
|
*/
|
|
48
|
-
accentColor?:
|
|
77
|
+
accentColor?: AccentColor;
|
|
49
78
|
/** Launcher style. Default "solid" — the floating action button. */
|
|
50
79
|
variant?: LauncherVariant;
|
|
51
80
|
/** Launcher diameter in px (height when `label` is set). Default 52. */
|
|
@@ -71,23 +100,56 @@ export interface SupportButtonProps {
|
|
|
71
100
|
* installed — it is an optional peer dependency.
|
|
72
101
|
*/
|
|
73
102
|
expoProjectId?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Pre-selects a language in the picker the first time a user opens support.
|
|
105
|
+
* Pass it when the host app already knows what the user reads — an in-app
|
|
106
|
+
* language setting beats the device locale, which is what the SDK falls
|
|
107
|
+
* back to. It is a default, not an override: the user still chooses, and a
|
|
108
|
+
* choice they have already made always wins.
|
|
109
|
+
*/
|
|
110
|
+
defaultLanguage?: Language;
|
|
74
111
|
}
|
|
75
112
|
|
|
76
113
|
export interface ThreadMessage {
|
|
77
114
|
id: string;
|
|
78
115
|
sender: MessageSender;
|
|
116
|
+
/**
|
|
117
|
+
* Already in the user's language: their own messages as they wrote them,
|
|
118
|
+
* support replies translated. The server does the choosing — see the
|
|
119
|
+
* get-threads function.
|
|
120
|
+
*/
|
|
79
121
|
body: string;
|
|
80
122
|
created_at: string;
|
|
81
|
-
|
|
123
|
+
/**
|
|
124
|
+
* A support reply whose translation hasn't landed yet. `body` currently
|
|
125
|
+
* holds the English original; it will change under the reader shortly, so
|
|
126
|
+
* the UI says so rather than letting it flicker.
|
|
127
|
+
*/
|
|
128
|
+
translation_pending: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* 'review_request': a support reply that carries a store review button.
|
|
131
|
+
* Absent from servers older than 0.4, where every message is text.
|
|
132
|
+
*/
|
|
133
|
+
kind?: 'text' | 'review_request';
|
|
82
134
|
}
|
|
83
135
|
|
|
136
|
+
/** Where a review request's button points; set per app in the dashboard. */
|
|
137
|
+
export interface StoreListing {
|
|
138
|
+
app_store_id: string | null;
|
|
139
|
+
play_store_package: string | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Note the absence of any admin identity: end users see one support team,
|
|
144
|
+
* not the people staffing it, so the names never leave the server. The
|
|
145
|
+
* dashboard still shows who wrote and who resolved what.
|
|
146
|
+
*/
|
|
84
147
|
export interface Thread {
|
|
85
148
|
id: string;
|
|
86
149
|
type: QueryType;
|
|
87
150
|
status: QueryStatus;
|
|
88
151
|
created_at: string;
|
|
89
152
|
resolved_at: string | null;
|
|
90
|
-
resolved_by_name: string | null;
|
|
91
153
|
app_version: string;
|
|
92
154
|
unread_count: number;
|
|
93
155
|
last_message: {
|
|
@@ -101,13 +163,23 @@ export interface Thread {
|
|
|
101
163
|
export interface ThreadsResponse {
|
|
102
164
|
threads: Thread[];
|
|
103
165
|
total_unread: number;
|
|
166
|
+
/** Null when this user has never picked one — the SDK then asks. */
|
|
167
|
+
language: string | null;
|
|
168
|
+
/** The app's store listings, for review requests. */
|
|
169
|
+
store?: StoreListing;
|
|
104
170
|
}
|
|
105
171
|
|
|
172
|
+
/**
|
|
173
|
+
* English labels, kept for host apps that render their own entry points.
|
|
174
|
+
* Inside the sheet the labels come from the string table instead, so they
|
|
175
|
+
* follow the user's language.
|
|
176
|
+
*/
|
|
106
177
|
export const QUERY_TYPE_LABEL: Record<QueryType, string> = {
|
|
107
178
|
feedback: 'Feedback',
|
|
108
179
|
feature_request: 'Feature request',
|
|
109
180
|
bug_report: 'Bug report',
|
|
181
|
+
support: 'Support',
|
|
110
182
|
};
|
|
111
183
|
|
|
112
|
-
export const USER_MESSAGE_MIN =
|
|
113
|
-
export const USER_MESSAGE_MAX =
|
|
184
|
+
export const USER_MESSAGE_MIN = 1;
|
|
185
|
+
export const USER_MESSAGE_MAX = 1000;
|
package/src/useSupportThreads.ts
CHANGED
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
SupportApiError,
|
|
22
22
|
type ApiConfig,
|
|
23
23
|
} from './api';
|
|
24
|
-
import type {
|
|
24
|
+
import type { StringKey } from './i18n';
|
|
25
|
+
import type { StoreListing, Thread } from './types';
|
|
25
26
|
import {
|
|
26
27
|
latestAdminMessageAt,
|
|
27
28
|
pruneWatermarks,
|
|
@@ -30,11 +31,11 @@ import {
|
|
|
30
31
|
type ReadWatermarks,
|
|
31
32
|
} from './unread';
|
|
32
33
|
|
|
33
|
-
// Poll cadence by context: fast where latency is felt (
|
|
34
|
+
// Poll cadence by context: fast where latency is felt (looking at the chat,
|
|
34
35
|
// waiting for a reply), slow when the sheet is closed — and the idle tick
|
|
35
36
|
// uses the count-only endpoint, so it costs ~18 bytes instead of the full
|
|
36
37
|
// thread list.
|
|
37
|
-
const
|
|
38
|
+
const CHAT_POLL_MS = 5_000;
|
|
38
39
|
const SHEET_POLL_MS = 15_000;
|
|
39
40
|
export const IDLE_POLL_MS = 60_000;
|
|
40
41
|
const MIN_POLL_MS = 3_000;
|
|
@@ -50,10 +51,14 @@ const MARK_READ_BACKOFF_MS = 1_500;
|
|
|
50
51
|
* an integration mistake — the end user retrying forever won't fix a key
|
|
51
52
|
* that doesn't match the app — so the real cause is logged for whoever is
|
|
52
53
|
* running the build instead of hidden behind "couldn't load".
|
|
54
|
+
*
|
|
55
|
+
* Returns a key rather than a message: the hook has no business knowing what
|
|
56
|
+
* language the sheet is in, and resolving it here would make every callback
|
|
57
|
+
* depend on the translator.
|
|
53
58
|
*/
|
|
54
|
-
function
|
|
59
|
+
function loadErrorKey(e: unknown): StringKey {
|
|
55
60
|
if (e instanceof SupportApiError) {
|
|
56
|
-
if (e.offline) return
|
|
61
|
+
if (e.offline) return 'error.offline';
|
|
57
62
|
if (e.status === 401 || e.status === 403) {
|
|
58
63
|
if (__DEV__) {
|
|
59
64
|
console.error(
|
|
@@ -62,11 +67,11 @@ function loadErrorMessage(e: unknown): string {
|
|
|
62
67
|
'`appName` matches that app exactly.',
|
|
63
68
|
);
|
|
64
69
|
}
|
|
65
|
-
return '
|
|
70
|
+
return 'error.unavailable';
|
|
66
71
|
}
|
|
67
|
-
if (e.status === 429) return '
|
|
72
|
+
if (e.status === 429) return 'error.loadRateLimited';
|
|
68
73
|
}
|
|
69
|
-
return
|
|
74
|
+
return 'error.loadFailed';
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
interface ReadAttempt {
|
|
@@ -87,7 +92,18 @@ export interface SupportThreadsState {
|
|
|
87
92
|
/** User-pulled refresh with content already on screen. */
|
|
88
93
|
refreshing: boolean;
|
|
89
94
|
/** Set only when there is nothing cached; background failures stay quiet. */
|
|
90
|
-
|
|
95
|
+
errorKey: StringKey | null;
|
|
96
|
+
/** The language this user picked, or null if they never have. */
|
|
97
|
+
serverLanguage: string | null;
|
|
98
|
+
/**
|
|
99
|
+
* False until the first successful load. `serverLanguage === null` only
|
|
100
|
+
* means "never picked" once this is true — before that it means "don't
|
|
101
|
+
* know yet", and showing the picker on a guess would nag a user who
|
|
102
|
+
* answered on another device.
|
|
103
|
+
*/
|
|
104
|
+
languageLoaded: boolean;
|
|
105
|
+
/** The app's store listings, for review request buttons. */
|
|
106
|
+
store: StoreListing | null;
|
|
91
107
|
/** User-initiated refresh (pull-to-refresh, retry button). */
|
|
92
108
|
refresh: () => void;
|
|
93
109
|
/** Awaited by send/submit handlers so the list reflects the write. */
|
|
@@ -99,7 +115,7 @@ export function useSupportThreads({
|
|
|
99
115
|
revenuecatId,
|
|
100
116
|
enabled,
|
|
101
117
|
sheetOpen,
|
|
102
|
-
|
|
118
|
+
chatVisible,
|
|
103
119
|
idlePollMs,
|
|
104
120
|
}: {
|
|
105
121
|
config: ApiConfig;
|
|
@@ -107,14 +123,21 @@ export function useSupportThreads({
|
|
|
107
123
|
/** False when props failed validation — no requests are made. */
|
|
108
124
|
enabled: boolean;
|
|
109
125
|
sheetOpen: boolean;
|
|
110
|
-
|
|
126
|
+
/**
|
|
127
|
+
* The chat is on screen. It shows every conversation at once, so all of
|
|
128
|
+
* them are read while it is — including replies that arrive meanwhile.
|
|
129
|
+
*/
|
|
130
|
+
chatVisible: boolean;
|
|
111
131
|
idlePollMs: number;
|
|
112
132
|
}): SupportThreadsState {
|
|
113
133
|
const [threads, setThreads] = useState<Thread[] | null>(null);
|
|
114
134
|
const [totalUnread, setTotalUnread] = useState(0);
|
|
115
135
|
const [loading, setLoading] = useState(false);
|
|
116
136
|
const [refreshing, setRefreshing] = useState(false);
|
|
117
|
-
const [
|
|
137
|
+
const [errorKey, setErrorKey] = useState<StringKey | null>(null);
|
|
138
|
+
const [serverLanguage, setServerLanguage] = useState<string | null>(null);
|
|
139
|
+
const [languageLoaded, setLanguageLoaded] = useState(false);
|
|
140
|
+
const [store, setStore] = useState<StoreListing | null>(null);
|
|
118
141
|
|
|
119
142
|
const mounted = useRef(true);
|
|
120
143
|
/** Last server payload, before local read reconciliation. */
|
|
@@ -124,11 +147,11 @@ export function useSupportThreads({
|
|
|
124
147
|
const requestSeq = useRef(0);
|
|
125
148
|
const appliedSeq = useRef(0);
|
|
126
149
|
const readEpoch = useRef(0);
|
|
127
|
-
const
|
|
150
|
+
const chatVisibleRef = useRef(chatVisible);
|
|
128
151
|
|
|
129
152
|
useEffect(() => {
|
|
130
|
-
|
|
131
|
-
}, [
|
|
153
|
+
chatVisibleRef.current = chatVisible;
|
|
154
|
+
}, [chatVisible]);
|
|
132
155
|
|
|
133
156
|
useEffect(() => {
|
|
134
157
|
mounted.current = true;
|
|
@@ -235,6 +258,11 @@ export function useSupportThreads({
|
|
|
235
258
|
[publish, sendRead],
|
|
236
259
|
);
|
|
237
260
|
|
|
261
|
+
/** Marks every conversation read — the chat shows them all at once. */
|
|
262
|
+
const markAllRead = useCallback(() => {
|
|
263
|
+
for (const thread of rawThreads.current ?? []) markThreadRead(thread.id);
|
|
264
|
+
}, [markThreadRead]);
|
|
265
|
+
|
|
238
266
|
const fetchThreads = useCallback(
|
|
239
267
|
async (mode: 'initial' | 'user' | 'background') => {
|
|
240
268
|
if (!enabled) return;
|
|
@@ -256,18 +284,20 @@ export function useSupportThreads({
|
|
|
256
284
|
|
|
257
285
|
rawThreads.current = data.threads;
|
|
258
286
|
watermarks.current = pruneWatermarks(watermarks.current, data.threads);
|
|
259
|
-
|
|
287
|
+
setErrorKey(null);
|
|
288
|
+
setServerLanguage(data.language);
|
|
289
|
+
setLanguageLoaded(true);
|
|
290
|
+
setStore(data.store ?? null);
|
|
260
291
|
publish();
|
|
261
292
|
|
|
262
|
-
// Replies that arrived while the user is
|
|
293
|
+
// Replies that arrived while the user is looking at the chat are
|
|
263
294
|
// read on arrival — otherwise the badge climbed while they read.
|
|
264
|
-
|
|
265
|
-
if (active !== null) markThreadRead(active);
|
|
295
|
+
if (chatVisibleRef.current) markAllRead();
|
|
266
296
|
flushReads();
|
|
267
297
|
} catch (e) {
|
|
268
298
|
if (!mounted.current) return;
|
|
269
299
|
// A failed background poll must not blow away cached threads.
|
|
270
|
-
if (rawThreads.current === null)
|
|
300
|
+
if (rawThreads.current === null) setErrorKey(loadErrorKey(e));
|
|
271
301
|
} finally {
|
|
272
302
|
if (mounted.current) {
|
|
273
303
|
if (spinner) setRefreshing(false);
|
|
@@ -275,7 +305,7 @@ export function useSupportThreads({
|
|
|
275
305
|
}
|
|
276
306
|
}
|
|
277
307
|
},
|
|
278
|
-
[config, enabled, revenuecatId, publish,
|
|
308
|
+
[config, enabled, revenuecatId, publish, markAllRead, flushReads],
|
|
279
309
|
);
|
|
280
310
|
|
|
281
311
|
/** Badge-only refresh — the cheap endpoint, used while idle. */
|
|
@@ -307,10 +337,10 @@ export function useSupportThreads({
|
|
|
307
337
|
if (sheetOpen) void fetchThreads('background');
|
|
308
338
|
}, [sheetOpen, fetchThreads]);
|
|
309
339
|
|
|
310
|
-
// Opening
|
|
340
|
+
// Opening the chat marks everything read immediately, before the next poll.
|
|
311
341
|
useEffect(() => {
|
|
312
|
-
if (
|
|
313
|
-
}, [
|
|
342
|
+
if (chatVisible) markAllRead();
|
|
343
|
+
}, [chatVisible, markAllRead]);
|
|
314
344
|
|
|
315
345
|
// Cadence follows what the user is looking at; the app returning to the
|
|
316
346
|
// foreground always ticks.
|
|
@@ -319,8 +349,8 @@ export function useSupportThreads({
|
|
|
319
349
|
const idle = !sheetOpen;
|
|
320
350
|
const intervalMs = idle
|
|
321
351
|
? idlePollMs
|
|
322
|
-
:
|
|
323
|
-
?
|
|
352
|
+
: chatVisible
|
|
353
|
+
? CHAT_POLL_MS
|
|
324
354
|
: SHEET_POLL_MS;
|
|
325
355
|
|
|
326
356
|
const tick = () => {
|
|
@@ -348,7 +378,7 @@ export function useSupportThreads({
|
|
|
348
378
|
}, [
|
|
349
379
|
enabled,
|
|
350
380
|
sheetOpen,
|
|
351
|
-
|
|
381
|
+
chatVisible,
|
|
352
382
|
idlePollMs,
|
|
353
383
|
fetchThreads,
|
|
354
384
|
fetchCount,
|
|
@@ -365,7 +395,10 @@ export function useSupportThreads({
|
|
|
365
395
|
totalUnread,
|
|
366
396
|
loading,
|
|
367
397
|
refreshing,
|
|
368
|
-
|
|
398
|
+
errorKey,
|
|
399
|
+
serverLanguage,
|
|
400
|
+
languageLoaded,
|
|
401
|
+
store,
|
|
369
402
|
refresh,
|
|
370
403
|
reload,
|
|
371
404
|
};
|
package/src/utils.ts
CHANGED
|
@@ -1,32 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const minutes = Math.floor(seconds / 60);
|
|
7
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
8
|
-
const hours = Math.floor(minutes / 60);
|
|
9
|
-
if (hours < 24) return `${hours}h ago`;
|
|
10
|
-
const days = Math.floor(hours / 24);
|
|
11
|
-
if (days < 7) return `${days}d ago`;
|
|
12
|
-
const weeks = Math.floor(days / 7);
|
|
13
|
-
if (weeks < 5) return `${weeks}w ago`;
|
|
14
|
-
const months = Math.floor(days / 30);
|
|
15
|
-
if (months < 12) return `${months}mo ago`;
|
|
16
|
-
return `${Math.floor(days / 365)}y ago`;
|
|
17
|
-
}
|
|
1
|
+
import type { Translator } from './i18n';
|
|
2
|
+
import type { SupportUser, UserPlan } from './types';
|
|
3
|
+
|
|
4
|
+
// Dates and times follow the language the user picked, not the device's:
|
|
5
|
+
// a sheet that says "Traduction…" above "2 days ago" reads as broken.
|
|
18
6
|
|
|
19
|
-
export function formatDate(iso: string): string {
|
|
20
|
-
return new Date(iso).toLocaleDateString(
|
|
7
|
+
export function formatDate(iso: string, strings: Translator): string {
|
|
8
|
+
return new Date(iso).toLocaleDateString(strings.locale, {
|
|
21
9
|
year: 'numeric',
|
|
22
10
|
month: 'short',
|
|
23
11
|
day: 'numeric',
|
|
24
12
|
});
|
|
25
13
|
}
|
|
26
14
|
|
|
27
|
-
/** "14:32" in the
|
|
28
|
-
export function formatTime(iso: string): string {
|
|
29
|
-
return new Date(iso).toLocaleTimeString(
|
|
15
|
+
/** "14:32" in the user's locale — shown under a group of messages. */
|
|
16
|
+
export function formatTime(iso: string, strings: Translator): string {
|
|
17
|
+
return new Date(iso).toLocaleTimeString(strings.locale, {
|
|
30
18
|
hour: 'numeric',
|
|
31
19
|
minute: '2-digit',
|
|
32
20
|
});
|
|
@@ -43,13 +31,13 @@ export function isSameDay(a: string, b: string): boolean {
|
|
|
43
31
|
}
|
|
44
32
|
|
|
45
33
|
/** Date separator label: Today / Yesterday / "Mon, 5 Jan". */
|
|
46
|
-
export function dayLabel(iso: string): string {
|
|
34
|
+
export function dayLabel(iso: string, strings: Translator): string {
|
|
47
35
|
const now = new Date();
|
|
48
|
-
if (isSameDay(iso, now.toISOString())) return '
|
|
36
|
+
if (isSameDay(iso, now.toISOString())) return strings.t('time.today');
|
|
49
37
|
const yesterday = new Date(now.getTime() - 86_400_000);
|
|
50
|
-
if (isSameDay(iso, yesterday.toISOString())) return '
|
|
38
|
+
if (isSameDay(iso, yesterday.toISOString())) return strings.t('time.yesterday');
|
|
51
39
|
const date = new Date(iso);
|
|
52
|
-
return date.toLocaleDateString(
|
|
40
|
+
return date.toLocaleDateString(strings.locale, {
|
|
53
41
|
weekday: 'short',
|
|
54
42
|
month: 'short',
|
|
55
43
|
day: 'numeric',
|
|
@@ -57,6 +45,13 @@ export function dayLabel(iso: string): string {
|
|
|
57
45
|
});
|
|
58
46
|
}
|
|
59
47
|
|
|
48
|
+
const PLANS: UserPlan[] = ['free', 'trial', 'pro'];
|
|
49
|
+
|
|
50
|
+
/** The user's plan, reading the deprecated `isPaid` when `plan` is absent. */
|
|
51
|
+
export function userPlan(user: SupportUser): UserPlan {
|
|
52
|
+
return user.plan ?? (user.isPaid ? 'pro' : 'free');
|
|
53
|
+
}
|
|
54
|
+
|
|
60
55
|
/** Dev-mode prop validation with actionable messages. */
|
|
61
56
|
export function validateProps(props: {
|
|
62
57
|
appName?: unknown;
|
|
@@ -78,13 +73,16 @@ export function validateProps(props: {
|
|
|
78
73
|
}
|
|
79
74
|
const user = props.user as Record<string, unknown> | undefined;
|
|
80
75
|
if (!user || typeof user !== 'object') {
|
|
81
|
-
return 'SupportButton: `user` is required — { name,
|
|
76
|
+
return 'SupportButton: `user` is required — { name, plan, revenuecatId, appVersion }.';
|
|
82
77
|
}
|
|
83
78
|
if (typeof user.name !== 'string' || user.name.length === 0) {
|
|
84
79
|
return 'SupportButton: `user.name` is required (string).';
|
|
85
80
|
}
|
|
86
|
-
if (
|
|
87
|
-
return 'SupportButton: `user.
|
|
81
|
+
if (user.plan !== undefined && !PLANS.includes(user.plan as UserPlan)) {
|
|
82
|
+
return 'SupportButton: `user.plan` must be "free", "trial" or "pro".';
|
|
83
|
+
}
|
|
84
|
+
if (user.plan === undefined && typeof user.isPaid !== 'boolean') {
|
|
85
|
+
return 'SupportButton: `user.plan` is required — "free", "trial" or "pro".';
|
|
88
86
|
}
|
|
89
87
|
if (typeof user.revenuecatId !== 'string' || user.revenuecatId.length === 0) {
|
|
90
88
|
return 'SupportButton: `user.revenuecatId` is required (string) — the RevenueCat app user id.';
|
package/src/version.ts
ADDED