@webority/mobile-support 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/lib/commonjs/SupportProvider.js +335 -0
  4. package/lib/commonjs/client.js +164 -0
  5. package/lib/commonjs/clipboard.js +10 -0
  6. package/lib/commonjs/components/Attachments.js +246 -0
  7. package/lib/commonjs/components/NewSupportTicket.js +210 -0
  8. package/lib/commonjs/components/PhotoTray.js +141 -0
  9. package/lib/commonjs/components/SupportContact.js +134 -0
  10. package/lib/commonjs/components/SupportTicketDetail.js +348 -0
  11. package/lib/commonjs/components/SupportTicketList.js +279 -0
  12. package/lib/commonjs/device.js +29 -0
  13. package/lib/commonjs/files.js +37 -0
  14. package/lib/commonjs/format.js +29 -0
  15. package/lib/commonjs/index.js +97 -0
  16. package/lib/commonjs/package.json +1 -0
  17. package/lib/commonjs/photos.js +67 -0
  18. package/lib/commonjs/report/ReportIssueButton.js +57 -0
  19. package/lib/commonjs/report/captureAndReport.js +34 -0
  20. package/lib/commonjs/report/index.js +20 -0
  21. package/lib/commonjs/share/index.js +44 -0
  22. package/lib/commonjs/types.js +2 -0
  23. package/lib/commonjs/validation.js +45 -0
  24. package/lib/module/SupportProvider.js +323 -0
  25. package/lib/module/client.js +156 -0
  26. package/lib/module/clipboard.js +5 -0
  27. package/lib/module/components/Attachments.js +240 -0
  28. package/lib/module/components/NewSupportTicket.js +205 -0
  29. package/lib/module/components/PhotoTray.js +136 -0
  30. package/lib/module/components/SupportContact.js +129 -0
  31. package/lib/module/components/SupportTicketDetail.js +343 -0
  32. package/lib/module/components/SupportTicketList.js +273 -0
  33. package/lib/module/device.js +24 -0
  34. package/lib/module/files.js +31 -0
  35. package/lib/module/format.js +23 -0
  36. package/lib/module/index.js +10 -0
  37. package/lib/module/package.json +1 -0
  38. package/lib/module/photos.js +61 -0
  39. package/lib/module/report/ReportIssueButton.js +52 -0
  40. package/lib/module/report/captureAndReport.js +29 -0
  41. package/lib/module/report/index.js +12 -0
  42. package/lib/module/share/index.js +37 -0
  43. package/lib/module/types.js +2 -0
  44. package/lib/module/validation.js +38 -0
  45. package/lib/typescript/commonjs/SupportProvider.d.ts +82 -0
  46. package/lib/typescript/commonjs/client.d.ts +63 -0
  47. package/lib/typescript/commonjs/clipboard.d.ts +3 -0
  48. package/lib/typescript/commonjs/components/Attachments.d.ts +16 -0
  49. package/lib/typescript/commonjs/components/NewSupportTicket.d.ts +12 -0
  50. package/lib/typescript/commonjs/components/PhotoTray.d.ts +17 -0
  51. package/lib/typescript/commonjs/components/SupportContact.d.ts +7 -0
  52. package/lib/typescript/commonjs/components/SupportTicketDetail.d.ts +7 -0
  53. package/lib/typescript/commonjs/components/SupportTicketList.d.ts +28 -0
  54. package/lib/typescript/commonjs/device.d.ts +6 -0
  55. package/lib/typescript/commonjs/files.d.ts +10 -0
  56. package/lib/typescript/commonjs/format.d.ts +10 -0
  57. package/lib/typescript/commonjs/index.d.ts +15 -0
  58. package/lib/typescript/commonjs/package.json +1 -0
  59. package/lib/typescript/commonjs/photos.d.ts +16 -0
  60. package/lib/typescript/commonjs/report/ReportIssueButton.d.ts +14 -0
  61. package/lib/typescript/commonjs/report/captureAndReport.d.ts +15 -0
  62. package/lib/typescript/commonjs/report/index.d.ts +11 -0
  63. package/lib/typescript/commonjs/share/index.d.ts +10 -0
  64. package/lib/typescript/commonjs/types.d.ts +152 -0
  65. package/lib/typescript/commonjs/validation.d.ts +10 -0
  66. package/lib/typescript/module/SupportProvider.d.ts +82 -0
  67. package/lib/typescript/module/client.d.ts +63 -0
  68. package/lib/typescript/module/clipboard.d.ts +3 -0
  69. package/lib/typescript/module/components/Attachments.d.ts +16 -0
  70. package/lib/typescript/module/components/NewSupportTicket.d.ts +12 -0
  71. package/lib/typescript/module/components/PhotoTray.d.ts +17 -0
  72. package/lib/typescript/module/components/SupportContact.d.ts +7 -0
  73. package/lib/typescript/module/components/SupportTicketDetail.d.ts +7 -0
  74. package/lib/typescript/module/components/SupportTicketList.d.ts +28 -0
  75. package/lib/typescript/module/device.d.ts +6 -0
  76. package/lib/typescript/module/files.d.ts +10 -0
  77. package/lib/typescript/module/format.d.ts +10 -0
  78. package/lib/typescript/module/index.d.ts +15 -0
  79. package/lib/typescript/module/package.json +1 -0
  80. package/lib/typescript/module/photos.d.ts +16 -0
  81. package/lib/typescript/module/report/ReportIssueButton.d.ts +14 -0
  82. package/lib/typescript/module/report/captureAndReport.d.ts +15 -0
  83. package/lib/typescript/module/report/index.d.ts +11 -0
  84. package/lib/typescript/module/share/index.d.ts +10 -0
  85. package/lib/typescript/module/types.d.ts +152 -0
  86. package/lib/typescript/module/validation.d.ts +10 -0
  87. package/package.json +124 -0
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+
3
+ import { AppIcon, IconButton, Spinner, Text, toast, useTheme, ZoomableImage } from '@webority/mobile-ui';
4
+ import { useEffect, useMemo, useState } from 'react';
5
+ import { Image, Modal, Pressable, StyleSheet, View } from 'react-native';
6
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
7
+ import { formatBytes, isImage } from "../format.js";
8
+ import { useAttachmentLoader, useAttachmentOpener } from "../SupportProvider.js";
9
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
+ const AttachmentThumb = ({
11
+ attachment,
12
+ onOpen
13
+ }) => {
14
+ const theme = useTheme();
15
+ const styles = useMemo(() => buildStyles(theme), [theme]);
16
+ const load = useAttachmentLoader();
17
+ const [state, setState] = useState({
18
+ status: 'loading'
19
+ });
20
+ useEffect(() => {
21
+ let cancelled = false;
22
+ load(attachment).then(uri => !cancelled && setState({
23
+ status: 'ready',
24
+ uri
25
+ }), () => !cancelled && setState({
26
+ status: 'error'
27
+ }));
28
+ return () => {
29
+ cancelled = true;
30
+ };
31
+ }, [load, attachment]);
32
+ if (state.status === 'loading') {
33
+ return /*#__PURE__*/_jsx(View, {
34
+ style: styles.thumb,
35
+ testID: `support-attachment-${attachment.id}`,
36
+ children: /*#__PURE__*/_jsx(Spinner, {
37
+ size: "sm",
38
+ accessibilityLabel: `Loading ${attachment.fileName}`
39
+ })
40
+ });
41
+ }
42
+ if (state.status === 'error') {
43
+ return /*#__PURE__*/_jsx(View, {
44
+ style: styles.thumb,
45
+ accessible: true,
46
+ accessibilityLabel: `${attachment.fileName} could not be loaded`,
47
+ testID: `support-attachment-${attachment.id}`,
48
+ children: /*#__PURE__*/_jsx(Text, {
49
+ variant: "caption",
50
+ color: "tertiary",
51
+ align: "center",
52
+ children: "Could not load"
53
+ })
54
+ });
55
+ }
56
+ return /*#__PURE__*/_jsx(Pressable, {
57
+ onPress: () => onOpen(state.uri, attachment.fileName),
58
+ accessibilityRole: "imagebutton",
59
+ accessibilityLabel: `Open ${attachment.fileName}`,
60
+ testID: `support-attachment-${attachment.id}`,
61
+ children: /*#__PURE__*/_jsx(Image, {
62
+ source: {
63
+ uri: state.uri
64
+ },
65
+ style: styles.thumb,
66
+ resizeMode: "cover"
67
+ })
68
+ });
69
+ };
70
+
71
+ /**
72
+ * A file that is not an image. Tappable when the app gave SupportProvider an
73
+ * `openFile`: it is downloaded through the signed-in client, then handed to the OS.
74
+ */
75
+ const FileRow = ({
76
+ attachment
77
+ }) => {
78
+ const theme = useTheme();
79
+ const styles = useMemo(() => buildStyles(theme), [theme]);
80
+ const open = useAttachmentOpener();
81
+ const [opening, setOpening] = useState(false);
82
+ const label = `${attachment.fileName}, ${formatBytes(attachment.sizeBytes)}`;
83
+ if (!open) {
84
+ return /*#__PURE__*/_jsx(Text, {
85
+ variant: "caption",
86
+ color: "secondary",
87
+ testID: `support-file-${attachment.id}`,
88
+ children: label
89
+ });
90
+ }
91
+ const onPress = async () => {
92
+ setOpening(true);
93
+ try {
94
+ await open(attachment);
95
+ } catch {
96
+ toast.error(`Could not open ${attachment.fileName}. Try again.`);
97
+ } finally {
98
+ setOpening(false);
99
+ }
100
+ };
101
+ return /*#__PURE__*/_jsxs(Pressable, {
102
+ onPress: () => void onPress(),
103
+ disabled: opening,
104
+ accessibilityRole: "button",
105
+ accessibilityLabel: `Open ${attachment.fileName}`,
106
+ accessibilityState: {
107
+ busy: opening,
108
+ disabled: opening
109
+ },
110
+ style: styles.fileRow,
111
+ testID: `support-file-${attachment.id}`,
112
+ children: [/*#__PURE__*/_jsx(AppIcon, {
113
+ name: "document",
114
+ size: 16,
115
+ color: theme.colors.text.link
116
+ }), /*#__PURE__*/_jsx(Text, {
117
+ variant: "caption",
118
+ color: "link",
119
+ children: label
120
+ }), opening && /*#__PURE__*/_jsx(Spinner, {
121
+ size: "sm",
122
+ accessibilityLabel: `Opening ${attachment.fileName}`
123
+ })]
124
+ });
125
+ };
126
+
127
+ /** A message's files: images as tappable thumbnails, anything else as a file row. */
128
+ export const AttachmentList = ({
129
+ attachments,
130
+ onOpenImage
131
+ }) => {
132
+ const theme = useTheme();
133
+ const styles = useMemo(() => buildStyles(theme), [theme]);
134
+ if (attachments.length === 0) {
135
+ return null;
136
+ }
137
+ const images = attachments.filter(a => isImage(a.contentType));
138
+ const others = attachments.filter(a => !isImage(a.contentType));
139
+ return /*#__PURE__*/_jsxs(View, {
140
+ style: styles.list,
141
+ children: [images.length > 0 && /*#__PURE__*/_jsx(View, {
142
+ style: styles.thumbRow,
143
+ children: images.map(a => /*#__PURE__*/_jsx(AttachmentThumb, {
144
+ attachment: a,
145
+ onOpen: onOpenImage
146
+ }, a.id))
147
+ }), others.map(a => /*#__PURE__*/_jsx(FileRow, {
148
+ attachment: a
149
+ }, a.id))]
150
+ });
151
+ };
152
+
153
+ /** Full-screen view of one image with pinch to zoom. */
154
+ export const ImageViewer = ({
155
+ image,
156
+ onClose
157
+ }) => {
158
+ const theme = useTheme();
159
+ const styles = useMemo(() => buildStyles(theme), [theme]);
160
+ return /*#__PURE__*/_jsx(Modal, {
161
+ visible: image !== null,
162
+ onRequestClose: onClose,
163
+ animationType: "fade",
164
+ transparent: false,
165
+ children: /*#__PURE__*/_jsxs(GestureHandlerRootView, {
166
+ style: styles.viewer,
167
+ testID: "support-image-viewer",
168
+ children: [image && /*#__PURE__*/_jsx(ZoomableImage, {
169
+ accessibilityLabel: image.name,
170
+ style: styles.viewerImageWrap,
171
+ children: /*#__PURE__*/_jsx(Image, {
172
+ source: {
173
+ uri: image.uri
174
+ },
175
+ style: styles.viewerImage,
176
+ resizeMode: "contain"
177
+ })
178
+ }), /*#__PURE__*/_jsx(View, {
179
+ style: styles.viewerClose,
180
+ children: /*#__PURE__*/_jsx(IconButton, {
181
+ icon: ({
182
+ color,
183
+ size
184
+ }) => /*#__PURE__*/_jsx(AppIcon, {
185
+ name: "close-circle",
186
+ color: color,
187
+ size: size
188
+ }),
189
+ accessibilityLabel: "Close image",
190
+ onPress: onClose,
191
+ variant: "glass"
192
+ })
193
+ })]
194
+ })
195
+ });
196
+ };
197
+ const THUMB = 84;
198
+ const buildStyles = theme => StyleSheet.create({
199
+ list: {
200
+ gap: theme.spacing.xs,
201
+ marginTop: theme.spacing.sm
202
+ },
203
+ fileRow: {
204
+ flexDirection: 'row',
205
+ alignItems: 'center',
206
+ gap: theme.spacing.xxs,
207
+ alignSelf: 'flex-start'
208
+ },
209
+ thumbRow: {
210
+ flexDirection: 'row',
211
+ flexWrap: 'wrap',
212
+ gap: theme.spacing.xs
213
+ },
214
+ thumb: {
215
+ width: THUMB,
216
+ height: THUMB,
217
+ borderRadius: theme.radius.sm,
218
+ backgroundColor: theme.colors.background.secondary,
219
+ alignItems: 'center',
220
+ justifyContent: 'center',
221
+ overflow: 'hidden'
222
+ },
223
+ viewer: {
224
+ flex: 1,
225
+ backgroundColor: theme.colors.background.inverse
226
+ },
227
+ viewerImageWrap: {
228
+ flex: 1
229
+ },
230
+ viewerImage: {
231
+ flex: 1,
232
+ width: '100%'
233
+ },
234
+ viewerClose: {
235
+ position: 'absolute',
236
+ top: theme.spacing['2xl'],
237
+ right: theme.spacing.md
238
+ }
239
+ });
240
+ //# sourceMappingURL=Attachments.js.map
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+
3
+ import { Button, Input, Select, Spinner, Text, useForm, useTheme } from '@webority/mobile-ui';
4
+ import { useMemo, useState } from 'react';
5
+ import { ScrollView, StyleSheet, View } from 'react-native';
6
+ import { toSupportApiError } from "../client.js";
7
+ import { useCreateSupportTicket, useSupportConfig } from "../SupportProvider.js";
8
+ import { validateBody, validateCategory, validateFiles, validateSubject } from "../validation.js";
9
+ import { PhotoTray } from "./PhotoTray.js";
10
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
+ const TicketForm = ({
12
+ config,
13
+ styles,
14
+ onCreated,
15
+ onCancel,
16
+ initialDraft
17
+ }) => {
18
+ const createTicket = useCreateSupportTicket();
19
+ const [photos, setPhotos] = useState([]);
20
+ const [screenshots, setScreenshots] = useState(initialDraft?.screenshots ?? []);
21
+ const [filesError, setFilesError] = useState(null);
22
+ const [submitError, setSubmitError] = useState(null);
23
+ const form = useForm({
24
+ initialValues: {
25
+ category: initialDraft?.category ?? '',
26
+ subject: initialDraft?.subject ?? '',
27
+ body: initialDraft?.body ?? ''
28
+ },
29
+ validate: {
30
+ category: v => validateCategory(v, config),
31
+ subject: v => validateSubject(v, config),
32
+ body: v => validateBody(v, config)
33
+ },
34
+ onSubmit: async values => {
35
+ const fileProblem = validateFiles([...screenshots, ...photos], config);
36
+ setFilesError(fileProblem ?? null);
37
+ if (fileProblem) {
38
+ return;
39
+ }
40
+ setSubmitError(null);
41
+ try {
42
+ const created = await createTicket({
43
+ category: values.category,
44
+ subject: values.subject.trim(),
45
+ body: values.body.trim(),
46
+ pageUrl: initialDraft?.pageUrl,
47
+ userAgent: initialDraft?.userAgent,
48
+ appVersion: initialDraft?.appVersion,
49
+ files: photos,
50
+ screenshots
51
+ });
52
+ onCreated(created);
53
+ } catch (e) {
54
+ setSubmitError(toSupportApiError(e).message);
55
+ }
56
+ }
57
+ });
58
+ const category = form.getFieldProps('category');
59
+ const subject = form.getFieldProps('subject');
60
+ const body = form.getFieldProps('body');
61
+ const attached = screenshots.length + photos.length;
62
+ return /*#__PURE__*/_jsxs(ScrollView, {
63
+ contentContainerStyle: styles.content,
64
+ keyboardShouldPersistTaps: "handled",
65
+ children: [/*#__PURE__*/_jsx(Text, {
66
+ variant: "bodySmall",
67
+ color: "secondary",
68
+ children: "Tell us what you need. We reply by email and in this ticket."
69
+ }), /*#__PURE__*/_jsx(Select, {
70
+ label: "Category",
71
+ placeholder: "Choose a category",
72
+ required: true,
73
+ options: config.categories.map(c => ({
74
+ label: c,
75
+ value: c
76
+ })),
77
+ value: category.value || undefined,
78
+ onChange: v => category.setValue(String(v), true),
79
+ error: category.error,
80
+ testID: "support-new-category"
81
+ }), /*#__PURE__*/_jsx(Input, {
82
+ label: "Subject",
83
+ placeholder: "A short summary",
84
+ required: true,
85
+ maxLength: config.subjectMaxLength,
86
+ value: subject.value,
87
+ onChangeText: subject.onChangeText,
88
+ onBlur: subject.onBlur,
89
+ error: subject.error,
90
+ testID: "support-new-subject"
91
+ }), /*#__PURE__*/_jsx(Input, {
92
+ label: "Message",
93
+ placeholder: "What happened, and what did you expect?",
94
+ required: true,
95
+ multiline: true,
96
+ maxLength: config.bodyMaxLength,
97
+ value: body.value,
98
+ onChangeText: body.onChangeText,
99
+ onBlur: body.onBlur,
100
+ error: body.error,
101
+ testID: "support-new-body"
102
+ }), screenshots.length > 0 && /*#__PURE__*/_jsx(PhotoTray, {
103
+ label: "Screenshot",
104
+ photos: screenshots,
105
+ onChange: setScreenshots,
106
+ remaining: 0,
107
+ allowAdd: false,
108
+ maxBytes: config.maxAttachmentBytes,
109
+ disabled: form.isSubmitting
110
+ }), /*#__PURE__*/_jsx(PhotoTray, {
111
+ label: "Photos",
112
+ photos: photos,
113
+ onChange: setPhotos,
114
+ remaining: config.maxAttachmentsPerMessage - attached,
115
+ maxBytes: config.maxAttachmentBytes,
116
+ disabled: form.isSubmitting
117
+ }), /*#__PURE__*/_jsx(Text, {
118
+ variant: "caption",
119
+ color: "tertiary",
120
+ children: `Attach up to ${config.maxAttachmentsPerMessage} files.`
121
+ }), (filesError || submitError) && /*#__PURE__*/_jsx(Text, {
122
+ variant: "bodySmall",
123
+ color: "error",
124
+ accessibilityLiveRegion: "polite",
125
+ children: filesError ?? submitError
126
+ }), /*#__PURE__*/_jsxs(View, {
127
+ style: styles.actions,
128
+ children: [onCancel && /*#__PURE__*/_jsx(Button, {
129
+ title: "Cancel",
130
+ variant: "ghost",
131
+ tone: "neutral",
132
+ onPress: onCancel,
133
+ disabled: form.isSubmitting
134
+ }), /*#__PURE__*/_jsx(Button, {
135
+ title: "Send ticket",
136
+ loading: form.isSubmitting,
137
+ disabled: form.isSubmitting,
138
+ onPress: form.submit,
139
+ testID: "support-new-submit"
140
+ })]
141
+ })]
142
+ });
143
+ };
144
+ export const NewSupportTicket = props => {
145
+ const theme = useTheme();
146
+ const styles = useMemo(() => buildStyles(theme), [theme]);
147
+ const {
148
+ config,
149
+ loading,
150
+ error,
151
+ reload
152
+ } = useSupportConfig();
153
+ const testID = props.testID ?? 'support-new-ticket';
154
+ if (!config) {
155
+ return /*#__PURE__*/_jsx(View, {
156
+ style: styles.center,
157
+ testID: testID,
158
+ children: loading ? /*#__PURE__*/_jsx(Spinner, {
159
+ accessibilityLabel: "Loading"
160
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
161
+ children: [/*#__PURE__*/_jsx(Text, {
162
+ variant: "body",
163
+ color: "error",
164
+ align: "center",
165
+ children: error?.message ?? 'Support is not available right now.'
166
+ }), /*#__PURE__*/_jsx(Button, {
167
+ title: "Try again",
168
+ variant: "ghost",
169
+ onPress: () => void reload()
170
+ })]
171
+ })
172
+ });
173
+ }
174
+ return /*#__PURE__*/_jsx(View, {
175
+ style: styles.root,
176
+ testID: testID,
177
+ children: /*#__PURE__*/_jsx(TicketForm, {
178
+ ...props,
179
+ config: config,
180
+ styles: styles
181
+ })
182
+ });
183
+ };
184
+ const buildStyles = theme => StyleSheet.create({
185
+ root: {
186
+ flex: 1
187
+ },
188
+ content: {
189
+ padding: theme.spacing.md,
190
+ gap: theme.spacing.md
191
+ },
192
+ center: {
193
+ flex: 1,
194
+ alignItems: 'center',
195
+ justifyContent: 'center',
196
+ padding: theme.spacing.xl,
197
+ gap: theme.spacing.sm
198
+ },
199
+ actions: {
200
+ flexDirection: 'row',
201
+ justifyContent: 'flex-end',
202
+ gap: theme.spacing.sm
203
+ }
204
+ });
205
+ //# sourceMappingURL=NewSupportTicket.js.map
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+
3
+ import { AppIcon, Button, IconButton, Text, useTheme } from '@webority/mobile-ui';
4
+ import { useMemo, useState } from 'react';
5
+ import { Image, StyleSheet, View } from 'react-native';
6
+ import { pickPhotos } from "../photos.js";
7
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ /** Photos on a message: thumbnails with remove buttons, plus library and camera buttons. */
9
+ export const PhotoTray = ({
10
+ photos,
11
+ onChange,
12
+ remaining,
13
+ maxBytes,
14
+ disabled,
15
+ label,
16
+ allowAdd = true
17
+ }) => {
18
+ const theme = useTheme();
19
+ const styles = useMemo(() => buildStyles(theme), [theme]);
20
+ const [picking, setPicking] = useState(null);
21
+ const [error, setError] = useState(null);
22
+ const add = async source => {
23
+ setError(null);
24
+ setPicking(source);
25
+ try {
26
+ const picked = await pickPhotos(source, {
27
+ maxCount: remaining,
28
+ maxBytes
29
+ });
30
+ if (picked.length > 0) {
31
+ onChange([...photos, ...picked]);
32
+ }
33
+ } catch (e) {
34
+ setError(e instanceof Error && e.name === 'PhotoPermissionError' ? e.message : 'That photo could not be added. Try another one.');
35
+ } finally {
36
+ setPicking(null);
37
+ }
38
+ };
39
+ const full = remaining <= 0;
40
+ return /*#__PURE__*/_jsxs(View, {
41
+ style: styles.root,
42
+ children: [label ? /*#__PURE__*/_jsx(Text, {
43
+ variant: "label",
44
+ color: "secondary",
45
+ children: label
46
+ }) : null, photos.length > 0 && /*#__PURE__*/_jsx(View, {
47
+ style: styles.row,
48
+ children: photos.map((photo, index) => /*#__PURE__*/_jsxs(View, {
49
+ style: styles.item,
50
+ children: [/*#__PURE__*/_jsx(Image, {
51
+ source: {
52
+ uri: photo.uri
53
+ },
54
+ style: styles.thumb,
55
+ accessibilityLabel: photo.name
56
+ }), /*#__PURE__*/_jsx(View, {
57
+ style: styles.remove,
58
+ children: /*#__PURE__*/_jsx(IconButton, {
59
+ size: "sm",
60
+ variant: "glass",
61
+ icon: ({
62
+ color,
63
+ size
64
+ }) => /*#__PURE__*/_jsx(AppIcon, {
65
+ name: "trash",
66
+ color: color,
67
+ size: size
68
+ }),
69
+ accessibilityLabel: `Remove ${photo.name}`,
70
+ disabled: disabled,
71
+ onPress: () => onChange(photos.filter((_, i) => i !== index))
72
+ })
73
+ })]
74
+ }, `${photo.uri}-${index}`))
75
+ }), allowAdd && /*#__PURE__*/_jsxs(View, {
76
+ style: styles.actions,
77
+ children: [/*#__PURE__*/_jsx(Button, {
78
+ size: "sm",
79
+ variant: "outline",
80
+ tone: "neutral",
81
+ leftIcon: "gallery",
82
+ title: "Add photo",
83
+ accessibilityLabel: "Add a photo from your library",
84
+ loading: picking === 'library',
85
+ disabled: disabled || full || picking !== null,
86
+ onPress: () => void add('library')
87
+ }), /*#__PURE__*/_jsx(Button, {
88
+ size: "sm",
89
+ variant: "outline",
90
+ tone: "neutral",
91
+ leftIcon: "camera",
92
+ title: "Take photo",
93
+ accessibilityLabel: "Take a photo with your camera",
94
+ loading: picking === 'camera',
95
+ disabled: disabled || full || picking !== null,
96
+ onPress: () => void add('camera')
97
+ })]
98
+ }), error ? /*#__PURE__*/_jsx(Text, {
99
+ variant: "caption",
100
+ color: "error",
101
+ accessibilityLiveRegion: "polite",
102
+ children: error
103
+ }) : null]
104
+ });
105
+ };
106
+ const THUMB = 72;
107
+ const buildStyles = theme => StyleSheet.create({
108
+ root: {
109
+ gap: theme.spacing.xs
110
+ },
111
+ row: {
112
+ flexDirection: 'row',
113
+ flexWrap: 'wrap',
114
+ gap: theme.spacing.xs
115
+ },
116
+ item: {
117
+ width: THUMB,
118
+ height: THUMB
119
+ },
120
+ thumb: {
121
+ width: THUMB,
122
+ height: THUMB,
123
+ borderRadius: theme.radius.sm,
124
+ backgroundColor: theme.colors.background.secondary
125
+ },
126
+ remove: {
127
+ position: 'absolute',
128
+ top: theme.spacing.xxs,
129
+ right: theme.spacing.xxs
130
+ },
131
+ actions: {
132
+ flexDirection: 'row',
133
+ gap: theme.spacing.sm
134
+ }
135
+ });
136
+ //# sourceMappingURL=PhotoTray.js.map
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+
3
+ import { Button, Card, Spinner, Text, toast, useTheme } from '@webority/mobile-ui';
4
+ import { useMemo } from 'react';
5
+ import { Linking, StyleSheet, View } from 'react-native';
6
+ import { copyText } from "../clipboard.js";
7
+ import { useSupportConfig } from "../SupportProvider.js";
8
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
+ /** How to reach the support team: email with copy and compose, phone, and hours. */
10
+ export const SupportContact = ({
11
+ testID = 'support-contact'
12
+ }) => {
13
+ const theme = useTheme();
14
+ const styles = useMemo(() => buildStyles(theme), [theme]);
15
+ const {
16
+ config,
17
+ loading,
18
+ error
19
+ } = useSupportConfig();
20
+ if (!config) {
21
+ return /*#__PURE__*/_jsx(Card, {
22
+ variant: "outlined",
23
+ testID: testID,
24
+ children: loading ? /*#__PURE__*/_jsx(Spinner, {
25
+ size: "sm",
26
+ accessibilityLabel: "Loading contact details"
27
+ }) : /*#__PURE__*/_jsx(Text, {
28
+ variant: "bodySmall",
29
+ color: "error",
30
+ children: error?.message ?? 'Contact details are not available right now.'
31
+ })
32
+ });
33
+ }
34
+ const email = config.supportEmail;
35
+ const phone = config.supportPhone?.trim();
36
+ const hours = config.supportHours?.trim();
37
+ const copyEmail = async () => {
38
+ try {
39
+ await copyText(email);
40
+ toast.success('Email address copied.');
41
+ } catch {
42
+ toast.error('Could not copy the email address.');
43
+ }
44
+ };
45
+ const open = (url, failure) => {
46
+ Linking.openURL(url).catch(() => toast.error(failure));
47
+ };
48
+ return /*#__PURE__*/_jsxs(Card, {
49
+ variant: "outlined",
50
+ bodyStyle: styles.body,
51
+ testID: testID,
52
+ children: [/*#__PURE__*/_jsx(Text, {
53
+ variant: "title",
54
+ accessibilityRole: "header",
55
+ children: `Contact ${config.supportName || config.productName}`
56
+ }), /*#__PURE__*/_jsxs(View, {
57
+ style: styles.row,
58
+ children: [/*#__PURE__*/_jsx(Text, {
59
+ variant: "label",
60
+ color: "secondary",
61
+ children: "Email"
62
+ }), /*#__PURE__*/_jsx(Text, {
63
+ variant: "body",
64
+ selectable: true,
65
+ testID: "support-contact-email",
66
+ children: email || '-'
67
+ })]
68
+ }), email ? /*#__PURE__*/_jsxs(View, {
69
+ style: styles.actions,
70
+ children: [/*#__PURE__*/_jsx(Button, {
71
+ title: "Copy",
72
+ size: "sm",
73
+ variant: "outline",
74
+ tone: "neutral",
75
+ leftIcon: "copy",
76
+ accessibilityLabel: `Copy ${email}`,
77
+ onPress: () => void copyEmail(),
78
+ testID: "support-contact-copy"
79
+ }), /*#__PURE__*/_jsx(Button, {
80
+ title: "Send email",
81
+ size: "sm",
82
+ variant: "outline",
83
+ leftIcon: "mail",
84
+ accessibilityLabel: `Send an email to ${email}`,
85
+ onPress: () => open(`mailto:${email}`, 'No email app is set up on this device.'),
86
+ testID: "support-contact-mail"
87
+ })]
88
+ }) : null, phone ? /*#__PURE__*/_jsxs(View, {
89
+ style: styles.row,
90
+ children: [/*#__PURE__*/_jsx(Text, {
91
+ variant: "label",
92
+ color: "secondary",
93
+ children: "Phone"
94
+ }), /*#__PURE__*/_jsx(Button, {
95
+ title: phone,
96
+ size: "sm",
97
+ variant: "link",
98
+ leftIcon: "call",
99
+ accessibilityLabel: `Call ${phone}`,
100
+ onPress: () => open(`tel:${phone.replace(/[^\d+]/g, '')}`, 'This device cannot make calls.'),
101
+ testID: "support-contact-phone"
102
+ })]
103
+ }) : null, hours ? /*#__PURE__*/_jsxs(View, {
104
+ style: styles.row,
105
+ children: [/*#__PURE__*/_jsx(Text, {
106
+ variant: "label",
107
+ color: "secondary",
108
+ children: "Hours"
109
+ }), /*#__PURE__*/_jsx(Text, {
110
+ variant: "bodySmall",
111
+ children: hours
112
+ })]
113
+ }) : null]
114
+ });
115
+ };
116
+ const buildStyles = theme => StyleSheet.create({
117
+ body: {
118
+ gap: theme.spacing.sm
119
+ },
120
+ row: {
121
+ gap: theme.spacing.xxs,
122
+ alignItems: 'flex-start'
123
+ },
124
+ actions: {
125
+ flexDirection: 'row',
126
+ gap: theme.spacing.sm
127
+ }
128
+ });
129
+ //# sourceMappingURL=SupportContact.js.map