@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.
Files changed (242) hide show
  1. package/lib/commonjs/api.js +61 -12
  2. package/lib/commonjs/api.js.map +1 -1
  3. package/lib/commonjs/components/BottomSheet.js +1 -1
  4. package/lib/commonjs/components/BottomSheet.js.map +1 -1
  5. package/lib/commonjs/components/LauncherButton.js +4 -4
  6. package/lib/commonjs/components/LauncherButton.js.map +1 -1
  7. package/lib/commonjs/components/SupportButton.js +192 -66
  8. package/lib/commonjs/components/SupportButton.js.map +1 -1
  9. package/lib/commonjs/components/icons.js +117 -110
  10. package/lib/commonjs/components/icons.js.map +1 -1
  11. package/lib/commonjs/components/primitives.js +3 -154
  12. package/lib/commonjs/components/primitives.js.map +1 -1
  13. package/lib/commonjs/components/screens/ChatScreen.js +487 -0
  14. package/lib/commonjs/components/screens/ChatScreen.js.map +1 -0
  15. package/lib/commonjs/components/screens/LanguageScreen.js +150 -0
  16. package/lib/commonjs/components/screens/LanguageScreen.js.map +1 -0
  17. package/lib/commonjs/i18n/index.js +101 -0
  18. package/lib/commonjs/i18n/index.js.map +1 -0
  19. package/lib/commonjs/i18n/languages.js +95 -0
  20. package/lib/commonjs/i18n/languages.js.map +1 -0
  21. package/lib/commonjs/i18n/strings/de.js +75 -0
  22. package/lib/commonjs/i18n/strings/de.js.map +1 -0
  23. package/lib/commonjs/i18n/strings/en.js +91 -0
  24. package/lib/commonjs/i18n/strings/en.js.map +1 -0
  25. package/lib/commonjs/i18n/strings/es.js +75 -0
  26. package/lib/commonjs/i18n/strings/es.js.map +1 -0
  27. package/lib/commonjs/i18n/strings/fr.js +75 -0
  28. package/lib/commonjs/i18n/strings/fr.js.map +1 -0
  29. package/lib/commonjs/i18n/strings/hi.js +75 -0
  30. package/lib/commonjs/i18n/strings/hi.js.map +1 -0
  31. package/lib/commonjs/i18n/strings/it.js +75 -0
  32. package/lib/commonjs/i18n/strings/it.js.map +1 -0
  33. package/lib/commonjs/i18n/strings/ja.js +75 -0
  34. package/lib/commonjs/i18n/strings/ja.js.map +1 -0
  35. package/lib/commonjs/i18n/strings/ko.js +75 -0
  36. package/lib/commonjs/i18n/strings/ko.js.map +1 -0
  37. package/lib/commonjs/i18n/strings/nl.js +75 -0
  38. package/lib/commonjs/i18n/strings/nl.js.map +1 -0
  39. package/lib/commonjs/i18n/strings/pl.js +75 -0
  40. package/lib/commonjs/i18n/strings/pl.js.map +1 -0
  41. package/lib/commonjs/i18n/strings/pt.js +75 -0
  42. package/lib/commonjs/i18n/strings/pt.js.map +1 -0
  43. package/lib/commonjs/i18n/strings/ru.js +75 -0
  44. package/lib/commonjs/i18n/strings/ru.js.map +1 -0
  45. package/lib/commonjs/i18n/strings/tr.js +75 -0
  46. package/lib/commonjs/i18n/strings/tr.js.map +1 -0
  47. package/lib/commonjs/i18n/strings/zh.js +75 -0
  48. package/lib/commonjs/i18n/strings/zh.js.map +1 -0
  49. package/lib/commonjs/index.js +38 -0
  50. package/lib/commonjs/index.js.map +1 -1
  51. package/lib/commonjs/push.js +78 -3
  52. package/lib/commonjs/push.js.map +1 -1
  53. package/lib/commonjs/review.js +54 -0
  54. package/lib/commonjs/review.js.map +1 -0
  55. package/lib/commonjs/theme.js +142 -23
  56. package/lib/commonjs/theme.js.map +1 -1
  57. package/lib/commonjs/types.js +32 -3
  58. package/lib/commonjs/types.js.map +1 -1
  59. package/lib/commonjs/useSupportThreads.js +41 -24
  60. package/lib/commonjs/useSupportThreads.js.map +1 -1
  61. package/lib/commonjs/utils.js +25 -30
  62. package/lib/commonjs/utils.js.map +1 -1
  63. package/lib/commonjs/version.js +13 -0
  64. package/lib/commonjs/version.js.map +1 -0
  65. package/lib/module/api.js +57 -10
  66. package/lib/module/api.js.map +1 -1
  67. package/lib/module/components/BottomSheet.js +1 -1
  68. package/lib/module/components/BottomSheet.js.map +1 -1
  69. package/lib/module/components/LauncherButton.js +5 -5
  70. package/lib/module/components/LauncherButton.js.map +1 -1
  71. package/lib/module/components/SupportButton.js +199 -73
  72. package/lib/module/components/SupportButton.js.map +1 -1
  73. package/lib/module/components/icons.js +115 -107
  74. package/lib/module/components/icons.js.map +1 -1
  75. package/lib/module/components/primitives.js +5 -147
  76. package/lib/module/components/primitives.js.map +1 -1
  77. package/lib/module/components/screens/ChatScreen.js +485 -0
  78. package/lib/module/components/screens/ChatScreen.js.map +1 -0
  79. package/lib/module/components/screens/LanguageScreen.js +147 -0
  80. package/lib/module/components/screens/LanguageScreen.js.map +1 -0
  81. package/lib/module/i18n/index.js +56 -0
  82. package/lib/module/i18n/index.js.map +1 -0
  83. package/lib/module/i18n/languages.js +88 -0
  84. package/lib/module/i18n/languages.js.map +1 -0
  85. package/lib/module/i18n/strings/de.js +71 -0
  86. package/lib/module/i18n/strings/de.js.map +1 -0
  87. package/lib/module/i18n/strings/en.js +87 -0
  88. package/lib/module/i18n/strings/en.js.map +1 -0
  89. package/lib/module/i18n/strings/es.js +71 -0
  90. package/lib/module/i18n/strings/es.js.map +1 -0
  91. package/lib/module/i18n/strings/fr.js +71 -0
  92. package/lib/module/i18n/strings/fr.js.map +1 -0
  93. package/lib/module/i18n/strings/hi.js +71 -0
  94. package/lib/module/i18n/strings/hi.js.map +1 -0
  95. package/lib/module/i18n/strings/it.js +71 -0
  96. package/lib/module/i18n/strings/it.js.map +1 -0
  97. package/lib/module/i18n/strings/ja.js +71 -0
  98. package/lib/module/i18n/strings/ja.js.map +1 -0
  99. package/lib/module/i18n/strings/ko.js +71 -0
  100. package/lib/module/i18n/strings/ko.js.map +1 -0
  101. package/lib/module/i18n/strings/nl.js +71 -0
  102. package/lib/module/i18n/strings/nl.js.map +1 -0
  103. package/lib/module/i18n/strings/pl.js +71 -0
  104. package/lib/module/i18n/strings/pl.js.map +1 -0
  105. package/lib/module/i18n/strings/pt.js +71 -0
  106. package/lib/module/i18n/strings/pt.js.map +1 -0
  107. package/lib/module/i18n/strings/ru.js +71 -0
  108. package/lib/module/i18n/strings/ru.js.map +1 -0
  109. package/lib/module/i18n/strings/tr.js +71 -0
  110. package/lib/module/i18n/strings/tr.js.map +1 -0
  111. package/lib/module/i18n/strings/zh.js +71 -0
  112. package/lib/module/i18n/strings/zh.js.map +1 -0
  113. package/lib/module/index.js +2 -0
  114. package/lib/module/index.js.map +1 -1
  115. package/lib/module/push.js +77 -3
  116. package/lib/module/push.js.map +1 -1
  117. package/lib/module/review.js +48 -0
  118. package/lib/module/review.js.map +1 -0
  119. package/lib/module/theme.js +141 -23
  120. package/lib/module/theme.js.map +1 -1
  121. package/lib/module/types.js +32 -3
  122. package/lib/module/types.js.map +1 -1
  123. package/lib/module/useSupportThreads.js +41 -24
  124. package/lib/module/useSupportThreads.js.map +1 -1
  125. package/lib/module/utils.js +24 -29
  126. package/lib/module/utils.js.map +1 -1
  127. package/lib/module/version.js +9 -0
  128. package/lib/module/version.js.map +1 -0
  129. package/lib/typescript/api.d.ts +40 -10
  130. package/lib/typescript/api.d.ts.map +1 -1
  131. package/lib/typescript/components/SupportButton.d.ts +2 -2
  132. package/lib/typescript/components/SupportButton.d.ts.map +1 -1
  133. package/lib/typescript/components/icons.d.ts +10 -14
  134. package/lib/typescript/components/icons.d.ts.map +1 -1
  135. package/lib/typescript/components/primitives.d.ts +3 -30
  136. package/lib/typescript/components/primitives.d.ts.map +1 -1
  137. package/lib/typescript/components/screens/ChatScreen.d.ts +21 -0
  138. package/lib/typescript/components/screens/ChatScreen.d.ts.map +1 -0
  139. package/lib/typescript/components/screens/LanguageScreen.d.ts +30 -0
  140. package/lib/typescript/components/screens/LanguageScreen.d.ts.map +1 -0
  141. package/lib/typescript/i18n/index.d.ts +15 -0
  142. package/lib/typescript/i18n/index.d.ts.map +1 -0
  143. package/lib/typescript/i18n/languages.d.ts +36 -0
  144. package/lib/typescript/i18n/languages.d.ts.map +1 -0
  145. package/lib/typescript/i18n/strings/de.d.ts +3 -0
  146. package/lib/typescript/i18n/strings/de.d.ts.map +1 -0
  147. package/lib/typescript/i18n/strings/en.d.ts +86 -0
  148. package/lib/typescript/i18n/strings/en.d.ts.map +1 -0
  149. package/lib/typescript/i18n/strings/es.d.ts +3 -0
  150. package/lib/typescript/i18n/strings/es.d.ts.map +1 -0
  151. package/lib/typescript/i18n/strings/fr.d.ts +3 -0
  152. package/lib/typescript/i18n/strings/fr.d.ts.map +1 -0
  153. package/lib/typescript/i18n/strings/hi.d.ts +3 -0
  154. package/lib/typescript/i18n/strings/hi.d.ts.map +1 -0
  155. package/lib/typescript/i18n/strings/it.d.ts +3 -0
  156. package/lib/typescript/i18n/strings/it.d.ts.map +1 -0
  157. package/lib/typescript/i18n/strings/ja.d.ts +3 -0
  158. package/lib/typescript/i18n/strings/ja.d.ts.map +1 -0
  159. package/lib/typescript/i18n/strings/ko.d.ts +3 -0
  160. package/lib/typescript/i18n/strings/ko.d.ts.map +1 -0
  161. package/lib/typescript/i18n/strings/nl.d.ts +3 -0
  162. package/lib/typescript/i18n/strings/nl.d.ts.map +1 -0
  163. package/lib/typescript/i18n/strings/pl.d.ts +3 -0
  164. package/lib/typescript/i18n/strings/pl.d.ts.map +1 -0
  165. package/lib/typescript/i18n/strings/pt.d.ts +3 -0
  166. package/lib/typescript/i18n/strings/pt.d.ts.map +1 -0
  167. package/lib/typescript/i18n/strings/ru.d.ts +3 -0
  168. package/lib/typescript/i18n/strings/ru.d.ts.map +1 -0
  169. package/lib/typescript/i18n/strings/tr.d.ts +3 -0
  170. package/lib/typescript/i18n/strings/tr.d.ts.map +1 -0
  171. package/lib/typescript/i18n/strings/zh.d.ts +3 -0
  172. package/lib/typescript/i18n/strings/zh.d.ts.map +1 -0
  173. package/lib/typescript/index.d.ts +4 -2
  174. package/lib/typescript/index.d.ts.map +1 -1
  175. package/lib/typescript/push.d.ts +14 -2
  176. package/lib/typescript/push.d.ts.map +1 -1
  177. package/lib/typescript/review.d.ts +15 -0
  178. package/lib/typescript/review.d.ts.map +1 -0
  179. package/lib/typescript/theme.d.ts +23 -5
  180. package/lib/typescript/theme.d.ts.map +1 -1
  181. package/lib/typescript/types.d.ts +86 -13
  182. package/lib/typescript/types.d.ts.map +1 -1
  183. package/lib/typescript/useSupportThreads.d.ts +20 -4
  184. package/lib/typescript/useSupportThreads.d.ts.map +1 -1
  185. package/lib/typescript/utils.d.ts +8 -6
  186. package/lib/typescript/utils.d.ts.map +1 -1
  187. package/lib/typescript/version.d.ts +7 -0
  188. package/lib/typescript/version.d.ts.map +1 -0
  189. package/package.json +2 -2
  190. package/src/api.ts +75 -23
  191. package/src/components/BottomSheet.tsx +1 -1
  192. package/src/components/LauncherButton.tsx +4 -4
  193. package/src/components/SupportButton.tsx +244 -86
  194. package/src/components/icons.tsx +96 -101
  195. package/src/components/primitives.tsx +9 -161
  196. package/src/components/screens/ChatScreen.tsx +541 -0
  197. package/src/components/screens/LanguageScreen.tsx +172 -0
  198. package/src/i18n/index.ts +81 -0
  199. package/src/i18n/languages.ts +106 -0
  200. package/src/i18n/strings/de.ts +81 -0
  201. package/src/i18n/strings/en.ts +97 -0
  202. package/src/i18n/strings/es.ts +80 -0
  203. package/src/i18n/strings/fr.ts +81 -0
  204. package/src/i18n/strings/hi.ts +80 -0
  205. package/src/i18n/strings/it.ts +80 -0
  206. package/src/i18n/strings/ja.ts +80 -0
  207. package/src/i18n/strings/ko.ts +79 -0
  208. package/src/i18n/strings/nl.ts +80 -0
  209. package/src/i18n/strings/pl.ts +80 -0
  210. package/src/i18n/strings/pt.ts +80 -0
  211. package/src/i18n/strings/ru.ts +80 -0
  212. package/src/i18n/strings/tr.ts +80 -0
  213. package/src/i18n/strings/zh.ts +78 -0
  214. package/src/index.tsx +13 -0
  215. package/src/push.ts +100 -2
  216. package/src/review.ts +54 -0
  217. package/src/theme.ts +182 -28
  218. package/src/types.ts +85 -13
  219. package/src/useSupportThreads.ts +61 -28
  220. package/src/utils.ts +27 -29
  221. package/src/version.ts +6 -0
  222. package/lib/commonjs/components/screens/ComposeScreen.js +0 -228
  223. package/lib/commonjs/components/screens/ComposeScreen.js.map +0 -1
  224. package/lib/commonjs/components/screens/HomeScreen.js +0 -286
  225. package/lib/commonjs/components/screens/HomeScreen.js.map +0 -1
  226. package/lib/commonjs/components/screens/ThreadScreen.js +0 -327
  227. package/lib/commonjs/components/screens/ThreadScreen.js.map +0 -1
  228. package/lib/module/components/screens/ComposeScreen.js +0 -224
  229. package/lib/module/components/screens/ComposeScreen.js.map +0 -1
  230. package/lib/module/components/screens/HomeScreen.js +0 -282
  231. package/lib/module/components/screens/HomeScreen.js.map +0 -1
  232. package/lib/module/components/screens/ThreadScreen.js +0 -325
  233. package/lib/module/components/screens/ThreadScreen.js.map +0 -1
  234. package/lib/typescript/components/screens/ComposeScreen.d.ts +0 -10
  235. package/lib/typescript/components/screens/ComposeScreen.d.ts.map +0 -1
  236. package/lib/typescript/components/screens/HomeScreen.d.ts +0 -14
  237. package/lib/typescript/components/screens/HomeScreen.d.ts.map +0 -1
  238. package/lib/typescript/components/screens/ThreadScreen.d.ts +0 -10
  239. package/lib/typescript/components/screens/ThreadScreen.d.ts.map +0 -1
  240. package/src/components/screens/ComposeScreen.tsx +0 -233
  241. package/src/components/screens/HomeScreen.tsx +0 -287
  242. 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
- export type QueryType = 'feedback' | 'feature_request' | 'bug_report';
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
- /** Whether the user is on a paid plan. */
21
- isPaid: boolean;
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
- * Brand color (hex, e.g. "#FF5A1F") replacing the default accent across
45
- * the launcher, buttons, and the user's own message bubbles. The pressed
46
- * shade, the 12% wash, and a legible foreground are derived from it.
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?: string;
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
- admin_name: string | null;
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 = 10;
113
- export const USER_MESSAGE_MAX = 300;
184
+ export const USER_MESSAGE_MIN = 1;
185
+ export const USER_MESSAGE_MAX = 1000;
@@ -21,7 +21,8 @@ import {
21
21
  SupportApiError,
22
22
  type ApiConfig,
23
23
  } from './api';
24
- import type { Thread } from './types';
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 (staring at a thread
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 THREAD_POLL_MS = 5_000;
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 loadErrorMessage(e: unknown): string {
59
+ function loadErrorKey(e: unknown): StringKey {
55
60
  if (e instanceof SupportApiError) {
56
- if (e.offline) return "You're offline — check your connection";
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 'Support is unavailable right now';
70
+ return 'error.unavailable';
66
71
  }
67
- if (e.status === 429) return 'Too many requests — try again in a bit';
72
+ if (e.status === 429) return 'error.loadRateLimited';
68
73
  }
69
- return "Couldn't load your conversations";
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
- error: string | null;
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
- activeThreadId,
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
- activeThreadId: string | null;
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 [error, setError] = useState<string | null>(null);
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 activeThreadIdRef = useRef<string | null>(activeThreadId);
150
+ const chatVisibleRef = useRef(chatVisible);
128
151
 
129
152
  useEffect(() => {
130
- activeThreadIdRef.current = activeThreadId;
131
- }, [activeThreadId]);
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
- setError(null);
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 sitting in the thread are
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
- const active = activeThreadIdRef.current;
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) setError(loadErrorMessage(e));
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, markThreadRead, flushReads],
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 a thread marks it read immediately, before the next poll.
340
+ // Opening the chat marks everything read immediately, before the next poll.
311
341
  useEffect(() => {
312
- if (activeThreadId !== null) markThreadRead(activeThreadId);
313
- }, [activeThreadId, markThreadRead]);
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
- : activeThreadId !== null
323
- ? THREAD_POLL_MS
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
- activeThreadId,
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
- error,
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
- /** "2h ago" relative time — mirrors the web dashboard's formatting. */
2
- export function relativeTime(iso: string): string {
3
- const then = new Date(iso).getTime();
4
- const seconds = Math.max(0, Math.floor((Date.now() - then) / 1000));
5
- if (seconds < 60) return 'just now';
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(undefined, {
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 device's locale — shown under a group of messages. */
28
- export function formatTime(iso: string): string {
29
- return new Date(iso).toLocaleTimeString(undefined, {
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 'Today';
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 'Yesterday';
38
+ if (isSameDay(iso, yesterday.toISOString())) return strings.t('time.yesterday');
51
39
  const date = new Date(iso);
52
- return date.toLocaleDateString(undefined, {
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, isPaid, revenuecatId, appVersion }.';
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 (typeof user.isPaid !== 'boolean') {
87
- return 'SupportButton: `user.isPaid` is required (boolean).';
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
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Sent as `x-sdk-version` on every request. The backend uses its presence to
3
+ * tell 0.4+ (one support chat) from older builds still in the App Store, so
4
+ * keep it in step with package.json.
5
+ */
6
+ export const SDK_VERSION = '0.4.0';