@appsupportkit/support-sdk 0.1.2 → 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.
- package/lib/commonjs/api.js +17 -2
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/BottomSheet.js +67 -47
- package/lib/commonjs/components/BottomSheet.js.map +1 -1
- package/lib/commonjs/components/LauncherButton.js +177 -0
- package/lib/commonjs/components/LauncherButton.js.map +1 -0
- package/lib/commonjs/components/SupportButton.js +168 -190
- package/lib/commonjs/components/SupportButton.js.map +1 -1
- package/lib/commonjs/components/icons.js +110 -23
- package/lib/commonjs/components/icons.js.map +1 -1
- package/lib/commonjs/components/primitives.js +191 -44
- package/lib/commonjs/components/primitives.js.map +1 -1
- package/lib/commonjs/components/screens/ComposeScreen.js +108 -60
- package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/HomeScreen.js +221 -118
- package/lib/commonjs/components/screens/HomeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/LanguageScreen.js +150 -0
- package/lib/commonjs/components/screens/LanguageScreen.js.map +1 -0
- package/lib/commonjs/components/screens/ThreadScreen.js +237 -111
- package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -1
- package/lib/commonjs/i18n/index.js +101 -0
- package/lib/commonjs/i18n/index.js.map +1 -0
- package/lib/commonjs/i18n/languages.js +95 -0
- package/lib/commonjs/i18n/languages.js.map +1 -0
- package/lib/commonjs/i18n/strings/de.js +72 -0
- package/lib/commonjs/i18n/strings/de.js.map +1 -0
- package/lib/commonjs/i18n/strings/en.js +88 -0
- package/lib/commonjs/i18n/strings/en.js.map +1 -0
- package/lib/commonjs/i18n/strings/es.js +72 -0
- package/lib/commonjs/i18n/strings/es.js.map +1 -0
- package/lib/commonjs/i18n/strings/fr.js +72 -0
- package/lib/commonjs/i18n/strings/fr.js.map +1 -0
- package/lib/commonjs/i18n/strings/hi.js +72 -0
- package/lib/commonjs/i18n/strings/hi.js.map +1 -0
- package/lib/commonjs/i18n/strings/it.js +72 -0
- package/lib/commonjs/i18n/strings/it.js.map +1 -0
- package/lib/commonjs/i18n/strings/ja.js +72 -0
- package/lib/commonjs/i18n/strings/ja.js.map +1 -0
- package/lib/commonjs/i18n/strings/ko.js +72 -0
- package/lib/commonjs/i18n/strings/ko.js.map +1 -0
- package/lib/commonjs/i18n/strings/nl.js +72 -0
- package/lib/commonjs/i18n/strings/nl.js.map +1 -0
- package/lib/commonjs/i18n/strings/pl.js +72 -0
- package/lib/commonjs/i18n/strings/pl.js.map +1 -0
- package/lib/commonjs/i18n/strings/pt.js +72 -0
- package/lib/commonjs/i18n/strings/pt.js.map +1 -0
- package/lib/commonjs/i18n/strings/ru.js +72 -0
- package/lib/commonjs/i18n/strings/ru.js.map +1 -0
- package/lib/commonjs/i18n/strings/tr.js +72 -0
- package/lib/commonjs/i18n/strings/tr.js.map +1 -0
- package/lib/commonjs/i18n/strings/zh.js +72 -0
- package/lib/commonjs/i18n/strings/zh.js.map +1 -0
- package/lib/commonjs/index.js +31 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/theme.js +136 -17
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/commonjs/types.js +18 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/unread.js +92 -0
- package/lib/commonjs/unread.js.map +1 -0
- package/lib/commonjs/useSupportThreads.js +296 -0
- package/lib/commonjs/useSupportThreads.js.map +1 -0
- package/lib/commonjs/utils.js +58 -10
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/module/api.js +16 -2
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/BottomSheet.js +68 -48
- package/lib/module/components/BottomSheet.js.map +1 -1
- package/lib/module/components/LauncherButton.js +172 -0
- package/lib/module/components/LauncherButton.js.map +1 -0
- package/lib/module/components/SupportButton.js +172 -194
- package/lib/module/components/SupportButton.js.map +1 -1
- package/lib/module/components/icons.js +107 -22
- package/lib/module/components/icons.js.map +1 -1
- package/lib/module/components/primitives.js +187 -45
- package/lib/module/components/primitives.js.map +1 -1
- package/lib/module/components/screens/ComposeScreen.js +114 -64
- package/lib/module/components/screens/ComposeScreen.js.map +1 -1
- package/lib/module/components/screens/HomeScreen.js +224 -121
- package/lib/module/components/screens/HomeScreen.js.map +1 -1
- package/lib/module/components/screens/LanguageScreen.js +147 -0
- package/lib/module/components/screens/LanguageScreen.js.map +1 -0
- package/lib/module/components/screens/ThreadScreen.js +243 -115
- package/lib/module/components/screens/ThreadScreen.js.map +1 -1
- package/lib/module/i18n/index.js +56 -0
- package/lib/module/i18n/index.js.map +1 -0
- package/lib/module/i18n/languages.js +88 -0
- package/lib/module/i18n/languages.js.map +1 -0
- package/lib/module/i18n/strings/de.js +68 -0
- package/lib/module/i18n/strings/de.js.map +1 -0
- package/lib/module/i18n/strings/en.js +84 -0
- package/lib/module/i18n/strings/en.js.map +1 -0
- package/lib/module/i18n/strings/es.js +68 -0
- package/lib/module/i18n/strings/es.js.map +1 -0
- package/lib/module/i18n/strings/fr.js +68 -0
- package/lib/module/i18n/strings/fr.js.map +1 -0
- package/lib/module/i18n/strings/hi.js +68 -0
- package/lib/module/i18n/strings/hi.js.map +1 -0
- package/lib/module/i18n/strings/it.js +68 -0
- package/lib/module/i18n/strings/it.js.map +1 -0
- package/lib/module/i18n/strings/ja.js +68 -0
- package/lib/module/i18n/strings/ja.js.map +1 -0
- package/lib/module/i18n/strings/ko.js +68 -0
- package/lib/module/i18n/strings/ko.js.map +1 -0
- package/lib/module/i18n/strings/nl.js +68 -0
- package/lib/module/i18n/strings/nl.js.map +1 -0
- package/lib/module/i18n/strings/pl.js +68 -0
- package/lib/module/i18n/strings/pl.js.map +1 -0
- package/lib/module/i18n/strings/pt.js +68 -0
- package/lib/module/i18n/strings/pt.js.map +1 -0
- package/lib/module/i18n/strings/ru.js +68 -0
- package/lib/module/i18n/strings/ru.js.map +1 -0
- package/lib/module/i18n/strings/tr.js +68 -0
- package/lib/module/i18n/strings/tr.js.map +1 -0
- package/lib/module/i18n/strings/zh.js +68 -0
- package/lib/module/i18n/strings/zh.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/theme.js +135 -16
- package/lib/module/theme.js.map +1 -1
- package/lib/module/types.js +18 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/unread.js +83 -0
- package/lib/module/unread.js.map +1 -0
- package/lib/module/useSupportThreads.js +291 -0
- package/lib/module/useSupportThreads.js.map +1 -0
- package/lib/module/utils.js +55 -10
- package/lib/module/utils.js.map +1 -1
- package/lib/typescript/api.d.ts +9 -0
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/components/LauncherButton.d.ts +14 -0
- package/lib/typescript/components/LauncherButton.d.ts.map +1 -0
- package/lib/typescript/components/SupportButton.d.ts +3 -3
- package/lib/typescript/components/SupportButton.d.ts.map +1 -1
- package/lib/typescript/components/icons.d.ts +18 -2
- package/lib/typescript/components/icons.d.ts.map +1 -1
- package/lib/typescript/components/primitives.d.ts +42 -7
- package/lib/typescript/components/primitives.d.ts.map +1 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts +3 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/HomeScreen.d.ts +8 -2
- package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/LanguageScreen.d.ts +30 -0
- package/lib/typescript/components/screens/LanguageScreen.d.ts.map +1 -0
- package/lib/typescript/components/screens/ThreadScreen.d.ts +3 -1
- package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -1
- package/lib/typescript/i18n/index.d.ts +15 -0
- package/lib/typescript/i18n/index.d.ts.map +1 -0
- package/lib/typescript/i18n/languages.d.ts +36 -0
- package/lib/typescript/i18n/languages.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/de.d.ts +3 -0
- package/lib/typescript/i18n/strings/de.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/en.d.ts +83 -0
- package/lib/typescript/i18n/strings/en.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/es.d.ts +3 -0
- package/lib/typescript/i18n/strings/es.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/fr.d.ts +3 -0
- package/lib/typescript/i18n/strings/fr.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/hi.d.ts +3 -0
- package/lib/typescript/i18n/strings/hi.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/it.d.ts +3 -0
- package/lib/typescript/i18n/strings/it.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ja.d.ts +3 -0
- package/lib/typescript/i18n/strings/ja.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ko.d.ts +3 -0
- package/lib/typescript/i18n/strings/ko.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/nl.d.ts +3 -0
- package/lib/typescript/i18n/strings/nl.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/pl.d.ts +3 -0
- package/lib/typescript/i18n/strings/pl.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/pt.d.ts +3 -0
- package/lib/typescript/i18n/strings/pt.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ru.d.ts +3 -0
- package/lib/typescript/i18n/strings/ru.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/tr.d.ts +3 -0
- package/lib/typescript/i18n/strings/tr.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/zh.d.ts +3 -0
- package/lib/typescript/i18n/strings/zh.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/theme.d.ts +44 -3
- package/lib/typescript/theme.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +54 -2
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/unread.d.ts +22 -0
- package/lib/typescript/unread.d.ts.map +1 -0
- package/lib/typescript/useSupportThreads.d.ts +38 -0
- package/lib/typescript/useSupportThreads.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +8 -2
- package/lib/typescript/utils.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/api.ts +24 -1
- package/src/components/BottomSheet.tsx +61 -43
- package/src/components/LauncherButton.tsx +217 -0
- package/src/components/SupportButton.tsx +195 -210
- package/src/components/icons.tsx +113 -19
- package/src/components/primitives.tsx +230 -39
- package/src/components/screens/ComposeScreen.tsx +129 -63
- package/src/components/screens/HomeScreen.tsx +230 -120
- package/src/components/screens/LanguageScreen.tsx +172 -0
- package/src/components/screens/ThreadScreen.tsx +319 -122
- package/src/i18n/index.ts +81 -0
- package/src/i18n/languages.ts +106 -0
- package/src/i18n/strings/de.ts +78 -0
- package/src/i18n/strings/en.ts +94 -0
- package/src/i18n/strings/es.ts +77 -0
- package/src/i18n/strings/fr.ts +78 -0
- package/src/i18n/strings/hi.ts +77 -0
- package/src/i18n/strings/it.ts +77 -0
- package/src/i18n/strings/ja.ts +77 -0
- package/src/i18n/strings/ko.ts +76 -0
- package/src/i18n/strings/nl.ts +77 -0
- package/src/i18n/strings/pl.ts +77 -0
- package/src/i18n/strings/pt.ts +77 -0
- package/src/i18n/strings/ru.ts +77 -0
- package/src/i18n/strings/tr.ts +77 -0
- package/src/i18n/strings/zh.ts +75 -0
- package/src/index.tsx +9 -0
- package/src/theme.ts +165 -18
- package/src/types.ts +56 -2
- package/src/unread.ts +91 -0
- package/src/useSupportThreads.ts +392 -0
- package/src/utils.ts +48 -10
|
@@ -1,47 +1,49 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo,
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
3
|
import {
|
|
4
|
-
getThreads,
|
|
5
|
-
getUnreadCount,
|
|
6
|
-
markRead,
|
|
7
4
|
postMessage,
|
|
8
5
|
registerPush,
|
|
6
|
+
setLanguage as setLanguageRequest,
|
|
9
7
|
submitQuery,
|
|
10
8
|
SupportApiError,
|
|
11
9
|
type ApiConfig,
|
|
12
10
|
} from '../api';
|
|
11
|
+
import { createTranslator } from '../i18n';
|
|
12
|
+
import {
|
|
13
|
+
asLanguage,
|
|
14
|
+
DEFAULT_LANGUAGE,
|
|
15
|
+
deviceLanguage,
|
|
16
|
+
type Language,
|
|
17
|
+
} from '../i18n/languages';
|
|
13
18
|
import { ensurePushPermission, isPushAvailable } from '../push';
|
|
14
19
|
import { resolveTheme } from '../theme';
|
|
15
20
|
import type { QueryType, SupportButtonProps, Thread } from '../types';
|
|
21
|
+
import { IDLE_POLL_MS, useSupportThreads } from '../useSupportThreads';
|
|
16
22
|
import { validateProps } from '../utils';
|
|
17
23
|
import { BottomSheet } from './BottomSheet';
|
|
18
|
-
import {
|
|
24
|
+
import { LauncherButton } from './LauncherButton';
|
|
19
25
|
import { ComposeScreen } from './screens/ComposeScreen';
|
|
20
26
|
import { HomeScreen } from './screens/HomeScreen';
|
|
27
|
+
import { LanguageScreen } from './screens/LanguageScreen';
|
|
21
28
|
import { ThreadScreen } from './screens/ThreadScreen';
|
|
22
29
|
|
|
23
30
|
type Screen =
|
|
24
31
|
| { name: 'home' }
|
|
32
|
+
| { name: 'language' }
|
|
25
33
|
| { name: 'compose'; type: QueryType }
|
|
26
34
|
| { name: 'thread'; threadId: string };
|
|
27
35
|
|
|
28
|
-
// Poll cadence by context: fast where latency is felt (staring at a thread
|
|
29
|
-
// waiting for a reply), slow when the sheet is closed — and the idle tick
|
|
30
|
-
// uses the count-only endpoint, so it costs ~18 bytes instead of the full
|
|
31
|
-
// thread list.
|
|
32
|
-
const THREAD_POLL_MS = 5_000;
|
|
33
|
-
const SHEET_POLL_MS = 15_000;
|
|
34
|
-
const IDLE_POLL_MS = 60_000;
|
|
35
|
-
const MIN_POLL_MS = 3_000;
|
|
36
|
-
|
|
37
36
|
/**
|
|
38
|
-
* The single entry point of the SDK: a
|
|
39
|
-
* opens the support bottom sheet.
|
|
40
|
-
*
|
|
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.
|
|
41
40
|
*/
|
|
42
41
|
export function SupportButton(props: SupportButtonProps) {
|
|
43
42
|
const validationError = validateProps(props);
|
|
44
|
-
const theme =
|
|
43
|
+
const theme = useMemo(
|
|
44
|
+
() => resolveTheme(props.theme, props.accentColor),
|
|
45
|
+
[props.theme, props.accentColor],
|
|
46
|
+
);
|
|
45
47
|
|
|
46
48
|
const config: ApiConfig = useMemo(
|
|
47
49
|
() => ({
|
|
@@ -54,85 +56,57 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
54
56
|
|
|
55
57
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
56
58
|
const [screen, setScreen] = useState<Screen>({ name: 'home' });
|
|
57
|
-
const [threads, setThreads] = useState<Thread[] | null>(null);
|
|
58
|
-
const [totalUnread, setTotalUnread] = useState(0);
|
|
59
|
-
const [loading, setLoading] = useState(false);
|
|
60
|
-
const [error, setError] = useState<string | null>(null);
|
|
61
|
-
const mounted = useRef(true);
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
setError(
|
|
82
|
-
e instanceof SupportApiError && e.offline
|
|
83
|
-
? "You're offline — check your connection"
|
|
84
|
-
: "Couldn't load threads",
|
|
85
|
-
);
|
|
86
|
-
} finally {
|
|
87
|
-
if (mounted.current) setLoading(false);
|
|
88
|
-
}
|
|
89
|
-
}, [config, props.user.revenuecatId, validationError]);
|
|
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
|
+
});
|
|
90
78
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}, [config, props.user.revenuecatId, validationError]);
|
|
101
|
-
|
|
102
|
-
// Poll on mount…
|
|
103
|
-
useEffect(() => {
|
|
104
|
-
void refresh();
|
|
105
|
-
}, [refresh]);
|
|
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);
|
|
106
88
|
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
const idlePollMs = props.pollIntervalMs ?? IDLE_POLL_MS;
|
|
110
|
-
const screenName = screen.name;
|
|
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.
|
|
111
91
|
useEffect(() => {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
: screenName === 'thread'
|
|
116
|
-
? THREAD_POLL_MS
|
|
117
|
-
: SHEET_POLL_MS;
|
|
92
|
+
const stored = asLanguage(serverLanguage);
|
|
93
|
+
if (stored !== null) setChosen(stored);
|
|
94
|
+
}, [serverLanguage]);
|
|
118
95
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const appStateSub = AppState.addEventListener('change', (state) => {
|
|
128
|
-
if (state === 'active') tick();
|
|
129
|
-
});
|
|
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
|
+
);
|
|
130
103
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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;
|
|
136
110
|
|
|
137
111
|
/**
|
|
138
112
|
* Asks for notification permission the moment the sheet opens, then
|
|
@@ -141,7 +115,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
141
115
|
* support flow waits on it or breaks if it fails.
|
|
142
116
|
*/
|
|
143
117
|
const syncPushPermission = useCallback(async () => {
|
|
144
|
-
if (validationError || !isPushAvailable) return;
|
|
118
|
+
if (validationError !== null || !isPushAvailable) return;
|
|
145
119
|
try {
|
|
146
120
|
const registration = await ensurePushPermission(props.expoProjectId);
|
|
147
121
|
await registerPush(config, {
|
|
@@ -155,11 +129,9 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
155
129
|
}
|
|
156
130
|
}, [config, props.user.revenuecatId, props.expoProjectId, validationError]);
|
|
157
131
|
|
|
158
|
-
// …and on sheet open.
|
|
159
132
|
function openSheet() {
|
|
160
133
|
setScreen({ name: 'home' });
|
|
161
134
|
setSheetOpen(true);
|
|
162
|
-
void refresh();
|
|
163
135
|
// The OS prompt appears right as the sheet opens; if the user declines,
|
|
164
136
|
// everything below carries on exactly as before.
|
|
165
137
|
void syncPushPermission();
|
|
@@ -168,9 +140,11 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
168
140
|
function closeSheet() {
|
|
169
141
|
setSheetOpen(false);
|
|
170
142
|
setScreen({ name: 'home' });
|
|
143
|
+
setDraftLanguage(null);
|
|
144
|
+
setLanguageError(null);
|
|
171
145
|
}
|
|
172
146
|
|
|
173
|
-
if (validationError) {
|
|
147
|
+
if (validationError !== null) {
|
|
174
148
|
// Dev-mode: loud and actionable. Production: render nothing rather than
|
|
175
149
|
// a broken button.
|
|
176
150
|
if (__DEV__) {
|
|
@@ -193,32 +167,45 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
193
167
|
return null;
|
|
194
168
|
}
|
|
195
169
|
|
|
196
|
-
function
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
setThreads(
|
|
201
|
-
(prev) =>
|
|
202
|
-
prev?.map((t) =>
|
|
203
|
-
t.id === thread.id ? { ...t, unread_count: 0 } : t,
|
|
204
|
-
) ?? null,
|
|
205
|
-
);
|
|
206
|
-
setTotalUnread((n) => Math.max(0, n - thread.unread_count));
|
|
207
|
-
void markRead(config, {
|
|
208
|
-
queryId: thread.id,
|
|
209
|
-
revenuecatId: props.user.revenuecatId,
|
|
210
|
-
}).catch(() => {
|
|
211
|
-
// Non-fatal: the next get-threads reconciles the badge.
|
|
212
|
-
});
|
|
170
|
+
function writeErrorMessage(e: unknown): string {
|
|
171
|
+
if (e instanceof SupportApiError) {
|
|
172
|
+
if (e.status === 429) return strings.t('error.rateLimited');
|
|
173
|
+
if (e.offline) return strings.t('error.offline');
|
|
213
174
|
}
|
|
175
|
+
return strings.t('error.sendFailed');
|
|
214
176
|
}
|
|
215
177
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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);
|
|
220
208
|
}
|
|
221
|
-
return "Couldn't send — try again";
|
|
222
209
|
}
|
|
223
210
|
|
|
224
211
|
async function handleSubmit(
|
|
@@ -233,8 +220,9 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
233
220
|
isPaid: props.user.isPaid,
|
|
234
221
|
revenuecatId: props.user.revenuecatId,
|
|
235
222
|
appVersion: props.user.appVersion,
|
|
223
|
+
language,
|
|
236
224
|
});
|
|
237
|
-
void
|
|
225
|
+
void reload();
|
|
238
226
|
return null;
|
|
239
227
|
} catch (e) {
|
|
240
228
|
return writeErrorMessage(e);
|
|
@@ -250,113 +238,110 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
250
238
|
queryId: threadId,
|
|
251
239
|
revenuecatId: props.user.revenuecatId,
|
|
252
240
|
message,
|
|
241
|
+
language,
|
|
253
242
|
});
|
|
254
|
-
await
|
|
243
|
+
await reload();
|
|
255
244
|
return null;
|
|
256
245
|
} catch (e) {
|
|
257
246
|
return writeErrorMessage(e);
|
|
258
247
|
}
|
|
259
248
|
}
|
|
260
249
|
|
|
261
|
-
const activeThread =
|
|
250
|
+
const activeThread: Thread | null =
|
|
262
251
|
screen.name === 'thread'
|
|
263
|
-
? (threads ?? []).find((t) => t.id === screen.threadId) ?? null
|
|
252
|
+
? ((threads ?? []).find((t) => t.id === screen.threadId) ?? null)
|
|
264
253
|
: null;
|
|
265
254
|
|
|
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
|
+
);
|
|
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
|
+
|
|
266
302
|
return (
|
|
267
303
|
<>
|
|
268
|
-
<
|
|
304
|
+
<LauncherButton
|
|
305
|
+
theme={theme}
|
|
306
|
+
unread={totalUnread}
|
|
307
|
+
variant={props.variant}
|
|
308
|
+
size={props.size}
|
|
309
|
+
label={props.label}
|
|
310
|
+
style={props.style}
|
|
269
311
|
onPress={openSheet}
|
|
270
|
-
|
|
271
|
-
accessibilityLabel={
|
|
272
|
-
totalUnread > 0
|
|
273
|
-
? `Support, ${totalUnread} unread ${totalUnread === 1 ? 'reply' : 'replies'}`
|
|
274
|
-
: 'Support'
|
|
275
|
-
}
|
|
276
|
-
style={({ pressed }) => ({
|
|
277
|
-
width: 44,
|
|
278
|
-
height: 44,
|
|
279
|
-
borderRadius: 22,
|
|
280
|
-
borderWidth: 1,
|
|
281
|
-
borderColor: theme.colors.border,
|
|
282
|
-
backgroundColor: pressed
|
|
283
|
-
? theme.colors.surfaceHover
|
|
284
|
-
: theme.colors.surface,
|
|
285
|
-
alignItems: 'center',
|
|
286
|
-
justifyContent: 'center',
|
|
287
|
-
})}
|
|
288
|
-
>
|
|
289
|
-
<BubbleMark color={theme.colors.textPrimary} size={18} />
|
|
290
|
-
{totalUnread > 0 && (
|
|
291
|
-
<View
|
|
292
|
-
style={{
|
|
293
|
-
position: 'absolute',
|
|
294
|
-
top: -3,
|
|
295
|
-
right: -3,
|
|
296
|
-
minWidth: 16,
|
|
297
|
-
height: 16,
|
|
298
|
-
borderRadius: 8,
|
|
299
|
-
paddingHorizontal: 4,
|
|
300
|
-
backgroundColor: theme.colors.accent,
|
|
301
|
-
alignItems: 'center',
|
|
302
|
-
justifyContent: 'center',
|
|
303
|
-
}}
|
|
304
|
-
>
|
|
305
|
-
<Text
|
|
306
|
-
style={{
|
|
307
|
-
fontSize: 10,
|
|
308
|
-
fontWeight: '600',
|
|
309
|
-
color: theme.colors.accentForeground,
|
|
310
|
-
fontVariant: ['tabular-nums'],
|
|
311
|
-
}}
|
|
312
|
-
>
|
|
313
|
-
{totalUnread > 9 ? '9+' : totalUnread}
|
|
314
|
-
</Text>
|
|
315
|
-
</View>
|
|
316
|
-
)}
|
|
317
|
-
</Pressable>
|
|
312
|
+
/>
|
|
318
313
|
|
|
319
314
|
<BottomSheet visible={sheetOpen} onClose={closeSheet} theme={theme}>
|
|
320
|
-
{screen
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
</>
|
|
339
344
|
)}
|
|
340
|
-
{screen.name === 'thread' &&
|
|
341
|
-
(activeThread ? (
|
|
342
|
-
<ThreadScreen
|
|
343
|
-
theme={theme}
|
|
344
|
-
thread={activeThread}
|
|
345
|
-
onBack={() => setScreen({ name: 'home' })}
|
|
346
|
-
onSend={(message) => handleSend(activeThread.id, message)}
|
|
347
|
-
/>
|
|
348
|
-
) : (
|
|
349
|
-
<HomeScreen
|
|
350
|
-
theme={theme}
|
|
351
|
-
threads={threads}
|
|
352
|
-
loading={loading}
|
|
353
|
-
error={error}
|
|
354
|
-
onRetry={() => void refresh()}
|
|
355
|
-
onCompose={(type) => setScreen({ name: 'compose', type })}
|
|
356
|
-
onOpenThread={openThread}
|
|
357
|
-
onClose={closeSheet}
|
|
358
|
-
/>
|
|
359
|
-
))}
|
|
360
345
|
</BottomSheet>
|
|
361
346
|
</>
|
|
362
347
|
);
|
package/src/components/icons.tsx
CHANGED
|
@@ -145,43 +145,137 @@ export function CheckIcon({ color, size = 16 }: { color: string; size?: number }
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/**
|
|
149
|
-
export function
|
|
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
|
+
|
|
184
|
+
/** Send arrow: a stem plus a chevron head, both centered on the same axis. */
|
|
185
|
+
export function ArrowUpIcon({ color, size = 16 }: { color: string; size?: number }) {
|
|
186
|
+
const stroke = STROKE + 0.4;
|
|
187
|
+
const head = size * 0.44;
|
|
188
|
+
return (
|
|
189
|
+
<View
|
|
190
|
+
style={{
|
|
191
|
+
width: size,
|
|
192
|
+
height: size,
|
|
193
|
+
alignItems: 'center',
|
|
194
|
+
justifyContent: 'center',
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<View
|
|
198
|
+
style={{
|
|
199
|
+
position: 'absolute',
|
|
200
|
+
width: stroke,
|
|
201
|
+
height: size * 0.66,
|
|
202
|
+
borderRadius: stroke,
|
|
203
|
+
backgroundColor: color,
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
<View
|
|
207
|
+
style={{
|
|
208
|
+
position: 'absolute',
|
|
209
|
+
// Rotating a square by 45° lifts its apex by ~0.207 × side, so
|
|
210
|
+
// offset by that to land the point on top of the stem.
|
|
211
|
+
top: size * 0.17 + head * 0.207,
|
|
212
|
+
width: head,
|
|
213
|
+
height: head,
|
|
214
|
+
borderTopWidth: stroke,
|
|
215
|
+
borderLeftWidth: stroke,
|
|
216
|
+
borderTopLeftRadius: stroke,
|
|
217
|
+
borderColor: color,
|
|
218
|
+
transform: [{ rotate: '45deg' }],
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
</View>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
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.
|
|
229
|
+
*/
|
|
230
|
+
export function ChatBubbleMark({
|
|
231
|
+
color,
|
|
232
|
+
dotColor,
|
|
233
|
+
size = 24,
|
|
234
|
+
}: {
|
|
235
|
+
color: string;
|
|
236
|
+
/** The surface behind the mark — the dots are punched out in this color. */
|
|
237
|
+
dotColor: string;
|
|
238
|
+
size?: number;
|
|
239
|
+
}) {
|
|
240
|
+
const bubbleHeight = size * 0.82;
|
|
150
241
|
const dot: ViewStyle = {
|
|
151
|
-
width: size * 0.
|
|
152
|
-
height: size * 0.
|
|
242
|
+
width: size * 0.13,
|
|
243
|
+
height: size * 0.13,
|
|
153
244
|
borderRadius: size,
|
|
154
|
-
backgroundColor:
|
|
245
|
+
backgroundColor: dotColor,
|
|
155
246
|
};
|
|
156
247
|
return (
|
|
157
|
-
<View style={{
|
|
248
|
+
<View style={{ width: size, height: size, justifyContent: 'flex-start' }}>
|
|
158
249
|
<View
|
|
159
250
|
style={{
|
|
160
251
|
width: size,
|
|
161
|
-
height:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
borderRadius: size * 0.28,
|
|
252
|
+
height: bubbleHeight,
|
|
253
|
+
borderRadius: size * 0.33,
|
|
254
|
+
backgroundColor: color,
|
|
165
255
|
flexDirection: 'row',
|
|
166
256
|
alignItems: 'center',
|
|
167
257
|
justifyContent: 'center',
|
|
168
|
-
gap: size * 0.
|
|
258
|
+
gap: size * 0.115,
|
|
169
259
|
}}
|
|
170
260
|
>
|
|
171
261
|
<View style={dot} />
|
|
172
262
|
<View style={dot} />
|
|
173
263
|
<View style={dot} />
|
|
174
264
|
</View>
|
|
265
|
+
{/* Tail, drawn as a border triangle so it needs no svg. */}
|
|
175
266
|
<View
|
|
176
267
|
style={{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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,
|
|
185
279
|
}}
|
|
186
280
|
/>
|
|
187
281
|
</View>
|