@harkenapp/sdk-react-native 0.0.1-alpha.1 → 0.0.1-alpha.2

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 (278) hide show
  1. package/README.md +44 -7
  2. package/app.plugin.cjs +12 -17
  3. package/dist/__mocks__/async-storage.d.ts +16 -0
  4. package/dist/__mocks__/async-storage.d.ts.map +1 -0
  5. package/dist/__mocks__/async-storage.js +39 -0
  6. package/dist/__mocks__/async-storage.js.map +1 -0
  7. package/dist/__mocks__/expo-document-picker.d.ts +26 -0
  8. package/dist/__mocks__/expo-document-picker.d.ts.map +1 -0
  9. package/dist/__mocks__/expo-document-picker.js +25 -0
  10. package/dist/__mocks__/expo-document-picker.js.map +1 -0
  11. package/dist/__mocks__/expo-file-system.d.ts +42 -0
  12. package/dist/__mocks__/expo-file-system.d.ts.map +1 -0
  13. package/dist/__mocks__/expo-file-system.js +37 -0
  14. package/dist/__mocks__/expo-file-system.js.map +1 -0
  15. package/dist/__mocks__/expo-image-picker.d.ts +30 -0
  16. package/dist/__mocks__/expo-image-picker.d.ts.map +1 -0
  17. package/dist/__mocks__/expo-image-picker.js +30 -0
  18. package/dist/__mocks__/expo-image-picker.js.map +1 -0
  19. package/dist/__mocks__/expo-secure-store.d.ts +15 -0
  20. package/dist/__mocks__/expo-secure-store.d.ts.map +1 -0
  21. package/dist/__mocks__/expo-secure-store.js +30 -0
  22. package/dist/__mocks__/expo-secure-store.js.map +1 -0
  23. package/dist/__mocks__/react-native.d.ts +73 -0
  24. package/dist/__mocks__/react-native.d.ts.map +1 -0
  25. package/dist/__mocks__/react-native.js +45 -0
  26. package/dist/__mocks__/react-native.js.map +1 -0
  27. package/dist/api/client.d.ts +8 -8
  28. package/dist/api/client.d.ts.map +1 -1
  29. package/dist/api/client.js +17 -19
  30. package/dist/api/client.js.map +1 -1
  31. package/dist/api/client.test.d.ts +2 -0
  32. package/dist/api/client.test.d.ts.map +1 -0
  33. package/dist/api/client.test.js +417 -0
  34. package/dist/api/client.test.js.map +1 -0
  35. package/dist/api/errors.d.ts +3 -3
  36. package/dist/api/errors.d.ts.map +1 -1
  37. package/dist/api/errors.js +3 -3
  38. package/dist/api/errors.js.map +1 -1
  39. package/dist/api/errors.test.d.ts +2 -0
  40. package/dist/api/errors.test.d.ts.map +1 -0
  41. package/dist/api/errors.test.js +155 -0
  42. package/dist/api/errors.test.js.map +1 -0
  43. package/dist/api/index.d.ts +6 -6
  44. package/dist/api/index.d.ts.map +1 -1
  45. package/dist/api/index.js.map +1 -1
  46. package/dist/api/retry.d.ts +1 -1
  47. package/dist/api/retry.d.ts.map +1 -1
  48. package/dist/api/retry.js.map +1 -1
  49. package/dist/api/retry.test.d.ts +2 -0
  50. package/dist/api/retry.test.d.ts.map +1 -0
  51. package/dist/api/retry.test.js +193 -0
  52. package/dist/api/retry.test.js.map +1 -0
  53. package/dist/attachments/FeedbackSheet.d.ts +36 -13
  54. package/dist/attachments/FeedbackSheet.d.ts.map +1 -1
  55. package/dist/attachments/FeedbackSheet.js +50 -30
  56. package/dist/attachments/FeedbackSheet.js.map +1 -1
  57. package/dist/attachments/index.d.ts +2 -2
  58. package/dist/components/AttachmentGrid.d.ts +12 -4
  59. package/dist/components/AttachmentGrid.d.ts.map +1 -1
  60. package/dist/components/AttachmentGrid.js +44 -34
  61. package/dist/components/AttachmentGrid.js.map +1 -1
  62. package/dist/components/AttachmentPicker.d.ts +3 -3
  63. package/dist/components/AttachmentPicker.d.ts.map +1 -1
  64. package/dist/components/AttachmentPicker.js +34 -36
  65. package/dist/components/AttachmentPicker.js.map +1 -1
  66. package/dist/components/AttachmentPreview.d.ts +10 -4
  67. package/dist/components/AttachmentPreview.d.ts.map +1 -1
  68. package/dist/components/AttachmentPreview.js +48 -34
  69. package/dist/components/AttachmentPreview.js.map +1 -1
  70. package/dist/components/CategorySelector.d.ts +3 -3
  71. package/dist/components/CategorySelector.d.ts.map +1 -1
  72. package/dist/components/CategorySelector.js +21 -27
  73. package/dist/components/CategorySelector.js.map +1 -1
  74. package/dist/components/FeedbackForm.d.ts +3 -3
  75. package/dist/components/FeedbackForm.d.ts.map +1 -1
  76. package/dist/components/FeedbackForm.js +7 -8
  77. package/dist/components/FeedbackForm.js.map +1 -1
  78. package/dist/components/FeedbackSheet.d.ts +34 -11
  79. package/dist/components/FeedbackSheet.d.ts.map +1 -1
  80. package/dist/components/FeedbackSheet.js +46 -28
  81. package/dist/components/FeedbackSheet.js.map +1 -1
  82. package/dist/components/ThemedButton.d.ts +16 -5
  83. package/dist/components/ThemedButton.d.ts.map +1 -1
  84. package/dist/components/ThemedButton.js +38 -29
  85. package/dist/components/ThemedButton.js.map +1 -1
  86. package/dist/components/ThemedText.d.ts +3 -3
  87. package/dist/components/ThemedText.d.ts.map +1 -1
  88. package/dist/components/ThemedText.js +1 -1
  89. package/dist/components/ThemedText.js.map +1 -1
  90. package/dist/components/ThemedTextInput.d.ts +11 -2
  91. package/dist/components/ThemedTextInput.d.ts.map +1 -1
  92. package/dist/components/ThemedTextInput.js +19 -9
  93. package/dist/components/ThemedTextInput.js.map +1 -1
  94. package/dist/components/UploadStatusOverlay.d.ts +11 -3
  95. package/dist/components/UploadStatusOverlay.d.ts.map +1 -1
  96. package/dist/components/UploadStatusOverlay.js +59 -76
  97. package/dist/components/UploadStatusOverlay.js.map +1 -1
  98. package/dist/components/index.d.ts +18 -18
  99. package/dist/components/index.d.ts.map +1 -1
  100. package/dist/components/index.js.map +1 -1
  101. package/dist/context/HarkenContext.d.ts +20 -15
  102. package/dist/context/HarkenContext.d.ts.map +1 -1
  103. package/dist/context/HarkenContext.js +20 -17
  104. package/dist/context/HarkenContext.js.map +1 -1
  105. package/dist/context/index.d.ts +2 -2
  106. package/dist/domain/index.d.ts +2 -2
  107. package/dist/domain/index.d.ts.map +1 -1
  108. package/dist/domain/index.js.map +1 -1
  109. package/dist/hooks/index.d.ts +5 -5
  110. package/dist/hooks/useAnonymousId.js +1 -1
  111. package/dist/hooks/useAnonymousId.test.d.ts +2 -0
  112. package/dist/hooks/useAnonymousId.test.d.ts.map +1 -0
  113. package/dist/hooks/useAnonymousId.test.js +154 -0
  114. package/dist/hooks/useAnonymousId.test.js.map +1 -0
  115. package/dist/hooks/useAttachmentPicker.d.ts +3 -3
  116. package/dist/hooks/useAttachmentPicker.js +7 -7
  117. package/dist/hooks/useAttachmentStatus.d.ts +1 -1
  118. package/dist/hooks/useAttachmentStatus.d.ts.map +1 -1
  119. package/dist/hooks/useAttachmentStatus.js.map +1 -1
  120. package/dist/hooks/useAttachmentUpload.d.ts +2 -2
  121. package/dist/hooks/useAttachmentUpload.d.ts.map +1 -1
  122. package/dist/hooks/useAttachmentUpload.js +5 -5
  123. package/dist/hooks/useAttachmentUpload.js.map +1 -1
  124. package/dist/hooks/useAttachmentUpload.test.d.ts +2 -0
  125. package/dist/hooks/useAttachmentUpload.test.d.ts.map +1 -0
  126. package/dist/hooks/useAttachmentUpload.test.js +542 -0
  127. package/dist/hooks/useAttachmentUpload.test.js.map +1 -0
  128. package/dist/hooks/useFeedback.d.ts +4 -4
  129. package/dist/hooks/useFeedback.d.ts.map +1 -1
  130. package/dist/hooks/useFeedback.js +3 -5
  131. package/dist/hooks/useFeedback.js.map +1 -1
  132. package/dist/hooks/useFeedback.test.d.ts +2 -0
  133. package/dist/hooks/useFeedback.test.d.ts.map +1 -0
  134. package/dist/hooks/useFeedback.test.js +299 -0
  135. package/dist/hooks/useFeedback.test.js.map +1 -0
  136. package/dist/hooks/useHarkenContext.d.ts +1 -1
  137. package/dist/hooks/useHarkenContext.js +1 -1
  138. package/dist/hooks/useHarkenTheme.d.ts +27 -3
  139. package/dist/hooks/useHarkenTheme.d.ts.map +1 -1
  140. package/dist/hooks/useHarkenTheme.js +26 -2
  141. package/dist/hooks/useHarkenTheme.js.map +1 -1
  142. package/dist/index.d.ts +28 -28
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js.map +1 -1
  145. package/dist/services/index.d.ts +3 -3
  146. package/dist/services/index.d.ts.map +1 -1
  147. package/dist/services/index.js.map +1 -1
  148. package/dist/services/uploadQueueService.d.ts +2 -2
  149. package/dist/services/uploadQueueService.d.ts.map +1 -1
  150. package/dist/services/uploadQueueService.js +16 -17
  151. package/dist/services/uploadQueueService.js.map +1 -1
  152. package/dist/services/uploadQueueService.test.d.ts +2 -0
  153. package/dist/services/uploadQueueService.test.d.ts.map +1 -0
  154. package/dist/services/uploadQueueService.test.js +426 -0
  155. package/dist/services/uploadQueueService.test.js.map +1 -0
  156. package/dist/services/uploadQueueStorage.d.ts +1 -1
  157. package/dist/services/uploadQueueStorage.d.ts.map +1 -1
  158. package/dist/services/uploadQueueStorage.js +4 -4
  159. package/dist/services/uploadQueueStorage.js.map +1 -1
  160. package/dist/services/uploadQueueStorage.test.d.ts +2 -0
  161. package/dist/services/uploadQueueStorage.test.d.ts.map +1 -0
  162. package/dist/services/uploadQueueStorage.test.js +200 -0
  163. package/dist/services/uploadQueueStorage.test.js.map +1 -0
  164. package/dist/storage/IdentityStore.d.ts +1 -1
  165. package/dist/storage/IdentityStore.d.ts.map +1 -1
  166. package/dist/storage/IdentityStore.js.map +1 -1
  167. package/dist/storage/IdentityStore.test.d.ts +2 -0
  168. package/dist/storage/IdentityStore.test.d.ts.map +1 -0
  169. package/dist/storage/IdentityStore.test.js +176 -0
  170. package/dist/storage/IdentityStore.test.js.map +1 -0
  171. package/dist/storage/SecureStoreAdapter.d.ts +1 -1
  172. package/dist/storage/SecureStoreAdapter.test.d.ts +2 -0
  173. package/dist/storage/SecureStoreAdapter.test.d.ts.map +1 -0
  174. package/dist/storage/SecureStoreAdapter.test.js +114 -0
  175. package/dist/storage/SecureStoreAdapter.test.js.map +1 -0
  176. package/dist/storage/defaultStorage.d.ts +1 -1
  177. package/dist/storage/defaultStorage.js +4 -4
  178. package/dist/storage/defaultStorage.test.d.ts +2 -0
  179. package/dist/storage/defaultStorage.test.d.ts.map +1 -0
  180. package/dist/storage/defaultStorage.test.js +159 -0
  181. package/dist/storage/defaultStorage.test.js.map +1 -0
  182. package/dist/storage/index.d.ts +5 -5
  183. package/dist/storage/types.js +1 -1
  184. package/dist/theme/defaults.d.ts +14 -3
  185. package/dist/theme/defaults.d.ts.map +1 -1
  186. package/dist/theme/defaults.js +58 -43
  187. package/dist/theme/defaults.js.map +1 -1
  188. package/dist/theme/index.d.ts +3 -2
  189. package/dist/theme/index.d.ts.map +1 -1
  190. package/dist/theme/index.js +4 -1
  191. package/dist/theme/index.js.map +1 -1
  192. package/dist/theme/resolver.d.ts +16 -0
  193. package/dist/theme/resolver.d.ts.map +1 -0
  194. package/dist/theme/resolver.js +375 -0
  195. package/dist/theme/resolver.js.map +1 -0
  196. package/dist/theme/resolver.test.d.ts +2 -0
  197. package/dist/theme/resolver.test.d.ts.map +1 -0
  198. package/dist/theme/resolver.test.js +344 -0
  199. package/dist/theme/resolver.test.js.map +1 -0
  200. package/dist/theme/types.d.ts +378 -5
  201. package/dist/theme/types.d.ts.map +1 -1
  202. package/dist/types/config.d.ts +4 -4
  203. package/dist/types/index.d.ts +2 -2
  204. package/dist/utils/index.d.ts +1 -1
  205. package/dist/utils/uuid.d.ts.map +1 -1
  206. package/dist/utils/uuid.js +4 -5
  207. package/dist/utils/uuid.js.map +1 -1
  208. package/dist/utils/uuid.test.d.ts +2 -0
  209. package/dist/utils/uuid.test.d.ts.map +1 -0
  210. package/dist/utils/uuid.test.js +78 -0
  211. package/dist/utils/uuid.test.js.map +1 -0
  212. package/package.json +21 -13
  213. package/src/@types/expo-file-system-legacy.d.ts +3 -3
  214. package/src/__mocks__/async-storage.ts +46 -0
  215. package/src/__mocks__/expo-document-picker.ts +41 -0
  216. package/src/__mocks__/expo-file-system.ts +62 -0
  217. package/src/__mocks__/expo-image-picker.ts +48 -0
  218. package/src/__mocks__/expo-secure-store.ts +29 -0
  219. package/src/__mocks__/react-native.ts +46 -0
  220. package/src/api/client.test.ts +515 -0
  221. package/src/api/client.ts +45 -64
  222. package/src/api/errors.test.ts +193 -0
  223. package/src/api/errors.ts +7 -11
  224. package/src/api/index.ts +6 -10
  225. package/src/api/retry.test.ts +251 -0
  226. package/src/api/retry.ts +3 -6
  227. package/src/attachments/FeedbackSheet.tsx +100 -80
  228. package/src/attachments/index.ts +2 -2
  229. package/src/components/AttachmentGrid.tsx +54 -45
  230. package/src/components/AttachmentPicker.tsx +43 -54
  231. package/src/components/AttachmentPreview.tsx +51 -47
  232. package/src/components/CategorySelector.tsx +29 -35
  233. package/src/components/FeedbackForm.tsx +23 -35
  234. package/src/components/FeedbackSheet.tsx +89 -68
  235. package/src/components/ThemedButton.tsx +49 -47
  236. package/src/components/ThemedText.tsx +7 -10
  237. package/src/components/ThemedTextInput.tsx +23 -13
  238. package/src/components/UploadStatusOverlay.tsx +66 -89
  239. package/src/components/index.ts +18 -21
  240. package/src/context/HarkenContext.tsx +29 -28
  241. package/src/context/index.ts +2 -2
  242. package/src/domain/index.ts +2 -5
  243. package/src/domain/upload-queue.ts +5 -5
  244. package/src/hooks/index.ts +5 -5
  245. package/src/hooks/useAnonymousId.test.ts +189 -0
  246. package/src/hooks/useAnonymousId.ts +3 -3
  247. package/src/hooks/useAttachmentPicker.ts +12 -12
  248. package/src/hooks/useAttachmentStatus.ts +12 -16
  249. package/src/hooks/useAttachmentUpload.test.ts +632 -0
  250. package/src/hooks/useAttachmentUpload.ts +45 -54
  251. package/src/hooks/useFeedback.test.ts +376 -0
  252. package/src/hooks/useFeedback.ts +12 -14
  253. package/src/hooks/useHarkenContext.ts +4 -4
  254. package/src/hooks/useHarkenTheme.ts +30 -6
  255. package/src/index.ts +28 -52
  256. package/src/services/index.ts +3 -9
  257. package/src/services/uploadQueueService.test.ts +489 -0
  258. package/src/services/uploadQueueService.ts +40 -56
  259. package/src/services/uploadQueueStorage.test.ts +243 -0
  260. package/src/services/uploadQueueStorage.ts +7 -9
  261. package/src/storage/IdentityStore.test.ts +173 -0
  262. package/src/storage/IdentityStore.ts +4 -5
  263. package/src/storage/SecureStoreAdapter.test.ts +147 -0
  264. package/src/storage/SecureStoreAdapter.ts +1 -1
  265. package/src/storage/defaultStorage.test.ts +159 -0
  266. package/src/storage/defaultStorage.ts +6 -6
  267. package/src/storage/index.ts +5 -5
  268. package/src/storage/types.ts +1 -1
  269. package/src/theme/defaults.ts +75 -46
  270. package/src/theme/index.ts +15 -2
  271. package/src/theme/resolver.test.ts +411 -0
  272. package/src/theme/resolver.ts +446 -0
  273. package/src/theme/types.ts +453 -15
  274. package/src/types/config.ts +4 -4
  275. package/src/types/index.ts +2 -2
  276. package/src/utils/index.ts +1 -1
  277. package/src/utils/uuid.test.ts +85 -0
  278. package/src/utils/uuid.ts +4 -7
package/src/index.ts CHANGED
@@ -20,33 +20,25 @@
20
20
  */
21
21
 
22
22
  // Provider and context
23
- export { HarkenProvider } from './context';
24
- export type { HarkenContextValue } from './context';
23
+ export { HarkenProvider } from "./context";
24
+ export type { HarkenContextValue } from "./context";
25
25
 
26
26
  // Hooks (core)
27
- export {
28
- useHarkenTheme,
29
- useHarkenContext,
30
- useAnonymousId,
31
- useFeedback,
32
- } from './hooks';
33
- export type { SubmitFeedbackParams, UseFeedbackResult } from './hooks';
27
+ export { useHarkenTheme, useHarkenContext, useAnonymousId, useFeedback } from "./hooks";
28
+ export type { SubmitFeedbackParams, UseFeedbackResult } from "./hooks";
34
29
 
35
30
  // Hooks (attachments)
36
- export { useAttachmentUpload } from './hooks/useAttachmentUpload';
37
- export type {
38
- AttachmentState,
39
- UseAttachmentUploadResult,
40
- } from './hooks/useAttachmentUpload';
31
+ export { useAttachmentUpload } from "./hooks/useAttachmentUpload";
32
+ export type { AttachmentState, UseAttachmentUploadResult } from "./hooks/useAttachmentUpload";
41
33
 
42
- export { useAttachmentPicker } from './hooks/useAttachmentPicker';
34
+ export { useAttachmentPicker } from "./hooks/useAttachmentPicker";
43
35
  export type {
44
36
  AttachmentSourceConfig,
45
37
  UseAttachmentPickerResult,
46
- } from './hooks/useAttachmentPicker';
38
+ } from "./hooks/useAttachmentPicker";
47
39
 
48
- export { useAttachmentStatus } from './hooks/useAttachmentStatus';
49
- export type { AttachmentStatus } from './hooks/useAttachmentStatus';
40
+ export { useAttachmentStatus } from "./hooks/useAttachmentStatus";
41
+ export type { AttachmentStatus } from "./hooks/useAttachmentStatus";
50
42
 
51
43
  // Theme system
52
44
  export type {
@@ -58,7 +50,7 @@ export type {
58
50
  PartialHarkenTheme,
59
51
  TextWeight,
60
52
  ThemeMode,
61
- } from './theme';
53
+ } from "./theme";
62
54
 
63
55
  export {
64
56
  lightColors,
@@ -69,18 +61,14 @@ export {
69
61
  lightTheme,
70
62
  darkTheme,
71
63
  createTheme,
72
- } from './theme';
64
+ } from "./theme";
73
65
 
74
66
  // Storage and identity
75
- export type { SecureStorage } from './storage';
76
- export {
77
- createSecureStoreAdapter,
78
- createMemoryStorage,
79
- IdentityStore,
80
- } from './storage';
67
+ export type { SecureStorage } from "./storage";
68
+ export { createSecureStoreAdapter, createMemoryStorage, IdentityStore } from "./storage";
81
69
 
82
70
  // Utilities
83
- export { generateUUID } from './utils';
71
+ export { generateUUID } from "./utils";
84
72
 
85
73
  // Components (core)
86
74
  export {
@@ -90,7 +78,7 @@ export {
90
78
  CategorySelector,
91
79
  FeedbackForm,
92
80
  DEFAULT_CATEGORIES,
93
- } from './components';
81
+ } from "./components";
94
82
 
95
83
  export type {
96
84
  ThemedTextProps,
@@ -102,7 +90,7 @@ export type {
102
90
  CategoryOption,
103
91
  FeedbackFormProps,
104
92
  FeedbackFormData,
105
- } from './components';
93
+ } from "./components";
106
94
 
107
95
  // Components (attachments)
108
96
  export {
@@ -110,7 +98,7 @@ export {
110
98
  UploadStatusOverlay,
111
99
  AttachmentPreview,
112
100
  AttachmentGrid,
113
- } from './components';
101
+ } from "./components";
114
102
 
115
103
  export type {
116
104
  AttachmentPickerProps,
@@ -120,11 +108,11 @@ export type {
120
108
  UploadStatusLabels,
121
109
  AttachmentPreviewProps,
122
110
  AttachmentGridProps,
123
- } from './components';
111
+ } from "./components";
124
112
 
125
113
  // FeedbackSheet (with full attachment support)
126
- export { FeedbackSheet } from './attachments/FeedbackSheet';
127
- export type { FeedbackSheetProps } from './attachments/FeedbackSheet';
114
+ export { FeedbackSheet } from "./attachments/FeedbackSheet";
115
+ export type { FeedbackSheetProps } from "./attachments/FeedbackSheet";
128
116
 
129
117
  // API client
130
118
  export {
@@ -135,8 +123,8 @@ export {
135
123
  HarkenNetworkError,
136
124
  withRetry,
137
125
  DEFAULT_RETRY_CONFIG,
138
- } from './api';
139
- export type { HarkenClientConfig, RetryConfig } from './api';
126
+ } from "./api";
127
+ export type { HarkenClientConfig, RetryConfig } from "./api";
140
128
 
141
129
  // Configuration types
142
130
  export type {
@@ -145,24 +133,12 @@ export type {
145
133
  FeedbackCategory,
146
134
  Platform,
147
135
  DeviceMetadata,
148
- } from './types';
136
+ } from "./types";
149
137
 
150
138
  // Domain types
151
- export { UploadPhase, DEFAULT_UPLOAD_RETRY_CONFIG } from './domain';
152
- export type {
153
- QueueItem,
154
- QueueStatus,
155
- UploadProgress,
156
- UploadRetryConfig,
157
- } from './domain';
139
+ export { UploadPhase, DEFAULT_UPLOAD_RETRY_CONFIG } from "./domain";
140
+ export type { QueueItem, QueueStatus, UploadProgress, UploadRetryConfig } from "./domain";
158
141
 
159
142
  // Services (for advanced usage)
160
- export {
161
- UploadQueueService,
162
- uploadQueueService,
163
- UploadQueueStorage,
164
- } from './services';
165
- export type {
166
- UploadQueueServiceConfig,
167
- EnqueueParams,
168
- } from './services';
143
+ export { UploadQueueService, uploadQueueService, UploadQueueStorage } from "./services";
144
+ export type { UploadQueueServiceConfig, EnqueueParams } from "./services";
@@ -1,11 +1,5 @@
1
- export {
2
- UploadQueueService,
3
- uploadQueueService,
4
- } from './uploadQueueService';
1
+ export { UploadQueueService, uploadQueueService } from "./uploadQueueService";
5
2
 
6
- export type {
7
- UploadQueueServiceConfig,
8
- EnqueueParams,
9
- } from './uploadQueueService';
3
+ export type { UploadQueueServiceConfig, EnqueueParams } from "./uploadQueueService";
10
4
 
11
- export { UploadQueueStorage } from './uploadQueueStorage';
5
+ export { UploadQueueStorage } from "./uploadQueueStorage";
@@ -0,0 +1,489 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { UploadPhase } from "../domain";
3
+ import type { QueueItem, UploadProgress } from "../domain";
4
+
5
+ // Mock storage - use vi.hoisted to ensure variables are available for mocks
6
+ const { mockLoadQueue, mockSaveQueue, mockClearQueue } = vi.hoisted(() => ({
7
+ mockLoadQueue: vi.fn(),
8
+ mockSaveQueue: vi.fn(),
9
+ mockClearQueue: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("./uploadQueueStorage", () => ({
13
+ UploadQueueStorage: class MockStorage {
14
+ loadQueue = mockLoadQueue;
15
+ saveQueue = mockSaveQueue;
16
+ clearQueue = mockClearQueue;
17
+ },
18
+ }));
19
+
20
+ // Mock FileSystem
21
+ const { mockUploadTask } = vi.hoisted(() => ({
22
+ mockUploadTask: {
23
+ uploadAsync: vi.fn(),
24
+ cancelAsync: vi.fn(),
25
+ },
26
+ }));
27
+
28
+ vi.mock("expo-file-system/legacy", () => ({
29
+ createUploadTask: vi.fn(() => mockUploadTask),
30
+ FileSystemUploadType: { BINARY_CONTENT: "binary" },
31
+ FileSystemSessionType: { BACKGROUND: "background" },
32
+ }));
33
+
34
+ // Mock NetInfo
35
+ vi.mock("@react-native-community/netinfo", () => ({
36
+ default: {
37
+ addEventListener: vi.fn(() => vi.fn()),
38
+ },
39
+ }));
40
+
41
+ // Mock generateUUID
42
+ const uuidState = vi.hoisted(() => ({ counter: 0 }));
43
+ vi.mock("../utils", () => ({
44
+ generateUUID: () => `uuid-${++uuidState.counter}`,
45
+ }));
46
+
47
+ // Import after mocks are set up
48
+ import { UploadQueueService } from "./uploadQueueService";
49
+
50
+ // Mock client
51
+ function createMockClient() {
52
+ return {
53
+ createAttachmentUpload: vi.fn().mockResolvedValue({
54
+ attachment_id: "att_123",
55
+ upload_url: "https://storage.example.com/upload",
56
+ upload_expires_at: new Date(Date.now() + 3600000).toISOString(),
57
+ }),
58
+ confirmAttachment: vi.fn().mockResolvedValue({}),
59
+ reportAttachmentFailure: vi.fn().mockResolvedValue({}),
60
+ };
61
+ }
62
+
63
+ describe("UploadQueueService", () => {
64
+ let service: UploadQueueService;
65
+ let mockClient: ReturnType<typeof createMockClient>;
66
+
67
+ beforeEach(() => {
68
+ vi.clearAllMocks();
69
+ uuidState.counter = 0;
70
+ mockLoadQueue.mockResolvedValue([]);
71
+ mockSaveQueue.mockResolvedValue(undefined);
72
+ mockUploadTask.uploadAsync.mockResolvedValue({ status: 200 });
73
+
74
+ // Get fresh instance by destroying any existing one
75
+ service = UploadQueueService.getInstance();
76
+ service.destroy();
77
+ service = UploadQueueService.getInstance();
78
+
79
+ mockClient = createMockClient();
80
+ });
81
+
82
+ afterEach(() => {
83
+ service.destroy();
84
+ });
85
+
86
+ describe("initialization", () => {
87
+ it("loads persisted queue on initialize", async () => {
88
+ // Use an item that's already COMPLETED so it doesn't get processed
89
+ const persistedItem: QueueItem = {
90
+ id: "queue_1",
91
+ attachmentId: "att_persisted",
92
+ localUri: "file:///photo.jpg",
93
+ uploadUrl: "https://storage.example.com/upload",
94
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
95
+ mimeType: "image/jpeg",
96
+ fileName: "photo.jpg",
97
+ fileSize: 1000,
98
+ phase: UploadPhase.COMPLETED, // Already done, won't be processed
99
+ progress: 1,
100
+ attemptNumber: 1,
101
+ maxAttempts: 3,
102
+ createdAt: new Date().toISOString(),
103
+ completedAt: new Date().toISOString(),
104
+ };
105
+ mockLoadQueue.mockResolvedValue([persistedItem]);
106
+
107
+ await service.initialize({ client: mockClient as never });
108
+
109
+ const status = service.getQueueStatus();
110
+ expect(status.total).toBe(1);
111
+ expect(status.completed).toBe(1);
112
+ });
113
+
114
+ it("resets uploading items to queued on initialize", async () => {
115
+ const uploadingItem: QueueItem = {
116
+ id: "queue_1",
117
+ attachmentId: "att_interrupted",
118
+ localUri: "file:///photo.jpg",
119
+ uploadUrl: "https://storage.example.com/upload",
120
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
121
+ mimeType: "image/jpeg",
122
+ fileName: "photo.jpg",
123
+ fileSize: 1000,
124
+ phase: UploadPhase.UPLOADING, // Was mid-upload when app was killed
125
+ progress: 0.5,
126
+ attemptNumber: 1,
127
+ maxAttempts: 3,
128
+ createdAt: new Date().toISOString(),
129
+ };
130
+ mockLoadQueue.mockResolvedValue([uploadingItem]);
131
+
132
+ // processQueue() only processes items in QUEUED phase, so receiving
133
+ // any progress event for this item proves it was reset from UPLOADING to QUEUED
134
+ const progressReceived = new Promise<UploadProgress>((resolve) => {
135
+ service.onProgress((p) => {
136
+ if (p.attachmentId === "att_interrupted") resolve(p);
137
+ });
138
+ });
139
+
140
+ await service.initialize({ client: mockClient as never });
141
+
142
+ // Wait for a progress event on the interrupted item
143
+ const progress = await progressReceived;
144
+
145
+ // The item was processed (received progress), which proves it was reset
146
+ // to QUEUED since processQueue() skips non-QUEUED items
147
+ expect(progress.attachmentId).toBe("att_interrupted");
148
+ // Should have progressed beyond initial state
149
+ expect([UploadPhase.UPLOADING, UploadPhase.CONFIRMING, UploadPhase.COMPLETED]).toContain(
150
+ progress.phase
151
+ );
152
+ });
153
+ });
154
+
155
+ describe("enqueue", () => {
156
+ it("creates queue item and calls presign API", async () => {
157
+ await service.initialize({ client: mockClient as never });
158
+
159
+ const result = await service.enqueue({
160
+ localUri: "file:///new-photo.jpg",
161
+ mimeType: "image/jpeg",
162
+ fileName: "new-photo.jpg",
163
+ fileSize: 2000,
164
+ });
165
+
166
+ expect(result.attachmentId).toBe("att_123");
167
+ expect(mockClient.createAttachmentUpload).toHaveBeenCalledWith({
168
+ filename: "new-photo.jpg",
169
+ content_type: "image/jpeg",
170
+ size: 2000,
171
+ });
172
+
173
+ // Item exists (may have started processing already)
174
+ const item = service.getItemByAttachmentId("att_123");
175
+ expect(item).toBeDefined();
176
+ });
177
+
178
+ it("persists queue after enqueue", async () => {
179
+ await service.initialize({ client: mockClient as never });
180
+
181
+ await service.enqueue({
182
+ localUri: "file:///photo.jpg",
183
+ mimeType: "image/jpeg",
184
+ fileName: "photo.jpg",
185
+ fileSize: 1000,
186
+ });
187
+
188
+ expect(mockSaveQueue).toHaveBeenCalled();
189
+ });
190
+ });
191
+
192
+ describe("progress events", () => {
193
+ it("fires progress callback on state changes", async () => {
194
+ await service.initialize({ client: mockClient as never });
195
+
196
+ const progressEvents: { phase: UploadPhase; progress: number }[] = [];
197
+ service.onProgress((progress) => {
198
+ progressEvents.push({ phase: progress.phase, progress: progress.progress });
199
+ });
200
+
201
+ await service.enqueue({
202
+ localUri: "file:///photo.jpg",
203
+ mimeType: "image/jpeg",
204
+ fileName: "photo.jpg",
205
+ fileSize: 1000,
206
+ });
207
+
208
+ // Should have emitted initial QUEUED progress
209
+ expect(progressEvents.length).toBeGreaterThan(0);
210
+ expect(progressEvents[0]).toMatchObject({
211
+ phase: UploadPhase.QUEUED,
212
+ progress: 0,
213
+ });
214
+ });
215
+
216
+ it("unsubscribe stops receiving events", async () => {
217
+ await service.initialize({ client: mockClient as never });
218
+
219
+ let callCount = 0;
220
+ const unsubscribe = service.onProgress(() => {
221
+ callCount++;
222
+ });
223
+
224
+ await service.enqueue({
225
+ localUri: "file:///photo1.jpg",
226
+ mimeType: "image/jpeg",
227
+ fileName: "photo1.jpg",
228
+ fileSize: 1000,
229
+ });
230
+
231
+ const countAfterFirst = callCount;
232
+
233
+ unsubscribe();
234
+
235
+ await service.enqueue({
236
+ localUri: "file:///photo2.jpg",
237
+ mimeType: "image/jpeg",
238
+ fileName: "photo2.jpg",
239
+ fileSize: 1000,
240
+ });
241
+
242
+ // Count should not increase after unsubscribe
243
+ expect(callCount).toBe(countAfterFirst);
244
+ });
245
+ });
246
+
247
+ describe("retry scheduling", () => {
248
+ it("resets attempt count and clears error on retry", async () => {
249
+ const failedItem: QueueItem = {
250
+ id: "queue_1",
251
+ attachmentId: "att_failed",
252
+ localUri: "file:///photo.jpg",
253
+ uploadUrl: "https://storage.example.com/upload",
254
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
255
+ mimeType: "image/jpeg",
256
+ fileName: "photo.jpg",
257
+ fileSize: 1000,
258
+ phase: UploadPhase.FAILED,
259
+ progress: 0,
260
+ attemptNumber: 2,
261
+ maxAttempts: 3,
262
+ createdAt: new Date().toISOString(),
263
+ lastError: "Previous error",
264
+ };
265
+ mockLoadQueue.mockResolvedValue([failedItem]);
266
+
267
+ await service.initialize({ client: mockClient as never });
268
+
269
+ // Verify item is in failed state
270
+ expect(service.getItemByAttachmentId("att_failed")?.phase).toBe(UploadPhase.FAILED);
271
+ expect(service.getItemByAttachmentId("att_failed")?.attemptNumber).toBe(2);
272
+
273
+ // Retry should reset state and trigger processing
274
+ await service.retryItem("att_failed");
275
+
276
+ // After retry, item should exist and lastError should be cleared
277
+ // Note: attemptNumber may be 0 or 1 depending on whether processing started
278
+ const item = service.getItemByAttachmentId("att_failed");
279
+ expect(item).toBeDefined();
280
+ expect(item?.lastError).toBeUndefined(); // Error cleared
281
+ // Phase should no longer be FAILED
282
+ expect(item?.phase).not.toBe(UploadPhase.FAILED);
283
+ });
284
+
285
+ it("throws when retrying non-failed item", async () => {
286
+ // Use COMPLETED item since QUEUED will start processing
287
+ const completedItem: QueueItem = {
288
+ id: "queue_1",
289
+ attachmentId: "att_completed",
290
+ localUri: "file:///photo.jpg",
291
+ uploadUrl: "https://storage.example.com/upload",
292
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
293
+ mimeType: "image/jpeg",
294
+ fileName: "photo.jpg",
295
+ fileSize: 1000,
296
+ phase: UploadPhase.COMPLETED,
297
+ progress: 1,
298
+ attemptNumber: 1,
299
+ maxAttempts: 3,
300
+ createdAt: new Date().toISOString(),
301
+ };
302
+ mockLoadQueue.mockResolvedValue([completedItem]);
303
+
304
+ await service.initialize({ client: mockClient as never });
305
+
306
+ await expect(service.retryItem("att_completed")).rejects.toThrow("not in failed state");
307
+ });
308
+ });
309
+
310
+ describe("failure handling", () => {
311
+ it("transitions to FAILED after max attempts exceeded", async () => {
312
+ // Create item that has already used all attempts
313
+ const maxedOutItem: QueueItem = {
314
+ id: "queue_1",
315
+ attachmentId: "att_maxed",
316
+ localUri: "file:///photo.jpg",
317
+ uploadUrl: "https://storage.example.com/upload",
318
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
319
+ mimeType: "image/jpeg",
320
+ fileName: "photo.jpg",
321
+ fileSize: 1000,
322
+ phase: UploadPhase.QUEUED,
323
+ progress: 0,
324
+ attemptNumber: 2, // Will be 3 after next attempt
325
+ maxAttempts: 3,
326
+ createdAt: new Date().toISOString(),
327
+ };
328
+ mockLoadQueue.mockResolvedValue([maxedOutItem]);
329
+
330
+ // Make upload fail
331
+ mockUploadTask.uploadAsync.mockResolvedValue({ status: 500 });
332
+
333
+ // Wait for FAILED phase progress event (deterministic signal)
334
+ const failedReceived = new Promise<void>((resolve) => {
335
+ service.onProgress((p) => {
336
+ if (p.phase === UploadPhase.FAILED) resolve();
337
+ });
338
+ });
339
+
340
+ await service.initialize({ client: mockClient as never });
341
+
342
+ // Wait for the failure to be processed
343
+ await failedReceived;
344
+
345
+ const item = service.getItemByAttachmentId("att_maxed");
346
+ expect(item?.phase).toBe(UploadPhase.FAILED);
347
+ expect(item?.attemptNumber).toBe(3);
348
+ });
349
+ });
350
+
351
+ describe("queue status", () => {
352
+ it("returns correct counts by phase", async () => {
353
+ // Use only terminal states (COMPLETED, FAILED) to avoid async processing
354
+ const items: QueueItem[] = [
355
+ {
356
+ id: "queue_1",
357
+ attachmentId: "att_1",
358
+ localUri: "file:///1.jpg",
359
+ uploadUrl: "https://storage.example.com/1",
360
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
361
+ mimeType: "image/jpeg",
362
+ fileName: "1.jpg",
363
+ fileSize: 1000,
364
+ phase: UploadPhase.COMPLETED,
365
+ progress: 1,
366
+ attemptNumber: 1,
367
+ maxAttempts: 3,
368
+ createdAt: new Date().toISOString(),
369
+ completedAt: new Date().toISOString(),
370
+ },
371
+ {
372
+ id: "queue_2",
373
+ attachmentId: "att_2",
374
+ localUri: "file:///2.jpg",
375
+ uploadUrl: "https://storage.example.com/2",
376
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
377
+ mimeType: "image/jpeg",
378
+ fileName: "2.jpg",
379
+ fileSize: 1000,
380
+ phase: UploadPhase.COMPLETED,
381
+ progress: 1,
382
+ attemptNumber: 1,
383
+ maxAttempts: 3,
384
+ createdAt: new Date().toISOString(),
385
+ completedAt: new Date().toISOString(),
386
+ },
387
+ {
388
+ id: "queue_3",
389
+ attachmentId: "att_3",
390
+ localUri: "file:///3.jpg",
391
+ uploadUrl: "https://storage.example.com/3",
392
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
393
+ mimeType: "image/jpeg",
394
+ fileName: "3.jpg",
395
+ fileSize: 1000,
396
+ phase: UploadPhase.FAILED,
397
+ progress: 0,
398
+ attemptNumber: 3,
399
+ maxAttempts: 3,
400
+ createdAt: new Date().toISOString(),
401
+ lastError: "Upload failed",
402
+ },
403
+ ];
404
+ mockLoadQueue.mockResolvedValue(items);
405
+
406
+ await service.initialize({ client: mockClient as never });
407
+
408
+ const status = service.getQueueStatus();
409
+ expect(status.total).toBe(3);
410
+ expect(status.completed).toBe(2);
411
+ expect(status.failed).toBe(1);
412
+ });
413
+ });
414
+
415
+ describe("cancelItem", () => {
416
+ it("removes item from queue", async () => {
417
+ const item: QueueItem = {
418
+ id: "queue_1",
419
+ attachmentId: "att_cancel",
420
+ localUri: "file:///photo.jpg",
421
+ uploadUrl: "https://storage.example.com/upload",
422
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
423
+ mimeType: "image/jpeg",
424
+ fileName: "photo.jpg",
425
+ fileSize: 1000,
426
+ phase: UploadPhase.QUEUED,
427
+ progress: 0,
428
+ attemptNumber: 0,
429
+ maxAttempts: 3,
430
+ createdAt: new Date().toISOString(),
431
+ };
432
+ mockLoadQueue.mockResolvedValue([item]);
433
+
434
+ await service.initialize({ client: mockClient as never });
435
+
436
+ expect(service.getQueueStatus().total).toBe(1);
437
+
438
+ await service.cancelItem("att_cancel");
439
+
440
+ expect(service.getQueueStatus().total).toBe(0);
441
+ expect(mockSaveQueue).toHaveBeenCalled();
442
+ });
443
+ });
444
+
445
+ describe("clearCompleted", () => {
446
+ it("removes only completed items", async () => {
447
+ const items: QueueItem[] = [
448
+ {
449
+ id: "queue_1",
450
+ attachmentId: "att_completed",
451
+ localUri: "file:///1.jpg",
452
+ uploadUrl: "https://storage.example.com/1",
453
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
454
+ mimeType: "image/jpeg",
455
+ fileName: "1.jpg",
456
+ fileSize: 1000,
457
+ phase: UploadPhase.COMPLETED,
458
+ progress: 1,
459
+ attemptNumber: 1,
460
+ maxAttempts: 3,
461
+ createdAt: new Date().toISOString(),
462
+ },
463
+ {
464
+ id: "queue_2",
465
+ attachmentId: "att_queued",
466
+ localUri: "file:///2.jpg",
467
+ uploadUrl: "https://storage.example.com/2",
468
+ uploadExpiresAt: new Date(Date.now() + 3600000).toISOString(),
469
+ mimeType: "image/jpeg",
470
+ fileName: "2.jpg",
471
+ fileSize: 1000,
472
+ phase: UploadPhase.QUEUED,
473
+ progress: 0,
474
+ attemptNumber: 0,
475
+ maxAttempts: 3,
476
+ createdAt: new Date().toISOString(),
477
+ },
478
+ ];
479
+ mockLoadQueue.mockResolvedValue(items);
480
+
481
+ await service.initialize({ client: mockClient as never });
482
+
483
+ await service.clearCompleted();
484
+
485
+ expect(service.getItemByAttachmentId("att_completed")).toBeUndefined();
486
+ expect(service.getItemByAttachmentId("att_queued")).toBeDefined();
487
+ });
488
+ });
489
+ });