@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
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { StyleProp, ViewStyle } from 'react-native';
2
+ import type { Language } from './i18n/languages';
2
3
 
3
4
  export type QueryType = 'feedback' | 'feature_request' | 'bug_report';
4
5
  export type QueryStatus = 'open' | 'resolved';
@@ -71,23 +72,45 @@ export interface SupportButtonProps {
71
72
  * installed — it is an optional peer dependency.
72
73
  */
73
74
  expoProjectId?: string;
75
+ /**
76
+ * Pre-selects a language in the picker the first time a user opens support.
77
+ * Pass it when the host app already knows what the user reads — an in-app
78
+ * language setting beats the device locale, which is what the SDK falls
79
+ * back to. It is a default, not an override: the user still chooses, and a
80
+ * choice they have already made always wins.
81
+ */
82
+ defaultLanguage?: Language;
74
83
  }
75
84
 
76
85
  export interface ThreadMessage {
77
86
  id: string;
78
87
  sender: MessageSender;
88
+ /**
89
+ * Already in the user's language: their own messages as they wrote them,
90
+ * support replies translated. The server does the choosing — see the
91
+ * get-threads function.
92
+ */
79
93
  body: string;
80
94
  created_at: string;
81
- admin_name: string | null;
95
+ /**
96
+ * A support reply whose translation hasn't landed yet. `body` currently
97
+ * holds the English original; it will change under the reader shortly, so
98
+ * the UI says so rather than letting it flicker.
99
+ */
100
+ translation_pending: boolean;
82
101
  }
83
102
 
103
+ /**
104
+ * Note the absence of any admin identity: end users see one support team,
105
+ * not the people staffing it, so the names never leave the server. The
106
+ * dashboard still shows who wrote and who resolved what.
107
+ */
84
108
  export interface Thread {
85
109
  id: string;
86
110
  type: QueryType;
87
111
  status: QueryStatus;
88
112
  created_at: string;
89
113
  resolved_at: string | null;
90
- resolved_by_name: string | null;
91
114
  app_version: string;
92
115
  unread_count: number;
93
116
  last_message: {
@@ -101,8 +124,15 @@ export interface Thread {
101
124
  export interface ThreadsResponse {
102
125
  threads: Thread[];
103
126
  total_unread: number;
127
+ /** Null when this user has never picked one — the SDK then asks. */
128
+ language: string | null;
104
129
  }
105
130
 
131
+ /**
132
+ * English labels, kept for host apps that render their own entry points.
133
+ * Inside the sheet the labels come from the string table instead, so they
134
+ * follow the user's language.
135
+ */
106
136
  export const QUERY_TYPE_LABEL: Record<QueryType, string> = {
107
137
  feedback: 'Feedback',
108
138
  feature_request: 'Feature request',
@@ -21,6 +21,7 @@ import {
21
21
  SupportApiError,
22
22
  type ApiConfig,
23
23
  } from './api';
24
+ import type { StringKey } from './i18n';
24
25
  import type { Thread } from './types';
25
26
  import {
26
27
  latestAdminMessageAt,
@@ -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,16 @@ 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;
91
105
  /** User-initiated refresh (pull-to-refresh, retry button). */
92
106
  refresh: () => void;
93
107
  /** Awaited by send/submit handlers so the list reflects the write. */
@@ -114,7 +128,9 @@ export function useSupportThreads({
114
128
  const [totalUnread, setTotalUnread] = useState(0);
115
129
  const [loading, setLoading] = useState(false);
116
130
  const [refreshing, setRefreshing] = useState(false);
117
- const [error, setError] = useState<string | null>(null);
131
+ const [errorKey, setErrorKey] = useState<StringKey | null>(null);
132
+ const [serverLanguage, setServerLanguage] = useState<string | null>(null);
133
+ const [languageLoaded, setLanguageLoaded] = useState(false);
118
134
 
119
135
  const mounted = useRef(true);
120
136
  /** Last server payload, before local read reconciliation. */
@@ -256,7 +272,9 @@ export function useSupportThreads({
256
272
 
257
273
  rawThreads.current = data.threads;
258
274
  watermarks.current = pruneWatermarks(watermarks.current, data.threads);
259
- setError(null);
275
+ setErrorKey(null);
276
+ setServerLanguage(data.language);
277
+ setLanguageLoaded(true);
260
278
  publish();
261
279
 
262
280
  // Replies that arrived while the user is sitting in the thread are
@@ -267,7 +285,7 @@ export function useSupportThreads({
267
285
  } catch (e) {
268
286
  if (!mounted.current) return;
269
287
  // A failed background poll must not blow away cached threads.
270
- if (rawThreads.current === null) setError(loadErrorMessage(e));
288
+ if (rawThreads.current === null) setErrorKey(loadErrorKey(e));
271
289
  } finally {
272
290
  if (mounted.current) {
273
291
  if (spinner) setRefreshing(false);
@@ -365,7 +383,9 @@ export function useSupportThreads({
365
383
  totalUnread,
366
384
  loading,
367
385
  refreshing,
368
- error,
386
+ errorKey,
387
+ serverLanguage,
388
+ languageLoaded,
369
389
  refresh,
370
390
  reload,
371
391
  };
package/src/utils.ts CHANGED
@@ -1,32 +1,37 @@
1
+ import type { Translator } from './i18n';
2
+
3
+ // Dates and times follow the language the user picked, not the device's:
4
+ // a sheet that says "Traduction…" above "2 days ago" reads as broken.
5
+
1
6
  /** "2h ago" relative time — mirrors the web dashboard's formatting. */
2
- export function relativeTime(iso: string): string {
7
+ export function relativeTime(iso: string, strings: Translator): string {
3
8
  const then = new Date(iso).getTime();
4
9
  const seconds = Math.max(0, Math.floor((Date.now() - then) / 1000));
5
- if (seconds < 60) return 'just now';
10
+ if (seconds < 60) return strings.t('time.justNow');
6
11
  const minutes = Math.floor(seconds / 60);
7
- if (minutes < 60) return `${minutes}m ago`;
12
+ if (minutes < 60) return strings.t('time.minutes', { count: minutes });
8
13
  const hours = Math.floor(minutes / 60);
9
- if (hours < 24) return `${hours}h ago`;
14
+ if (hours < 24) return strings.t('time.hours', { count: hours });
10
15
  const days = Math.floor(hours / 24);
11
- if (days < 7) return `${days}d ago`;
16
+ if (days < 7) return strings.t('time.days', { count: days });
12
17
  const weeks = Math.floor(days / 7);
13
- if (weeks < 5) return `${weeks}w ago`;
18
+ if (weeks < 5) return strings.t('time.weeks', { count: weeks });
14
19
  const months = Math.floor(days / 30);
15
- if (months < 12) return `${months}mo ago`;
16
- return `${Math.floor(days / 365)}y ago`;
20
+ if (months < 12) return strings.t('time.months', { count: months });
21
+ return strings.t('time.years', { count: Math.floor(days / 365) });
17
22
  }
18
23
 
19
- export function formatDate(iso: string): string {
20
- return new Date(iso).toLocaleDateString(undefined, {
24
+ export function formatDate(iso: string, strings: Translator): string {
25
+ return new Date(iso).toLocaleDateString(strings.locale, {
21
26
  year: 'numeric',
22
27
  month: 'short',
23
28
  day: 'numeric',
24
29
  });
25
30
  }
26
31
 
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, {
32
+ /** "14:32" in the user's locale — shown under a group of messages. */
33
+ export function formatTime(iso: string, strings: Translator): string {
34
+ return new Date(iso).toLocaleTimeString(strings.locale, {
30
35
  hour: 'numeric',
31
36
  minute: '2-digit',
32
37
  });
@@ -43,13 +48,13 @@ export function isSameDay(a: string, b: string): boolean {
43
48
  }
44
49
 
45
50
  /** Date separator label: Today / Yesterday / "Mon, 5 Jan". */
46
- export function dayLabel(iso: string): string {
51
+ export function dayLabel(iso: string, strings: Translator): string {
47
52
  const now = new Date();
48
- if (isSameDay(iso, now.toISOString())) return 'Today';
53
+ if (isSameDay(iso, now.toISOString())) return strings.t('time.today');
49
54
  const yesterday = new Date(now.getTime() - 86_400_000);
50
- if (isSameDay(iso, yesterday.toISOString())) return 'Yesterday';
55
+ if (isSameDay(iso, yesterday.toISOString())) return strings.t('time.yesterday');
51
56
  const date = new Date(iso);
52
- return date.toLocaleDateString(undefined, {
57
+ return date.toLocaleDateString(strings.locale, {
53
58
  weekday: 'short',
54
59
  month: 'short',
55
60
  day: 'numeric',