@webority/mobile-support 0.0.13 → 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.
Files changed (45) hide show
  1. package/lib/commonjs/SupportProvider.js +1 -335
  2. package/lib/commonjs/client.js +1 -149
  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 -143
  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 +1 -2
  40. package/lib/module/validation.js +1 -38
  41. package/lib/typescript/commonjs/SupportProvider.d.ts +4 -0
  42. package/lib/typescript/commonjs/format.d.ts +6 -4
  43. package/lib/typescript/module/SupportProvider.d.ts +4 -0
  44. package/lib/typescript/module/format.d.ts +6 -4
  45. package/package.json +8 -6
@@ -1,20 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- Object.defineProperty(exports, "ReportIssueButton", {
7
- enumerable: true,
8
- get: function () {
9
- return _ReportIssueButton.ReportIssueButton;
10
- }
11
- });
12
- Object.defineProperty(exports, "captureAndReport", {
13
- enumerable: true,
14
- get: function () {
15
- return _captureAndReport.captureAndReport;
16
- }
17
- });
18
- var _captureAndReport = require("./captureAndReport.js");
19
- var _ReportIssueButton = require("./ReportIssueButton.js");
20
- //# sourceMappingURL=index.js.map
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"ReportIssueButton",{enumerable:!0,get:function(){return _ReportIssueButton.ReportIssueButton}}),Object.defineProperty(exports,"captureAndReport",{enumerable:!0,get:function(){return _captureAndReport.captureAndReport}});var _captureAndReport=require("./captureAndReport.js"),_ReportIssueButton=require("./ReportIssueButton.js");
@@ -1,44 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.shareSupportFile = exports.SupportShareUnavailableError = void 0;
7
- var _share = require("@webority/mobile-core/share");
8
- /**
9
- * Opens a support attachment through the system share sheet, by way of
10
- * mobile-core's `/share`. A separate entry point because that reaches
11
- * expo-sharing, an optional peer: Metro resolves every require reachable from an
12
- * import, so keeping it off the main barrel lets an app skip the package when it
13
- * does not open files.
14
- */
15
-
16
- class SupportShareUnavailableError extends Error {
17
- constructor() {
18
- super('Opening files is not available on this device.');
19
- this.name = 'SupportShareUnavailableError';
20
- }
21
- }
22
-
23
- /**
24
- * Hands a downloaded attachment to the OS share sheet, where the person can open
25
- * it in a viewer or save it. Pass it to `SupportProvider` as `openFile`.
26
- */
27
- exports.SupportShareUnavailableError = SupportShareUnavailableError;
28
- const shareSupportFile = async file => {
29
- try {
30
- await (0, _share.shareFile)(file.uri, {
31
- mimeType: file.contentType,
32
- dialogTitle: file.name
33
- });
34
- } catch (error) {
35
- // By name, not instanceof: a second copy of mobile-core would fail instanceof.
36
- const e = error;
37
- if (e?.name === 'ShareError' && e.code === 'unavailable') {
38
- throw new SupportShareUnavailableError();
39
- }
40
- throw error;
41
- }
42
- };
43
- exports.shareSupportFile = shareSupportFile;
44
- //# sourceMappingURL=index.js.map
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.shareSupportFile=exports.SupportShareUnavailableError=void 0;var _share=require("@webority/mobile-core/share");class SupportShareUnavailableError extends Error{constructor(){super("Opening files is not available on this device."),this.name="SupportShareUnavailableError"}}exports.SupportShareUnavailableError=SupportShareUnavailableError;const shareSupportFile=async r=>{try{await(0,_share.shareFile)(r.uri,{mimeType:r.contentType,dialogTitle:r.name})}catch(r){const e=r;if("ShareError"===e?.name&&"unavailable"===e.code)throw new SupportShareUnavailableError;throw r}};exports.shareSupportFile=shareSupportFile;
@@ -1,2 +1 @@
1
- "use strict";
2
- //# sourceMappingURL=types.js.map
1
+ "use strict";
@@ -1,45 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.validateSubject = exports.validateFiles = exports.validateCategory = exports.validateBody = void 0;
7
- const validateCategory = (category, config) => config.categories.includes(category) ? undefined : 'Choose one of the listed categories.';
8
-
9
- /** Limits come from GET /settings, so the product's own values are the only copy. */
10
- exports.validateCategory = validateCategory;
11
- const validateSubject = (subject, limits) => {
12
- const trimmed = subject.trim();
13
- if (!trimmed) {
14
- return 'Add a short subject.';
15
- }
16
- return trimmed.length > limits.subjectMaxLength ? `Keep the subject under ${limits.subjectMaxLength} characters.` : undefined;
17
- };
18
- exports.validateSubject = validateSubject;
19
- const validateBody = (body, limits) => {
20
- const trimmed = body.trim();
21
- if (!trimmed) {
22
- return 'Write a message.';
23
- }
24
- return trimmed.length > limits.bodyMaxLength ? `Keep the message under ${limits.bodyMaxLength} characters.` : undefined;
25
- };
26
-
27
- /** Checks every file on one message against the product's limits. The first problem wins. */
28
- exports.validateBody = validateBody;
29
- const validateFiles = (files, config) => {
30
- if (files.length > config.maxAttachmentsPerMessage) {
31
- return `Attach up to ${config.maxAttachmentsPerMessage} files to one message.`;
32
- }
33
- for (const file of files) {
34
- if (config.allowedContentTypes.length > 0 && !config.allowedContentTypes.includes(file.type)) {
35
- return `${file.name} can't be attached. Use an image, a PDF or a text file.`;
36
- }
37
- if (file.size !== undefined && file.size > config.maxAttachmentBytes) {
38
- const mb = Math.floor(config.maxAttachmentBytes / (1024 * 1024));
39
- return `${file.name} is too large. The limit is ${mb} MB per file.`;
40
- }
41
- }
42
- return undefined;
43
- };
44
- exports.validateFiles = validateFiles;
45
- //# sourceMappingURL=validation.js.map
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.validateSubject=exports.validateFiles=exports.validateCategory=exports.validateBody=void 0;const validateCategory=(e,t)=>t.categories.includes(e)?void 0:"Choose one of the listed categories.";exports.validateCategory=validateCategory;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."};exports.validateSubject=validateSubject;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."};exports.validateBody=validateBody;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.`}}};exports.validateFiles=validateFiles;
@@ -1,323 +1 @@
1
- "use strict";
2
-
3
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
- import { toSupportApiError } from "./client.js";
5
- import { saveAttachmentToCache } from "./files.js";
6
-
7
- /** A downloaded attachment, ready for the OS to open. */
8
-
9
- /** Hands a downloaded file to the OS. `shareSupportFile` from the `/share` entry is one. */
10
- import { jsx as _jsx } from "react/jsx-runtime";
11
- const SupportContext = /*#__PURE__*/createContext(null);
12
- export const SupportProvider = ({
13
- client,
14
- openFile,
15
- children
16
- }) => {
17
- const [config, setConfig] = useState(null);
18
- const [configError, setConfigError] = useState(null);
19
- const [configLoading, setConfigLoading] = useState(true);
20
- const [revision, setRevision] = useState(0);
21
- // Attachments never change once stored, so each is fetched once per session.
22
- const attachments = useRef(new Map());
23
- const reloadConfig = useCallback(async () => {
24
- setConfigLoading(true);
25
- try {
26
- setConfig(await client.getConfig());
27
- setConfigError(null);
28
- } catch (error) {
29
- setConfigError(toSupportApiError(error));
30
- } finally {
31
- setConfigLoading(false);
32
- }
33
- }, [client]);
34
- useEffect(() => {
35
- attachments.current.clear();
36
- void reloadConfig();
37
- }, [reloadConfig]);
38
- const loadAttachment = useCallback(attachment => {
39
- const cached = attachments.current.get(attachment.id);
40
- if (cached) {
41
- return cached;
42
- }
43
- const pending = client.getAttachmentDataUri(attachment);
44
- attachments.current.set(attachment.id, pending);
45
- pending.catch(() => attachments.current.delete(attachment.id));
46
- return pending;
47
- }, [client]);
48
- const openAttachment = useMemo(() => openFile ? async attachment => {
49
- const uri = await saveAttachmentToCache(attachment, () => client.getAttachmentBytes(attachment.id));
50
- await openFile({
51
- uri,
52
- name: attachment.fileName,
53
- contentType: attachment.contentType
54
- });
55
- } : null, [client, openFile]);
56
- const bumpRevision = useCallback(() => setRevision(r => r + 1), []);
57
- const value = useMemo(() => ({
58
- client,
59
- config,
60
- configError,
61
- configLoading,
62
- reloadConfig,
63
- loadAttachment,
64
- openAttachment,
65
- revision,
66
- bumpRevision
67
- }), [client, config, configError, configLoading, reloadConfig, loadAttachment, openAttachment, revision, bumpRevision]);
68
- return /*#__PURE__*/_jsx(SupportContext.Provider, {
69
- value: value,
70
- children: children
71
- });
72
- };
73
- const useSupportContext = () => {
74
- const ctx = useContext(SupportContext);
75
- if (!ctx) {
76
- throw new Error('[@webority/mobile-support] Support screens and hooks need a <SupportProvider client={...}> above them.');
77
- }
78
- return ctx;
79
- };
80
- export const useSupportClient = () => useSupportContext().client;
81
-
82
- /** @internal Used by the attachment thumbnails. */
83
- export const useAttachmentLoader = () => useSupportContext().loadAttachment;
84
-
85
- /** @internal Used by the file rows; null when the app gave no `openFile`. */
86
- export const useAttachmentOpener = () => useSupportContext().openAttachment;
87
- export const useSupportConfig = () => {
88
- const {
89
- config,
90
- configLoading,
91
- configError,
92
- reloadConfig
93
- } = useSupportContext();
94
- return {
95
- config,
96
- loading: configLoading,
97
- error: configError,
98
- reload: reloadConfig
99
- };
100
- };
101
- const DEFAULT_PAGE_SIZE = 20;
102
- export const useSupportTickets = (options = {}) => {
103
- const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
104
- const {
105
- client,
106
- revision
107
- } = useSupportContext();
108
- const [status, setStatus] = useState(null);
109
- const [search, setSearch] = useState('');
110
- const [tickets, setTickets] = useState([]);
111
- const [counts, setCounts] = useState(null);
112
- const [page, setPage] = useState(0);
113
- const [totalPages, setTotalPages] = useState(0);
114
- const [loading, setLoading] = useState(true);
115
- const [refreshing, setRefreshing] = useState(false);
116
- const [loadingMore, setLoadingMore] = useState(false);
117
- const [error, setError] = useState(null);
118
- const [loadMoreError, setLoadMoreError] = useState(null);
119
- const mounted = useRef(true);
120
- // Each first-page load takes a new number; a response carrying an older one
121
- // belongs to a filter or search the person has already left, and is dropped.
122
- const sequence = useRef(0);
123
- const moreInFlight = useRef(false);
124
- // The client whose counts are on screen; null until a load with counts lands.
125
- const countsFrom = useRef(null);
126
- useEffect(() => {
127
- mounted.current = true;
128
- return () => {
129
- mounted.current = false;
130
- };
131
- }, []);
132
- const query = useCallback(nextPage => ({
133
- status: status ?? undefined,
134
- search: search.trim() || undefined,
135
- page: nextPage,
136
- pageSize
137
- }), [status, search, pageSize]);
138
- const loadFirstPage = useCallback(async mode => {
139
- sequence.current += 1;
140
- const seq = sequence.current;
141
- const current = () => mounted.current && seq === sequence.current;
142
- moreInFlight.current = false;
143
- setLoadingMore(false);
144
- if (mode === 'refresh') {
145
- setRefreshing(true);
146
- } else if (mode === 'replace') {
147
- setLoading(true);
148
- setTickets([]);
149
- }
150
- // Counts ignore the status and search, so a filter change keeps the ones it has.
151
- const withCounts = mode !== 'replace' || countsFrom.current !== client;
152
- try {
153
- const [first, nextCounts] = await Promise.all([client.listTickets(query(1)), withCounts ? client.getTicketCounts() : null]);
154
- if (current()) {
155
- setTickets(first.items);
156
- setPage(first.page);
157
- setTotalPages(first.totalPages);
158
- if (nextCounts) {
159
- setCounts(nextCounts);
160
- countsFrom.current = client;
161
- }
162
- setError(null);
163
- setLoadMoreError(null);
164
- }
165
- } catch (e) {
166
- if (current()) {
167
- setError(toSupportApiError(e));
168
- }
169
- } finally {
170
- if (current()) {
171
- setLoading(false);
172
- setRefreshing(false);
173
- }
174
- }
175
- }, [client, query]);
176
- const seenRevision = useRef(revision);
177
- useEffect(() => {
178
- // A write elsewhere refetches without blanking the rows on screen.
179
- const mode = seenRevision.current === revision ? 'replace' : 'quiet';
180
- seenRevision.current = revision;
181
- void loadFirstPage(mode);
182
- }, [loadFirstPage, revision]);
183
- const hasMore = page < totalPages;
184
- const loadMore = useCallback(async () => {
185
- if (moreInFlight.current || loading || refreshing || !hasMore) {
186
- return;
187
- }
188
- const seq = sequence.current;
189
- const current = () => mounted.current && seq === sequence.current;
190
- moreInFlight.current = true;
191
- setLoadingMore(true);
192
- setLoadMoreError(null);
193
- try {
194
- const next = await client.listTickets(query(page + 1));
195
- if (current()) {
196
- // Order is by last update, so a ticket that changed between two pages
197
- // can arrive twice.
198
- setTickets(prev => {
199
- const ids = new Set(prev.map(t => t.id));
200
- return [...prev, ...next.items.filter(t => !ids.has(t.id))];
201
- });
202
- setPage(next.page);
203
- setTotalPages(next.totalPages);
204
- }
205
- } catch (e) {
206
- if (current()) {
207
- setLoadMoreError(toSupportApiError(e));
208
- }
209
- } finally {
210
- if (current()) {
211
- moreInFlight.current = false;
212
- setLoadingMore(false);
213
- }
214
- }
215
- }, [client, query, page, hasMore, loading, refreshing]);
216
- const refresh = useCallback(() => loadFirstPage('refresh'), [loadFirstPage]);
217
- const createTicket = useCreateSupportTicket();
218
- return {
219
- tickets,
220
- counts,
221
- status,
222
- search,
223
- setStatus,
224
- setSearch,
225
- loading,
226
- refreshing,
227
- loadingMore,
228
- hasMore,
229
- error,
230
- loadMoreError,
231
- refresh,
232
- loadMore,
233
- createTicket
234
- };
235
- };
236
-
237
- /** Raises a ticket and tells every mounted ticket list to refetch. */
238
- export const useCreateSupportTicket = () => {
239
- const {
240
- client,
241
- bumpRevision
242
- } = useSupportContext();
243
- return useCallback(async input => {
244
- const created = await client.createTicket(input);
245
- bumpRevision();
246
- return created;
247
- }, [client, bumpRevision]);
248
- };
249
- export const useSupportTicket = id => {
250
- const {
251
- client,
252
- bumpRevision
253
- } = useSupportContext();
254
- const [ticket, setTicket] = useState(null);
255
- const [loading, setLoading] = useState(true);
256
- const [error, setError] = useState(null);
257
- const [actionError, setActionError] = useState(null);
258
- const [busy, setBusy] = useState(false);
259
- const mounted = useRef(true);
260
- useEffect(() => {
261
- mounted.current = true;
262
- return () => {
263
- mounted.current = false;
264
- };
265
- }, []);
266
- const reload = useCallback(async () => {
267
- setLoading(true);
268
- try {
269
- const next = await client.getTicket(id);
270
- if (mounted.current) {
271
- setTicket(next);
272
- setError(null);
273
- }
274
- } catch (e) {
275
- if (mounted.current) {
276
- setError(toSupportApiError(e));
277
- }
278
- } finally {
279
- if (mounted.current) {
280
- setLoading(false);
281
- }
282
- }
283
- }, [client, id]);
284
- useEffect(() => {
285
- void reload();
286
- }, [reload]);
287
- const act = useCallback(async run => {
288
- setBusy(true);
289
- setActionError(null);
290
- try {
291
- const next = await run();
292
- if (mounted.current) {
293
- setTicket(next);
294
- }
295
- bumpRevision();
296
- return true;
297
- } catch (e) {
298
- if (mounted.current) {
299
- setActionError(toSupportApiError(e));
300
- }
301
- return false;
302
- } finally {
303
- if (mounted.current) {
304
- setBusy(false);
305
- }
306
- }
307
- }, [bumpRevision]);
308
- const reply = useCallback(input => act(() => client.reply(id, input)), [act, client, id]);
309
- const reopen = useCallback(() => act(() => client.reopen(id)), [act, client, id]);
310
- const rate = useCallback(input => act(() => client.rate(id, input)), [act, client, id]);
311
- return {
312
- ticket,
313
- loading,
314
- error,
315
- actionError,
316
- busy,
317
- reload,
318
- reply,
319
- reopen,
320
- rate
321
- };
322
- };
323
- //# sourceMappingURL=SupportProvider.js.map
1
+ "use strict";import{createContext,useCallback,useContext,useEffect,useMemo,useRef,useState}from"react";import{toSupportApiError}from"./client.js";import{saveAttachmentToCache}from"./files.js";import{DEFAULT_TIME_ZONE}from"./format.js";import{jsx as _jsx}from"react/jsx-runtime";const SupportContext=createContext(null);export const SupportProvider=({client:e,openFile:t,timeZone:r=DEFAULT_TIME_ZONE,children:n})=>{const[o,u]=useState(null),[s,c]=useState(null),[a,l]=useState(!0),[i,p]=useState(0),S=useRef(new Map),f=useCallback(async()=>{l(!0);try{u(await e.getConfig()),c(null)}catch(e){c(toSupportApiError(e))}finally{l(!1)}},[e]);useEffect(()=>{S.current.clear(),f()},[f]);const m=useCallback(t=>{const r=S.current.get(t.id);if(r)return r;const n=e.getAttachmentDataUri(t);return S.current.set(t.id,n),n.catch(()=>S.current.delete(t.id)),n},[e]),C=useMemo(()=>t?async r=>{const n=await saveAttachmentToCache(r,()=>e.getAttachmentBytes(r.id));await t({uri:n,name:r.fileName,contentType:r.contentType})}:null,[e,t]),h=useCallback(()=>p(e=>e+1),[]),d=useMemo(()=>({client:e,config:o,configError:s,configLoading:a,reloadConfig:f,loadAttachment:m,openAttachment:C,timeZone:r,revision:i,bumpRevision:h}),[e,o,s,a,f,m,C,r,i,h]);return _jsx(SupportContext.Provider,{value:d,children:n})};const useSupportContext=()=>{const e=useContext(SupportContext);if(!e)throw new Error("[@webority/mobile-support] Support screens and hooks need a <SupportProvider client={...}> above them.");return e};export const useSupportClient=()=>useSupportContext().client;export const useAttachmentLoader=()=>useSupportContext().loadAttachment;export const useAttachmentOpener=()=>useSupportContext().openAttachment;export const useSupportTimeZone=()=>useSupportContext().timeZone;export const useSupportConfig=()=>{const{config:e,configLoading:t,configError:r,reloadConfig:n}=useSupportContext();return{config:e,loading:t,error:r,reload:n}};const DEFAULT_PAGE_SIZE=20;export const useSupportTickets=(e={})=>{const t=e.pageSize??20,{client:r,revision:n}=useSupportContext(),[o,u]=useState(null),[s,c]=useState(""),[a,l]=useState([]),[i,p]=useState(null),[S,f]=useState(0),[m,C]=useState(0),[h,d]=useState(!0),[g,x]=useState(!1),[k,E]=useState(!1),[y,b]=useState(null),[A,T]=useState(null),v=useRef(!0),w=useRef(0),R=useRef(!1),M=useRef(null);useEffect(()=>(v.current=!0,()=>{v.current=!1}),[]);const _=useCallback(e=>({status:o??void 0,search:s.trim()||void 0,page:e,pageSize:t}),[o,s,t]),j=useCallback(async e=>{w.current+=1;const t=w.current,current=()=>v.current&&t===w.current;R.current=!1,E(!1),"refresh"===e?x(!0):"replace"===e&&(d(!0),l([]));const n="replace"!==e||M.current!==r;try{const[e,t]=await Promise.all([r.listTickets(_(1)),n?r.getTicketCounts():null]);current()&&(l(e.items),f(e.page),C(e.totalPages),t&&(p(t),M.current=r),b(null),T(null))}catch(e){current()&&b(toSupportApiError(e))}finally{current()&&(d(!1),x(!1))}},[r,_]),P=useRef(n);useEffect(()=>{const e=P.current===n?"replace":"quiet";P.current=n,j(e)},[j,n]);const Z=S<m,L=useCallback(async()=>{if(R.current||h||g||!Z)return;const e=w.current,current=()=>v.current&&e===w.current;R.current=!0,E(!0),T(null);try{const e=await r.listTickets(_(S+1));current()&&(l(t=>{const r=new Set(t.map(e=>e.id));return[...t,...e.items.filter(e=>!r.has(e.id))]}),f(e.page),C(e.totalPages))}catch(e){current()&&T(toSupportApiError(e))}finally{current()&&(R.current=!1,E(!1))}},[r,_,S,Z,h,g]),D=useCallback(()=>j("refresh"),[j]),F=useCreateSupportTicket();return{tickets:a,counts:i,status:o,search:s,setStatus:u,setSearch:c,loading:h,refreshing:g,loadingMore:k,hasMore:Z,error:y,loadMoreError:A,refresh:D,loadMore:L,createTicket:F}};export const useCreateSupportTicket=()=>{const{client:e,bumpRevision:t}=useSupportContext();return useCallback(async r=>{const n=await e.createTicket(r);return t(),n},[e,t])};export const useSupportTicket=e=>{const{client:t,bumpRevision:r}=useSupportContext(),[n,o]=useState(null),[u,s]=useState(!0),[c,a]=useState(null),[l,i]=useState(null),[p,S]=useState(!1),f=useRef(!0);useEffect(()=>(f.current=!0,()=>{f.current=!1}),[]);const m=useCallback(async()=>{s(!0);try{const r=await t.getTicket(e);f.current&&(o(r),a(null))}catch(e){f.current&&a(toSupportApiError(e))}finally{f.current&&s(!1)}},[t,e]);useEffect(()=>{m()},[m]);const C=useCallback(async e=>{S(!0),i(null);try{const t=await e();return f.current&&o(t),r(),!0}catch(e){return f.current&&i(toSupportApiError(e)),!1}finally{f.current&&S(!1)}},[r]),h=useCallback(r=>C(()=>t.reply(e,r)),[C,t,e]),d=useCallback(()=>C(()=>t.reopen(e)),[C,t,e]),g=useCallback(r=>C(()=>t.rate(e,r)),[C,t,e]);return{ticket:n,loading:u,error:c,actionError:l,busy:p,reload:m,reply:h,reopen:d,rate:g}};
@@ -1,143 +1 @@
1
- "use strict";
2
-
3
- import { encodeBase64 } from '@webority/mobile-core';
4
-
5
- /**
6
- * The slice of an HTTP client the support client needs. The app's signed-in
7
- * axios instance (mobile-core's `userClient`, or its own) satisfies it as is, so
8
- * whatever it adds to a request (bearer token, tenant header, refresh on 401)
9
- * applies to support calls too. Declared structurally so no axios type leaks into
10
- * this package's declarations.
11
- */
12
-
13
- export const OFFLINE_MESSAGE = 'You are offline. Check your connection and try again.';
14
- export const GENERIC_MESSAGE = 'Something went wrong. Try again in a moment.';
15
-
16
- /**
17
- * A failed support call. `message` is written for the person and is shown as is;
18
- * `code` is the backend's stable code, for branching only.
19
- */
20
- export class SupportApiError extends Error {
21
- constructor(code, message, status) {
22
- super(message);
23
- this.name = 'SupportApiError';
24
- this.code = code;
25
- this.status = status;
26
- }
27
- }
28
- /** Turns whatever the HTTP client threw into a SupportApiError. */
29
- export const toSupportApiError = error => {
30
- if (error instanceof SupportApiError) {
31
- return error;
32
- }
33
- const e = error ?? {};
34
- // By name, not instanceof: a second copy of mobile-core would fail instanceof.
35
- if (e.name === 'RequestOfflineError') {
36
- return new SupportApiError('offline', OFFLINE_MESSAGE, null);
37
- }
38
- const status = typeof e.response?.status === 'number' ? e.response.status : null;
39
- const data = e.response?.data;
40
- if (data && typeof data.message === 'string' && data.message.trim()) {
41
- const code = typeof data.code === 'string' ? data.code : 'unknown';
42
- return new SupportApiError(code, data.message, status);
43
- }
44
- if (!e.response) {
45
- return new SupportApiError('network', OFFLINE_MESSAGE, null);
46
- }
47
- return new SupportApiError('unknown', GENERIC_MESSAGE, status);
48
- };
49
-
50
- // Photo uploads get the fleet's multipart timeout, not the 30 s JSON one.
51
- const UPLOAD_TIMEOUT_MS = 300000;
52
- const MULTIPART = {
53
- 'Content-Type': 'multipart/form-data'
54
- };
55
- const appendParts = (form, field, parts) => {
56
- for (const part of parts ?? []) {
57
- // React Native's FormData takes a { uri, name, type } object as a file part.
58
- form.append(field, {
59
- uri: part.uri,
60
- name: part.name,
61
- type: part.type
62
- });
63
- }
64
- };
65
- const appendText = (form, field, value) => {
66
- if (value !== undefined && value !== '') {
67
- form.append(field, value);
68
- }
69
- };
70
- const queryString = query => {
71
- const pairs = [];
72
- const add = (key, value) => {
73
- if (value !== undefined && value !== '') {
74
- pairs.push(`${key}=${encodeURIComponent(String(value))}`);
75
- }
76
- };
77
- add('status', query.status);
78
- add('search', query.search?.trim());
79
- add('page', query.page);
80
- add('pageSize', query.pageSize);
81
- return pairs.length > 0 ? `?${pairs.join('&')}` : '';
82
- };
83
- export const createSupportClient = (http, basePath = '/api/support') => {
84
- const base = basePath.replace(/\/+$/, '');
85
- const ticketUrl = id => `${base}/tickets/${encodeURIComponent(id)}`;
86
- const call = async request => {
87
- try {
88
- const response = await request();
89
- return response.data;
90
- } catch (error) {
91
- throw toSupportApiError(error);
92
- }
93
- };
94
- const getBytes = id => call(() => http.get(`${base}/attachments/${encodeURIComponent(id)}`, {
95
- responseType: 'arraybuffer'
96
- }));
97
- return {
98
- basePath: base,
99
- getConfig: () => call(() => http.get(`${base}/settings`)),
100
- listTickets: (query = {}) => call(() => http.get(`${base}/tickets${queryString(query)}`)),
101
- getTicketCounts: () => call(() => http.get(`${base}/tickets/counts`)),
102
- getTicket: id => call(() => http.get(ticketUrl(id))),
103
- createTicket: input => {
104
- const form = new FormData();
105
- form.append('category', input.category);
106
- form.append('subject', input.subject);
107
- form.append('body', input.body);
108
- appendText(form, 'pageUrl', input.pageUrl);
109
- appendText(form, 'userAgent', input.userAgent);
110
- appendText(form, 'appVersion', input.appVersion);
111
- appendParts(form, 'files', input.files);
112
- appendParts(form, 'screenshots', input.screenshots);
113
- return call(() => http.post(`${base}/tickets`, form, {
114
- headers: MULTIPART,
115
- timeout: UPLOAD_TIMEOUT_MS
116
- }));
117
- },
118
- reply: (id, input) => {
119
- const form = new FormData();
120
- form.append('body', input.body);
121
- appendParts(form, 'files', input.files);
122
- appendParts(form, 'screenshots', input.screenshots);
123
- return call(() => http.post(`${ticketUrl(id)}/replies`, form, {
124
- headers: MULTIPART,
125
- timeout: UPLOAD_TIMEOUT_MS
126
- }));
127
- },
128
- reopen: id => call(() => http.post(`${ticketUrl(id)}/reopen`)),
129
- rate: (id, input) => call(() => http.post(`${ticketUrl(id)}/rating`, {
130
- satisfaction: input.satisfaction,
131
- comment: input.comment?.trim() ? input.comment.trim() : null
132
- })),
133
- getAttachmentBytes: getBytes,
134
- getAttachmentDataUri: async attachment => {
135
- const bytes = await getBytes(attachment.id);
136
- return `data:${attachment.contentType};base64,${encodeBase64(bytes)}`;
137
- },
138
- confirmAction: token => call(() => http.post(`${base}/actions/confirm`, {
139
- token
140
- }))
141
- };
142
- };
143
- //# sourceMappingURL=client.js.map
1
+ "use strict";import{encodeBase64}from"@webority/mobile-core";export const OFFLINE_MESSAGE="You are offline. Check your connection and try again.";export const GENERIC_MESSAGE="Something went wrong. Try again in a moment.";export class SupportApiError extends Error{constructor(e,t,r){super(t),this.name="SupportApiError",this.code=e,this.status=r}}export const toSupportApiError=e=>{if(e instanceof SupportApiError)return e;const t=e??{};if("RequestOfflineError"===t.name)return new SupportApiError("offline",OFFLINE_MESSAGE,null);const r="number"==typeof t.response?.status?t.response.status:null,n=t.response?.data;if(n&&"string"==typeof n.message&&n.message.trim()){const e="string"==typeof n.code?n.code:"unknown";return new SupportApiError(e,n.message,r)}return t.response?new SupportApiError("unknown",GENERIC_MESSAGE,r):new SupportApiError("network",OFFLINE_MESSAGE,null)};const UPLOAD_TIMEOUT_MS=3e5,MULTIPART={"Content-Type":"multipart/form-data"},appendParts=(e,t,r)=>{for(const n of r??[])e.append(t,{uri:n.uri,name:n.name,type:n.type})},appendText=(e,t,r)=>{void 0!==r&&""!==r&&e.append(t,r)},queryString=e=>{const t=[],add=(e,r)=>{void 0!==r&&""!==r&&t.push(`${e}=${encodeURIComponent(String(r))}`)};return add("status",e.status),add("search",e.search?.trim()),add("page",e.page),add("pageSize",e.pageSize),t.length>0?`?${t.join("&")}`:""};export const createSupportClient=(e,t="/api/support")=>{const r=t.replace(/\/+$/,""),ticketUrl=e=>`${r}/tickets/${encodeURIComponent(e)}`,call=async e=>{try{return(await e()).data}catch(e){throw toSupportApiError(e)}},getBytes=t=>call(()=>e.get(`${r}/attachments/${encodeURIComponent(t)}`,{responseType:"arraybuffer"}));return{basePath:r,getConfig:()=>call(()=>e.get(`${r}/settings`)),listTickets:(t={})=>call(()=>e.get(`${r}/tickets${queryString(t)}`)),getTicketCounts:()=>call(()=>e.get(`${r}/tickets/counts`)),getTicket:t=>call(()=>e.get(ticketUrl(t))),createTicket:t=>{const n=new FormData;return n.append("category",t.category),n.append("subject",t.subject),n.append("body",t.body),appendText(n,"pageUrl",t.pageUrl),appendText(n,"userAgent",t.userAgent),appendText(n,"appVersion",t.appVersion),appendParts(n,"files",t.files),appendParts(n,"screenshots",t.screenshots),call(()=>e.post(`${r}/tickets`,n,{headers:MULTIPART,timeout:3e5}))},reply:(t,r)=>{const n=new FormData;return n.append("body",r.body),appendParts(n,"files",r.files),appendParts(n,"screenshots",r.screenshots),call(()=>e.post(`${ticketUrl(t)}/replies`,n,{headers:MULTIPART,timeout:3e5}))},reopen:t=>call(()=>e.post(`${ticketUrl(t)}/reopen`)),rate:(t,r)=>call(()=>e.post(`${ticketUrl(t)}/rating`,{satisfaction:r.satisfaction,comment:r.comment?.trim()?r.comment.trim():null})),getAttachmentBytes:getBytes,getAttachmentDataUri:async e=>{const t=await getBytes(e.id);return`data:${e.contentType};base64,${encodeBase64(t)}`},confirmAction:t=>call(()=>e.post(`${r}/actions/confirm`,{token:t}))}};
@@ -1,5 +1 @@
1
- "use strict";
2
-
3
- /** The one place `expo-clipboard` is named. */
4
- export const copyText = text => require('expo-clipboard').setStringAsync(text);
5
- //# sourceMappingURL=clipboard.js.map
1
+ "use strict";export const copyText=e=>require("expo-clipboard").setStringAsync(e);