@appsupportkit/support-sdk 0.1.2 → 0.2.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/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 +54 -176
- package/lib/commonjs/components/SupportButton.js.map +1 -1
- package/lib/commonjs/components/icons.js +74 -23
- package/lib/commonjs/components/icons.js.map +1 -1
- package/lib/commonjs/components/primitives.js +183 -38
- package/lib/commonjs/components/primitives.js.map +1 -1
- package/lib/commonjs/components/screens/ComposeScreen.js +115 -58
- package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/HomeScreen.js +170 -115
- package/lib/commonjs/components/screens/HomeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/ThreadScreen.js +222 -108
- package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -1
- 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 +7 -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 +286 -0
- package/lib/commonjs/useSupportThreads.js.map +1 -0
- package/lib/commonjs/utils.js +33 -0
- package/lib/commonjs/utils.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 +57 -179
- package/lib/module/components/SupportButton.js.map +1 -1
- package/lib/module/components/icons.js +72 -22
- package/lib/module/components/icons.js.map +1 -1
- package/lib/module/components/primitives.js +179 -39
- package/lib/module/components/primitives.js.map +1 -1
- package/lib/module/components/screens/ComposeScreen.js +118 -61
- package/lib/module/components/screens/ComposeScreen.js.map +1 -1
- package/lib/module/components/screens/HomeScreen.js +172 -117
- package/lib/module/components/screens/HomeScreen.js.map +1 -1
- package/lib/module/components/screens/ThreadScreen.js +228 -112
- package/lib/module/components/screens/ThreadScreen.js.map +1 -1
- 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 +7 -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 +281 -0
- package/lib/module/useSupportThreads.js.map +1 -0
- package/lib/module/utils.js +30 -0
- package/lib/module/utils.js.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 +13 -2
- package/lib/typescript/components/icons.d.ts.map +1 -1
- package/lib/typescript/components/primitives.d.ts +35 -4
- package/lib/typescript/components/primitives.d.ts.map +1 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/HomeScreen.d.ts +3 -2
- package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -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 +22 -0
- 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 +28 -0
- package/lib/typescript/useSupportThreads.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +5 -0
- package/lib/typescript/utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/BottomSheet.tsx +61 -43
- package/src/components/LauncherButton.tsx +217 -0
- package/src/components/SupportButton.tsx +52 -196
- package/src/components/icons.tsx +77 -19
- package/src/components/primitives.tsx +217 -36
- package/src/components/screens/ComposeScreen.tsx +131 -61
- package/src/components/screens/HomeScreen.tsx +168 -117
- package/src/components/screens/ThreadScreen.tsx +289 -119
- package/src/index.tsx +1 -0
- package/src/theme.ts +165 -18
- package/src/types.ts +24 -0
- package/src/unread.ts +91 -0
- package/src/useSupportThreads.ts +372 -0
- package/src/utils.ts +33 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomeScreen.d.ts","sourceRoot":"","sources":["../../../../src/components/screens/HomeScreen.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AA+
|
|
1
|
+
{"version":3,"file":"HomeScreen.d.ts","sourceRoot":"","sources":["../../../../src/components/screens/HomeScreen.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AA+IrD,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,OAAO,EACP,OAAO,EACP,UAAU,EACV,KAAK,EACL,SAAS,EACT,SAAS,EACT,YAAY,EACZ,OAAO,GACR,EAAE;IACD,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IACrC,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,+BAyHA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThreadScreen.d.ts","sourceRoot":"","sources":["../../../../src/components/screens/ThreadScreen.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"ThreadScreen.d.ts","sourceRoot":"","sources":["../../../../src/components/screens/ThreadScreen.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,aAAa,CAAC;AA2KzD,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,MAAM,EACN,MAAM,EACN,MAAM,GACP,EAAE;IACD,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,gEAAgE;IAChE,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACrD,+BA4MA"}
|
|
@@ -2,6 +2,6 @@ export { SupportButton } from './components/SupportButton';
|
|
|
2
2
|
export { supportDarkTheme, supportLightTheme, resolveTheme, type SupportTheme, } from './theme';
|
|
3
3
|
export { SupportApiError } from './api';
|
|
4
4
|
export { isPushAvailable } from './push';
|
|
5
|
-
export type { MessageSender, PushPermissionStatus, QueryStatus, QueryType, SupportButtonProps, SupportUser, Thread, ThreadMessage, ThreadsResponse, ThemeName, } from './types';
|
|
5
|
+
export type { LauncherVariant, MessageSender, PushPermissionStatus, QueryStatus, QueryType, SupportButtonProps, SupportUser, Thread, ThreadMessage, ThreadsResponse, ThemeName, } from './types';
|
|
6
6
|
export { QUERY_TYPE_LABEL, USER_MESSAGE_MAX, USER_MESSAGE_MIN, } from './types';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,WAAW,EACX,MAAM,EACN,aAAa,EACb,eAAe,EACf,SAAS,GACV,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AACzC,YAAY,EACV,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,WAAW,EACX,MAAM,EACN,aAAa,EACb,eAAe,EACf,SAAS,GACV,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
|
|
@@ -1,13 +1,54 @@
|
|
|
1
|
-
import { motion,
|
|
1
|
+
import { motion, spacing, type ThemeColors } from '@appsupportkit/design-tokens';
|
|
2
|
+
import type { ViewStyle } from 'react-native';
|
|
2
3
|
import type { ThemeName } from './types';
|
|
4
|
+
/** Rounder than the dashboard: touch targets read better with more radius. */
|
|
5
|
+
export declare const supportRadii: {
|
|
6
|
+
readonly control: 10;
|
|
7
|
+
readonly card: 14;
|
|
8
|
+
readonly cardLg: 18;
|
|
9
|
+
/** Message bubbles. */
|
|
10
|
+
readonly bubble: 18;
|
|
11
|
+
/** The tight corner on a bubble's sender side. */
|
|
12
|
+
readonly bubbleTail: 6;
|
|
13
|
+
readonly sheet: 24;
|
|
14
|
+
readonly pill: 999;
|
|
15
|
+
readonly full: 9999;
|
|
16
|
+
};
|
|
17
|
+
export interface SupportShadows {
|
|
18
|
+
/** Floating launcher button. */
|
|
19
|
+
launcher: ViewStyle;
|
|
20
|
+
/** The bottom sheet itself. */
|
|
21
|
+
sheet: ViewStyle;
|
|
22
|
+
/** Unread badge, so it lifts off the button. */
|
|
23
|
+
badge: ViewStyle;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The launcher sits on the host app's own background rather than on an SDK
|
|
27
|
+
* surface, so the two themes need different treatment: on near-black a
|
|
28
|
+
* solid fill needs a hairline to separate from the background and a black
|
|
29
|
+
* shadow, while on white the accent-tinted shadow does that work and a
|
|
30
|
+
* borderless mark has to be darkened to stay legible.
|
|
31
|
+
*/
|
|
32
|
+
export interface LauncherPalette {
|
|
33
|
+
/** Hairline drawn over the solid fill. */
|
|
34
|
+
solidRing: string;
|
|
35
|
+
/** Mark color for the borderless (minimal) variant. */
|
|
36
|
+
minimalMark: string;
|
|
37
|
+
}
|
|
3
38
|
export interface SupportTheme {
|
|
4
39
|
name: ThemeName;
|
|
5
40
|
colors: ThemeColors;
|
|
6
41
|
spacing: typeof spacing;
|
|
7
|
-
radii: typeof
|
|
42
|
+
radii: typeof supportRadii;
|
|
8
43
|
motion: typeof motion;
|
|
44
|
+
shadows: SupportShadows;
|
|
45
|
+
launcher: LauncherPalette;
|
|
9
46
|
}
|
|
10
47
|
export declare const supportDarkTheme: SupportTheme;
|
|
11
48
|
export declare const supportLightTheme: SupportTheme;
|
|
12
|
-
|
|
49
|
+
/**
|
|
50
|
+
* @param name "light" | "dark" (default dark).
|
|
51
|
+
* @param accentColor Optional brand color (hex) replacing the default accent.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveTheme(name: ThemeName | undefined, accentColor?: string): SupportTheme;
|
|
13
54
|
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/theme.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/theme.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,MAAM,EAEN,OAAO,EACP,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,8EAA8E;AAC9E,eAAO,MAAM,YAAY;;;;IAKvB,uBAAuB;;IAEvB,kDAAkD;;;;;CAI1C,CAAC;AAEX,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,QAAQ,EAAE,SAAS,CAAC;IACpB,+BAA+B;IAC/B,KAAK,EAAE,SAAS,CAAC;IACjB,gDAAgD;IAChD,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxB,KAAK,EAAE,OAAO,YAAY,CAAC;IAC3B,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAmFD,eAAO,MAAM,gBAAgB,EAAE,YAA6C,CAAC;AAC7E,eAAO,MAAM,iBAAiB,EAAE,YAA+C,CAAC;AA+BhF;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,SAAS,GAAG,SAAS,EAC3B,WAAW,CAAC,EAAE,MAAM,GACnB,YAAY,CAGd"}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
1
2
|
export type QueryType = 'feedback' | 'feature_request' | 'bug_report';
|
|
2
3
|
export type QueryStatus = 'open' | 'resolved';
|
|
3
4
|
export type MessageSender = 'user' | 'admin';
|
|
4
5
|
export type ThemeName = 'dark' | 'light';
|
|
5
6
|
export type PushPermissionStatus = 'undetermined' | 'granted' | 'denied';
|
|
7
|
+
/**
|
|
8
|
+
* How the launcher renders:
|
|
9
|
+
* solid — accent fill, elevated. The floating action button.
|
|
10
|
+
* soft — app surface + hairline border. Sits in a header or toolbar.
|
|
11
|
+
* minimal — bare mark, no fill or border. Sits in a nav bar.
|
|
12
|
+
*/
|
|
13
|
+
export type LauncherVariant = 'solid' | 'soft' | 'minimal';
|
|
6
14
|
export interface SupportUser {
|
|
7
15
|
/** Display name shown to the support team. */
|
|
8
16
|
name: string;
|
|
@@ -28,6 +36,20 @@ export interface SupportButtonProps {
|
|
|
28
36
|
supabaseUrl: string;
|
|
29
37
|
/** Visual theme. Matches the admin dashboard tokens. Default "dark". */
|
|
30
38
|
theme?: ThemeName;
|
|
39
|
+
/**
|
|
40
|
+
* Brand color (hex, e.g. "#FF5A1F") replacing the default accent across
|
|
41
|
+
* the launcher, buttons, and the user's own message bubbles. The pressed
|
|
42
|
+
* shade, the 12% wash, and a legible foreground are derived from it.
|
|
43
|
+
*/
|
|
44
|
+
accentColor?: string;
|
|
45
|
+
/** Launcher style. Default "solid" — the floating action button. */
|
|
46
|
+
variant?: LauncherVariant;
|
|
47
|
+
/** Launcher diameter in px (height when `label` is set). Default 52. */
|
|
48
|
+
size?: number;
|
|
49
|
+
/** Renders the launcher as a pill with this text beside the mark. */
|
|
50
|
+
label?: string;
|
|
51
|
+
/** Style applied to the launcher — position it, add margins, etc. */
|
|
52
|
+
style?: StyleProp<ViewStyle>;
|
|
31
53
|
/**
|
|
32
54
|
* Idle poll interval (ms) — how often the unread badge is refreshed while
|
|
33
55
|
* the sheet is CLOSED, using a lightweight count-only endpoint. Default
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,iBAAiB,GAAG,YAAY,CAAC;AACtE,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,CAAC;AAC9C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAC7C,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AACzC,MAAM,MAAM,oBAAoB,GAAG,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEzE,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,MAAM,EAAE,OAAO,CAAC;IAChB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,aAAa,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,IAAI,CAAC;IACT,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAItD,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,gBAAgB,MAAM,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,iBAAiB,GAAG,YAAY,CAAC;AACtE,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,CAAC;AAC9C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAC7C,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AACzC,MAAM,MAAM,oBAAoB,GAAG,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEzE;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,MAAM,EAAE,OAAO,CAAC;IAChB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,aAAa,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,IAAI,CAAC;IACT,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAItD,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,gBAAgB,MAAM,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Thread } from './types';
|
|
2
|
+
/** Watermark map: thread id → `created_at` (ms) of the newest seen reply. */
|
|
3
|
+
export type ReadWatermarks = Map<string, number>;
|
|
4
|
+
/** `created_at` (ms) of the newest admin message, or 0 if there are none. */
|
|
5
|
+
export declare function latestAdminMessageAt(thread: Thread): number;
|
|
6
|
+
/** Admin messages in `thread` newer than `watermark`. */
|
|
7
|
+
export declare function unreadSince(thread: Thread, watermark: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* Returns `thread` with `unread_count` narrowed by what this device knows
|
|
10
|
+
* the user has read. Identity is preserved when nothing changes so lists
|
|
11
|
+
* stay referentially stable between polls.
|
|
12
|
+
*/
|
|
13
|
+
export declare function reconcileThread(thread: Thread, watermarks: ReadWatermarks): Thread;
|
|
14
|
+
export declare function reconcileThreads(threads: Thread[], watermarks: ReadWatermarks): Thread[];
|
|
15
|
+
export declare function sumUnread(threads: Thread[]): number;
|
|
16
|
+
/**
|
|
17
|
+
* Drops watermarks the server has caught up on (its own count is already 0)
|
|
18
|
+
* and ones for threads that no longer exist, so the map can't grow without
|
|
19
|
+
* bound over a long session.
|
|
20
|
+
*/
|
|
21
|
+
export declare function pruneWatermarks(watermarks: ReadWatermarks, serverThreads: Thread[]): ReadWatermarks;
|
|
22
|
+
//# sourceMappingURL=unread.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unread.d.ts","sourceRoot":"","sources":["../../src/unread.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC,6EAA6E;AAC7E,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEjD,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAQ3D;AAED,yDAAyD;AACzD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQrE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,cAAc,GACzB,MAAM,CAKR;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,EAAE,cAAc,GACzB,MAAM,EAAE,CAEV;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,cAAc,EAC1B,aAAa,EAAE,MAAM,EAAE,GACtB,cAAc,CAShB"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ApiConfig } from './api';
|
|
2
|
+
import type { Thread } from './types';
|
|
3
|
+
export declare const IDLE_POLL_MS = 60000;
|
|
4
|
+
export interface SupportThreadsState {
|
|
5
|
+
/** Server threads with unread counts narrowed by local reads. */
|
|
6
|
+
threads: Thread[] | null;
|
|
7
|
+
totalUnread: number;
|
|
8
|
+
/** First load only — there is nothing cached to show yet. */
|
|
9
|
+
loading: boolean;
|
|
10
|
+
/** User-pulled refresh with content already on screen. */
|
|
11
|
+
refreshing: boolean;
|
|
12
|
+
/** Set only when there is nothing cached; background failures stay quiet. */
|
|
13
|
+
error: string | null;
|
|
14
|
+
/** User-initiated refresh (pull-to-refresh, retry button). */
|
|
15
|
+
refresh: () => void;
|
|
16
|
+
/** Awaited by send/submit handlers so the list reflects the write. */
|
|
17
|
+
reload: () => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function useSupportThreads({ config, revenuecatId, enabled, sheetOpen, activeThreadId, idlePollMs, }: {
|
|
20
|
+
config: ApiConfig;
|
|
21
|
+
revenuecatId: string;
|
|
22
|
+
/** False when props failed validation — no requests are made. */
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
sheetOpen: boolean;
|
|
25
|
+
activeThreadId: string | null;
|
|
26
|
+
idlePollMs: number;
|
|
27
|
+
}): SupportThreadsState;
|
|
28
|
+
//# sourceMappingURL=useSupportThreads.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSupportThreads.d.ts","sourceRoot":"","sources":["../../src/useSupportThreads.ts"],"names":[],"mappings":"AAgBA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAetC,eAAO,MAAM,YAAY,QAAS,CAAC;AA0CnC,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAC;IACjB,0DAA0D;IAC1D,UAAU,EAAE,OAAO,CAAC;IACpB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,sEAAsE;IACtE,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,YAAY,EACZ,OAAO,EACP,SAAS,EACT,cAAc,EACd,UAAU,GACX,EAAE;IACD,MAAM,EAAE,SAAS,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,mBAAmB,CAoQtB"}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/** "2h ago" relative time — mirrors the web dashboard's formatting. */
|
|
2
2
|
export declare function relativeTime(iso: string): string;
|
|
3
3
|
export declare function formatDate(iso: string): string;
|
|
4
|
+
/** "14:32" in the device's locale — shown under a group of messages. */
|
|
5
|
+
export declare function formatTime(iso: string): string;
|
|
6
|
+
export declare function isSameDay(a: string, b: string): boolean;
|
|
7
|
+
/** Date separator label: Today / Yesterday / "Mon, 5 Jan". */
|
|
8
|
+
export declare function dayLabel(iso: string): string;
|
|
4
9
|
/** Dev-mode prop validation with actionable messages. */
|
|
5
10
|
export declare function validateProps(props: {
|
|
6
11
|
appName?: unknown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAehD;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED,yDAAyD;AACzD,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,GAAG,MAAM,GAAG,IAAI,CA8BhB"}
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAehD;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAK9C;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAQvD;AAED,8DAA8D;AAC9D,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAY5C;AAED,yDAAyD;AACzD,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,GAAG,MAAM,GAAG,IAAI,CA8BhB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsupportkit/support-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "In-app customer support for React Native: feedback, feature requests, and bug reports with two-way threads.",
|
|
5
5
|
"main": "./lib/commonjs/index.js",
|
|
6
6
|
"module": "./lib/module/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@appsupportkit/design-tokens": "^0.
|
|
22
|
+
"@appsupportkit/design-tokens": "^0.2.0"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"expo-notifications": ">=0.27.0",
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// Bottom sheet built on the built-in Modal + Animated APIs.
|
|
2
2
|
//
|
|
3
3
|
// Why no sheet library: the SDK needs exactly one detent, a drag handle, a
|
|
4
|
-
// backdrop fade, and a
|
|
4
|
+
// backdrop fade, and a short ease-out slide — all achievable with core APIs.
|
|
5
5
|
// Skipping reanimated/gesture-handler keeps the package free of native
|
|
6
6
|
// dependencies and install friction for consumers.
|
|
7
|
-
|
|
7
|
+
//
|
|
8
|
+
// `visible` drives a single animation path: the sheet stays mounted until
|
|
9
|
+
// its exit finishes, so closing from the header button, the backdrop, the
|
|
10
|
+
// hardware back button, or a drag all animate identically — and a reopen
|
|
11
|
+
// always starts from off-screen instead of appearing already in place.
|
|
12
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
8
13
|
import {
|
|
9
14
|
Animated,
|
|
10
15
|
Easing,
|
|
@@ -17,7 +22,8 @@ import {
|
|
|
17
22
|
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
|
18
23
|
import type { SupportTheme } from '../theme';
|
|
19
24
|
|
|
20
|
-
const SLIDE_MS =
|
|
25
|
+
const SLIDE_MS = 260;
|
|
26
|
+
const FADE_MS = 200;
|
|
21
27
|
|
|
22
28
|
export function BottomSheet({
|
|
23
29
|
visible,
|
|
@@ -31,57 +37,67 @@ export function BottomSheet({
|
|
|
31
37
|
children: ReactNode;
|
|
32
38
|
}) {
|
|
33
39
|
const { height: windowHeight } = useWindowDimensions();
|
|
34
|
-
const sheetHeight = Math.min(windowHeight * 0.
|
|
40
|
+
const sheetHeight = Math.min(windowHeight * 0.88, 720);
|
|
35
41
|
|
|
42
|
+
const [rendered, setRendered] = useState(false);
|
|
36
43
|
const translateY = useRef(new Animated.Value(sheetHeight)).current;
|
|
37
44
|
const backdrop = useRef(new Animated.Value(0)).current;
|
|
45
|
+
/** Whether the entrance has already played for the current open. */
|
|
46
|
+
const shown = useRef(false);
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
toValue: 0,
|
|
43
|
-
duration: SLIDE_MS,
|
|
44
|
-
easing: Easing.out(Easing.cubic),
|
|
45
|
-
useNativeDriver: true,
|
|
46
|
-
}),
|
|
47
|
-
Animated.timing(backdrop, {
|
|
48
|
-
toValue: 1,
|
|
49
|
-
duration: SLIDE_MS,
|
|
50
|
-
easing: Easing.out(Easing.cubic),
|
|
51
|
-
useNativeDriver: true,
|
|
52
|
-
}),
|
|
53
|
-
]).start();
|
|
54
|
-
}, [translateY, backdrop]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (visible) setRendered(true);
|
|
50
|
+
}, [visible]);
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!rendered) return;
|
|
54
|
+
|
|
55
|
+
if (visible && !shown.current) {
|
|
56
|
+
shown.current = true;
|
|
57
|
+
translateY.setValue(sheetHeight);
|
|
58
58
|
Animated.parallel([
|
|
59
59
|
Animated.timing(translateY, {
|
|
60
|
-
toValue:
|
|
60
|
+
toValue: 0,
|
|
61
61
|
duration: SLIDE_MS,
|
|
62
|
+
easing: Easing.bezier(0.16, 1, 0.3, 1),
|
|
63
|
+
useNativeDriver: true,
|
|
64
|
+
}),
|
|
65
|
+
Animated.timing(backdrop, {
|
|
66
|
+
toValue: 1,
|
|
67
|
+
duration: FADE_MS,
|
|
62
68
|
easing: Easing.out(Easing.cubic),
|
|
63
69
|
useNativeDriver: true,
|
|
64
70
|
}),
|
|
71
|
+
]).start();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!visible && shown.current) {
|
|
76
|
+
shown.current = false;
|
|
77
|
+
// Starts from wherever the sheet is now, so a drag that crosses the
|
|
78
|
+
// dismiss threshold continues rather than jumping.
|
|
79
|
+
Animated.parallel([
|
|
80
|
+
Animated.timing(translateY, {
|
|
81
|
+
toValue: sheetHeight,
|
|
82
|
+
duration: theme.motion.slow,
|
|
83
|
+
easing: Easing.in(Easing.cubic),
|
|
84
|
+
useNativeDriver: true,
|
|
85
|
+
}),
|
|
65
86
|
Animated.timing(backdrop, {
|
|
66
87
|
toValue: 0,
|
|
67
|
-
duration:
|
|
68
|
-
easing: Easing.
|
|
88
|
+
duration: theme.motion.slow,
|
|
89
|
+
easing: Easing.in(Easing.cubic),
|
|
69
90
|
useNativeDriver: true,
|
|
70
91
|
}),
|
|
71
92
|
]).start(({ finished }) => {
|
|
72
|
-
if (finished)
|
|
93
|
+
if (finished) setRendered(false);
|
|
73
94
|
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (visible) animateIn();
|
|
80
|
-
}, [visible, animateIn]);
|
|
95
|
+
}
|
|
96
|
+
}, [rendered, visible, sheetHeight, translateY, backdrop, theme.motion.slow]);
|
|
81
97
|
|
|
82
98
|
const requestClose = useCallback(() => {
|
|
83
|
-
|
|
84
|
-
}, [
|
|
99
|
+
onClose();
|
|
100
|
+
}, [onClose]);
|
|
85
101
|
|
|
86
102
|
// Drag handle: follow the finger downward; past 25% (or a flick), dismiss.
|
|
87
103
|
const panResponder = useRef(
|
|
@@ -97,7 +113,7 @@ export function BottomSheet({
|
|
|
97
113
|
} else {
|
|
98
114
|
Animated.timing(translateY, {
|
|
99
115
|
toValue: 0,
|
|
100
|
-
duration:
|
|
116
|
+
duration: theme.motion.slow,
|
|
101
117
|
easing: Easing.out(Easing.cubic),
|
|
102
118
|
useNativeDriver: true,
|
|
103
119
|
}).start();
|
|
@@ -106,7 +122,7 @@ export function BottomSheet({
|
|
|
106
122
|
}),
|
|
107
123
|
).current;
|
|
108
124
|
|
|
109
|
-
if (!
|
|
125
|
+
if (!rendered) return null;
|
|
110
126
|
|
|
111
127
|
return (
|
|
112
128
|
<Modal
|
|
@@ -125,7 +141,7 @@ export function BottomSheet({
|
|
|
125
141
|
right: 0,
|
|
126
142
|
bottom: 0,
|
|
127
143
|
left: 0,
|
|
128
|
-
backgroundColor: 'rgba(0,0,0,0.
|
|
144
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
129
145
|
opacity: backdrop,
|
|
130
146
|
}}
|
|
131
147
|
>
|
|
@@ -139,25 +155,27 @@ export function BottomSheet({
|
|
|
139
155
|
<Animated.View
|
|
140
156
|
style={{
|
|
141
157
|
height: sheetHeight,
|
|
142
|
-
backgroundColor: theme.colors.
|
|
158
|
+
backgroundColor: theme.colors.bg,
|
|
143
159
|
borderTopLeftRadius: theme.radii.sheet,
|
|
144
160
|
borderTopRightRadius: theme.radii.sheet,
|
|
145
161
|
borderWidth: 1,
|
|
146
162
|
borderBottomWidth: 0,
|
|
147
163
|
borderColor: theme.colors.border,
|
|
164
|
+
overflow: 'hidden',
|
|
148
165
|
transform: [{ translateY }],
|
|
166
|
+
...theme.shadows.sheet,
|
|
149
167
|
}}
|
|
150
168
|
>
|
|
151
169
|
{/* Drag handle */}
|
|
152
170
|
<View
|
|
153
171
|
{...panResponder.panHandlers}
|
|
154
|
-
style={{ alignItems: 'center',
|
|
172
|
+
style={{ alignItems: 'center', paddingTop: 10, paddingBottom: 6 }}
|
|
155
173
|
>
|
|
156
174
|
<View
|
|
157
175
|
style={{
|
|
158
|
-
width:
|
|
159
|
-
height:
|
|
160
|
-
borderRadius:
|
|
176
|
+
width: 40,
|
|
177
|
+
height: 5,
|
|
178
|
+
borderRadius: 3,
|
|
161
179
|
backgroundColor: theme.colors.borderStrong,
|
|
162
180
|
}}
|
|
163
181
|
/>
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// The launcher: the one piece of the SDK that lives inside the host app's
|
|
2
|
+
// own UI, so it has to look intentional next to anything. Three variants
|
|
3
|
+
// cover the placements that actually occur — a floating action button, a
|
|
4
|
+
// bordered button in a header, and a bare icon in a toolbar.
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
Animated,
|
|
8
|
+
Easing,
|
|
9
|
+
Pressable,
|
|
10
|
+
Text,
|
|
11
|
+
View,
|
|
12
|
+
type StyleProp,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import type { SupportTheme } from '../theme';
|
|
16
|
+
import type { LauncherVariant } from '../types';
|
|
17
|
+
import { ChatBubbleMark } from './icons';
|
|
18
|
+
|
|
19
|
+
const PRESS_SCALE = 0.93;
|
|
20
|
+
|
|
21
|
+
/** Counts stay legible: three digits in a pill is not a design. */
|
|
22
|
+
function badgeLabel(unread: number): string {
|
|
23
|
+
return unread > 99 ? '99+' : String(unread);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function LauncherButton({
|
|
27
|
+
theme,
|
|
28
|
+
unread,
|
|
29
|
+
variant = 'solid',
|
|
30
|
+
size = 52,
|
|
31
|
+
label,
|
|
32
|
+
style,
|
|
33
|
+
onPress,
|
|
34
|
+
}: {
|
|
35
|
+
theme: SupportTheme;
|
|
36
|
+
unread: number;
|
|
37
|
+
variant?: LauncherVariant;
|
|
38
|
+
size?: number;
|
|
39
|
+
/** Renders a pill with text next to the mark instead of a circle. */
|
|
40
|
+
label?: string;
|
|
41
|
+
style?: StyleProp<ViewStyle>;
|
|
42
|
+
onPress: () => void;
|
|
43
|
+
}) {
|
|
44
|
+
const scale = useRef(new Animated.Value(1)).current;
|
|
45
|
+
const pulse = useRef(new Animated.Value(0)).current;
|
|
46
|
+
const previousUnread = useRef(unread);
|
|
47
|
+
|
|
48
|
+
// A new reply pulses the halo twice, then stops. Enough to catch the eye
|
|
49
|
+
// on the way past; not a permanently animating element in someone's app.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (unread > previousUnread.current) {
|
|
52
|
+
pulse.setValue(0);
|
|
53
|
+
const animation = Animated.loop(
|
|
54
|
+
Animated.timing(pulse, {
|
|
55
|
+
toValue: 1,
|
|
56
|
+
duration: 1600,
|
|
57
|
+
easing: Easing.out(Easing.ease),
|
|
58
|
+
useNativeDriver: true,
|
|
59
|
+
}),
|
|
60
|
+
{ iterations: 2 },
|
|
61
|
+
);
|
|
62
|
+
animation.start(() => pulse.setValue(0));
|
|
63
|
+
previousUnread.current = unread;
|
|
64
|
+
return () => animation.stop();
|
|
65
|
+
}
|
|
66
|
+
previousUnread.current = unread;
|
|
67
|
+
return undefined;
|
|
68
|
+
}, [unread, pulse]);
|
|
69
|
+
|
|
70
|
+
const press = (to: number) =>
|
|
71
|
+
Animated.timing(scale, {
|
|
72
|
+
toValue: to,
|
|
73
|
+
duration: to === 1 ? theme.motion.base : theme.motion.fast,
|
|
74
|
+
easing: Easing.out(Easing.cubic),
|
|
75
|
+
useNativeDriver: true,
|
|
76
|
+
}).start();
|
|
77
|
+
|
|
78
|
+
const solid = variant === 'solid';
|
|
79
|
+
const soft = variant === 'soft';
|
|
80
|
+
const fill = solid
|
|
81
|
+
? theme.colors.accent
|
|
82
|
+
: soft
|
|
83
|
+
? theme.colors.surface
|
|
84
|
+
: 'transparent';
|
|
85
|
+
const markColor = solid
|
|
86
|
+
? theme.colors.accentForeground
|
|
87
|
+
: soft
|
|
88
|
+
? theme.colors.accent
|
|
89
|
+
: theme.launcher.minimalMark;
|
|
90
|
+
// Dots are punched out of the bubble in whatever sits behind the mark —
|
|
91
|
+
// the button's own fill, or the app background when there isn't one.
|
|
92
|
+
const markHoleColor = solid
|
|
93
|
+
? theme.colors.accent
|
|
94
|
+
: soft
|
|
95
|
+
? theme.colors.surface
|
|
96
|
+
: theme.colors.bg;
|
|
97
|
+
// Dark: a hairline separates the fill from a near-black app background.
|
|
98
|
+
// Light: the accent-tinted shadow already does that, so the ring only has
|
|
99
|
+
// to keep the edge from looking soft.
|
|
100
|
+
const border = solid
|
|
101
|
+
? { borderWidth: 1, borderColor: theme.launcher.solidRing }
|
|
102
|
+
: soft
|
|
103
|
+
? { borderWidth: 1, borderColor: theme.colors.border }
|
|
104
|
+
: { borderWidth: 0, borderColor: 'transparent' };
|
|
105
|
+
const badgeBg = solid ? theme.colors.accentForeground : theme.colors.accent;
|
|
106
|
+
const badgeFg = solid ? theme.colors.accent : theme.colors.accentForeground;
|
|
107
|
+
|
|
108
|
+
const height = label ? Math.max(44, size * 0.84) : size;
|
|
109
|
+
const markSize = Math.round(size * (label ? 0.36 : 0.42));
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Animated.View
|
|
113
|
+
style={[{ transform: [{ scale }] }, style]}
|
|
114
|
+
// The halo and badge sit outside the button's bounds.
|
|
115
|
+
pointerEvents="box-none"
|
|
116
|
+
>
|
|
117
|
+
{unread > 0 && (
|
|
118
|
+
<Animated.View
|
|
119
|
+
pointerEvents="none"
|
|
120
|
+
style={{
|
|
121
|
+
position: 'absolute',
|
|
122
|
+
top: 0,
|
|
123
|
+
left: 0,
|
|
124
|
+
width: label ? undefined : size,
|
|
125
|
+
right: label ? 0 : undefined,
|
|
126
|
+
height,
|
|
127
|
+
borderRadius: theme.radii.pill,
|
|
128
|
+
backgroundColor: theme.colors.accent,
|
|
129
|
+
opacity: pulse.interpolate({
|
|
130
|
+
inputRange: [0, 1],
|
|
131
|
+
outputRange: [0.28, 0],
|
|
132
|
+
}),
|
|
133
|
+
transform: [
|
|
134
|
+
{
|
|
135
|
+
scale: pulse.interpolate({
|
|
136
|
+
inputRange: [0, 1],
|
|
137
|
+
outputRange: [1, 1.5],
|
|
138
|
+
}),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<Pressable
|
|
146
|
+
onPress={onPress}
|
|
147
|
+
onPressIn={() => press(PRESS_SCALE)}
|
|
148
|
+
onPressOut={() => press(1)}
|
|
149
|
+
hitSlop={8}
|
|
150
|
+
accessibilityRole="button"
|
|
151
|
+
accessibilityLabel={
|
|
152
|
+
unread > 0
|
|
153
|
+
? `Support, ${unread} unread ${unread === 1 ? 'reply' : 'replies'}`
|
|
154
|
+
: 'Support'
|
|
155
|
+
}
|
|
156
|
+
style={{
|
|
157
|
+
height,
|
|
158
|
+
width: label ? undefined : size,
|
|
159
|
+
paddingHorizontal: label ? 18 : 0,
|
|
160
|
+
flexDirection: 'row',
|
|
161
|
+
alignItems: 'center',
|
|
162
|
+
justifyContent: 'center',
|
|
163
|
+
gap: 9,
|
|
164
|
+
borderRadius: theme.radii.pill,
|
|
165
|
+
backgroundColor: fill,
|
|
166
|
+
...border,
|
|
167
|
+
...(variant === 'minimal' ? null : theme.shadows.launcher),
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<ChatBubbleMark color={markColor} dotColor={markHoleColor} size={markSize} />
|
|
171
|
+
{label !== undefined && (
|
|
172
|
+
<Text
|
|
173
|
+
style={{
|
|
174
|
+
fontSize: 15,
|
|
175
|
+
fontWeight: '600',
|
|
176
|
+
letterSpacing: -0.1,
|
|
177
|
+
color: solid ? theme.colors.accentForeground : theme.colors.textPrimary,
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
{label}
|
|
181
|
+
</Text>
|
|
182
|
+
)}
|
|
183
|
+
</Pressable>
|
|
184
|
+
|
|
185
|
+
{unread > 0 && (
|
|
186
|
+
<View
|
|
187
|
+
pointerEvents="none"
|
|
188
|
+
style={{
|
|
189
|
+
position: 'absolute',
|
|
190
|
+
top: -5,
|
|
191
|
+
right: -5,
|
|
192
|
+
minWidth: 19,
|
|
193
|
+
height: 19,
|
|
194
|
+
borderRadius: theme.radii.pill,
|
|
195
|
+
paddingHorizontal: 5,
|
|
196
|
+
backgroundColor: badgeBg,
|
|
197
|
+
alignItems: 'center',
|
|
198
|
+
justifyContent: 'center',
|
|
199
|
+
...theme.shadows.badge,
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<Text
|
|
203
|
+
style={{
|
|
204
|
+
fontSize: 11,
|
|
205
|
+
lineHeight: 14,
|
|
206
|
+
fontWeight: '700',
|
|
207
|
+
color: badgeFg,
|
|
208
|
+
fontVariant: ['tabular-nums'],
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{badgeLabel(unread)}
|
|
212
|
+
</Text>
|
|
213
|
+
</View>
|
|
214
|
+
)}
|
|
215
|
+
</Animated.View>
|
|
216
|
+
);
|
|
217
|
+
}
|