@webority/mobile-support 0.0.12 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/lib/commonjs/SupportProvider.js +1 -335
  2. package/lib/commonjs/client.js +1 -164
  3. package/lib/commonjs/clipboard.js +1 -10
  4. package/lib/commonjs/components/Attachments.js +1 -246
  5. package/lib/commonjs/components/NewSupportTicket.js +1 -210
  6. package/lib/commonjs/components/PhotoTray.js +1 -141
  7. package/lib/commonjs/components/SupportContact.js +1 -134
  8. package/lib/commonjs/components/SupportTicketDetail.js +1 -348
  9. package/lib/commonjs/components/SupportTicketList.js +1 -279
  10. package/lib/commonjs/device.js +1 -29
  11. package/lib/commonjs/files.js +1 -37
  12. package/lib/commonjs/format.js +1 -29
  13. package/lib/commonjs/index.js +1 -97
  14. package/lib/commonjs/photos.js +1 -67
  15. package/lib/commonjs/report/ReportIssueButton.js +1 -57
  16. package/lib/commonjs/report/captureAndReport.js +1 -34
  17. package/lib/commonjs/report/index.js +1 -20
  18. package/lib/commonjs/share/index.js +1 -44
  19. package/lib/commonjs/types.js +1 -2
  20. package/lib/commonjs/validation.js +1 -45
  21. package/lib/module/SupportProvider.js +1 -323
  22. package/lib/module/client.js +1 -156
  23. package/lib/module/clipboard.js +1 -5
  24. package/lib/module/components/Attachments.js +1 -240
  25. package/lib/module/components/NewSupportTicket.js +1 -205
  26. package/lib/module/components/PhotoTray.js +1 -136
  27. package/lib/module/components/SupportContact.js +1 -129
  28. package/lib/module/components/SupportTicketDetail.js +1 -343
  29. package/lib/module/components/SupportTicketList.js +1 -273
  30. package/lib/module/device.js +1 -24
  31. package/lib/module/files.js +1 -31
  32. package/lib/module/format.js +1 -23
  33. package/lib/module/index.js +1 -10
  34. package/lib/module/photos.js +1 -61
  35. package/lib/module/report/ReportIssueButton.js +1 -52
  36. package/lib/module/report/captureAndReport.js +1 -29
  37. package/lib/module/report/index.js +1 -12
  38. package/lib/module/share/index.js +1 -37
  39. package/lib/module/types.js +0 -2
  40. package/lib/module/validation.js +1 -38
  41. package/lib/typescript/commonjs/SupportProvider.d.ts +4 -0
  42. package/lib/typescript/commonjs/client.d.ts +0 -1
  43. package/lib/typescript/commonjs/format.d.ts +6 -4
  44. package/lib/typescript/module/SupportProvider.d.ts +4 -0
  45. package/lib/typescript/module/client.d.ts +0 -1
  46. package/lib/typescript/module/format.d.ts +6 -4
  47. package/package.json +8 -6
@@ -1,273 +1 @@
1
- "use strict";
2
-
3
- import { Button, Card, Chip, EmptyState, SearchBar, Spinner, StatusBadge, Text, useTheme } from '@webority/mobile-ui';
4
- import { useMemo } from 'react';
5
- import { FlatList, RefreshControl, ScrollView, StyleSheet, View } from 'react-native';
6
- import { formatWhen } from "../format.js";
7
- import { useSupportTickets } from "../SupportProvider.js";
8
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
9
- export const STATUS_TONES = {
10
- Open: 'warning',
11
- InProgress: 'info',
12
- Resolved: 'success',
13
- Closed: 'neutral'
14
- };
15
-
16
- // The counts endpoint carries numbers only, so the chip wording lives here. It
17
- // matches the server's own labels (Webority.Support SupportDisplay).
18
- const STATUS_FILTERS = [{
19
- value: 'Open',
20
- label: 'Open',
21
- count: 'open'
22
- }, {
23
- value: 'InProgress',
24
- label: 'In progress',
25
- count: 'inProgress'
26
- }, {
27
- value: 'Resolved',
28
- label: 'Resolved',
29
- count: 'resolved'
30
- }, {
31
- value: 'Closed',
32
- label: 'Closed',
33
- count: 'closed'
34
- }];
35
- const ALL = 'all';
36
- /**
37
- * All, then each status that has tickets. The selected status stays even at
38
- * zero, so the chip the person is on never vanishes under them.
39
- */
40
- export const statusFilterOptions = (counts, selected) => [{
41
- value: ALL,
42
- label: 'All',
43
- count: counts.total
44
- }, ...STATUS_FILTERS.filter(s => counts[s.count] > 0 || s.value === selected).map(s => ({
45
- value: s.value,
46
- label: s.label,
47
- count: counts[s.count]
48
- }))];
49
- const SEARCH_DEBOUNCE_MS = 400;
50
- export const SupportTicketList = ({
51
- onOpenTicket,
52
- onNewTicket,
53
- header,
54
- pageSize,
55
- testID = 'support-ticket-list'
56
- }) => {
57
- const theme = useTheme();
58
- const styles = useMemo(() => buildStyles(theme), [theme]);
59
- const {
60
- tickets,
61
- counts,
62
- status,
63
- search,
64
- setStatus,
65
- setSearch,
66
- loading,
67
- refreshing,
68
- loadingMore,
69
- error,
70
- loadMoreError,
71
- refresh,
72
- loadMore
73
- } = useSupportTickets({
74
- pageSize
75
- });
76
- const hasAnyTicket = (counts?.total ?? 0) > 0;
77
- const filtered = status !== null || search.trim() !== '';
78
- const listHeader = /*#__PURE__*/_jsxs(View, {
79
- style: styles.header,
80
- children: [header, onNewTicket && /*#__PURE__*/_jsx(Button, {
81
- title: "New ticket",
82
- leftIcon: "add",
83
- onPress: onNewTicket,
84
- fullWidth: true
85
- }), error && /*#__PURE__*/_jsxs(View, {
86
- style: styles.error,
87
- accessibilityLiveRegion: "polite",
88
- children: [/*#__PURE__*/_jsx(Text, {
89
- variant: "bodySmall",
90
- color: "error",
91
- children: error.message
92
- }), /*#__PURE__*/_jsx(Button, {
93
- title: "Try again",
94
- size: "sm",
95
- variant: "ghost",
96
- onPress: () => void refresh()
97
- })]
98
- }), counts && hasAnyTicket && /*#__PURE__*/_jsxs(_Fragment, {
99
- children: [/*#__PURE__*/_jsx(SearchBar, {
100
- value: search,
101
- onChangeText: setSearch,
102
- debounceMs: SEARCH_DEBOUNCE_MS,
103
- placeholder: "Search by subject or ticket number",
104
- accessibilityLabel: "Search tickets",
105
- testID: "support-ticket-search"
106
- }), /*#__PURE__*/_jsx(ScrollView, {
107
- horizontal: true,
108
- showsHorizontalScrollIndicator: false,
109
- contentContainerStyle: styles.filters,
110
- accessibilityRole: "tablist",
111
- accessibilityLabel: "Filter by status",
112
- children: statusFilterOptions(counts, status).map(o => /*#__PURE__*/_jsx(Chip, {
113
- label: `${o.label} (${o.count})`,
114
- selected: (status ?? ALL) === o.value,
115
- onPress: () => setStatus(o.value === ALL ? null : o.value),
116
- accessibilityLabel: `${o.label}, ${o.count} tickets`,
117
- testID: `support-filter-${o.value}`
118
- }, o.value))
119
- })]
120
- })]
121
- });
122
- if (loading && counts === null && !error) {
123
- return /*#__PURE__*/_jsx(View, {
124
- style: styles.center,
125
- testID: testID,
126
- children: /*#__PURE__*/_jsx(Spinner, {
127
- accessibilityLabel: "Loading your tickets"
128
- })
129
- });
130
- }
131
- const empty = () => {
132
- if (loading) {
133
- return /*#__PURE__*/_jsx(View, {
134
- style: styles.footer,
135
- children: /*#__PURE__*/_jsx(Spinner, {
136
- accessibilityLabel: "Loading your tickets"
137
- })
138
- });
139
- }
140
- if (error) {
141
- return null;
142
- }
143
- if (!hasAnyTicket) {
144
- return /*#__PURE__*/_jsx(EmptyState, {
145
- title: "No tickets yet",
146
- description: "Raise a ticket and it will show here with its status.",
147
- action: onNewTicket ? {
148
- label: 'New ticket',
149
- onPress: onNewTicket
150
- } : undefined
151
- });
152
- }
153
- return /*#__PURE__*/_jsx(EmptyState, {
154
- title: "No tickets match",
155
- description: filtered ? 'Try another status or search.' : undefined
156
- });
157
- };
158
- const footer = loadingMore ? /*#__PURE__*/_jsx(View, {
159
- style: styles.footer,
160
- children: /*#__PURE__*/_jsx(Spinner, {
161
- size: "sm",
162
- accessibilityLabel: "Loading more tickets"
163
- })
164
- }) : loadMoreError ? /*#__PURE__*/_jsxs(View, {
165
- style: [styles.footer, styles.error],
166
- accessibilityLiveRegion: "polite",
167
- children: [/*#__PURE__*/_jsx(Text, {
168
- variant: "bodySmall",
169
- color: "error",
170
- children: loadMoreError.message
171
- }), /*#__PURE__*/_jsx(Button, {
172
- title: "Load more",
173
- size: "sm",
174
- variant: "ghost",
175
- onPress: () => void loadMore()
176
- })]
177
- }) : null;
178
- return /*#__PURE__*/_jsx(FlatList, {
179
- testID: testID,
180
- data: tickets,
181
- keyExtractor: t => t.id,
182
- contentContainerStyle: styles.content,
183
- ListHeaderComponent: listHeader,
184
- ListFooterComponent: footer,
185
- onEndReached: () => void loadMore(),
186
- onEndReachedThreshold: 0.5,
187
- refreshControl: /*#__PURE__*/_jsx(RefreshControl, {
188
- refreshing: refreshing,
189
- onRefresh: () => void refresh()
190
- }),
191
- ListEmptyComponent: empty(),
192
- renderItem: ({
193
- item
194
- }) => /*#__PURE__*/_jsxs(Card, {
195
- onPress: () => onOpenTicket(item),
196
- accessibilityLabel: `${item.subject}, ${item.statusLabel}, ticket ${item.number}`,
197
- bodyStyle: styles.card,
198
- testID: `support-ticket-${item.id}`,
199
- children: [/*#__PURE__*/_jsxs(View, {
200
- style: styles.cardTop,
201
- children: [/*#__PURE__*/_jsx(Text, {
202
- variant: "caption",
203
- color: "tertiary",
204
- children: item.number
205
- }), /*#__PURE__*/_jsx(StatusBadge, {
206
- label: item.statusLabel,
207
- tone: STATUS_TONES[item.status] ?? 'neutral',
208
- size: "sm"
209
- })]
210
- }), /*#__PURE__*/_jsx(Text, {
211
- variant: "title",
212
- numberOfLines: 2,
213
- children: item.subject
214
- }), /*#__PURE__*/_jsx(Text, {
215
- variant: "bodySmall",
216
- color: "secondary",
217
- children: item.category
218
- }), /*#__PURE__*/_jsxs(View, {
219
- style: styles.cardBottom,
220
- children: [/*#__PURE__*/_jsx(Text, {
221
- variant: "caption",
222
- color: "tertiary",
223
- children: `Updated ${formatWhen(item.updatedDateTimeUtc)}`
224
- }), item.awaitingCustomer && /*#__PURE__*/_jsx(Text, {
225
- variant: "caption",
226
- color: "warning",
227
- children: "Waiting for your reply"
228
- })]
229
- })]
230
- })
231
- });
232
- };
233
- const buildStyles = theme => StyleSheet.create({
234
- content: {
235
- padding: theme.spacing.md,
236
- gap: theme.spacing.sm
237
- },
238
- header: {
239
- gap: theme.spacing.sm,
240
- marginBottom: theme.spacing.xs
241
- },
242
- filters: {
243
- gap: theme.spacing.xs
244
- },
245
- error: {
246
- gap: theme.spacing.xs,
247
- alignItems: 'flex-start'
248
- },
249
- center: {
250
- flex: 1,
251
- alignItems: 'center',
252
- justifyContent: 'center',
253
- padding: theme.spacing.xl
254
- },
255
- footer: {
256
- paddingVertical: theme.spacing.md,
257
- alignItems: 'center'
258
- },
259
- card: {
260
- gap: theme.spacing.xxs
261
- },
262
- cardTop: {
263
- flexDirection: 'row',
264
- justifyContent: 'space-between',
265
- alignItems: 'center'
266
- },
267
- cardBottom: {
268
- flexDirection: 'row',
269
- justifyContent: 'space-between',
270
- marginTop: theme.spacing.xxs
271
- }
272
- });
273
- //# sourceMappingURL=SupportTicketList.js.map
1
+ import{Button as e,Card as t,Chip as r,EmptyState as i,SearchBar as o,Spinner as s,StatusBadge as a,Text as n,useTheme as l}from"@webority/mobile-ui";import{useMemo as c}from"react";import{FlatList as d,RefreshControl as p,ScrollView as u,StyleSheet as m,View as g}from"react-native";import{formatWhen as b}from"../format.js";import{useSupportTickets as h,useSupportTimeZone as y}from"../SupportProvider.js";import{jsx as f,jsxs as v,Fragment as S}from"react/jsx-runtime";export const STATUS_TONES={Open:"warning",InProgress:"info",Resolved:"success",Closed:"neutral"};const k=[{value:"Open",label:"Open",count:"open"},{value:"InProgress",label:"In progress",count:"inProgress"},{value:"Resolved",label:"Resolved",count:"resolved"},{value:"Closed",label:"Closed",count:"closed"}],x="all";export const statusFilterOptions=(e,t)=>[{value:x,label:"All",count:e.total},...k.filter(r=>e[r.count]>0||r.value===t).map(t=>({value:t.value,label:t.label,count:e[t.count]}))];export const SupportTicketList=({onOpenTicket:m,onNewTicket:k,header:L,pageSize:T,testID:I="support-ticket-list"})=>{const w=l(),C=c(()=>buildStyles(w),[w]),{tickets:j,counts:P,status:R,search:$,setStatus:D,setSearch:O,loading:z,refreshing:E,loadingMore:N,error:F,loadMoreError:M,refresh:U,loadMore:A}=h({pageSize:T}),B=y(),H=(P?.total??0)>0,W=null!==R||""!==$.trim(),_=v(g,{style:C.header,children:[L,k&&f(e,{title:"New ticket",leftIcon:"add",onPress:k,fullWidth:!0}),F&&v(g,{style:C.error,accessibilityLiveRegion:"polite",children:[f(n,{variant:"bodySmall",color:"error",children:F.message}),f(e,{title:"Try again",size:"sm",variant:"ghost",onPress:()=>{U()}})]}),P&&H&&v(S,{children:[f(o,{value:$,onChangeText:O,debounceMs:400,placeholder:"Search by subject or ticket number",accessibilityLabel:"Search tickets",testID:"support-ticket-search"}),f(u,{horizontal:!0,showsHorizontalScrollIndicator:!1,contentContainerStyle:C.filters,accessibilityRole:"tablist",accessibilityLabel:"Filter by status",children:statusFilterOptions(P,R).map(e=>f(r,{label:`${e.label} (${e.count})`,selected:(R??x)===e.value,onPress:()=>D(e.value===x?null:e.value),accessibilityLabel:`${e.label}, ${e.count} tickets`,testID:`support-filter-${e.value}`},e.value))})]})]});if(z&&null===P&&!F)return f(g,{style:C.center,testID:I,children:f(s,{accessibilityLabel:"Loading your tickets"})});const V=N?f(g,{style:C.footer,children:f(s,{size:"sm",accessibilityLabel:"Loading more tickets"})}):M?v(g,{style:[C.footer,C.error],accessibilityLiveRegion:"polite",children:[f(n,{variant:"bodySmall",color:"error",children:M.message}),f(e,{title:"Load more",size:"sm",variant:"ghost",onPress:()=>{A()}})]}):null;return f(d,{testID:I,data:j,keyExtractor:e=>e.id,contentContainerStyle:C.content,ListHeaderComponent:_,ListFooterComponent:V,onEndReached:()=>{A()},onEndReachedThreshold:.5,refreshControl:f(p,{refreshing:E,onRefresh:()=>{U()}}),ListEmptyComponent:z?f(g,{style:C.footer,children:f(s,{accessibilityLabel:"Loading your tickets"})}):F?null:f(i,H?{title:"No tickets match",description:W?"Try another status or search.":void 0}:{title:"No tickets yet",description:"Raise a ticket and it will show here with its status.",action:k?{label:"New ticket",onPress:k}:void 0}),renderItem:({item:e})=>v(t,{onPress:()=>m(e),accessibilityLabel:`${e.subject}, ${e.statusLabel}, ticket ${e.number}`,bodyStyle:C.card,testID:`support-ticket-${e.id}`,children:[v(g,{style:C.cardTop,children:[f(n,{variant:"caption",color:"tertiary",children:e.number}),f(a,{label:e.statusLabel,tone:STATUS_TONES[e.status]??"neutral",size:"sm"})]}),f(n,{variant:"title",numberOfLines:2,children:e.subject}),f(n,{variant:"bodySmall",color:"secondary",children:e.category}),v(g,{style:C.cardBottom,children:[f(n,{variant:"caption",color:"tertiary",children:`Updated ${b(e.updatedDateTimeUtc,B)}`}),e.awaitingCustomer&&f(n,{variant:"caption",color:"warning",children:"Waiting for your reply"})]})]})})};const buildStyles=e=>m.create({content:{padding:e.spacing.md,gap:e.spacing.sm},header:{gap:e.spacing.sm,marginBottom:e.spacing.xs},filters:{gap:e.spacing.xs},error:{gap:e.spacing.xs,alignItems:"flex-start"},center:{flex:1,alignItems:"center",justifyContent:"center",padding:e.spacing.xl},footer:{paddingVertical:e.spacing.md,alignItems:"center"},card:{gap:e.spacing.xxs},cardTop:{flexDirection:"row",justifyContent:"space-between",alignItems:"center"},cardBottom:{flexDirection:"row",justifyContent:"space-between",marginTop:e.spacing.xxs}});
@@ -1,24 +1 @@
1
- "use strict";
2
-
3
- import { getDeviceContext } from '@webority/mobile-core';
4
- import { Platform } from 'react-native';
5
- const PLATFORM_NAMES = {
6
- ios: 'iOS',
7
- android: 'Android'
8
- };
9
-
10
- /**
11
- * The device line a ticket carries as its user agent: platform, OS version and,
12
- * when the app registered one with mobile-core's `setDeviceContext`, the model.
13
- */
14
- export const describeDevice = () => {
15
- const context = getDeviceContext();
16
- const platform = PLATFORM_NAMES[Platform.OS] ?? Platform.OS;
17
- const osVersion = context?.osVersion ?? String(Platform.Version ?? '');
18
- const parts = [`${platform} ${osVersion}`.trim()];
19
- if (context?.deviceModel) {
20
- parts.push(context.deviceModel);
21
- }
22
- return parts.join('; ');
23
- };
24
- //# sourceMappingURL=device.js.map
1
+ import{getDeviceContext as o}from"@webority/mobile-core";import{Platform as e}from"react-native";const i={ios:"iOS",android:"Android"};export const describeDevice=()=>{const r=o(),t=[`${i[e.OS]??e.OS} ${r?.osVersion??String(e.Version??"")}`.trim()];return r?.deviceModel&&t.push(r.deviceModel),t.join("; ")};
@@ -1,31 +1 @@
1
- "use strict";
2
-
3
- import { File, Paths } from 'expo-file-system';
4
- const FOLDER = 'support-attachments';
5
-
6
- /** A server-supplied name reduced to one path segment, so it cannot leave the cache folder. */
7
- export const safeFileName = name => {
8
- const last = name.split(/[/\\]/).pop() ?? '';
9
- const cleaned = Array.from(last).filter(char => (char.codePointAt(0) ?? 0) > 0x1f).join('').trim();
10
- return cleaned === '' || cleaned === '.' || cleaned === '..' ? 'attachment' : cleaned;
11
- };
12
-
13
- /**
14
- * Writes an attachment to the app's cache and returns its `file://` URI. The id
15
- * prefix keeps two tickets' `report.pdf` apart. Attachments never change once
16
- * stored, so a file already on disk is reused rather than fetched again.
17
- */
18
- export const saveAttachmentToCache = async (attachment, loadBytes) => {
19
- const file = new File(Paths.cache, FOLDER, `${attachment.id}-${safeFileName(attachment.fileName)}`);
20
- if (file.exists && file.size > 0) {
21
- return file.uri;
22
- }
23
- const bytes = await loadBytes();
24
- file.create({
25
- intermediates: true,
26
- overwrite: true
27
- });
28
- file.write(new Uint8Array(bytes));
29
- return file.uri;
30
- };
31
- //# sourceMappingURL=files.js.map
1
+ import{File as e,Paths as t}from"expo-file-system";export const safeFileName=e=>{const t=e.split(/[/\\]/).pop()??"",r=Array.from(t).filter(e=>(e.codePointAt(0)??0)>31).join("").trim();return""===r||"."===r||".."===r?"attachment":r};export const saveAttachmentToCache=async(r,i)=>{const a=new e(t.cache,"support-attachments",`${r.id}-${safeFileName(r.fileName)}`);if(a.exists&&a.size>0)return a.uri;const n=await i();return a.create({intermediates:!0,overwrite:!0}),a.write(new Uint8Array(n)),a.uri};
@@ -1,23 +1 @@
1
- "use strict";
2
-
3
- import { formatDateTime } from '@webority/mobile-core';
4
-
5
- /**
6
- * A timestamp in the device's own time zone. The backend sends UTC; mobile-core's
7
- * formatter goes through Intl without pinning a zone, so the reader sees their
8
- * own clock.
9
- */
10
- export const formatWhen = utc => utc ? formatDateTime(utc) || '-' : '-';
11
-
12
- /** A byte count as a person reads it. */
13
- export const formatBytes = bytes => {
14
- if (bytes >= 1024 * 1024) {
15
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
16
- }
17
- if (bytes >= 1024) {
18
- return `${Math.round(bytes / 1024)} KB`;
19
- }
20
- return `${bytes} bytes`;
21
- };
22
- export const isImage = contentType => contentType.startsWith('image/');
23
- //# sourceMappingURL=format.js.map
1
+ export const DEFAULT_TIME_ZONE="Asia/Kolkata";export const formatWhen=(t,e="Asia/Kolkata")=>{if(!t)return"-";const o=new Date(t);if(Number.isNaN(o.getTime()))return"-";const r={};for(const t of new Intl.DateTimeFormat("en-US",{timeZone:e,day:"2-digit",month:"short",year:"numeric",hour:"numeric",minute:"2-digit",hour12:!0}).formatToParts(o))r[t.type]=t.value;return`${r.day}-${r.month}-${r.year}, ${r.hour}:${r.minute} ${(r.dayPeriod??"").toUpperCase()}`};export const formatBytes=t=>t>=1048576?`${(t/1048576).toFixed(1)} MB`:t>=1024?`${Math.round(t/1024)} KB`:`${t} bytes`;export const isImage=t=>t.startsWith("image/");
@@ -1,10 +1 @@
1
- "use strict";
2
-
3
- export { createSupportClient, SupportApiError, toSupportApiError } from "./client.js";
4
- export { NewSupportTicket } from "./components/NewSupportTicket.js";
5
- export { SupportContact } from "./components/SupportContact.js";
6
- export { SupportTicketDetail } from "./components/SupportTicketDetail.js";
7
- export { SupportTicketList } from "./components/SupportTicketList.js";
8
- export { describeDevice } from "./device.js";
9
- export { SupportProvider, useCreateSupportTicket, useSupportClient, useSupportConfig, useSupportTicket, useSupportTickets } from "./SupportProvider.js";
10
- //# sourceMappingURL=index.js.map
1
+ export{createSupportClient,SupportApiError,toSupportApiError}from"./client.js";export{NewSupportTicket}from"./components/NewSupportTicket.js";export{SupportContact}from"./components/SupportContact.js";export{SupportTicketDetail}from"./components/SupportTicketDetail.js";export{SupportTicketList}from"./components/SupportTicketList.js";export{describeDevice}from"./device.js";export{SupportProvider,useCreateSupportTicket,useSupportClient,useSupportConfig,useSupportTicket,useSupportTickets}from"./SupportProvider.js";
@@ -1,61 +1 @@
1
- "use strict";
2
-
3
- import { compressImage } from '@webority/mobile-core/compressor';
4
- /** The one place `expo-image-picker` is named. */
5
- const picker = () => require('expo-image-picker');
6
- export class PhotoPermissionError extends Error {
7
- constructor(source) {
8
- super(source === 'camera' ? 'Allow camera access in Settings to take a photo.' : 'Allow photo access in Settings to attach a photo.');
9
- this.name = 'PhotoPermissionError';
10
- this.source = source;
11
- }
12
- }
13
-
14
- // The fleet capture pass: quality 0.8, no editing, then bound to 1920 px.
15
- const CAPTURE_OPTIONS = {
16
- mediaTypes: ['images'],
17
- quality: 0.8,
18
- allowsEditing: false
19
- };
20
- const MAX_SIDE = 1920;
21
-
22
- /**
23
- * Lets the person pick or take photos and returns them ready to upload,
24
- * compressed to fit the product's per-file limit. An empty list means they
25
- * cancelled.
26
- */
27
- export const pickPhotos = async (source, options) => {
28
- if (options.maxCount <= 0) {
29
- return [];
30
- }
31
- const m = picker();
32
- const permission = source === 'camera' ? await m.requestCameraPermissionsAsync() : await m.requestMediaLibraryPermissionsAsync();
33
- if (!permission.granted) {
34
- throw new PhotoPermissionError(source);
35
- }
36
- const result = source === 'camera' ? await m.launchCameraAsync(CAPTURE_OPTIONS) : await m.launchImageLibraryAsync({
37
- ...CAPTURE_OPTIONS,
38
- allowsMultipleSelection: options.maxCount > 1,
39
- selectionLimit: options.maxCount
40
- });
41
- if (result.canceled || !result.assets) {
42
- return [];
43
- }
44
- const parts = [];
45
- for (const asset of result.assets.slice(0, options.maxCount)) {
46
- const compressed = await compressImage(asset.uri, {
47
- maxSize: MAX_SIDE,
48
- maxBytes: options.maxBytes,
49
- fileSize: asset.fileSize ?? undefined,
50
- name: asset.fileName ?? `photo-${Date.now()}-${parts.length + 1}`
51
- });
52
- parts.push({
53
- uri: compressed.part.uri,
54
- name: compressed.part.name,
55
- type: compressed.part.type,
56
- size: compressed.size ?? undefined
57
- });
58
- }
59
- return parts;
60
- };
61
- //# sourceMappingURL=photos.js.map
1
+ import{compressImage as e}from"@webority/mobile-core/compressor";export class PhotoPermissionError extends Error{constructor(e){super("camera"===e?"Allow camera access in Settings to take a photo.":"Allow photo access in Settings to attach a photo."),this.name="PhotoPermissionError",this.source=e}}const t={mediaTypes:["images"],quality:.8,allowsEditing:!1};export const pickPhotos=async(a,o)=>{if(o.maxCount<=0)return[];const s=require("expo-image-picker");if(!("camera"===a?await s.requestCameraPermissionsAsync():await s.requestMediaLibraryPermissionsAsync()).granted)throw new PhotoPermissionError(a);const r="camera"===a?await s.launchCameraAsync(t):await s.launchImageLibraryAsync({...t,allowsMultipleSelection:o.maxCount>1,selectionLimit:o.maxCount});if(r.canceled||!r.assets)return[];const i=[];for(const t of r.assets.slice(0,o.maxCount)){const a=await e(t.uri,{maxSize:1920,maxBytes:o.maxBytes,fileSize:t.fileSize??void 0,name:t.fileName??`photo-${Date.now()}-${i.length+1}`});i.push({uri:a.part.uri,name:a.part.name,type:a.part.type,size:a.size??void 0})}return i};
@@ -1,52 +1 @@
1
- "use strict";
2
-
3
- import { Button } from '@webority/mobile-ui';
4
- import { useState } from 'react';
5
- import { captureAndReport } from "./captureAndReport.js";
6
- import { jsx as _jsx } from "react/jsx-runtime";
7
- /** A button that screenshots the current screen and hands a prefilled ticket draft to the app. */
8
- export const ReportIssueButton = ({
9
- onReport,
10
- onCaptureError,
11
- screenName,
12
- appVersion,
13
- category,
14
- subject,
15
- title = 'Report an issue',
16
- accessibilityLabel = 'Report an issue with this screen',
17
- testID = 'support-report-issue',
18
- ...buttonProps
19
- }) => {
20
- const [capturing, setCapturing] = useState(false);
21
- const press = async () => {
22
- setCapturing(true);
23
- try {
24
- onReport(await captureAndReport({
25
- screenName,
26
- appVersion,
27
- category,
28
- subject
29
- }));
30
- } catch (error) {
31
- onCaptureError?.(error);
32
- onReport({
33
- category,
34
- subject,
35
- pageUrl: screenName,
36
- appVersion
37
- });
38
- } finally {
39
- setCapturing(false);
40
- }
41
- };
42
- return /*#__PURE__*/_jsx(Button, {
43
- leftIcon: "message-question",
44
- ...buttonProps,
45
- title: title,
46
- accessibilityLabel: accessibilityLabel,
47
- testID: testID,
48
- loading: capturing,
49
- onPress: () => void press()
50
- });
51
- };
52
- //# sourceMappingURL=ReportIssueButton.js.map
1
+ import{Button as e}from"@webority/mobile-ui";import{useState as t}from"react";import{captureAndReport as r}from"./captureAndReport.js";import{jsx as s}from"react/jsx-runtime";export const ReportIssueButton=({onReport:o,onCaptureError:i,screenName:a,appVersion:c,category:n,subject:p,title:u="Report an issue",accessibilityLabel:m="Report an issue with this screen",testID:l="support-report-issue",...b})=>{const[y,f]=t(!1);return s(e,{leftIcon:"message-question",...b,title:u,accessibilityLabel:m,testID:l,loading:y,onPress:()=>{(async()=>{f(!0);try{o(await r({screenName:a,appVersion:c,category:n,subject:p}))}catch(e){i?.(e),o({category:n,subject:p,pageUrl:a,appVersion:c})}finally{f(!1)}})()}})};
@@ -1,29 +1 @@
1
- "use strict";
2
-
3
- import { describeDevice } from "../device.js";
4
- /** The one place `react-native-view-shot` is named, and only this subpath reaches it. */
5
- const viewShot = () => require('react-native-view-shot');
6
- /**
7
- * Captures what is on screen right now and returns a draft for NewSupportTicket
8
- * with the image under `screenshots`, plus the page, device and app version.
9
- */
10
- export const captureAndReport = async options => {
11
- const uri = await viewShot().captureScreen({
12
- format: 'jpg',
13
- quality: 0.8,
14
- result: 'tmpfile'
15
- });
16
- return {
17
- category: options.category,
18
- subject: options.subject,
19
- pageUrl: options.screenName,
20
- userAgent: describeDevice(),
21
- appVersion: options.appVersion,
22
- screenshots: [{
23
- uri,
24
- name: `screenshot-${Date.now()}.jpg`,
25
- type: 'image/jpeg'
26
- }]
27
- };
28
- };
29
- //# sourceMappingURL=captureAndReport.js.map
1
+ import{describeDevice as e}from"../device.js";export const captureAndReport=async t=>{const r=await require("react-native-view-shot").captureScreen({format:"jpg",quality:.8,result:"tmpfile"});return{category:t.category,subject:t.subject,pageUrl:t.screenName,userAgent:e(),appVersion:t.appVersion,screenshots:[{uri:r,name:`screenshot-${Date.now()}.jpg`,type:"image/jpeg"}]}};
@@ -1,12 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * Report an issue. A separate entry point because it names react-native-view-shot,
5
- * an optional peer: Metro resolves every require reachable from an import, so
6
- * keeping it off the main barrel lets an app skip the package when it does not
7
- * use this button.
8
- */
9
-
10
- export { captureAndReport } from "./captureAndReport.js";
11
- export { ReportIssueButton } from "./ReportIssueButton.js";
12
- //# sourceMappingURL=index.js.map
1
+ export{captureAndReport}from"./captureAndReport.js";export{ReportIssueButton}from"./ReportIssueButton.js";
@@ -1,37 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * Opens a support attachment through the system share sheet, by way of
5
- * mobile-core's `/share`. A separate entry point because that reaches
6
- * expo-sharing, an optional peer: Metro resolves every require reachable from an
7
- * import, so keeping it off the main barrel lets an app skip the package when it
8
- * does not open files.
9
- */
10
- import { shareFile } from '@webority/mobile-core/share';
11
- export class SupportShareUnavailableError extends Error {
12
- constructor() {
13
- super('Opening files is not available on this device.');
14
- this.name = 'SupportShareUnavailableError';
15
- }
16
- }
17
-
18
- /**
19
- * Hands a downloaded attachment to the OS share sheet, where the person can open
20
- * it in a viewer or save it. Pass it to `SupportProvider` as `openFile`.
21
- */
22
- export const shareSupportFile = async file => {
23
- try {
24
- await shareFile(file.uri, {
25
- mimeType: file.contentType,
26
- dialogTitle: file.name
27
- });
28
- } catch (error) {
29
- // By name, not instanceof: a second copy of mobile-core would fail instanceof.
30
- const e = error;
31
- if (e?.name === 'ShareError' && e.code === 'unavailable') {
32
- throw new SupportShareUnavailableError();
33
- }
34
- throw error;
35
- }
36
- };
37
- //# sourceMappingURL=index.js.map
1
+ import{shareFile as r}from"@webority/mobile-core/share";export class SupportShareUnavailableError extends Error{constructor(){super("Opening files is not available on this device."),this.name="SupportShareUnavailableError"}}export const shareSupportFile=async e=>{try{await r(e.uri,{mimeType:e.contentType,dialogTitle:e.name})}catch(r){const e=r;if("ShareError"===e?.name&&"unavailable"===e.code)throw new SupportShareUnavailableError;throw r}};
@@ -1,2 +0,0 @@
1
- "use strict";
2
- //# sourceMappingURL=types.js.map
@@ -1,38 +1 @@
1
- "use strict";
2
-
3
- export const validateCategory = (category, config) => config.categories.includes(category) ? undefined : 'Choose one of the listed categories.';
4
-
5
- /** Limits come from GET /settings, so the product's own values are the only copy. */
6
-
7
- export const validateSubject = (subject, limits) => {
8
- const trimmed = subject.trim();
9
- if (!trimmed) {
10
- return 'Add a short subject.';
11
- }
12
- return trimmed.length > limits.subjectMaxLength ? `Keep the subject under ${limits.subjectMaxLength} characters.` : undefined;
13
- };
14
- export const validateBody = (body, limits) => {
15
- const trimmed = body.trim();
16
- if (!trimmed) {
17
- return 'Write a message.';
18
- }
19
- return trimmed.length > limits.bodyMaxLength ? `Keep the message under ${limits.bodyMaxLength} characters.` : undefined;
20
- };
21
-
22
- /** Checks every file on one message against the product's limits. The first problem wins. */
23
- export const validateFiles = (files, config) => {
24
- if (files.length > config.maxAttachmentsPerMessage) {
25
- return `Attach up to ${config.maxAttachmentsPerMessage} files to one message.`;
26
- }
27
- for (const file of files) {
28
- if (config.allowedContentTypes.length > 0 && !config.allowedContentTypes.includes(file.type)) {
29
- return `${file.name} can't be attached. Use an image, a PDF or a text file.`;
30
- }
31
- if (file.size !== undefined && file.size > config.maxAttachmentBytes) {
32
- const mb = Math.floor(config.maxAttachmentBytes / (1024 * 1024));
33
- return `${file.name} is too large. The limit is ${mb} MB per file.`;
34
- }
35
- }
36
- return undefined;
37
- };
38
- //# sourceMappingURL=validation.js.map
1
+ export const validateCategory=(e,t)=>t.categories.includes(e)?void 0:"Choose one of the listed categories.";export const validateSubject=(e,t)=>{const a=e.trim();return a?a.length>t.subjectMaxLength?`Keep the subject under ${t.subjectMaxLength} characters.`:void 0:"Add a short subject."};export const validateBody=(e,t)=>{const a=e.trim();return a?a.length>t.bodyMaxLength?`Keep the message under ${t.bodyMaxLength} characters.`:void 0:"Write a message."};export const validateFiles=(e,t)=>{if(e.length>t.maxAttachmentsPerMessage)return`Attach up to ${t.maxAttachmentsPerMessage} files to one message.`;for(const a of e){if(t.allowedContentTypes.length>0&&!t.allowedContentTypes.includes(a.type))return`${a.name} can't be attached. Use an image, a PDF or a text file.`;if(void 0!==a.size&&a.size>t.maxAttachmentBytes){const e=Math.floor(t.maxAttachmentBytes/1048576);return`${a.name} is too large. The limit is ${e} MB per file.`}}};
@@ -18,6 +18,8 @@ export interface SupportProviderProps {
18
18
  * attachments show their name and size only.
19
19
  */
20
20
  openFile?: SupportFileOpener;
21
+ /** IANA time zone ticket times show in. Defaults to `Asia/Kolkata` (IST). */
22
+ timeZone?: string;
21
23
  children: React.ReactNode;
22
24
  }
23
25
  export declare const SupportProvider: React.FC<SupportProviderProps>;
@@ -26,6 +28,8 @@ export declare const useSupportClient: () => SupportClient;
26
28
  export declare const useAttachmentLoader: () => (attachment: Pick<SupportAttachmentView, "id" | "contentType">) => Promise<string>;
27
29
  /** @internal Used by the file rows; null when the app gave no `openFile`. */
28
30
  export declare const useAttachmentOpener: () => AttachmentOpener | null;
31
+ /** @internal Used by the ticket screens to format times. */
32
+ export declare const useSupportTimeZone: () => string;
29
33
  export interface SupportConfigState {
30
34
  config: SupportContactInfo | null;
31
35
  loading: boolean;
@@ -32,7 +32,6 @@ export declare class SupportApiError extends Error {
32
32
  }
33
33
  /** Turns whatever the HTTP client threw into a SupportApiError. */
34
34
  export declare const toSupportApiError: (error: unknown) => SupportApiError;
35
- export declare const encodeBase64: (buffer: ArrayBuffer) => string;
36
35
  export interface SupportClient {
37
36
  readonly basePath: string;
38
37
  getConfig(): Promise<SupportContactInfo>;