@appsupportkit/support-sdk 0.2.0 → 0.3.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 (196) hide show
  1. package/lib/commonjs/api.js +17 -2
  2. package/lib/commonjs/api.js.map +1 -1
  3. package/lib/commonjs/components/SupportButton.js +123 -23
  4. package/lib/commonjs/components/SupportButton.js.map +1 -1
  5. package/lib/commonjs/components/icons.js +36 -0
  6. package/lib/commonjs/components/icons.js.map +1 -1
  7. package/lib/commonjs/components/primitives.js +9 -7
  8. package/lib/commonjs/components/primitives.js.map +1 -1
  9. package/lib/commonjs/components/screens/ComposeScreen.js +14 -23
  10. package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -1
  11. package/lib/commonjs/components/screens/HomeScreen.js +70 -22
  12. package/lib/commonjs/components/screens/HomeScreen.js.map +1 -1
  13. package/lib/commonjs/components/screens/LanguageScreen.js +150 -0
  14. package/lib/commonjs/components/screens/LanguageScreen.js.map +1 -0
  15. package/lib/commonjs/components/screens/ThreadScreen.js +31 -19
  16. package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -1
  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 +72 -0
  22. package/lib/commonjs/i18n/strings/de.js.map +1 -0
  23. package/lib/commonjs/i18n/strings/en.js +88 -0
  24. package/lib/commonjs/i18n/strings/en.js.map +1 -0
  25. package/lib/commonjs/i18n/strings/es.js +72 -0
  26. package/lib/commonjs/i18n/strings/es.js.map +1 -0
  27. package/lib/commonjs/i18n/strings/fr.js +72 -0
  28. package/lib/commonjs/i18n/strings/fr.js.map +1 -0
  29. package/lib/commonjs/i18n/strings/hi.js +72 -0
  30. package/lib/commonjs/i18n/strings/hi.js.map +1 -0
  31. package/lib/commonjs/i18n/strings/it.js +72 -0
  32. package/lib/commonjs/i18n/strings/it.js.map +1 -0
  33. package/lib/commonjs/i18n/strings/ja.js +72 -0
  34. package/lib/commonjs/i18n/strings/ja.js.map +1 -0
  35. package/lib/commonjs/i18n/strings/ko.js +72 -0
  36. package/lib/commonjs/i18n/strings/ko.js.map +1 -0
  37. package/lib/commonjs/i18n/strings/nl.js +72 -0
  38. package/lib/commonjs/i18n/strings/nl.js.map +1 -0
  39. package/lib/commonjs/i18n/strings/pl.js +72 -0
  40. package/lib/commonjs/i18n/strings/pl.js.map +1 -0
  41. package/lib/commonjs/i18n/strings/pt.js +72 -0
  42. package/lib/commonjs/i18n/strings/pt.js.map +1 -0
  43. package/lib/commonjs/i18n/strings/ru.js +72 -0
  44. package/lib/commonjs/i18n/strings/ru.js.map +1 -0
  45. package/lib/commonjs/i18n/strings/tr.js +72 -0
  46. package/lib/commonjs/i18n/strings/tr.js.map +1 -0
  47. package/lib/commonjs/i18n/strings/zh.js +72 -0
  48. package/lib/commonjs/i18n/strings/zh.js.map +1 -0
  49. package/lib/commonjs/index.js +31 -0
  50. package/lib/commonjs/index.js.map +1 -1
  51. package/lib/commonjs/types.js +11 -0
  52. package/lib/commonjs/types.js.map +1 -1
  53. package/lib/commonjs/useSupportThreads.js +19 -9
  54. package/lib/commonjs/useSupportThreads.js.map +1 -1
  55. package/lib/commonjs/utils.js +32 -17
  56. package/lib/commonjs/utils.js.map +1 -1
  57. package/lib/module/api.js +16 -2
  58. package/lib/module/api.js.map +1 -1
  59. package/lib/module/components/SupportButton.js +126 -26
  60. package/lib/module/components/SupportButton.js.map +1 -1
  61. package/lib/module/components/icons.js +35 -0
  62. package/lib/module/components/icons.js.map +1 -1
  63. package/lib/module/components/primitives.js +9 -7
  64. package/lib/module/components/primitives.js.map +1 -1
  65. package/lib/module/components/screens/ComposeScreen.js +15 -22
  66. package/lib/module/components/screens/ComposeScreen.js.map +1 -1
  67. package/lib/module/components/screens/HomeScreen.js +71 -23
  68. package/lib/module/components/screens/HomeScreen.js.map +1 -1
  69. package/lib/module/components/screens/LanguageScreen.js +147 -0
  70. package/lib/module/components/screens/LanguageScreen.js.map +1 -0
  71. package/lib/module/components/screens/ThreadScreen.js +31 -19
  72. package/lib/module/components/screens/ThreadScreen.js.map +1 -1
  73. package/lib/module/i18n/index.js +56 -0
  74. package/lib/module/i18n/index.js.map +1 -0
  75. package/lib/module/i18n/languages.js +88 -0
  76. package/lib/module/i18n/languages.js.map +1 -0
  77. package/lib/module/i18n/strings/de.js +68 -0
  78. package/lib/module/i18n/strings/de.js.map +1 -0
  79. package/lib/module/i18n/strings/en.js +84 -0
  80. package/lib/module/i18n/strings/en.js.map +1 -0
  81. package/lib/module/i18n/strings/es.js +68 -0
  82. package/lib/module/i18n/strings/es.js.map +1 -0
  83. package/lib/module/i18n/strings/fr.js +68 -0
  84. package/lib/module/i18n/strings/fr.js.map +1 -0
  85. package/lib/module/i18n/strings/hi.js +68 -0
  86. package/lib/module/i18n/strings/hi.js.map +1 -0
  87. package/lib/module/i18n/strings/it.js +68 -0
  88. package/lib/module/i18n/strings/it.js.map +1 -0
  89. package/lib/module/i18n/strings/ja.js +68 -0
  90. package/lib/module/i18n/strings/ja.js.map +1 -0
  91. package/lib/module/i18n/strings/ko.js +68 -0
  92. package/lib/module/i18n/strings/ko.js.map +1 -0
  93. package/lib/module/i18n/strings/nl.js +68 -0
  94. package/lib/module/i18n/strings/nl.js.map +1 -0
  95. package/lib/module/i18n/strings/pl.js +68 -0
  96. package/lib/module/i18n/strings/pl.js.map +1 -0
  97. package/lib/module/i18n/strings/pt.js +68 -0
  98. package/lib/module/i18n/strings/pt.js.map +1 -0
  99. package/lib/module/i18n/strings/ru.js +68 -0
  100. package/lib/module/i18n/strings/ru.js.map +1 -0
  101. package/lib/module/i18n/strings/tr.js +68 -0
  102. package/lib/module/i18n/strings/tr.js.map +1 -0
  103. package/lib/module/i18n/strings/zh.js +68 -0
  104. package/lib/module/i18n/strings/zh.js.map +1 -0
  105. package/lib/module/index.js +1 -0
  106. package/lib/module/index.js.map +1 -1
  107. package/lib/module/types.js +11 -0
  108. package/lib/module/types.js.map +1 -1
  109. package/lib/module/useSupportThreads.js +19 -9
  110. package/lib/module/useSupportThreads.js.map +1 -1
  111. package/lib/module/utils.js +32 -17
  112. package/lib/module/utils.js.map +1 -1
  113. package/lib/typescript/api.d.ts +9 -0
  114. package/lib/typescript/api.d.ts.map +1 -1
  115. package/lib/typescript/components/SupportButton.d.ts.map +1 -1
  116. package/lib/typescript/components/icons.d.ts +5 -0
  117. package/lib/typescript/components/icons.d.ts.map +1 -1
  118. package/lib/typescript/components/primitives.d.ts +7 -3
  119. package/lib/typescript/components/primitives.d.ts.map +1 -1
  120. package/lib/typescript/components/screens/ComposeScreen.d.ts +3 -1
  121. package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -1
  122. package/lib/typescript/components/screens/HomeScreen.d.ts +6 -1
  123. package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -1
  124. package/lib/typescript/components/screens/LanguageScreen.d.ts +30 -0
  125. package/lib/typescript/components/screens/LanguageScreen.d.ts.map +1 -0
  126. package/lib/typescript/components/screens/ThreadScreen.d.ts +3 -1
  127. package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -1
  128. package/lib/typescript/i18n/index.d.ts +15 -0
  129. package/lib/typescript/i18n/index.d.ts.map +1 -0
  130. package/lib/typescript/i18n/languages.d.ts +36 -0
  131. package/lib/typescript/i18n/languages.d.ts.map +1 -0
  132. package/lib/typescript/i18n/strings/de.d.ts +3 -0
  133. package/lib/typescript/i18n/strings/de.d.ts.map +1 -0
  134. package/lib/typescript/i18n/strings/en.d.ts +83 -0
  135. package/lib/typescript/i18n/strings/en.d.ts.map +1 -0
  136. package/lib/typescript/i18n/strings/es.d.ts +3 -0
  137. package/lib/typescript/i18n/strings/es.d.ts.map +1 -0
  138. package/lib/typescript/i18n/strings/fr.d.ts +3 -0
  139. package/lib/typescript/i18n/strings/fr.d.ts.map +1 -0
  140. package/lib/typescript/i18n/strings/hi.d.ts +3 -0
  141. package/lib/typescript/i18n/strings/hi.d.ts.map +1 -0
  142. package/lib/typescript/i18n/strings/it.d.ts +3 -0
  143. package/lib/typescript/i18n/strings/it.d.ts.map +1 -0
  144. package/lib/typescript/i18n/strings/ja.d.ts +3 -0
  145. package/lib/typescript/i18n/strings/ja.d.ts.map +1 -0
  146. package/lib/typescript/i18n/strings/ko.d.ts +3 -0
  147. package/lib/typescript/i18n/strings/ko.d.ts.map +1 -0
  148. package/lib/typescript/i18n/strings/nl.d.ts +3 -0
  149. package/lib/typescript/i18n/strings/nl.d.ts.map +1 -0
  150. package/lib/typescript/i18n/strings/pl.d.ts +3 -0
  151. package/lib/typescript/i18n/strings/pl.d.ts.map +1 -0
  152. package/lib/typescript/i18n/strings/pt.d.ts +3 -0
  153. package/lib/typescript/i18n/strings/pt.d.ts.map +1 -0
  154. package/lib/typescript/i18n/strings/ru.d.ts +3 -0
  155. package/lib/typescript/i18n/strings/ru.d.ts.map +1 -0
  156. package/lib/typescript/i18n/strings/tr.d.ts +3 -0
  157. package/lib/typescript/i18n/strings/tr.d.ts.map +1 -0
  158. package/lib/typescript/i18n/strings/zh.d.ts +3 -0
  159. package/lib/typescript/i18n/strings/zh.d.ts.map +1 -0
  160. package/lib/typescript/index.d.ts +1 -0
  161. package/lib/typescript/index.d.ts.map +1 -1
  162. package/lib/typescript/types.d.ts +32 -2
  163. package/lib/typescript/types.d.ts.map +1 -1
  164. package/lib/typescript/useSupportThreads.d.ts +11 -1
  165. package/lib/typescript/useSupportThreads.d.ts.map +1 -1
  166. package/lib/typescript/utils.d.ts +6 -5
  167. package/lib/typescript/utils.d.ts.map +1 -1
  168. package/package.json +2 -2
  169. package/src/api.ts +24 -1
  170. package/src/components/SupportButton.tsx +162 -33
  171. package/src/components/icons.tsx +36 -0
  172. package/src/components/primitives.tsx +14 -4
  173. package/src/components/screens/ComposeScreen.tsx +19 -23
  174. package/src/components/screens/HomeScreen.tsx +84 -25
  175. package/src/components/screens/LanguageScreen.tsx +172 -0
  176. package/src/components/screens/ThreadScreen.tsx +48 -21
  177. package/src/i18n/index.ts +81 -0
  178. package/src/i18n/languages.ts +106 -0
  179. package/src/i18n/strings/de.ts +78 -0
  180. package/src/i18n/strings/en.ts +94 -0
  181. package/src/i18n/strings/es.ts +77 -0
  182. package/src/i18n/strings/fr.ts +78 -0
  183. package/src/i18n/strings/hi.ts +77 -0
  184. package/src/i18n/strings/it.ts +77 -0
  185. package/src/i18n/strings/ja.ts +77 -0
  186. package/src/i18n/strings/ko.ts +76 -0
  187. package/src/i18n/strings/nl.ts +77 -0
  188. package/src/i18n/strings/pl.ts +77 -0
  189. package/src/i18n/strings/pt.ts +77 -0
  190. package/src/i18n/strings/ru.ts +77 -0
  191. package/src/i18n/strings/tr.ts +77 -0
  192. package/src/i18n/strings/zh.ts +75 -0
  193. package/src/index.tsx +8 -0
  194. package/src/types.ts +32 -2
  195. package/src/useSupportThreads.ts +30 -10
  196. package/src/utils.ts +22 -17
@@ -1,12 +1,20 @@
1
- import { useCallback, useMemo, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { Text, View } from 'react-native';
3
3
  import {
4
4
  postMessage,
5
5
  registerPush,
6
+ setLanguage as setLanguageRequest,
6
7
  submitQuery,
7
8
  SupportApiError,
8
9
  type ApiConfig,
9
10
  } from '../api';
11
+ import { createTranslator } from '../i18n';
12
+ import {
13
+ asLanguage,
14
+ DEFAULT_LANGUAGE,
15
+ deviceLanguage,
16
+ type Language,
17
+ } from '../i18n/languages';
10
18
  import { ensurePushPermission, isPushAvailable } from '../push';
11
19
  import { resolveTheme } from '../theme';
12
20
  import type { QueryType, SupportButtonProps, Thread } from '../types';
@@ -16,10 +24,12 @@ import { BottomSheet } from './BottomSheet';
16
24
  import { LauncherButton } from './LauncherButton';
17
25
  import { ComposeScreen } from './screens/ComposeScreen';
18
26
  import { HomeScreen } from './screens/HomeScreen';
27
+ import { LanguageScreen } from './screens/LanguageScreen';
19
28
  import { ThreadScreen } from './screens/ThreadScreen';
20
29
 
21
30
  type Screen =
22
31
  | { name: 'home' }
32
+ | { name: 'language' }
23
33
  | { name: 'compose'; type: QueryType }
24
34
  | { name: 'thread'; threadId: string };
25
35
 
@@ -47,15 +57,56 @@ export function SupportButton(props: SupportButtonProps) {
47
57
  const [sheetOpen, setSheetOpen] = useState(false);
48
58
  const [screen, setScreen] = useState<Screen>({ name: 'home' });
49
59
 
50
- const { threads, totalUnread, loading, refreshing, error, refresh, reload } =
51
- useSupportThreads({
52
- config,
53
- revenuecatId: props.user.revenuecatId,
54
- enabled: validationError === null,
55
- sheetOpen,
56
- activeThreadId: screen.name === 'thread' ? screen.threadId : null,
57
- idlePollMs: props.pollIntervalMs ?? IDLE_POLL_MS,
58
- });
60
+ const {
61
+ threads,
62
+ totalUnread,
63
+ loading,
64
+ refreshing,
65
+ errorKey,
66
+ serverLanguage,
67
+ languageLoaded,
68
+ refresh,
69
+ reload,
70
+ } = useSupportThreads({
71
+ config,
72
+ revenuecatId: props.user.revenuecatId,
73
+ enabled: validationError === null,
74
+ sheetOpen,
75
+ activeThreadId: screen.name === 'thread' ? screen.threadId : null,
76
+ idlePollMs: props.pollIntervalMs ?? IDLE_POLL_MS,
77
+ });
78
+
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
+ const [draftLanguage, setDraftLanguage] = useState<Language | null>(null);
86
+ const [savingLanguage, setSavingLanguage] = useState(false);
87
+ const [languageError, setLanguageError] = useState<string | null>(null);
88
+
89
+ // A preference saved on another device (or an earlier install) wins over
90
+ // any local guess, and answers the "have they chosen?" question for free.
91
+ useEffect(() => {
92
+ const stored = asLanguage(serverLanguage);
93
+ if (stored !== null) setChosen(stored);
94
+ }, [serverLanguage]);
95
+
96
+ const fallbackLanguage =
97
+ props.defaultLanguage ?? deviceLanguage() ?? DEFAULT_LANGUAGE;
98
+ const language = chosen ?? fallbackLanguage;
99
+ const strings = useMemo(
100
+ () => createTranslator(draftLanguage ?? language),
101
+ [draftLanguage, language],
102
+ );
103
+
104
+ /**
105
+ * Ask once, on first open — but only once the server has told us whether
106
+ * this user already answered. Asking before then would show the picker to
107
+ * someone who chose months ago on another device.
108
+ */
109
+ const mustChooseLanguage = languageLoaded && chosen === null;
59
110
 
60
111
  /**
61
112
  * Asks for notification permission the moment the sheet opens, then
@@ -89,6 +140,8 @@ export function SupportButton(props: SupportButtonProps) {
89
140
  function closeSheet() {
90
141
  setSheetOpen(false);
91
142
  setScreen({ name: 'home' });
143
+ setDraftLanguage(null);
144
+ setLanguageError(null);
92
145
  }
93
146
 
94
147
  if (validationError !== null) {
@@ -116,10 +169,43 @@ export function SupportButton(props: SupportButtonProps) {
116
169
 
117
170
  function writeErrorMessage(e: unknown): string {
118
171
  if (e instanceof SupportApiError) {
119
- if (e.status === 429) return 'Too many requests — try again later';
120
- if (e.offline) return "You're offline — check your connection";
172
+ if (e.status === 429) return strings.t('error.rateLimited');
173
+ if (e.offline) return strings.t('error.offline');
174
+ }
175
+ return strings.t('error.sendFailed');
176
+ }
177
+
178
+ /**
179
+ * Persists the picked language and leaves the picker.
180
+ *
181
+ * A failed save keeps the user on the screen with a retry: getting this
182
+ * wrong means every message they write is translated from the wrong
183
+ * language, which is worse than one more tap.
184
+ */
185
+ async function confirmLanguage() {
186
+ const next = draftLanguage ?? language;
187
+ setSavingLanguage(true);
188
+ setLanguageError(null);
189
+ try {
190
+ await setLanguageRequest(config, {
191
+ revenuecatId: props.user.revenuecatId,
192
+ language: next,
193
+ });
194
+ setChosen(next);
195
+ setDraftLanguage(null);
196
+ setScreen({ name: 'home' });
197
+ // On a change, the server re-queues existing replies for translation
198
+ // into the new language; they arrive over the next poll or two.
199
+ void reload();
200
+ } catch (e) {
201
+ setLanguageError(
202
+ e instanceof SupportApiError && e.offline
203
+ ? strings.t('error.offline')
204
+ : strings.t('lang.saveFailed'),
205
+ );
206
+ } finally {
207
+ setSavingLanguage(false);
121
208
  }
122
- return "Couldn't send — try again";
123
209
  }
124
210
 
125
211
  async function handleSubmit(
@@ -134,6 +220,7 @@ export function SupportButton(props: SupportButtonProps) {
134
220
  isPaid: props.user.isPaid,
135
221
  revenuecatId: props.user.revenuecatId,
136
222
  appVersion: props.user.appVersion,
223
+ language,
137
224
  });
138
225
  void reload();
139
226
  return null;
@@ -151,6 +238,7 @@ export function SupportButton(props: SupportButtonProps) {
151
238
  queryId: threadId,
152
239
  revenuecatId: props.user.revenuecatId,
153
240
  message,
241
+ language,
154
242
  });
155
243
  await reload();
156
244
  return null;
@@ -167,19 +255,50 @@ export function SupportButton(props: SupportButtonProps) {
167
255
  const home = (
168
256
  <HomeScreen
169
257
  theme={theme}
258
+ strings={strings}
259
+ language={language}
170
260
  threads={threads}
171
261
  loading={loading}
172
262
  refreshing={refreshing}
173
- error={error}
263
+ error={errorKey === null ? null : strings.t(errorKey)}
174
264
  onRefresh={refresh}
175
265
  onCompose={(type) => setScreen({ name: 'compose', type })}
176
266
  onOpenThread={(thread) =>
177
267
  setScreen({ name: 'thread', threadId: thread.id })
178
268
  }
269
+ onChangeLanguage={() => {
270
+ setDraftLanguage(language);
271
+ setLanguageError(null);
272
+ setScreen({ name: 'language' });
273
+ }}
179
274
  onClose={closeSheet}
180
275
  />
181
276
  );
182
277
 
278
+ const languagePicker = (
279
+ <LanguageScreen
280
+ theme={theme}
281
+ strings={strings}
282
+ selected={draftLanguage ?? language}
283
+ onSelect={setDraftLanguage}
284
+ onConfirm={() => void confirmLanguage()}
285
+ // No way back on first run: there is nothing behind it yet, so the
286
+ // only exit is choosing — or closing the sheet, which asks again.
287
+ onBack={
288
+ mustChooseLanguage
289
+ ? undefined
290
+ : () => {
291
+ setDraftLanguage(null);
292
+ setLanguageError(null);
293
+ setScreen({ name: 'home' });
294
+ }
295
+ }
296
+ onDismiss={mustChooseLanguage ? closeSheet : undefined}
297
+ saving={savingLanguage}
298
+ error={languageError}
299
+ />
300
+ );
301
+
183
302
  return (
184
303
  <>
185
304
  <LauncherButton
@@ -193,26 +312,36 @@ export function SupportButton(props: SupportButtonProps) {
193
312
  />
194
313
 
195
314
  <BottomSheet visible={sheetOpen} onClose={closeSheet} theme={theme}>
196
- {screen.name === 'home' && home}
197
- {screen.name === 'compose' && (
198
- <ComposeScreen
199
- theme={theme}
200
- type={screen.type}
201
- onBack={() => setScreen({ name: 'home' })}
202
- onSubmit={(message) => handleSubmit(screen.type, message)}
203
- />
315
+ {/* The one-time prompt outranks whatever screen the user is on: it
316
+ only ever fires before they have sent anything. */}
317
+ {mustChooseLanguage || screen.name === 'language' ? (
318
+ languagePicker
319
+ ) : (
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
+ </>
204
344
  )}
205
- {screen.name === 'thread' &&
206
- (activeThread !== null ? (
207
- <ThreadScreen
208
- theme={theme}
209
- thread={activeThread}
210
- onBack={() => setScreen({ name: 'home' })}
211
- onSend={(message) => handleSend(activeThread.id, message)}
212
- />
213
- ) : (
214
- home
215
- ))}
216
345
  </BottomSheet>
217
346
  </>
218
347
  );
@@ -145,6 +145,42 @@ export function CheckIcon({ color, size = 16 }: { color: string; size?: number }
145
145
  );
146
146
  }
147
147
 
148
+ /** Globe: circle, equator, and a pill standing in for the meridian curve. */
149
+ export function GlobeIcon({ color, size = 16 }: { color: string; size?: number }) {
150
+ const inner = size - STROKE * 2;
151
+ return (
152
+ <View
153
+ style={{
154
+ width: size,
155
+ height: size,
156
+ borderWidth: STROKE,
157
+ borderColor: color,
158
+ borderRadius: size / 2,
159
+ alignItems: 'center',
160
+ justifyContent: 'center',
161
+ }}
162
+ >
163
+ <View
164
+ style={{
165
+ position: 'absolute',
166
+ width: inner,
167
+ height: STROKE,
168
+ backgroundColor: color,
169
+ }}
170
+ />
171
+ <View
172
+ style={{
173
+ width: size * 0.52,
174
+ height: inner,
175
+ borderWidth: STROKE,
176
+ borderColor: color,
177
+ borderRadius: size * 0.26,
178
+ }}
179
+ />
180
+ </View>
181
+ );
182
+ }
183
+
148
184
  /** Send arrow: a stem plus a chevron head, both centered on the same axis. */
149
185
  export function ArrowUpIcon({ color, size = 16 }: { color: string; size?: number }) {
150
186
  const stroke = STROKE + 0.4;
@@ -9,9 +9,9 @@ import {
9
9
  type TextStyle,
10
10
  type ViewStyle,
11
11
  } from 'react-native';
12
+ import type { Translator } from '../i18n';
12
13
  import type { SupportTheme } from '../theme';
13
14
  import type { QueryStatus, QueryType } from '../types';
14
- import { QUERY_TYPE_LABEL } from '../types';
15
15
  import { AlertCircleIcon, ChatIcon, PlusCircleIcon } from './icons';
16
16
 
17
17
  export function typeIcon(type: QueryType, color: string, size = 15): ReactNode {
@@ -62,9 +62,11 @@ export function TypeGlyph({
62
62
  export function StatusIndicator({
63
63
  status,
64
64
  theme,
65
+ strings,
65
66
  }: {
66
67
  status: QueryStatus;
67
68
  theme: SupportTheme;
69
+ strings: Translator;
68
70
  }) {
69
71
  const color =
70
72
  status === 'open' ? theme.colors.statusOpen : theme.colors.statusResolved;
@@ -74,7 +76,7 @@ export function StatusIndicator({
74
76
  style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: color }}
75
77
  />
76
78
  <Text style={{ fontSize: 12, color }}>
77
- {status === 'open' ? 'Open' : 'Resolved'}
79
+ {strings.t(status === 'open' ? 'status.open' : 'status.resolved')}
78
80
  </Text>
79
81
  </View>
80
82
  );
@@ -351,17 +353,23 @@ export function ErrorState({
351
353
  message,
352
354
  onRetry,
353
355
  theme,
356
+ strings,
354
357
  }: {
355
358
  message: string;
356
359
  onRetry: () => void;
357
360
  theme: SupportTheme;
361
+ strings: Translator;
358
362
  }) {
359
363
  return (
360
364
  <View style={{ alignItems: 'center', paddingVertical: 40, gap: 12 }}>
361
365
  <Text style={{ fontSize: 14, color: theme.colors.textSecondary }}>
362
366
  {message}
363
367
  </Text>
364
- <SecondaryButton label="Try again" onPress={onRetry} theme={theme} />
368
+ <SecondaryButton
369
+ label={strings.t('error.tryAgain')}
370
+ onPress={onRetry}
371
+ theme={theme}
372
+ />
365
373
  </View>
366
374
  );
367
375
  }
@@ -452,9 +460,11 @@ export const textStyles = {
452
460
  export function TypeChipView({
453
461
  type,
454
462
  theme,
463
+ strings,
455
464
  }: {
456
465
  type: QueryType;
457
466
  theme: SupportTheme;
467
+ strings: Translator;
458
468
  }) {
459
469
  const color = typeColor(type, theme);
460
470
  return (
@@ -472,7 +482,7 @@ export function TypeChipView({
472
482
  >
473
483
  {typeIcon(type, color, 11)}
474
484
  <Text style={{ fontSize: 11.5, fontWeight: '600', color }}>
475
- {QUERY_TYPE_LABEL[type]}
485
+ {strings.t(`type.${type}`)}
476
486
  </Text>
477
487
  </View>
478
488
  );
@@ -8,9 +8,10 @@ import {
8
8
  TextInput,
9
9
  View,
10
10
  } from 'react-native';
11
+ import type { Translator } from '../../i18n';
11
12
  import type { SupportTheme } from '../../theme';
12
13
  import type { QueryType } from '../../types';
13
- import { QUERY_TYPE_LABEL, USER_MESSAGE_MAX, USER_MESSAGE_MIN } from '../../types';
14
+ import { USER_MESSAGE_MAX, USER_MESSAGE_MIN } from '../../types';
14
15
  import { CheckIcon, Chevron } from '../icons';
15
16
  import {
16
17
  IconButton,
@@ -19,18 +20,6 @@ import {
19
20
  TypeGlyph,
20
21
  } from '../primitives';
21
22
 
22
- const PROMPT: Record<QueryType, string> = {
23
- feedback: 'What should we know?',
24
- feature_request: 'What would you like to see?',
25
- bug_report: 'What happened, and what did you expect?',
26
- };
27
-
28
- const SUBTITLE: Record<QueryType, string> = {
29
- feedback: 'Good or bad — it goes straight to the team.',
30
- feature_request: 'Tell us what you are trying to do.',
31
- bug_report: 'Steps, screen, and what you expected all help.',
32
- };
33
-
34
23
  /** Scales in once on success — the only celebratory motion in the SDK. */
35
24
  function SentMark({ theme }: { theme: SupportTheme }) {
36
25
  const scale = useRef(new Animated.Value(0.6)).current;
@@ -75,11 +64,13 @@ function SentMark({ theme }: { theme: SupportTheme }) {
75
64
 
76
65
  export function ComposeScreen({
77
66
  theme,
67
+ strings,
78
68
  type,
79
69
  onBack,
80
70
  onSubmit,
81
71
  }: {
82
72
  theme: SupportTheme;
73
+ strings: Translator;
83
74
  type: QueryType;
84
75
  onBack: () => void;
85
76
  /** Resolves null on success, or a user-facing error message. */
@@ -119,13 +110,16 @@ export function ComposeScreen({
119
110
  }}
120
111
  >
121
112
  <SentMark theme={theme} />
122
- <Text style={textStyles.title(theme)}>Sent</Text>
113
+ <Text style={textStyles.title(theme)}>{strings.t('compose.sent')}</Text>
123
114
  <Text style={[textStyles.secondary(theme), { textAlign: 'center' }]}>
124
- We&apos;ll reply right here. You&apos;ll find the conversation under
125
- Open, and the button shows a badge when we answer.
115
+ {strings.t('compose.sentHint')}
126
116
  </Text>
127
117
  <View style={{ marginTop: 6 }}>
128
- <PrimaryButton label="Done" onPress={onBack} theme={theme} />
118
+ <PrimaryButton
119
+ label={strings.t('compose.done')}
120
+ onPress={onBack}
121
+ theme={theme}
122
+ />
129
123
  </View>
130
124
  </View>
131
125
  );
@@ -149,15 +143,17 @@ export function ComposeScreen({
149
143
  <IconButton
150
144
  onPress={onBack}
151
145
  theme={theme}
152
- accessibilityLabel="Back to conversations"
146
+ accessibilityLabel={strings.t('compose.back')}
153
147
  size={32}
154
148
  >
155
149
  <Chevron color={theme.colors.textSecondary} direction="left" size={12} />
156
150
  </IconButton>
157
151
  <TypeGlyph type={type} theme={theme} size={32} />
158
152
  <View style={{ flex: 1, gap: 1 }}>
159
- <Text style={textStyles.heading(theme)}>{QUERY_TYPE_LABEL[type]}</Text>
160
- <Text style={textStyles.meta(theme)}>{SUBTITLE[type]}</Text>
153
+ <Text style={textStyles.heading(theme)}>
154
+ {strings.t(`type.${type}`)}
155
+ </Text>
156
+ <Text style={textStyles.meta(theme)}>{strings.t(`sub.${type}`)}</Text>
161
157
  </View>
162
158
  </View>
163
159
 
@@ -171,7 +167,7 @@ export function ComposeScreen({
171
167
  multiline
172
168
  autoFocus
173
169
  editable={!submitting}
174
- placeholder={PROMPT[type]}
170
+ placeholder={strings.t(`prompt.${type}`)}
175
171
  placeholderTextColor={theme.colors.textTertiary}
176
172
  style={{
177
173
  minHeight: 140,
@@ -204,7 +200,7 @@ export function ComposeScreen({
204
200
  </Text>
205
201
  ) : tooShort ? (
206
202
  <Text style={textStyles.meta(theme)}>
207
- At least {USER_MESSAGE_MIN} characters
203
+ {strings.t('compose.minChars', { count: USER_MESSAGE_MIN })}
208
204
  </Text>
209
205
  ) : null}
210
206
  <Text
@@ -220,7 +216,7 @@ export function ComposeScreen({
220
216
 
221
217
  <View style={{ paddingTop: 12 }}>
222
218
  <PrimaryButton
223
- label={submitting ? 'Sending…' : 'Send'}
219
+ label={strings.t(submitting ? 'compose.sending' : 'compose.send')}
224
220
  onPress={handleSubmit}
225
221
  disabled={!valid || submitting}
226
222
  theme={theme}