@appsupportkit/support-sdk 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/api.js +61 -12
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/BottomSheet.js +1 -1
- package/lib/commonjs/components/BottomSheet.js.map +1 -1
- package/lib/commonjs/components/LauncherButton.js +4 -4
- package/lib/commonjs/components/LauncherButton.js.map +1 -1
- package/lib/commonjs/components/SupportButton.js +192 -66
- package/lib/commonjs/components/SupportButton.js.map +1 -1
- package/lib/commonjs/components/icons.js +117 -110
- package/lib/commonjs/components/icons.js.map +1 -1
- package/lib/commonjs/components/primitives.js +3 -154
- package/lib/commonjs/components/primitives.js.map +1 -1
- package/lib/commonjs/components/screens/ChatScreen.js +487 -0
- package/lib/commonjs/components/screens/ChatScreen.js.map +1 -0
- package/lib/commonjs/components/screens/LanguageScreen.js +150 -0
- package/lib/commonjs/components/screens/LanguageScreen.js.map +1 -0
- 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 +75 -0
- package/lib/commonjs/i18n/strings/de.js.map +1 -0
- package/lib/commonjs/i18n/strings/en.js +91 -0
- package/lib/commonjs/i18n/strings/en.js.map +1 -0
- package/lib/commonjs/i18n/strings/es.js +75 -0
- package/lib/commonjs/i18n/strings/es.js.map +1 -0
- package/lib/commonjs/i18n/strings/fr.js +75 -0
- package/lib/commonjs/i18n/strings/fr.js.map +1 -0
- package/lib/commonjs/i18n/strings/hi.js +75 -0
- package/lib/commonjs/i18n/strings/hi.js.map +1 -0
- package/lib/commonjs/i18n/strings/it.js +75 -0
- package/lib/commonjs/i18n/strings/it.js.map +1 -0
- package/lib/commonjs/i18n/strings/ja.js +75 -0
- package/lib/commonjs/i18n/strings/ja.js.map +1 -0
- package/lib/commonjs/i18n/strings/ko.js +75 -0
- package/lib/commonjs/i18n/strings/ko.js.map +1 -0
- package/lib/commonjs/i18n/strings/nl.js +75 -0
- package/lib/commonjs/i18n/strings/nl.js.map +1 -0
- package/lib/commonjs/i18n/strings/pl.js +75 -0
- package/lib/commonjs/i18n/strings/pl.js.map +1 -0
- package/lib/commonjs/i18n/strings/pt.js +75 -0
- package/lib/commonjs/i18n/strings/pt.js.map +1 -0
- package/lib/commonjs/i18n/strings/ru.js +75 -0
- package/lib/commonjs/i18n/strings/ru.js.map +1 -0
- package/lib/commonjs/i18n/strings/tr.js +75 -0
- package/lib/commonjs/i18n/strings/tr.js.map +1 -0
- package/lib/commonjs/i18n/strings/zh.js +75 -0
- package/lib/commonjs/i18n/strings/zh.js.map +1 -0
- package/lib/commonjs/index.js +38 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/push.js +78 -3
- package/lib/commonjs/push.js.map +1 -1
- package/lib/commonjs/review.js +54 -0
- package/lib/commonjs/review.js.map +1 -0
- package/lib/commonjs/theme.js +142 -23
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/commonjs/types.js +32 -3
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/useSupportThreads.js +41 -24
- package/lib/commonjs/useSupportThreads.js.map +1 -1
- package/lib/commonjs/utils.js +25 -30
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/commonjs/version.js +13 -0
- package/lib/commonjs/version.js.map +1 -0
- package/lib/module/api.js +57 -10
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/BottomSheet.js +1 -1
- package/lib/module/components/BottomSheet.js.map +1 -1
- package/lib/module/components/LauncherButton.js +5 -5
- package/lib/module/components/LauncherButton.js.map +1 -1
- package/lib/module/components/SupportButton.js +199 -73
- package/lib/module/components/SupportButton.js.map +1 -1
- package/lib/module/components/icons.js +115 -107
- package/lib/module/components/icons.js.map +1 -1
- package/lib/module/components/primitives.js +5 -147
- package/lib/module/components/primitives.js.map +1 -1
- package/lib/module/components/screens/ChatScreen.js +485 -0
- package/lib/module/components/screens/ChatScreen.js.map +1 -0
- package/lib/module/components/screens/LanguageScreen.js +147 -0
- package/lib/module/components/screens/LanguageScreen.js.map +1 -0
- 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 +71 -0
- package/lib/module/i18n/strings/de.js.map +1 -0
- package/lib/module/i18n/strings/en.js +87 -0
- package/lib/module/i18n/strings/en.js.map +1 -0
- package/lib/module/i18n/strings/es.js +71 -0
- package/lib/module/i18n/strings/es.js.map +1 -0
- package/lib/module/i18n/strings/fr.js +71 -0
- package/lib/module/i18n/strings/fr.js.map +1 -0
- package/lib/module/i18n/strings/hi.js +71 -0
- package/lib/module/i18n/strings/hi.js.map +1 -0
- package/lib/module/i18n/strings/it.js +71 -0
- package/lib/module/i18n/strings/it.js.map +1 -0
- package/lib/module/i18n/strings/ja.js +71 -0
- package/lib/module/i18n/strings/ja.js.map +1 -0
- package/lib/module/i18n/strings/ko.js +71 -0
- package/lib/module/i18n/strings/ko.js.map +1 -0
- package/lib/module/i18n/strings/nl.js +71 -0
- package/lib/module/i18n/strings/nl.js.map +1 -0
- package/lib/module/i18n/strings/pl.js +71 -0
- package/lib/module/i18n/strings/pl.js.map +1 -0
- package/lib/module/i18n/strings/pt.js +71 -0
- package/lib/module/i18n/strings/pt.js.map +1 -0
- package/lib/module/i18n/strings/ru.js +71 -0
- package/lib/module/i18n/strings/ru.js.map +1 -0
- package/lib/module/i18n/strings/tr.js +71 -0
- package/lib/module/i18n/strings/tr.js.map +1 -0
- package/lib/module/i18n/strings/zh.js +71 -0
- package/lib/module/i18n/strings/zh.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/push.js +77 -3
- package/lib/module/push.js.map +1 -1
- package/lib/module/review.js +48 -0
- package/lib/module/review.js.map +1 -0
- package/lib/module/theme.js +141 -23
- package/lib/module/theme.js.map +1 -1
- package/lib/module/types.js +32 -3
- package/lib/module/types.js.map +1 -1
- package/lib/module/useSupportThreads.js +41 -24
- package/lib/module/useSupportThreads.js.map +1 -1
- package/lib/module/utils.js +24 -29
- package/lib/module/utils.js.map +1 -1
- package/lib/module/version.js +9 -0
- package/lib/module/version.js.map +1 -0
- package/lib/typescript/api.d.ts +40 -10
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/SupportButton.d.ts +2 -2
- package/lib/typescript/components/SupportButton.d.ts.map +1 -1
- package/lib/typescript/components/icons.d.ts +10 -14
- package/lib/typescript/components/icons.d.ts.map +1 -1
- package/lib/typescript/components/primitives.d.ts +3 -30
- package/lib/typescript/components/primitives.d.ts.map +1 -1
- package/lib/typescript/components/screens/ChatScreen.d.ts +21 -0
- package/lib/typescript/components/screens/ChatScreen.d.ts.map +1 -0
- package/lib/typescript/components/screens/LanguageScreen.d.ts +30 -0
- package/lib/typescript/components/screens/LanguageScreen.d.ts.map +1 -0
- 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 +86 -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 +4 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/push.d.ts +14 -2
- package/lib/typescript/push.d.ts.map +1 -1
- package/lib/typescript/review.d.ts +15 -0
- package/lib/typescript/review.d.ts.map +1 -0
- package/lib/typescript/theme.d.ts +23 -5
- package/lib/typescript/theme.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +86 -13
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/useSupportThreads.d.ts +20 -4
- package/lib/typescript/useSupportThreads.d.ts.map +1 -1
- package/lib/typescript/utils.d.ts +8 -6
- package/lib/typescript/utils.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +7 -0
- package/lib/typescript/version.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/api.ts +75 -23
- package/src/components/BottomSheet.tsx +1 -1
- package/src/components/LauncherButton.tsx +4 -4
- package/src/components/SupportButton.tsx +244 -86
- package/src/components/icons.tsx +96 -101
- package/src/components/primitives.tsx +9 -161
- package/src/components/screens/ChatScreen.tsx +541 -0
- package/src/components/screens/LanguageScreen.tsx +172 -0
- package/src/i18n/index.ts +81 -0
- package/src/i18n/languages.ts +106 -0
- package/src/i18n/strings/de.ts +81 -0
- package/src/i18n/strings/en.ts +97 -0
- package/src/i18n/strings/es.ts +80 -0
- package/src/i18n/strings/fr.ts +81 -0
- package/src/i18n/strings/hi.ts +80 -0
- package/src/i18n/strings/it.ts +80 -0
- package/src/i18n/strings/ja.ts +80 -0
- package/src/i18n/strings/ko.ts +79 -0
- package/src/i18n/strings/nl.ts +80 -0
- package/src/i18n/strings/pl.ts +80 -0
- package/src/i18n/strings/pt.ts +80 -0
- package/src/i18n/strings/ru.ts +80 -0
- package/src/i18n/strings/tr.ts +80 -0
- package/src/i18n/strings/zh.ts +78 -0
- package/src/index.tsx +13 -0
- package/src/push.ts +100 -2
- package/src/review.ts +54 -0
- package/src/theme.ts +182 -28
- package/src/types.ts +85 -13
- package/src/useSupportThreads.ts +61 -28
- package/src/utils.ts +27 -29
- package/src/version.ts +6 -0
- package/lib/commonjs/components/screens/ComposeScreen.js +0 -228
- package/lib/commonjs/components/screens/ComposeScreen.js.map +0 -1
- package/lib/commonjs/components/screens/HomeScreen.js +0 -286
- package/lib/commonjs/components/screens/HomeScreen.js.map +0 -1
- package/lib/commonjs/components/screens/ThreadScreen.js +0 -327
- package/lib/commonjs/components/screens/ThreadScreen.js.map +0 -1
- package/lib/module/components/screens/ComposeScreen.js +0 -224
- package/lib/module/components/screens/ComposeScreen.js.map +0 -1
- package/lib/module/components/screens/HomeScreen.js +0 -282
- package/lib/module/components/screens/HomeScreen.js.map +0 -1
- package/lib/module/components/screens/ThreadScreen.js +0 -325
- package/lib/module/components/screens/ThreadScreen.js.map +0 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts +0 -10
- package/lib/typescript/components/screens/ComposeScreen.d.ts.map +0 -1
- package/lib/typescript/components/screens/HomeScreen.d.ts +0 -14
- package/lib/typescript/components/screens/HomeScreen.d.ts.map +0 -1
- package/lib/typescript/components/screens/ThreadScreen.d.ts +0 -10
- package/lib/typescript/components/screens/ThreadScreen.d.ts.map +0 -1
- package/src/components/screens/ComposeScreen.tsx +0 -233
- package/src/components/screens/HomeScreen.tsx +0 -287
- package/src/components/screens/ThreadScreen.tsx +0 -397
|
@@ -1,38 +1,65 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
-
import { Text, View } from 'react-native';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { Platform, Text, useColorScheme, View } from 'react-native';
|
|
3
3
|
import {
|
|
4
|
-
postMessage,
|
|
5
4
|
registerPush,
|
|
6
|
-
|
|
5
|
+
registerUser,
|
|
6
|
+
reviewOpened,
|
|
7
|
+
sendMessage,
|
|
8
|
+
setLanguage as setLanguageRequest,
|
|
7
9
|
SupportApiError,
|
|
8
10
|
type ApiConfig,
|
|
9
11
|
} from '../api';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
12
|
+
import { createTranslator } from '../i18n';
|
|
13
|
+
import {
|
|
14
|
+
asLanguage,
|
|
15
|
+
DEFAULT_LANGUAGE,
|
|
16
|
+
deviceLanguage,
|
|
17
|
+
type Language,
|
|
18
|
+
} from '../i18n/languages';
|
|
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';
|
|
13
27
|
import { IDLE_POLL_MS, useSupportThreads } from '../useSupportThreads';
|
|
14
|
-
import { validateProps } from '../utils';
|
|
28
|
+
import { userPlan, validateProps } from '../utils';
|
|
15
29
|
import { BottomSheet } from './BottomSheet';
|
|
16
30
|
import { LauncherButton } from './LauncherButton';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { ThreadScreen } from './screens/ThreadScreen';
|
|
31
|
+
import { ChatScreen } from './screens/ChatScreen';
|
|
32
|
+
import { LanguageScreen } from './screens/LanguageScreen';
|
|
20
33
|
|
|
21
|
-
type Screen =
|
|
22
|
-
| { name: 'home' }
|
|
23
|
-
| { name: 'compose'; type: QueryType }
|
|
24
|
-
| { name: 'thread'; threadId: string };
|
|
34
|
+
type Screen = 'chat' | 'language';
|
|
25
35
|
|
|
26
36
|
/**
|
|
27
37
|
* The single entry point of the SDK: a launcher with an unread badge that
|
|
28
|
-
* opens the support bottom sheet.
|
|
29
|
-
* handled by useSupportThreads.
|
|
38
|
+
* opens the support chat in a bottom sheet. Conversations, polling, and read
|
|
39
|
+
* receipts are handled by useSupportThreads.
|
|
30
40
|
*/
|
|
31
41
|
export function SupportButton(props: SupportButtonProps) {
|
|
32
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);
|
|
33
60
|
const theme = useMemo(
|
|
34
|
-
() =>
|
|
35
|
-
[
|
|
61
|
+
() => themeFor(themeName, accent, !perModeAccent),
|
|
62
|
+
[themeName, accent, perModeAccent],
|
|
36
63
|
);
|
|
37
64
|
|
|
38
65
|
const config: ApiConfig = useMemo(
|
|
@@ -45,17 +72,61 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
45
72
|
);
|
|
46
73
|
|
|
47
74
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
48
|
-
const [screen, setScreen] = useState<Screen>(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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);
|
|
83
|
+
|
|
84
|
+
const {
|
|
85
|
+
threads,
|
|
86
|
+
totalUnread,
|
|
87
|
+
loading,
|
|
88
|
+
errorKey,
|
|
89
|
+
serverLanguage,
|
|
90
|
+
languageLoaded,
|
|
91
|
+
store,
|
|
92
|
+
refresh,
|
|
93
|
+
reload,
|
|
94
|
+
} = useSupportThreads({
|
|
95
|
+
config,
|
|
96
|
+
revenuecatId: props.user.revenuecatId,
|
|
97
|
+
enabled: validationError === null,
|
|
98
|
+
sheetOpen,
|
|
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,
|
|
102
|
+
idlePollMs: props.pollIntervalMs ?? IDLE_POLL_MS,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const [draftLanguage, setDraftLanguage] = useState<Language | null>(null);
|
|
106
|
+
const [savingLanguage, setSavingLanguage] = useState(false);
|
|
107
|
+
const [languageError, setLanguageError] = useState<string | null>(null);
|
|
108
|
+
|
|
109
|
+
// A preference saved on another device (or an earlier install) wins over
|
|
110
|
+
// any local guess, and answers the "have they chosen?" question for free.
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const stored = asLanguage(serverLanguage);
|
|
113
|
+
if (stored !== null) setChosen(stored);
|
|
114
|
+
}, [serverLanguage]);
|
|
115
|
+
|
|
116
|
+
const fallbackLanguage =
|
|
117
|
+
props.defaultLanguage ?? deviceLanguage() ?? DEFAULT_LANGUAGE;
|
|
118
|
+
const language = chosen ?? fallbackLanguage;
|
|
119
|
+
const strings = useMemo(
|
|
120
|
+
() => createTranslator(draftLanguage ?? language),
|
|
121
|
+
[draftLanguage, language],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Ask once, on first open — but only once the server has told us whether
|
|
126
|
+
* this user already answered. Asking before then would show the picker to
|
|
127
|
+
* someone who chose months ago on another device.
|
|
128
|
+
*/
|
|
129
|
+
const mustChooseLanguage = languageLoaded && chosen === null;
|
|
59
130
|
|
|
60
131
|
/**
|
|
61
132
|
* Asks for notification permission the moment the sheet opens, then
|
|
@@ -78,8 +149,60 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
78
149
|
}
|
|
79
150
|
}, [config, props.user.revenuecatId, props.expoProjectId, validationError]);
|
|
80
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
|
+
|
|
81
204
|
function openSheet() {
|
|
82
|
-
setScreen(
|
|
205
|
+
setScreen('chat');
|
|
83
206
|
setSheetOpen(true);
|
|
84
207
|
// The OS prompt appears right as the sheet opens; if the user declines,
|
|
85
208
|
// everything below carries on exactly as before.
|
|
@@ -88,7 +211,9 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
88
211
|
|
|
89
212
|
function closeSheet() {
|
|
90
213
|
setSheetOpen(false);
|
|
91
|
-
setScreen(
|
|
214
|
+
setScreen('chat');
|
|
215
|
+
setDraftLanguage(null);
|
|
216
|
+
setLanguageError(null);
|
|
92
217
|
}
|
|
93
218
|
|
|
94
219
|
if (validationError !== null) {
|
|
@@ -116,41 +241,52 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
116
241
|
|
|
117
242
|
function writeErrorMessage(e: unknown): string {
|
|
118
243
|
if (e instanceof SupportApiError) {
|
|
119
|
-
if (e.status === 429) return '
|
|
120
|
-
if (e.offline) return
|
|
244
|
+
if (e.status === 429) return strings.t('error.rateLimited');
|
|
245
|
+
if (e.offline) return strings.t('error.offline');
|
|
121
246
|
}
|
|
122
|
-
return
|
|
247
|
+
return strings.t('error.sendFailed');
|
|
123
248
|
}
|
|
124
249
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Persists the picked language and leaves the picker.
|
|
252
|
+
*
|
|
253
|
+
* A failed save keeps the user on the screen with a retry: getting this
|
|
254
|
+
* wrong means every message they write is translated from the wrong
|
|
255
|
+
* language, which is worse than one more tap.
|
|
256
|
+
*/
|
|
257
|
+
async function confirmLanguage() {
|
|
258
|
+
const next = draftLanguage ?? language;
|
|
259
|
+
setSavingLanguage(true);
|
|
260
|
+
setLanguageError(null);
|
|
129
261
|
try {
|
|
130
|
-
await
|
|
131
|
-
queryType: type,
|
|
132
|
-
message,
|
|
133
|
-
userName: props.user.name,
|
|
134
|
-
isPaid: props.user.isPaid,
|
|
262
|
+
await setLanguageRequest(config, {
|
|
135
263
|
revenuecatId: props.user.revenuecatId,
|
|
136
|
-
|
|
264
|
+
language: next,
|
|
137
265
|
});
|
|
266
|
+
setChosen(next);
|
|
267
|
+
setDraftLanguage(null);
|
|
268
|
+
setScreen('chat');
|
|
269
|
+
// On a change, the server re-queues existing replies for translation
|
|
270
|
+
// into the new language; they arrive over the next poll or two.
|
|
138
271
|
void reload();
|
|
139
|
-
return null;
|
|
140
272
|
} catch (e) {
|
|
141
|
-
|
|
273
|
+
setLanguageError(
|
|
274
|
+
e instanceof SupportApiError && e.offline
|
|
275
|
+
? strings.t('error.offline')
|
|
276
|
+
: strings.t('lang.saveFailed'),
|
|
277
|
+
);
|
|
278
|
+
} finally {
|
|
279
|
+
setSavingLanguage(false);
|
|
142
280
|
}
|
|
143
281
|
}
|
|
144
282
|
|
|
145
|
-
async function handleSend(
|
|
146
|
-
threadId: string,
|
|
147
|
-
message: string,
|
|
148
|
-
): Promise<string | null> {
|
|
283
|
+
async function handleSend(message: string): Promise<string | null> {
|
|
149
284
|
try {
|
|
150
|
-
await
|
|
151
|
-
|
|
152
|
-
revenuecatId: props.user.revenuecatId,
|
|
285
|
+
await sendMessage(config, {
|
|
286
|
+
revenuecatId,
|
|
153
287
|
message,
|
|
288
|
+
language,
|
|
289
|
+
user: { name: userName, plan, appVersion, platform: storePlatform() },
|
|
154
290
|
});
|
|
155
291
|
await reload();
|
|
156
292
|
return null;
|
|
@@ -159,24 +295,42 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
159
295
|
}
|
|
160
296
|
}
|
|
161
297
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
298
|
+
const reviewLinks = storeLinks(store);
|
|
299
|
+
const reviewLabel =
|
|
300
|
+
reviewLinks === null
|
|
301
|
+
? null
|
|
302
|
+
: strings.t(Platform.OS === 'ios' ? 'review.appStore' : 'review.playStore');
|
|
303
|
+
|
|
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
|
+
}
|
|
166
312
|
|
|
167
|
-
const
|
|
168
|
-
<
|
|
313
|
+
const languagePicker = (
|
|
314
|
+
<LanguageScreen
|
|
169
315
|
theme={theme}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
316
|
+
strings={strings}
|
|
317
|
+
selected={draftLanguage ?? language}
|
|
318
|
+
onSelect={setDraftLanguage}
|
|
319
|
+
onConfirm={() => void confirmLanguage()}
|
|
320
|
+
// No way back on first run: there is nothing behind it yet, so the
|
|
321
|
+
// only exit is choosing — or closing the sheet, which asks again.
|
|
322
|
+
onBack={
|
|
323
|
+
mustChooseLanguage
|
|
324
|
+
? undefined
|
|
325
|
+
: () => {
|
|
326
|
+
setDraftLanguage(null);
|
|
327
|
+
setLanguageError(null);
|
|
328
|
+
setScreen('chat');
|
|
329
|
+
}
|
|
178
330
|
}
|
|
179
|
-
|
|
331
|
+
onDismiss={mustChooseLanguage ? closeSheet : undefined}
|
|
332
|
+
saving={savingLanguage}
|
|
333
|
+
error={languageError}
|
|
180
334
|
/>
|
|
181
335
|
);
|
|
182
336
|
|
|
@@ -193,26 +347,30 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
193
347
|
/>
|
|
194
348
|
|
|
195
349
|
<BottomSheet visible={sheetOpen} onClose={closeSheet} theme={theme}>
|
|
196
|
-
{screen
|
|
197
|
-
|
|
198
|
-
|
|
350
|
+
{/* The one-time prompt outranks whatever screen the user is on: it
|
|
351
|
+
only ever fires before they have sent anything. */}
|
|
352
|
+
{mustChooseLanguage || screen === 'language' ? (
|
|
353
|
+
languagePicker
|
|
354
|
+
) : (
|
|
355
|
+
<ChatScreen
|
|
199
356
|
theme={theme}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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}
|
|
203
372
|
/>
|
|
204
373
|
)}
|
|
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
374
|
</BottomSheet>
|
|
217
375
|
</>
|
|
218
376
|
);
|
package/src/components/icons.tsx
CHANGED
|
@@ -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,
|
|
@@ -145,6 +91,42 @@ export function CheckIcon({ color, size = 16 }: { color: string; size?: number }
|
|
|
145
91
|
);
|
|
146
92
|
}
|
|
147
93
|
|
|
94
|
+
/** Globe: circle, equator, and a pill standing in for the meridian curve. */
|
|
95
|
+
export function GlobeIcon({ color, size = 16 }: { color: string; size?: number }) {
|
|
96
|
+
const inner = size - STROKE * 2;
|
|
97
|
+
return (
|
|
98
|
+
<View
|
|
99
|
+
style={{
|
|
100
|
+
width: size,
|
|
101
|
+
height: size,
|
|
102
|
+
borderWidth: STROKE,
|
|
103
|
+
borderColor: color,
|
|
104
|
+
borderRadius: size / 2,
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
justifyContent: 'center',
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<View
|
|
110
|
+
style={{
|
|
111
|
+
position: 'absolute',
|
|
112
|
+
width: inner,
|
|
113
|
+
height: STROKE,
|
|
114
|
+
backgroundColor: color,
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
<View
|
|
118
|
+
style={{
|
|
119
|
+
width: size * 0.52,
|
|
120
|
+
height: inner,
|
|
121
|
+
borderWidth: STROKE,
|
|
122
|
+
borderColor: color,
|
|
123
|
+
borderRadius: size * 0.26,
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
148
130
|
/** Send arrow: a stem plus a chevron head, both centered on the same axis. */
|
|
149
131
|
export function ArrowUpIcon({ color, size = 16 }: { color: string; size?: number }) {
|
|
150
132
|
const stroke = STROKE + 0.4;
|
|
@@ -187,61 +169,74 @@ export function ArrowUpIcon({ color, size = 16 }: { color: string; size?: number
|
|
|
187
169
|
}
|
|
188
170
|
|
|
189
171
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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.
|
|
176
|
+
*/
|
|
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.
|
|
193
190
|
*/
|
|
194
|
-
export function
|
|
191
|
+
export function ConversationMark({
|
|
195
192
|
color,
|
|
196
|
-
|
|
193
|
+
holeColor,
|
|
197
194
|
size = 24,
|
|
198
195
|
}: {
|
|
199
196
|
color: string;
|
|
200
|
-
/** The surface behind the mark — the
|
|
201
|
-
|
|
197
|
+
/** The surface behind the mark — the gap between the bubbles is cut in this color. */
|
|
198
|
+
holeColor: string;
|
|
202
199
|
size?: number;
|
|
203
200
|
}) {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
|
|
211
233
|
return (
|
|
212
|
-
<View style={{ width: size, height: size
|
|
213
|
-
<View
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
backgroundColor: color,
|
|
219
|
-
flexDirection: 'row',
|
|
220
|
-
alignItems: 'center',
|
|
221
|
-
justifyContent: 'center',
|
|
222
|
-
gap: size * 0.115,
|
|
223
|
-
}}
|
|
224
|
-
>
|
|
225
|
-
<View style={dot} />
|
|
226
|
-
<View style={dot} />
|
|
227
|
-
<View style={dot} />
|
|
228
|
-
</View>
|
|
229
|
-
{/* Tail, drawn as a border triangle so it needs no svg. */}
|
|
230
|
-
<View
|
|
231
|
-
style={{
|
|
232
|
-
position: 'absolute',
|
|
233
|
-
left: size * 0.22,
|
|
234
|
-
top: bubbleHeight - 1,
|
|
235
|
-
width: 0,
|
|
236
|
-
height: 0,
|
|
237
|
-
borderLeftWidth: size * 0.09,
|
|
238
|
-
borderRightWidth: size * 0.16,
|
|
239
|
-
borderTopWidth: size * 0.2,
|
|
240
|
-
borderLeftColor: 'transparent',
|
|
241
|
-
borderRightColor: 'transparent',
|
|
242
|
-
borderTopColor: color,
|
|
243
|
-
}}
|
|
244
|
-
/>
|
|
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')} />
|
|
245
240
|
</View>
|
|
246
241
|
);
|
|
247
242
|
}
|