@appsupportkit/support-sdk 0.3.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 (208) hide show
  1. package/lib/commonjs/api.js +49 -15
  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 +123 -97
  8. package/lib/commonjs/components/SupportButton.js.map +1 -1
  9. package/lib/commonjs/components/icons.js +81 -110
  10. package/lib/commonjs/components/icons.js.map +1 -1
  11. package/lib/commonjs/components/primitives.js +0 -153
  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/i18n/strings/de.js +3 -0
  16. package/lib/commonjs/i18n/strings/de.js.map +1 -1
  17. package/lib/commonjs/i18n/strings/en.js +3 -0
  18. package/lib/commonjs/i18n/strings/en.js.map +1 -1
  19. package/lib/commonjs/i18n/strings/es.js +3 -0
  20. package/lib/commonjs/i18n/strings/es.js.map +1 -1
  21. package/lib/commonjs/i18n/strings/fr.js +3 -0
  22. package/lib/commonjs/i18n/strings/fr.js.map +1 -1
  23. package/lib/commonjs/i18n/strings/hi.js +3 -0
  24. package/lib/commonjs/i18n/strings/hi.js.map +1 -1
  25. package/lib/commonjs/i18n/strings/it.js +3 -0
  26. package/lib/commonjs/i18n/strings/it.js.map +1 -1
  27. package/lib/commonjs/i18n/strings/ja.js +3 -0
  28. package/lib/commonjs/i18n/strings/ja.js.map +1 -1
  29. package/lib/commonjs/i18n/strings/ko.js +3 -0
  30. package/lib/commonjs/i18n/strings/ko.js.map +1 -1
  31. package/lib/commonjs/i18n/strings/nl.js +3 -0
  32. package/lib/commonjs/i18n/strings/nl.js.map +1 -1
  33. package/lib/commonjs/i18n/strings/pl.js +3 -0
  34. package/lib/commonjs/i18n/strings/pl.js.map +1 -1
  35. package/lib/commonjs/i18n/strings/pt.js +3 -0
  36. package/lib/commonjs/i18n/strings/pt.js.map +1 -1
  37. package/lib/commonjs/i18n/strings/ru.js +3 -0
  38. package/lib/commonjs/i18n/strings/ru.js.map +1 -1
  39. package/lib/commonjs/i18n/strings/tr.js +3 -0
  40. package/lib/commonjs/i18n/strings/tr.js.map +1 -1
  41. package/lib/commonjs/i18n/strings/zh.js +3 -0
  42. package/lib/commonjs/i18n/strings/zh.js.map +1 -1
  43. package/lib/commonjs/index.js +7 -0
  44. package/lib/commonjs/index.js.map +1 -1
  45. package/lib/commonjs/push.js +78 -3
  46. package/lib/commonjs/push.js.map +1 -1
  47. package/lib/commonjs/review.js +54 -0
  48. package/lib/commonjs/review.js.map +1 -0
  49. package/lib/commonjs/theme.js +142 -23
  50. package/lib/commonjs/theme.js.map +1 -1
  51. package/lib/commonjs/types.js +21 -3
  52. package/lib/commonjs/types.js.map +1 -1
  53. package/lib/commonjs/useSupportThreads.js +22 -15
  54. package/lib/commonjs/useSupportThreads.js.map +1 -1
  55. package/lib/commonjs/utils.js +13 -33
  56. package/lib/commonjs/utils.js.map +1 -1
  57. package/lib/commonjs/version.js +13 -0
  58. package/lib/commonjs/version.js.map +1 -0
  59. package/lib/module/api.js +46 -13
  60. package/lib/module/api.js.map +1 -1
  61. package/lib/module/components/BottomSheet.js +1 -1
  62. package/lib/module/components/BottomSheet.js.map +1 -1
  63. package/lib/module/components/LauncherButton.js +5 -5
  64. package/lib/module/components/LauncherButton.js.map +1 -1
  65. package/lib/module/components/SupportButton.js +128 -102
  66. package/lib/module/components/SupportButton.js.map +1 -1
  67. package/lib/module/components/icons.js +80 -107
  68. package/lib/module/components/icons.js.map +1 -1
  69. package/lib/module/components/primitives.js +2 -146
  70. package/lib/module/components/primitives.js.map +1 -1
  71. package/lib/module/components/screens/ChatScreen.js +485 -0
  72. package/lib/module/components/screens/ChatScreen.js.map +1 -0
  73. package/lib/module/i18n/strings/de.js +3 -0
  74. package/lib/module/i18n/strings/de.js.map +1 -1
  75. package/lib/module/i18n/strings/en.js +3 -0
  76. package/lib/module/i18n/strings/en.js.map +1 -1
  77. package/lib/module/i18n/strings/es.js +3 -0
  78. package/lib/module/i18n/strings/es.js.map +1 -1
  79. package/lib/module/i18n/strings/fr.js +3 -0
  80. package/lib/module/i18n/strings/fr.js.map +1 -1
  81. package/lib/module/i18n/strings/hi.js +3 -0
  82. package/lib/module/i18n/strings/hi.js.map +1 -1
  83. package/lib/module/i18n/strings/it.js +3 -0
  84. package/lib/module/i18n/strings/it.js.map +1 -1
  85. package/lib/module/i18n/strings/ja.js +3 -0
  86. package/lib/module/i18n/strings/ja.js.map +1 -1
  87. package/lib/module/i18n/strings/ko.js +3 -0
  88. package/lib/module/i18n/strings/ko.js.map +1 -1
  89. package/lib/module/i18n/strings/nl.js +3 -0
  90. package/lib/module/i18n/strings/nl.js.map +1 -1
  91. package/lib/module/i18n/strings/pl.js +3 -0
  92. package/lib/module/i18n/strings/pl.js.map +1 -1
  93. package/lib/module/i18n/strings/pt.js +3 -0
  94. package/lib/module/i18n/strings/pt.js.map +1 -1
  95. package/lib/module/i18n/strings/ru.js +3 -0
  96. package/lib/module/i18n/strings/ru.js.map +1 -1
  97. package/lib/module/i18n/strings/tr.js +3 -0
  98. package/lib/module/i18n/strings/tr.js.map +1 -1
  99. package/lib/module/i18n/strings/zh.js +3 -0
  100. package/lib/module/i18n/strings/zh.js.map +1 -1
  101. package/lib/module/index.js +1 -0
  102. package/lib/module/index.js.map +1 -1
  103. package/lib/module/push.js +77 -3
  104. package/lib/module/push.js.map +1 -1
  105. package/lib/module/review.js +48 -0
  106. package/lib/module/review.js.map +1 -0
  107. package/lib/module/theme.js +141 -23
  108. package/lib/module/theme.js.map +1 -1
  109. package/lib/module/types.js +21 -3
  110. package/lib/module/types.js.map +1 -1
  111. package/lib/module/useSupportThreads.js +22 -15
  112. package/lib/module/useSupportThreads.js.map +1 -1
  113. package/lib/module/utils.js +12 -32
  114. package/lib/module/utils.js.map +1 -1
  115. package/lib/module/version.js +9 -0
  116. package/lib/module/version.js.map +1 -0
  117. package/lib/typescript/api.d.ts +33 -12
  118. package/lib/typescript/api.d.ts.map +1 -1
  119. package/lib/typescript/components/SupportButton.d.ts +2 -2
  120. package/lib/typescript/components/SupportButton.d.ts.map +1 -1
  121. package/lib/typescript/components/icons.d.ts +5 -14
  122. package/lib/typescript/components/icons.d.ts.map +1 -1
  123. package/lib/typescript/components/primitives.d.ts +0 -31
  124. package/lib/typescript/components/primitives.d.ts.map +1 -1
  125. package/lib/typescript/components/screens/ChatScreen.d.ts +21 -0
  126. package/lib/typescript/components/screens/ChatScreen.d.ts.map +1 -0
  127. package/lib/typescript/i18n/strings/de.d.ts.map +1 -1
  128. package/lib/typescript/i18n/strings/en.d.ts +3 -0
  129. package/lib/typescript/i18n/strings/en.d.ts.map +1 -1
  130. package/lib/typescript/i18n/strings/es.d.ts.map +1 -1
  131. package/lib/typescript/i18n/strings/fr.d.ts.map +1 -1
  132. package/lib/typescript/i18n/strings/hi.d.ts.map +1 -1
  133. package/lib/typescript/i18n/strings/it.d.ts.map +1 -1
  134. package/lib/typescript/i18n/strings/ja.d.ts.map +1 -1
  135. package/lib/typescript/i18n/strings/ko.d.ts.map +1 -1
  136. package/lib/typescript/i18n/strings/nl.d.ts.map +1 -1
  137. package/lib/typescript/i18n/strings/pl.d.ts.map +1 -1
  138. package/lib/typescript/i18n/strings/pt.d.ts.map +1 -1
  139. package/lib/typescript/i18n/strings/ru.d.ts.map +1 -1
  140. package/lib/typescript/i18n/strings/tr.d.ts.map +1 -1
  141. package/lib/typescript/i18n/strings/zh.d.ts.map +1 -1
  142. package/lib/typescript/index.d.ts +3 -2
  143. package/lib/typescript/index.d.ts.map +1 -1
  144. package/lib/typescript/push.d.ts +14 -2
  145. package/lib/typescript/push.d.ts.map +1 -1
  146. package/lib/typescript/review.d.ts +15 -0
  147. package/lib/typescript/review.d.ts.map +1 -0
  148. package/lib/typescript/theme.d.ts +23 -5
  149. package/lib/typescript/theme.d.ts.map +1 -1
  150. package/lib/typescript/types.d.ts +54 -11
  151. package/lib/typescript/types.d.ts.map +1 -1
  152. package/lib/typescript/useSupportThreads.d.ts +9 -3
  153. package/lib/typescript/useSupportThreads.d.ts.map +1 -1
  154. package/lib/typescript/utils.d.ts +3 -2
  155. package/lib/typescript/utils.d.ts.map +1 -1
  156. package/lib/typescript/version.d.ts +7 -0
  157. package/lib/typescript/version.d.ts.map +1 -0
  158. package/package.json +1 -1
  159. package/src/api.ts +59 -30
  160. package/src/components/BottomSheet.tsx +1 -1
  161. package/src/components/LauncherButton.tsx +4 -4
  162. package/src/components/SupportButton.tsx +140 -111
  163. package/src/components/icons.tsx +60 -101
  164. package/src/components/primitives.tsx +1 -163
  165. package/src/components/screens/ChatScreen.tsx +541 -0
  166. package/src/i18n/strings/de.ts +3 -0
  167. package/src/i18n/strings/en.ts +3 -0
  168. package/src/i18n/strings/es.ts +3 -0
  169. package/src/i18n/strings/fr.ts +3 -0
  170. package/src/i18n/strings/hi.ts +3 -0
  171. package/src/i18n/strings/it.ts +3 -0
  172. package/src/i18n/strings/ja.ts +3 -0
  173. package/src/i18n/strings/ko.ts +3 -0
  174. package/src/i18n/strings/nl.ts +3 -0
  175. package/src/i18n/strings/pl.ts +3 -0
  176. package/src/i18n/strings/pt.ts +3 -0
  177. package/src/i18n/strings/ru.ts +3 -0
  178. package/src/i18n/strings/tr.ts +3 -0
  179. package/src/i18n/strings/zh.ts +3 -0
  180. package/src/index.tsx +5 -0
  181. package/src/push.ts +100 -2
  182. package/src/review.ts +54 -0
  183. package/src/theme.ts +182 -28
  184. package/src/types.ts +53 -11
  185. package/src/useSupportThreads.ts +31 -18
  186. package/src/utils.ts +14 -21
  187. package/src/version.ts +6 -0
  188. package/lib/commonjs/components/screens/ComposeScreen.js +0 -219
  189. package/lib/commonjs/components/screens/ComposeScreen.js.map +0 -1
  190. package/lib/commonjs/components/screens/HomeScreen.js +0 -334
  191. package/lib/commonjs/components/screens/HomeScreen.js.map +0 -1
  192. package/lib/commonjs/components/screens/ThreadScreen.js +0 -339
  193. package/lib/commonjs/components/screens/ThreadScreen.js.map +0 -1
  194. package/lib/module/components/screens/ComposeScreen.js +0 -217
  195. package/lib/module/components/screens/ComposeScreen.js.map +0 -1
  196. package/lib/module/components/screens/HomeScreen.js +0 -330
  197. package/lib/module/components/screens/HomeScreen.js.map +0 -1
  198. package/lib/module/components/screens/ThreadScreen.js +0 -337
  199. package/lib/module/components/screens/ThreadScreen.js.map +0 -1
  200. package/lib/typescript/components/screens/ComposeScreen.d.ts +0 -12
  201. package/lib/typescript/components/screens/ComposeScreen.d.ts.map +0 -1
  202. package/lib/typescript/components/screens/HomeScreen.d.ts +0 -19
  203. package/lib/typescript/components/screens/HomeScreen.d.ts.map +0 -1
  204. package/lib/typescript/components/screens/ThreadScreen.d.ts +0 -12
  205. package/lib/typescript/components/screens/ThreadScreen.d.ts.map +0 -1
  206. package/src/components/screens/ComposeScreen.tsx +0 -229
  207. package/src/components/screens/HomeScreen.tsx +0 -346
  208. package/src/components/screens/ThreadScreen.tsx +0 -424
@@ -1,10 +1,11 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react';
2
- import { Text, View } from 'react-native';
2
+ import { Platform, Text, useColorScheme, View } from 'react-native';
3
3
  import {
4
- postMessage,
5
4
  registerPush,
5
+ registerUser,
6
+ reviewOpened,
7
+ sendMessage,
6
8
  setLanguage as setLanguageRequest,
7
- submitQuery,
8
9
  SupportApiError,
9
10
  type ApiConfig,
10
11
  } from '../api';
@@ -15,34 +16,50 @@ import {
15
16
  deviceLanguage,
16
17
  type Language,
17
18
  } from '../i18n/languages';
18
- import { ensurePushPermission, isPushAvailable } from '../push';
19
- import { resolveTheme } from '../theme';
20
- import type { QueryType, SupportButtonProps, Thread } from '../types';
19
+ import {
20
+ ensurePushPermission,
21
+ isPushAvailable,
22
+ onSupportNotificationTap,
23
+ } from '../push';
24
+ import { openStoreReview, storeLinks, storePlatform } from '../review';
25
+ import { themeFor } from '../theme';
26
+ import type { SupportButtonProps, ThemeName } from '../types';
21
27
  import { IDLE_POLL_MS, useSupportThreads } from '../useSupportThreads';
22
- import { validateProps } from '../utils';
28
+ import { userPlan, validateProps } from '../utils';
23
29
  import { BottomSheet } from './BottomSheet';
24
30
  import { LauncherButton } from './LauncherButton';
25
- import { ComposeScreen } from './screens/ComposeScreen';
26
- import { HomeScreen } from './screens/HomeScreen';
31
+ import { ChatScreen } from './screens/ChatScreen';
27
32
  import { LanguageScreen } from './screens/LanguageScreen';
28
- import { ThreadScreen } from './screens/ThreadScreen';
29
33
 
30
- type Screen =
31
- | { name: 'home' }
32
- | { name: 'language' }
33
- | { name: 'compose'; type: QueryType }
34
- | { name: 'thread'; threadId: string };
34
+ type Screen = 'chat' | 'language';
35
35
 
36
36
  /**
37
37
  * The single entry point of the SDK: a launcher with an unread badge that
38
- * opens the support bottom sheet. Threads, polling, and read receipts are
39
- * handled by useSupportThreads.
38
+ * opens the support chat in a bottom sheet. Conversations, polling, and read
39
+ * receipts are handled by useSupportThreads.
40
40
  */
41
41
  export function SupportButton(props: SupportButtonProps) {
42
42
  const validationError = validateProps(props);
43
+
44
+ // "system" (the default) follows the host app's appearance and re-renders
45
+ // when it changes.
46
+ const colorScheme = useColorScheme();
47
+ const themeName: ThemeName =
48
+ props.theme === 'light' || props.theme === 'dark'
49
+ ? props.theme
50
+ : colorScheme === 'dark'
51
+ ? 'dark'
52
+ : 'light';
53
+ // Picked down to one string first, so an inline `{ light, dark }` object
54
+ // doesn't rebuild the theme on every render.
55
+ const perModeAccent =
56
+ typeof props.accentColor === 'object' && props.accentColor !== null;
57
+ const accent = perModeAccent
58
+ ? (props.accentColor as { light: string; dark: string })[themeName]
59
+ : (props.accentColor as string | undefined);
43
60
  const theme = useMemo(
44
- () => resolveTheme(props.theme, props.accentColor),
45
- [props.theme, props.accentColor],
61
+ () => themeFor(themeName, accent, !perModeAccent),
62
+ [themeName, accent, perModeAccent],
46
63
  );
47
64
 
48
65
  const config: ApiConfig = useMemo(
@@ -55,16 +72,23 @@ export function SupportButton(props: SupportButtonProps) {
55
72
  );
56
73
 
57
74
  const [sheetOpen, setSheetOpen] = useState(false);
58
- const [screen, setScreen] = useState<Screen>({ name: 'home' });
75
+ const [screen, setScreen] = useState<Screen>('chat');
76
+
77
+ /**
78
+ * What the user picked, once they have picked. Until then the sheet is
79
+ * rendered in the host app's hint or the device's language, which is a
80
+ * guess — hence `chosen`, which is what decides whether to ask.
81
+ */
82
+ const [chosen, setChosen] = useState<Language | null>(null);
59
83
 
60
84
  const {
61
85
  threads,
62
86
  totalUnread,
63
87
  loading,
64
- refreshing,
65
88
  errorKey,
66
89
  serverLanguage,
67
90
  languageLoaded,
91
+ store,
68
92
  refresh,
69
93
  reload,
70
94
  } = useSupportThreads({
@@ -72,16 +96,12 @@ export function SupportButton(props: SupportButtonProps) {
72
96
  revenuecatId: props.user.revenuecatId,
73
97
  enabled: validationError === null,
74
98
  sheetOpen,
75
- activeThreadId: screen.name === 'thread' ? screen.threadId : null,
99
+ // Not while the first-run language prompt covers it: nothing behind
100
+ // the prompt has been seen, so nothing may be marked read.
101
+ chatVisible: sheetOpen && screen === 'chat' && chosen !== null,
76
102
  idlePollMs: props.pollIntervalMs ?? IDLE_POLL_MS,
77
103
  });
78
104
 
79
- /**
80
- * What the user picked, once they have picked. Until then the sheet is
81
- * rendered in the host app's hint or the device's language, which is a
82
- * guess — hence `chosen`, which is what decides whether to ask.
83
- */
84
- const [chosen, setChosen] = useState<Language | null>(null);
85
105
  const [draftLanguage, setDraftLanguage] = useState<Language | null>(null);
86
106
  const [savingLanguage, setSavingLanguage] = useState(false);
87
107
  const [languageError, setLanguageError] = useState<string | null>(null);
@@ -129,8 +149,60 @@ export function SupportButton(props: SupportButtonProps) {
129
149
  }
130
150
  }, [config, props.user.revenuecatId, props.expoProjectId, validationError]);
131
151
 
152
+ const { name: userName, revenuecatId, appVersion } = props.user;
153
+ const plan = userPlan(props.user);
154
+
155
+ /**
156
+ * Reports the user on load, and whenever who they are changes, so the team
157
+ * can find and message them before they ever open support. Refreshes the
158
+ * push token too when permission was already granted — never prompting:
159
+ * the OS prompt only appears once the user opens support.
160
+ */
161
+ useEffect(() => {
162
+ if (validationError !== null) return;
163
+ registerUser(config, {
164
+ revenuecatId,
165
+ user: { name: userName, plan, appVersion, platform: storePlatform() },
166
+ }).catch(() => {
167
+ // Best effort; the next load reports again.
168
+ });
169
+ if (!isPushAvailable) return;
170
+ void (async () => {
171
+ const registration = await ensurePushPermission(props.expoProjectId, {
172
+ prompt: false,
173
+ });
174
+ if (registration.status !== 'granted') return;
175
+ await registerPush(config, {
176
+ revenuecatId,
177
+ permissionStatus: registration.status,
178
+ expoPushToken: registration.token,
179
+ asked: false,
180
+ });
181
+ })().catch(() => {});
182
+ }, [
183
+ config,
184
+ validationError,
185
+ revenuecatId,
186
+ userName,
187
+ plan,
188
+ appVersion,
189
+ props.expoProjectId,
190
+ ]);
191
+
192
+ // A tapped support-reply notification opens the chat. If the threads
193
+ // haven't loaded yet (cold start), it fills in as soon as they do.
194
+ useEffect(() => {
195
+ if (validationError !== null) return;
196
+ return onSupportNotificationTap(() => {
197
+ setDraftLanguage(null);
198
+ setLanguageError(null);
199
+ setScreen('chat');
200
+ setSheetOpen(true);
201
+ });
202
+ }, [validationError]);
203
+
132
204
  function openSheet() {
133
- setScreen({ name: 'home' });
205
+ setScreen('chat');
134
206
  setSheetOpen(true);
135
207
  // The OS prompt appears right as the sheet opens; if the user declines,
136
208
  // everything below carries on exactly as before.
@@ -139,7 +211,7 @@ export function SupportButton(props: SupportButtonProps) {
139
211
 
140
212
  function closeSheet() {
141
213
  setSheetOpen(false);
142
- setScreen({ name: 'home' });
214
+ setScreen('chat');
143
215
  setDraftLanguage(null);
144
216
  setLanguageError(null);
145
217
  }
@@ -193,7 +265,7 @@ export function SupportButton(props: SupportButtonProps) {
193
265
  });
194
266
  setChosen(next);
195
267
  setDraftLanguage(null);
196
- setScreen({ name: 'home' });
268
+ setScreen('chat');
197
269
  // On a change, the server re-queues existing replies for translation
198
270
  // into the new language; they arrive over the next poll or two.
199
271
  void reload();
@@ -208,37 +280,13 @@ export function SupportButton(props: SupportButtonProps) {
208
280
  }
209
281
  }
210
282
 
211
- async function handleSubmit(
212
- type: QueryType,
213
- message: string,
214
- ): Promise<string | null> {
215
- try {
216
- await submitQuery(config, {
217
- queryType: type,
218
- message,
219
- userName: props.user.name,
220
- isPaid: props.user.isPaid,
221
- revenuecatId: props.user.revenuecatId,
222
- appVersion: props.user.appVersion,
223
- language,
224
- });
225
- void reload();
226
- return null;
227
- } catch (e) {
228
- return writeErrorMessage(e);
229
- }
230
- }
231
-
232
- async function handleSend(
233
- threadId: string,
234
- message: string,
235
- ): Promise<string | null> {
283
+ async function handleSend(message: string): Promise<string | null> {
236
284
  try {
237
- await postMessage(config, {
238
- queryId: threadId,
239
- revenuecatId: props.user.revenuecatId,
285
+ await sendMessage(config, {
286
+ revenuecatId,
240
287
  message,
241
288
  language,
289
+ user: { name: userName, plan, appVersion, platform: storePlatform() },
242
290
  });
243
291
  await reload();
244
292
  return null;
@@ -247,33 +295,20 @@ export function SupportButton(props: SupportButtonProps) {
247
295
  }
248
296
  }
249
297
 
250
- const activeThread: Thread | null =
251
- screen.name === 'thread'
252
- ? ((threads ?? []).find((t) => t.id === screen.threadId) ?? null)
253
- : null;
298
+ const reviewLinks = storeLinks(store);
299
+ const reviewLabel =
300
+ reviewLinks === null
301
+ ? null
302
+ : strings.t(Platform.OS === 'ios' ? 'review.appStore' : 'review.playStore');
254
303
 
255
- const home = (
256
- <HomeScreen
257
- theme={theme}
258
- strings={strings}
259
- language={language}
260
- threads={threads}
261
- loading={loading}
262
- refreshing={refreshing}
263
- error={errorKey === null ? null : strings.t(errorKey)}
264
- onRefresh={refresh}
265
- onCompose={(type) => setScreen({ name: 'compose', type })}
266
- onOpenThread={(thread) =>
267
- setScreen({ name: 'thread', threadId: thread.id })
268
- }
269
- onChangeLanguage={() => {
270
- setDraftLanguage(language);
271
- setLanguageError(null);
272
- setScreen({ name: 'language' });
273
- }}
274
- onClose={closeSheet}
275
- />
276
- );
304
+ /** Opens the store's review page and lets the team know it was tapped. */
305
+ function openReview(messageId: string) {
306
+ if (reviewLinks === null) return;
307
+ void openStoreReview(reviewLinks);
308
+ reviewOpened(config, { revenuecatId, messageId }).catch(() => {
309
+ // The receipt is for the dashboard; the user already has the store.
310
+ });
311
+ }
277
312
 
278
313
  const languagePicker = (
279
314
  <LanguageScreen
@@ -290,7 +325,7 @@ export function SupportButton(props: SupportButtonProps) {
290
325
  : () => {
291
326
  setDraftLanguage(null);
292
327
  setLanguageError(null);
293
- setScreen({ name: 'home' });
328
+ setScreen('chat');
294
329
  }
295
330
  }
296
331
  onDismiss={mustChooseLanguage ? closeSheet : undefined}
@@ -314,33 +349,27 @@ export function SupportButton(props: SupportButtonProps) {
314
349
  <BottomSheet visible={sheetOpen} onClose={closeSheet} theme={theme}>
315
350
  {/* The one-time prompt outranks whatever screen the user is on: it
316
351
  only ever fires before they have sent anything. */}
317
- {mustChooseLanguage || screen.name === 'language' ? (
352
+ {mustChooseLanguage || screen === 'language' ? (
318
353
  languagePicker
319
354
  ) : (
320
- <>
321
- {screen.name === 'home' && home}
322
- {screen.name === 'compose' && (
323
- <ComposeScreen
324
- theme={theme}
325
- strings={strings}
326
- type={screen.type}
327
- onBack={() => setScreen({ name: 'home' })}
328
- onSubmit={(message) => handleSubmit(screen.type, message)}
329
- />
330
- )}
331
- {screen.name === 'thread' &&
332
- (activeThread !== null ? (
333
- <ThreadScreen
334
- theme={theme}
335
- strings={strings}
336
- thread={activeThread}
337
- onBack={() => setScreen({ name: 'home' })}
338
- onSend={(message) => handleSend(activeThread.id, message)}
339
- />
340
- ) : (
341
- home
342
- ))}
343
- </>
355
+ <ChatScreen
356
+ theme={theme}
357
+ strings={strings}
358
+ language={language}
359
+ threads={threads}
360
+ loading={loading}
361
+ error={errorKey === null ? null : strings.t(errorKey)}
362
+ onRetry={refresh}
363
+ onSend={handleSend}
364
+ onChangeLanguage={() => {
365
+ setDraftLanguage(language);
366
+ setLanguageError(null);
367
+ setScreen('language');
368
+ }}
369
+ onClose={closeSheet}
370
+ reviewLabel={reviewLabel}
371
+ onOpenReview={openReview}
372
+ />
344
373
  )}
345
374
  </BottomSheet>
346
375
  </>
@@ -33,60 +33,6 @@ export function ChatIcon({ color, size = 16 }: { color: string; size?: number })
33
33
  );
34
34
  }
35
35
 
36
- export function PlusCircleIcon({ color, size = 16 }: { color: string; size?: number }) {
37
- const bar = size * 0.45;
38
- return (
39
- <View
40
- style={{
41
- width: size,
42
- height: size,
43
- borderWidth: STROKE,
44
- borderColor: color,
45
- borderRadius: size,
46
- alignItems: 'center',
47
- justifyContent: 'center',
48
- }}
49
- >
50
- <View style={{ width: bar, height: STROKE, backgroundColor: color }} />
51
- <View
52
- style={{
53
- width: STROKE,
54
- height: bar,
55
- backgroundColor: color,
56
- position: 'absolute',
57
- }}
58
- />
59
- </View>
60
- );
61
- }
62
-
63
- export function AlertCircleIcon({ color, size = 16 }: { color: string; size?: number }) {
64
- return (
65
- <View
66
- style={{
67
- width: size,
68
- height: size,
69
- borderWidth: STROKE,
70
- borderColor: color,
71
- borderRadius: size,
72
- alignItems: 'center',
73
- paddingTop: size * 0.2,
74
- }}
75
- >
76
- <View style={{ width: STROKE, height: size * 0.32, backgroundColor: color }} />
77
- <View
78
- style={{
79
- width: STROKE + 0.5,
80
- height: STROKE + 0.5,
81
- borderRadius: 2,
82
- backgroundColor: color,
83
- marginTop: size * 0.09,
84
- }}
85
- />
86
- </View>
87
- );
88
- }
89
-
90
36
  /** Chevron built from two borders, rotated. direction: right | left | down. */
91
37
  export function Chevron({
92
38
  color,
@@ -223,61 +169,74 @@ export function ArrowUpIcon({ color, size = 16 }: { color: string; size?: number
223
169
  }
224
170
 
225
171
  /**
226
- * The chat-bubble mark on the launcher button: a filled bubble with the
227
- * dots knocked out in the button's own fill color. Solid shapes read far
228
- * better than 1.5px strokes at launcher size and on top of a color fill.
172
+ * Geometry of the product mark on a 24-unit square: two overlapping speech
173
+ * bubbles, the front one cut free of the back by a gap. Kept to rounded
174
+ * rects and right-triangle tails so Views can draw it with no svg; the
175
+ * dashboard's LogoMark (apps/web/components/logo.tsx) draws the same numbers.
229
176
  */
230
- export function ChatBubbleMark({
177
+ const MARK = {
178
+ back: { x: 1, y: 2.5, w: 14.5, h: 11, r: 4.5 },
179
+ /** Right angle at top-left, apex straight down. */
180
+ backTail: { x: 3.5, y: 12.25, w: 4.25, h: 4.25 },
181
+ front: { x: 8.5, y: 8.75, w: 14.5, h: 10.5, r: 4.5 },
182
+ /** Right angle at top-right, apex straight down. */
183
+ frontTail: { x: 16.5, y: 18.25, w: 4.25, h: 4.25 },
184
+ gap: 2,
185
+ } as const;
186
+
187
+ /**
188
+ * The mark on the launcher button. Solid shapes read far better than 1.5px
189
+ * strokes at launcher size and on top of a color fill.
190
+ */
191
+ export function ConversationMark({
231
192
  color,
232
- dotColor,
193
+ holeColor,
233
194
  size = 24,
234
195
  }: {
235
196
  color: string;
236
- /** The surface behind the mark — the dots are punched out in this color. */
237
- dotColor: string;
197
+ /** The surface behind the mark — the gap between the bubbles is cut in this color. */
198
+ holeColor: string;
238
199
  size?: number;
239
200
  }) {
240
- const bubbleHeight = size * 0.82;
241
- const dot: ViewStyle = {
242
- width: size * 0.13,
243
- height: size * 0.13,
244
- borderRadius: size,
245
- backgroundColor: dotColor,
246
- };
201
+ const u = size / 24;
202
+ const rect = (
203
+ o: { x: number; y: number; w: number; h: number; r: number },
204
+ fill: string,
205
+ grow = 0,
206
+ ): ViewStyle => ({
207
+ position: 'absolute',
208
+ left: (o.x - grow) * u,
209
+ top: (o.y - grow) * u,
210
+ width: (o.w + grow * 2) * u,
211
+ height: (o.h + grow * 2) * u,
212
+ borderRadius: (o.r + grow) * u,
213
+ backgroundColor: fill,
214
+ });
215
+ // A zero-size box whose top border is colored and one side border is
216
+ // transparent renders as a right triangle; which side picks the corner.
217
+ const tail = (
218
+ o: { x: number; y: number; w: number; h: number },
219
+ side: 'left' | 'right',
220
+ ): ViewStyle => ({
221
+ position: 'absolute',
222
+ left: o.x * u,
223
+ top: o.y * u,
224
+ width: 0,
225
+ height: 0,
226
+ borderTopWidth: o.h * u,
227
+ borderTopColor: color,
228
+ ...(side === 'left'
229
+ ? { borderRightWidth: o.w * u, borderRightColor: 'transparent' }
230
+ : { borderLeftWidth: o.w * u, borderLeftColor: 'transparent' }),
231
+ });
232
+
247
233
  return (
248
- <View style={{ width: size, height: size, justifyContent: 'flex-start' }}>
249
- <View
250
- style={{
251
- width: size,
252
- height: bubbleHeight,
253
- borderRadius: size * 0.33,
254
- backgroundColor: color,
255
- flexDirection: 'row',
256
- alignItems: 'center',
257
- justifyContent: 'center',
258
- gap: size * 0.115,
259
- }}
260
- >
261
- <View style={dot} />
262
- <View style={dot} />
263
- <View style={dot} />
264
- </View>
265
- {/* Tail, drawn as a border triangle so it needs no svg. */}
266
- <View
267
- style={{
268
- position: 'absolute',
269
- left: size * 0.22,
270
- top: bubbleHeight - 1,
271
- width: 0,
272
- height: 0,
273
- borderLeftWidth: size * 0.09,
274
- borderRightWidth: size * 0.16,
275
- borderTopWidth: size * 0.2,
276
- borderLeftColor: 'transparent',
277
- borderRightColor: 'transparent',
278
- borderTopColor: color,
279
- }}
280
- />
234
+ <View style={{ width: size, height: size }}>
235
+ <View style={rect(MARK.back, color)} />
236
+ <View style={tail(MARK.backTail, 'left')} />
237
+ <View style={rect(MARK.front, holeColor, MARK.gap)} />
238
+ <View style={rect(MARK.front, color)} />
239
+ <View style={tail(MARK.frontTail, 'right')} />
281
240
  </View>
282
241
  );
283
242
  }