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

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 (235) hide show
  1. package/README.md +67 -0
  2. package/app.plugin.cjs +135 -0
  3. package/app.plugin.js +1 -0
  4. package/dist/api/client.d.ts +67 -0
  5. package/dist/api/client.d.ts.map +1 -0
  6. package/dist/api/client.js +163 -0
  7. package/dist/api/client.js.map +1 -0
  8. package/dist/api/errors.d.ts +46 -0
  9. package/dist/api/errors.d.ts.map +1 -0
  10. package/dist/api/errors.js +72 -0
  11. package/dist/api/errors.js.map +1 -0
  12. package/dist/api/index.d.ts +7 -0
  13. package/dist/api/index.d.ts.map +1 -0
  14. package/dist/api/index.js +20 -0
  15. package/dist/api/index.js.map +1 -0
  16. package/dist/api/retry.d.ts +29 -0
  17. package/dist/api/retry.d.ts.map +1 -0
  18. package/dist/api/retry.js +74 -0
  19. package/dist/api/retry.js.map +1 -0
  20. package/dist/attachments/FeedbackSheet.d.ts +88 -0
  21. package/dist/attachments/FeedbackSheet.d.ts.map +1 -0
  22. package/dist/attachments/FeedbackSheet.js +250 -0
  23. package/dist/attachments/FeedbackSheet.js.map +1 -0
  24. package/dist/attachments/index.d.ts +20 -0
  25. package/dist/attachments/index.d.ts.map +1 -0
  26. package/dist/attachments/index.js +40 -0
  27. package/dist/attachments/index.js.map +1 -0
  28. package/dist/components/AttachmentGrid.d.ts +94 -0
  29. package/dist/components/AttachmentGrid.d.ts.map +1 -0
  30. package/dist/components/AttachmentGrid.js +132 -0
  31. package/dist/components/AttachmentGrid.js.map +1 -0
  32. package/dist/components/AttachmentPicker.d.ts +98 -0
  33. package/dist/components/AttachmentPicker.d.ts.map +1 -0
  34. package/dist/components/AttachmentPicker.js +297 -0
  35. package/dist/components/AttachmentPicker.js.map +1 -0
  36. package/dist/components/AttachmentPreview.d.ts +78 -0
  37. package/dist/components/AttachmentPreview.d.ts.map +1 -0
  38. package/dist/components/AttachmentPreview.js +133 -0
  39. package/dist/components/AttachmentPreview.js.map +1 -0
  40. package/dist/components/CategorySelector.d.ts +77 -0
  41. package/dist/components/CategorySelector.d.ts.map +1 -0
  42. package/dist/components/CategorySelector.js +117 -0
  43. package/dist/components/CategorySelector.js.map +1 -0
  44. package/dist/components/FeedbackForm.d.ts +50 -0
  45. package/dist/components/FeedbackForm.d.ts.map +1 -0
  46. package/dist/components/FeedbackForm.js +141 -0
  47. package/dist/components/FeedbackForm.js.map +1 -0
  48. package/dist/components/FeedbackSheet.d.ts +75 -0
  49. package/dist/components/FeedbackSheet.d.ts.map +1 -0
  50. package/dist/components/FeedbackSheet.js +215 -0
  51. package/dist/components/FeedbackSheet.js.map +1 -0
  52. package/dist/components/ThemedButton.d.ts +23 -0
  53. package/dist/components/ThemedButton.d.ts.map +1 -0
  54. package/dist/components/ThemedButton.js +77 -0
  55. package/dist/components/ThemedButton.js.map +1 -0
  56. package/dist/components/ThemedText.d.ts +16 -0
  57. package/dist/components/ThemedText.d.ts.map +1 -0
  58. package/dist/components/ThemedText.js +44 -0
  59. package/dist/components/ThemedText.js.map +1 -0
  60. package/dist/components/ThemedTextInput.d.ts +13 -0
  61. package/dist/components/ThemedTextInput.d.ts.map +1 -0
  62. package/dist/components/ThemedTextInput.js +76 -0
  63. package/dist/components/ThemedTextInput.js.map +1 -0
  64. package/dist/components/UploadStatusOverlay.d.ts +82 -0
  65. package/dist/components/UploadStatusOverlay.d.ts.map +1 -0
  66. package/dist/components/UploadStatusOverlay.js +319 -0
  67. package/dist/components/UploadStatusOverlay.js.map +1 -0
  68. package/dist/components/index.d.ts +19 -0
  69. package/dist/components/index.d.ts.map +1 -0
  70. package/dist/components/index.js +28 -0
  71. package/dist/components/index.js.map +1 -0
  72. package/dist/context/HarkenContext.d.ts +62 -0
  73. package/dist/context/HarkenContext.d.ts.map +1 -0
  74. package/dist/context/HarkenContext.js +128 -0
  75. package/dist/context/HarkenContext.js.map +1 -0
  76. package/dist/context/index.d.ts +3 -0
  77. package/dist/context/index.d.ts.map +1 -0
  78. package/dist/context/index.js +7 -0
  79. package/dist/context/index.js.map +1 -0
  80. package/dist/domain/index.d.ts +3 -0
  81. package/dist/domain/index.d.ts.map +1 -0
  82. package/dist/domain/index.js +7 -0
  83. package/dist/domain/index.js.map +1 -0
  84. package/dist/domain/upload-queue.d.ts +116 -0
  85. package/dist/domain/upload-queue.d.ts.map +1 -0
  86. package/dist/domain/upload-queue.js +34 -0
  87. package/dist/domain/upload-queue.js.map +1 -0
  88. package/dist/hooks/index.d.ts +6 -0
  89. package/dist/hooks/index.d.ts.map +1 -0
  90. package/dist/hooks/index.js +16 -0
  91. package/dist/hooks/index.js.map +1 -0
  92. package/dist/hooks/useAnonymousId.d.ts +28 -0
  93. package/dist/hooks/useAnonymousId.d.ts.map +1 -0
  94. package/dist/hooks/useAnonymousId.js +59 -0
  95. package/dist/hooks/useAnonymousId.js.map +1 -0
  96. package/dist/hooks/useAttachmentPicker.d.ts +84 -0
  97. package/dist/hooks/useAttachmentPicker.d.ts.map +1 -0
  98. package/dist/hooks/useAttachmentPicker.js +181 -0
  99. package/dist/hooks/useAttachmentPicker.js.map +1 -0
  100. package/dist/hooks/useAttachmentStatus.d.ts +51 -0
  101. package/dist/hooks/useAttachmentStatus.d.ts.map +1 -0
  102. package/dist/hooks/useAttachmentStatus.js +69 -0
  103. package/dist/hooks/useAttachmentStatus.js.map +1 -0
  104. package/dist/hooks/useAttachmentUpload.d.ts +101 -0
  105. package/dist/hooks/useAttachmentUpload.d.ts.map +1 -0
  106. package/dist/hooks/useAttachmentUpload.js +293 -0
  107. package/dist/hooks/useAttachmentUpload.js.map +1 -0
  108. package/dist/hooks/useFeedback.d.ts +55 -0
  109. package/dist/hooks/useFeedback.d.ts.map +1 -0
  110. package/dist/hooks/useFeedback.js +96 -0
  111. package/dist/hooks/useFeedback.js.map +1 -0
  112. package/dist/hooks/useHarkenContext.d.ts +25 -0
  113. package/dist/hooks/useHarkenContext.d.ts.map +1 -0
  114. package/dist/hooks/useHarkenContext.js +35 -0
  115. package/dist/hooks/useHarkenContext.js.map +1 -0
  116. package/dist/hooks/useHarkenTheme.d.ts +26 -0
  117. package/dist/hooks/useHarkenTheme.d.ts.map +1 -0
  118. package/dist/hooks/useHarkenTheme.js +36 -0
  119. package/dist/hooks/useHarkenTheme.js.map +1 -0
  120. package/dist/index.d.ts +49 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +91 -0
  123. package/dist/index.js.map +1 -0
  124. package/dist/services/index.d.ts +4 -0
  125. package/dist/services/index.d.ts.map +1 -0
  126. package/dist/services/index.js +9 -0
  127. package/dist/services/index.js.map +1 -0
  128. package/dist/services/uploadQueueService.d.ts +193 -0
  129. package/dist/services/uploadQueueService.d.ts.map +1 -0
  130. package/dist/services/uploadQueueService.js +623 -0
  131. package/dist/services/uploadQueueService.js.map +1 -0
  132. package/dist/services/uploadQueueStorage.d.ts +30 -0
  133. package/dist/services/uploadQueueStorage.d.ts.map +1 -0
  134. package/dist/services/uploadQueueStorage.js +77 -0
  135. package/dist/services/uploadQueueStorage.js.map +1 -0
  136. package/dist/storage/IdentityStore.d.ts +38 -0
  137. package/dist/storage/IdentityStore.d.ts.map +1 -0
  138. package/dist/storage/IdentityStore.js +83 -0
  139. package/dist/storage/IdentityStore.js.map +1 -0
  140. package/dist/storage/SecureStoreAdapter.d.ts +28 -0
  141. package/dist/storage/SecureStoreAdapter.d.ts.map +1 -0
  142. package/dist/storage/SecureStoreAdapter.js +52 -0
  143. package/dist/storage/SecureStoreAdapter.js.map +1 -0
  144. package/dist/storage/defaultStorage.d.ts +20 -0
  145. package/dist/storage/defaultStorage.d.ts.map +1 -0
  146. package/dist/storage/defaultStorage.js +131 -0
  147. package/dist/storage/defaultStorage.js.map +1 -0
  148. package/dist/storage/index.d.ts +6 -0
  149. package/dist/storage/index.d.ts.map +1 -0
  150. package/dist/storage/index.js +13 -0
  151. package/dist/storage/index.js.map +1 -0
  152. package/dist/storage/types.d.ts +32 -0
  153. package/dist/storage/types.d.ts.map +1 -0
  154. package/dist/storage/types.js +11 -0
  155. package/dist/storage/types.js.map +1 -0
  156. package/dist/theme/defaults.d.ts +43 -0
  157. package/dist/theme/defaults.d.ts.map +1 -0
  158. package/dist/theme/defaults.js +128 -0
  159. package/dist/theme/defaults.js.map +1 -0
  160. package/dist/theme/index.d.ts +3 -0
  161. package/dist/theme/index.d.ts.map +1 -0
  162. package/dist/theme/index.js +14 -0
  163. package/dist/theme/index.js.map +1 -0
  164. package/dist/theme/types.d.ts +136 -0
  165. package/dist/theme/types.d.ts.map +1 -0
  166. package/dist/theme/types.js +3 -0
  167. package/dist/theme/types.js.map +1 -0
  168. package/dist/types/config.d.ts +100 -0
  169. package/dist/types/config.d.ts.map +1 -0
  170. package/dist/types/config.js +3 -0
  171. package/dist/types/config.js.map +1 -0
  172. package/dist/types/index.d.ts +3 -0
  173. package/dist/types/index.d.ts.map +1 -0
  174. package/dist/types/index.js +3 -0
  175. package/dist/types/index.js.map +1 -0
  176. package/dist/types/openapi.d.ts +601 -0
  177. package/dist/types/openapi.d.ts.map +1 -0
  178. package/dist/types/openapi.js +7 -0
  179. package/dist/types/openapi.js.map +1 -0
  180. package/dist/utils/index.d.ts +2 -0
  181. package/dist/utils/index.d.ts.map +1 -0
  182. package/dist/utils/index.js +6 -0
  183. package/dist/utils/index.js.map +1 -0
  184. package/dist/utils/uuid.d.ts +10 -0
  185. package/dist/utils/uuid.d.ts.map +1 -0
  186. package/dist/utils/uuid.js +60 -0
  187. package/dist/utils/uuid.js.map +1 -0
  188. package/package.json +124 -0
  189. package/src/@types/expo-file-system-legacy.d.ts +13 -0
  190. package/src/api/client.ts +250 -0
  191. package/src/api/errors.ts +84 -0
  192. package/src/api/index.ts +15 -0
  193. package/src/api/retry.ts +99 -0
  194. package/src/attachments/FeedbackSheet.tsx +400 -0
  195. package/src/attachments/index.ts +70 -0
  196. package/src/components/AttachmentGrid.tsx +247 -0
  197. package/src/components/AttachmentPicker.tsx +391 -0
  198. package/src/components/AttachmentPreview.tsx +210 -0
  199. package/src/components/CategorySelector.tsx +174 -0
  200. package/src/components/FeedbackForm.tsx +216 -0
  201. package/src/components/FeedbackSheet.tsx +321 -0
  202. package/src/components/ThemedButton.tsx +127 -0
  203. package/src/components/ThemedText.tsx +65 -0
  204. package/src/components/ThemedTextInput.tsx +65 -0
  205. package/src/components/UploadStatusOverlay.tsx +440 -0
  206. package/src/components/index.ts +39 -0
  207. package/src/context/HarkenContext.tsx +129 -0
  208. package/src/context/index.ts +2 -0
  209. package/src/domain/index.ts +12 -0
  210. package/src/domain/upload-queue.ts +131 -0
  211. package/src/hooks/index.ts +10 -0
  212. package/src/hooks/useAnonymousId.ts +68 -0
  213. package/src/hooks/useAttachmentPicker.ts +243 -0
  214. package/src/hooks/useAttachmentStatus.ts +86 -0
  215. package/src/hooks/useAttachmentUpload.ts +370 -0
  216. package/src/hooks/useFeedback.ts +139 -0
  217. package/src/hooks/useHarkenContext.ts +35 -0
  218. package/src/hooks/useHarkenTheme.ts +36 -0
  219. package/src/index.ts +168 -0
  220. package/src/services/index.ts +11 -0
  221. package/src/services/uploadQueueService.ts +727 -0
  222. package/src/services/uploadQueueStorage.ts +78 -0
  223. package/src/storage/IdentityStore.ts +89 -0
  224. package/src/storage/SecureStoreAdapter.ts +59 -0
  225. package/src/storage/defaultStorage.ts +109 -0
  226. package/src/storage/index.ts +5 -0
  227. package/src/storage/types.ts +34 -0
  228. package/src/theme/defaults.ts +151 -0
  229. package/src/theme/index.ts +23 -0
  230. package/src/theme/types.ts +157 -0
  231. package/src/types/config.ts +112 -0
  232. package/src/types/index.ts +10 -0
  233. package/src/types/openapi.ts +601 -0
  234. package/src/utils/index.ts +1 -0
  235. package/src/utils/uuid.ts +77 -0
@@ -0,0 +1,400 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ KeyboardAvoidingView,
5
+ Platform,
6
+ ScrollView,
7
+ Alert,
8
+ } from 'react-native';
9
+ import type { ViewStyle } from 'react-native';
10
+ import type { components } from '../types/index.js';
11
+ import { useHarkenTheme, useFeedback } from '../hooks';
12
+ import { ThemedText } from '../components/ThemedText';
13
+ import { ThemedTextInput } from '../components/ThemedTextInput';
14
+ import { ThemedButton } from '../components/ThemedButton';
15
+ import { CategorySelector, DEFAULT_CATEGORIES } from '../components/CategorySelector';
16
+ import type { CategoryOption } from '../components/CategorySelector';
17
+ import type { FeedbackCategory } from '../types';
18
+ import { useAttachmentPicker } from '../hooks/useAttachmentPicker';
19
+ import type { AttachmentSourceConfig } from '../hooks/useAttachmentPicker';
20
+ import { AttachmentGrid } from '../components/AttachmentGrid';
21
+ import { AttachmentPicker } from '../components/AttachmentPicker';
22
+
23
+ type FeedbackSubmissionResponse = components['schemas']['FeedbackSubmissionResponse'];
24
+
25
+ export interface FeedbackSheetProps {
26
+ /** Called when feedback is successfully submitted */
27
+ onSuccess?: (result: FeedbackSubmissionResponse) => void;
28
+ /** Called when submission fails */
29
+ onError?: (error: Error) => void;
30
+ /** Called when user cancels/dismisses the form */
31
+ onCancel?: () => void;
32
+
33
+ /** Title text */
34
+ title?: string;
35
+ /** Placeholder text for message input */
36
+ placeholder?: string;
37
+ /** Submit button text */
38
+ submitLabel?: string;
39
+ /** Cancel button text */
40
+ cancelLabel?: string;
41
+
42
+ /** Custom categories */
43
+ categories?: CategoryOption[];
44
+ /** Whether category selection is required */
45
+ requireCategory?: boolean;
46
+
47
+ /** Minimum message length */
48
+ minMessageLength?: number;
49
+ /** Maximum message length */
50
+ maxMessageLength?: number;
51
+
52
+ /** Whether to enable attachment support. @default true */
53
+ enableAttachments?: boolean;
54
+ /** Maximum number of attachments. @default 5 */
55
+ maxAttachments?: number;
56
+ /**
57
+ * Configure which attachment sources are available.
58
+ * If only one source is enabled, the picker modal is skipped.
59
+ * @default { camera: true, library: true, files: true }
60
+ */
61
+ attachmentSources?: AttachmentSourceConfig;
62
+
63
+ /** Message shown in success alert. Set to null to disable alert. */
64
+ successMessage?: string | null;
65
+ /** Whether to show success alert. @default true */
66
+ showSuccessAlert?: boolean;
67
+ /** Whether to clear form on success. @default true */
68
+ clearOnSuccess?: boolean;
69
+
70
+ /** Container style override */
71
+ containerStyle?: ViewStyle;
72
+ /** Form content style override */
73
+ formStyle?: ViewStyle;
74
+ }
75
+
76
+ /**
77
+ * A batteries-included feedback form component with full attachment support.
78
+ *
79
+ * This version is exported from '@harkenapp/sdk-react-native/attachments' and
80
+ * includes support for picking images, documents, and uploading them.
81
+ *
82
+ * For the version without attachment dependencies, import from the main entry point.
83
+ *
84
+ * @example
85
+ * ```tsx
86
+ * import { FeedbackSheet } from '@harkenapp/sdk-react-native/attachments';
87
+ *
88
+ * // Minimal usage with attachments
89
+ * <FeedbackSheet onSuccess={() => navigation.goBack()} />
90
+ *
91
+ * // With customization
92
+ * <FeedbackSheet
93
+ * title="Report a Bug"
94
+ * requireCategory
95
+ * enableAttachments
96
+ * maxAttachments={3}
97
+ * onSuccess={(result) => {
98
+ * analytics.track('feedback_submitted');
99
+ * navigation.goBack();
100
+ * }}
101
+ * onCancel={() => navigation.goBack()}
102
+ * />
103
+ *
104
+ * // With restricted attachment sources (photo library only)
105
+ * <FeedbackSheet
106
+ * attachmentSources={{ camera: false, library: true, files: false }}
107
+ * onSuccess={() => navigation.goBack()}
108
+ * />
109
+ * ```
110
+ */
111
+ export function FeedbackSheet({
112
+ onSuccess,
113
+ onError,
114
+ onCancel,
115
+ title = 'Send Feedback',
116
+ placeholder = 'What would you like to share?',
117
+ submitLabel = 'Submit',
118
+ cancelLabel = 'Cancel',
119
+ categories = DEFAULT_CATEGORIES,
120
+ requireCategory = false,
121
+ minMessageLength = 1,
122
+ maxMessageLength = 5000,
123
+ enableAttachments = true,
124
+ maxAttachments = 5,
125
+ attachmentSources,
126
+ successMessage = 'Thank you for your feedback!',
127
+ showSuccessAlert = true,
128
+ clearOnSuccess = true,
129
+ containerStyle,
130
+ formStyle,
131
+ }: FeedbackSheetProps): React.JSX.Element {
132
+ const theme = useHarkenTheme();
133
+ const { submitFeedback, isSubmitting, error, clearError, isInitializing } =
134
+ useFeedback();
135
+ const {
136
+ attachments,
137
+ removeAttachment,
138
+ retryAttachment,
139
+ getAttachmentIds,
140
+ hasActiveUploads,
141
+ openPicker,
142
+ pickerProps,
143
+ enabledSourceCount,
144
+ } = useAttachmentPicker(attachmentSources);
145
+
146
+ const [message, setMessage] = useState('');
147
+ const [category, setCategory] = useState<FeedbackCategory | null>(null);
148
+
149
+ const trimmedMessage = message.trim();
150
+ const isMessageValid =
151
+ trimmedMessage.length >= minMessageLength &&
152
+ trimmedMessage.length <= maxMessageLength;
153
+ const isCategoryValid = !requireCategory || category !== null;
154
+ const canSubmit = isMessageValid && isCategoryValid && !isSubmitting && !isInitializing;
155
+
156
+ const resetForm = useCallback(() => {
157
+ setMessage('');
158
+ setCategory(null);
159
+ clearError();
160
+ // Note: We don't clear attachments since they may still be uploading
161
+ // and can be reused. Users can manually remove them if needed.
162
+ }, [clearError]);
163
+
164
+ const handleSubmit = useCallback(async () => {
165
+ if (!canSubmit) return;
166
+
167
+ clearError();
168
+
169
+ try {
170
+ const result = await submitFeedback({
171
+ message: trimmedMessage,
172
+ category: category ?? 'other',
173
+ attachments: enableAttachments ? getAttachmentIds() : undefined,
174
+ });
175
+
176
+ const uploadNote = enableAttachments && hasActiveUploads
177
+ ? '\n\nAttachments are still uploading in the background.'
178
+ : '';
179
+
180
+ if (showSuccessAlert && successMessage) {
181
+ Alert.alert('Success', `${successMessage}${uploadNote}`, [
182
+ {
183
+ text: 'OK',
184
+ onPress: () => {
185
+ if (clearOnSuccess) {
186
+ resetForm();
187
+ }
188
+ onSuccess?.(result);
189
+ },
190
+ },
191
+ ]);
192
+ } else {
193
+ if (clearOnSuccess) {
194
+ resetForm();
195
+ }
196
+ onSuccess?.(result);
197
+ }
198
+ } catch (e) {
199
+ const errorMessage =
200
+ e instanceof Error ? e.message : 'Failed to submit feedback. Please try again.';
201
+ Alert.alert('Submission Failed', errorMessage);
202
+ onError?.(e instanceof Error ? e : new Error(errorMessage));
203
+ }
204
+ }, [
205
+ canSubmit,
206
+ clearError,
207
+ submitFeedback,
208
+ trimmedMessage,
209
+ category,
210
+ enableAttachments,
211
+ getAttachmentIds,
212
+ hasActiveUploads,
213
+ showSuccessAlert,
214
+ successMessage,
215
+ clearOnSuccess,
216
+ resetForm,
217
+ onSuccess,
218
+ onError,
219
+ ]);
220
+
221
+ const handleCancel = useCallback(() => {
222
+ resetForm();
223
+ onCancel?.();
224
+ }, [resetForm, onCancel]);
225
+
226
+ const baseContainerStyle: ViewStyle = {
227
+ flex: 1,
228
+ backgroundColor: theme.colors.background,
229
+ };
230
+
231
+ const contentStyle: ViewStyle = {
232
+ flexGrow: 1,
233
+ padding: theme.spacing.lg,
234
+ };
235
+
236
+ const sectionStyle: ViewStyle = {
237
+ marginBottom: theme.spacing.lg,
238
+ };
239
+
240
+ const buttonRowStyle: ViewStyle = {
241
+ flexDirection: 'row',
242
+ gap: theme.spacing.sm,
243
+ marginTop: theme.spacing.md,
244
+ };
245
+
246
+ const characterCount = trimmedMessage.length;
247
+ const showCharacterWarning = characterCount > maxMessageLength * 0.9;
248
+
249
+ if (isInitializing) {
250
+ return (
251
+ <View style={[baseContainerStyle, containerStyle, { justifyContent: 'center', alignItems: 'center' }]}>
252
+ <ThemedText variant="body" secondary>
253
+ Initializing...
254
+ </ThemedText>
255
+ </View>
256
+ );
257
+ }
258
+
259
+ return (
260
+ <>
261
+ <KeyboardAvoidingView
262
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
263
+ style={[baseContainerStyle, containerStyle]}
264
+ >
265
+ <ScrollView
266
+ contentContainerStyle={[contentStyle, formStyle]}
267
+ keyboardShouldPersistTaps="handled"
268
+ >
269
+ {/* Title */}
270
+ <View style={sectionStyle}>
271
+ <ThemedText variant="title">{title}</ThemedText>
272
+ </View>
273
+
274
+ {/* Category selector */}
275
+ <View style={sectionStyle}>
276
+ <ThemedText
277
+ variant="label"
278
+ secondary
279
+ style={{ marginBottom: theme.spacing.sm }}
280
+ >
281
+ Category{requireCategory ? '' : ' (optional)'}
282
+ </ThemedText>
283
+ <CategorySelector
284
+ value={category}
285
+ onChange={setCategory}
286
+ categories={categories}
287
+ disabled={isSubmitting}
288
+ />
289
+ </View>
290
+
291
+ {/* Message input */}
292
+ <View style={sectionStyle}>
293
+ <ThemedText
294
+ variant="label"
295
+ secondary
296
+ style={{ marginBottom: theme.spacing.sm }}
297
+ >
298
+ Message
299
+ </ThemedText>
300
+ <ThemedTextInput
301
+ value={message}
302
+ onChangeText={setMessage}
303
+ placeholder={placeholder}
304
+ multiline
305
+ numberOfLines={4}
306
+ textAlignVertical="top"
307
+ editable={!isSubmitting}
308
+ style={{ minHeight: 120 }}
309
+ maxLength={maxMessageLength + 100}
310
+ />
311
+ {showCharacterWarning && (
312
+ <ThemedText
313
+ variant="caption"
314
+ color={
315
+ characterCount > maxMessageLength
316
+ ? theme.colors.error
317
+ : theme.colors.textSecondary
318
+ }
319
+ style={{ marginTop: theme.spacing.xs, textAlign: 'right' }}
320
+ >
321
+ {characterCount}/{maxMessageLength}
322
+ </ThemedText>
323
+ )}
324
+ </View>
325
+
326
+ {/* Attachments */}
327
+ {enableAttachments && (
328
+ <View style={sectionStyle}>
329
+ <ThemedText
330
+ variant="label"
331
+ secondary
332
+ style={{ marginBottom: theme.spacing.sm }}
333
+ >
334
+ Attachments
335
+ </ThemedText>
336
+ <AttachmentGrid
337
+ attachments={attachments}
338
+ onRemove={removeAttachment}
339
+ onRetry={retryAttachment}
340
+ onAdd={openPicker}
341
+ maxAttachments={maxAttachments}
342
+ showAddButton={enabledSourceCount > 0}
343
+ />
344
+ </View>
345
+ )}
346
+
347
+ {/* Error display */}
348
+ {error && (
349
+ <View style={{ marginBottom: theme.spacing.md }}>
350
+ <ThemedText variant="caption" color={theme.colors.error}>
351
+ {error.message}
352
+ </ThemedText>
353
+ </View>
354
+ )}
355
+
356
+ {/* Buttons */}
357
+ <View style={buttonRowStyle}>
358
+ {onCancel && (
359
+ <View style={{ flex: 1 }}>
360
+ <ThemedButton
361
+ title={cancelLabel}
362
+ variant="secondary"
363
+ onPress={handleCancel}
364
+ disabled={isSubmitting}
365
+ fullWidth
366
+ />
367
+ </View>
368
+ )}
369
+ <View style={{ flex: onCancel ? 1 : undefined }}>
370
+ <ThemedButton
371
+ title={submitLabel}
372
+ variant="primary"
373
+ onPress={handleSubmit}
374
+ disabled={!canSubmit}
375
+ loading={isSubmitting}
376
+ fullWidth={!!onCancel}
377
+ />
378
+ </View>
379
+ </View>
380
+
381
+ {/* Upload status indicator */}
382
+ {enableAttachments && hasActiveUploads && (
383
+ <View style={{ marginTop: theme.spacing.sm }}>
384
+ <ThemedText
385
+ variant="caption"
386
+ color={theme.colors.primary}
387
+ style={{ textAlign: 'center' }}
388
+ >
389
+ Uploads in progress - you can still submit now
390
+ </ThemedText>
391
+ </View>
392
+ )}
393
+ </ScrollView>
394
+ </KeyboardAvoidingView>
395
+
396
+ {/* Attachment picker modal */}
397
+ {enableAttachments && <AttachmentPicker {...pickerProps} />}
398
+ </>
399
+ );
400
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Harken SDK - Attachment Features
3
+ *
4
+ * @deprecated Import from '@harkenapp/sdk-react-native' instead.
5
+ * This entry point is maintained for backwards compatibility.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // Preferred (single entry point)
10
+ * import { HarkenProvider, FeedbackSheet, useAttachmentUpload } from '@harkenapp/sdk-react-native';
11
+ *
12
+ * // Legacy (still works)
13
+ * import { FeedbackSheet } from '@harkenapp/sdk-react-native/attachments';
14
+ * ```
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+
19
+ // Re-export everything from the main entry point for backwards compatibility
20
+ export {
21
+ // Attachment hooks
22
+ useAttachmentUpload,
23
+ useAttachmentPicker,
24
+ useAttachmentStatus,
25
+
26
+ // Attachment components
27
+ AttachmentPicker,
28
+ UploadStatusOverlay,
29
+ AttachmentPreview,
30
+ AttachmentGrid,
31
+ FeedbackSheet,
32
+
33
+ // Services
34
+ UploadQueueService,
35
+ uploadQueueService,
36
+ UploadQueueStorage,
37
+
38
+ // Domain types
39
+ UploadPhase,
40
+ DEFAULT_UPLOAD_RETRY_CONFIG,
41
+ } from '../index';
42
+
43
+ export type {
44
+ // Attachment hook types
45
+ AttachmentState,
46
+ UseAttachmentUploadResult,
47
+ AttachmentSourceConfig,
48
+ UseAttachmentPickerResult,
49
+ AttachmentStatus,
50
+
51
+ // Attachment component types
52
+ AttachmentPickerProps,
53
+ AttachmentSource,
54
+ PickerOptionConfig,
55
+ UploadStatusOverlayProps,
56
+ UploadStatusLabels,
57
+ AttachmentPreviewProps,
58
+ AttachmentGridProps,
59
+ FeedbackSheetProps,
60
+
61
+ // Service types
62
+ UploadQueueServiceConfig,
63
+ EnqueueParams,
64
+
65
+ // Domain types
66
+ QueueItem,
67
+ QueueStatus,
68
+ UploadProgress,
69
+ UploadRetryConfig,
70
+ } from '../index';
@@ -0,0 +1,247 @@
1
+ import React from 'react';
2
+ import { View, Pressable, StyleSheet } from 'react-native';
3
+ import type { ViewStyle, StyleProp, ImageStyle } from 'react-native';
4
+ import { useHarkenTheme } from '../hooks';
5
+ import { ThemedText } from './ThemedText';
6
+ import { AttachmentPreview } from './AttachmentPreview';
7
+ import type { AttachmentState } from '../hooks/useAttachmentUpload';
8
+ import type { UploadStatusLabels } from './UploadStatusOverlay';
9
+
10
+ export interface AttachmentGridProps {
11
+ /** List of attachments to display */
12
+ attachments: AttachmentState[];
13
+ /** Callback to retry a failed attachment */
14
+ onRetry?: (attachmentId: string) => void;
15
+ /** Callback to remove an attachment */
16
+ onRemove?: (attachmentId: string) => void;
17
+ /** Callback when add button is pressed */
18
+ onAdd?: () => void;
19
+ /** Maximum number of attachments allowed (hides add button when reached) */
20
+ maxAttachments?: number;
21
+ /** Size of each preview tile */
22
+ tileSize?: number;
23
+ /** Gap between tiles */
24
+ gap?: number;
25
+ /** Whether to show the add button */
26
+ showAddButton?: boolean;
27
+ /** Disable interactions */
28
+ disabled?: boolean;
29
+ /** Additional container style */
30
+ style?: StyleProp<ViewStyle>;
31
+ /** Label for the add button (default: "Add") */
32
+ addButtonLabel?: string;
33
+ /** Icon for the add button (default: "+") */
34
+ addButtonIcon?: React.ReactNode | string;
35
+ /** Style for the add button */
36
+ addButtonStyle?: StyleProp<ViewStyle>;
37
+ /** Text to show when empty and no add button (default: "No attachments") */
38
+ emptyText?: string;
39
+ /** Custom renderer for the add button */
40
+ renderAddButton?: (onPress: () => void, disabled: boolean) => React.ReactNode;
41
+ /** Custom renderer for each attachment tile */
42
+ renderTile?: (
43
+ attachment: AttachmentState,
44
+ onRetry?: () => void,
45
+ onRemove?: () => void
46
+ ) => React.ReactNode;
47
+ /** Style for each tile container */
48
+ tileStyle?: StyleProp<ViewStyle>;
49
+ /** Style for tile images */
50
+ tileImageStyle?: StyleProp<ImageStyle>;
51
+ /** Custom labels for upload status overlay */
52
+ statusLabels?: UploadStatusLabels;
53
+ /** Custom file icon function (passed to AttachmentPreview) */
54
+ getFileIcon?: (mimeType: string) => React.ReactNode | string;
55
+ /** Custom placeholder renderer (passed to AttachmentPreview) */
56
+ renderPlaceholder?: (mimeType: string, fileName?: string) => React.ReactNode;
57
+ }
58
+
59
+ /**
60
+ * Grid component for displaying multiple attachments.
61
+ *
62
+ * Shows attachment previews with upload status and an optional add button.
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * // Basic usage
67
+ * const { attachments, pickImage, retryAttachment, removeAttachment } = useAttachmentUpload();
68
+ *
69
+ * <AttachmentGrid
70
+ * attachments={attachments}
71
+ * onRetry={retryAttachment}
72
+ * onRemove={removeAttachment}
73
+ * onAdd={() => pickImage('library')}
74
+ * maxAttachments={5}
75
+ * />
76
+ *
77
+ * // With custom add button
78
+ * <AttachmentGrid
79
+ * attachments={attachments}
80
+ * onAdd={showPicker}
81
+ * addButtonIcon={<PlusIcon />}
82
+ * addButtonLabel="Upload"
83
+ * addButtonStyle={{ borderColor: 'blue' }}
84
+ * />
85
+ *
86
+ * // With fully custom renderers
87
+ * <AttachmentGrid
88
+ * attachments={attachments}
89
+ * renderAddButton={(onPress, disabled) => (
90
+ * <MyButton onPress={onPress} disabled={disabled}>Add File</MyButton>
91
+ * )}
92
+ * renderTile={(attachment, onRetry, onRemove) => (
93
+ * <MyTile
94
+ * key={attachment.attachmentId}
95
+ * {...attachment}
96
+ * onRetry={onRetry}
97
+ * onRemove={onRemove}
98
+ * />
99
+ * )}
100
+ * />
101
+ * ```
102
+ */
103
+ export function AttachmentGrid({
104
+ attachments,
105
+ onRetry,
106
+ onRemove,
107
+ onAdd,
108
+ maxAttachments = 10,
109
+ tileSize = 80,
110
+ gap,
111
+ showAddButton = true,
112
+ disabled = false,
113
+ style,
114
+ addButtonLabel = 'Add',
115
+ addButtonIcon = '+',
116
+ addButtonStyle,
117
+ emptyText = 'No attachments',
118
+ renderAddButton,
119
+ renderTile,
120
+ tileStyle,
121
+ tileImageStyle,
122
+ statusLabels,
123
+ getFileIcon,
124
+ renderPlaceholder,
125
+ }: AttachmentGridProps): React.JSX.Element {
126
+ const theme = useHarkenTheme();
127
+ const effectiveGap = gap ?? theme.spacing.sm;
128
+
129
+ const canAddMore = attachments.length < maxAttachments;
130
+ const shouldShowAddButton = showAddButton && canAddMore && onAdd;
131
+
132
+ return (
133
+ <View
134
+ style={[
135
+ styles.container,
136
+ { gap: effectiveGap },
137
+ style,
138
+ ]}
139
+ >
140
+ {attachments.map((attachment) => {
141
+ const handleRetry = onRetry ? () => onRetry(attachment.attachmentId) : undefined;
142
+ const handleRemove = onRemove ? () => onRemove(attachment.attachmentId) : undefined;
143
+
144
+ // Use custom renderer if provided
145
+ if (renderTile) {
146
+ return (
147
+ <React.Fragment key={attachment.attachmentId}>
148
+ {renderTile(attachment, handleRetry, handleRemove)}
149
+ </React.Fragment>
150
+ );
151
+ }
152
+
153
+ return (
154
+ <AttachmentPreview
155
+ key={attachment.attachmentId}
156
+ uri={attachment.localUri}
157
+ mimeType={attachment.mimeType}
158
+ fileName={attachment.fileName}
159
+ phase={attachment.phase}
160
+ progress={attachment.progress}
161
+ error={attachment.error}
162
+ onRetry={handleRetry}
163
+ onRemove={handleRemove}
164
+ size={tileSize}
165
+ style={tileStyle}
166
+ imageStyle={tileImageStyle}
167
+ statusLabels={statusLabels}
168
+ getFileIcon={getFileIcon}
169
+ renderPlaceholder={renderPlaceholder}
170
+ />
171
+ );
172
+ })}
173
+
174
+ {shouldShowAddButton && (
175
+ renderAddButton ? (
176
+ renderAddButton(onAdd, disabled)
177
+ ) : (
178
+ <Pressable
179
+ onPress={onAdd}
180
+ disabled={disabled}
181
+ style={({ pressed }) => [
182
+ styles.addButton,
183
+ {
184
+ width: tileSize,
185
+ height: tileSize,
186
+ borderRadius: theme.radii.md,
187
+ backgroundColor: pressed
188
+ ? theme.colors.border
189
+ : theme.colors.backgroundSecondary,
190
+ borderWidth: 2,
191
+ borderColor: theme.colors.border,
192
+ borderStyle: 'dashed',
193
+ opacity: disabled ? 0.5 : 1,
194
+ },
195
+ addButtonStyle,
196
+ ]}
197
+ >
198
+ {typeof addButtonIcon === 'string' ? (
199
+ <ThemedText style={[styles.addIcon, { color: theme.colors.textSecondary }]}>
200
+ {addButtonIcon}
201
+ </ThemedText>
202
+ ) : (
203
+ addButtonIcon
204
+ )}
205
+ <ThemedText variant="caption" secondary>
206
+ {addButtonLabel}
207
+ </ThemedText>
208
+ </Pressable>
209
+ )
210
+ )}
211
+
212
+ {attachments.length === 0 && !shouldShowAddButton && (
213
+ <View
214
+ style={[
215
+ styles.emptyState,
216
+ {
217
+ padding: theme.spacing.md,
218
+ },
219
+ ]}
220
+ >
221
+ <ThemedText variant="caption" secondary>
222
+ {emptyText}
223
+ </ThemedText>
224
+ </View>
225
+ )}
226
+ </View>
227
+ );
228
+ }
229
+
230
+ const styles = StyleSheet.create({
231
+ container: {
232
+ flexDirection: 'row',
233
+ flexWrap: 'wrap',
234
+ },
235
+ addButton: {
236
+ alignItems: 'center',
237
+ justifyContent: 'center',
238
+ },
239
+ addIcon: {
240
+ fontSize: 28,
241
+ fontWeight: '300',
242
+ lineHeight: 32,
243
+ },
244
+ emptyState: {
245
+ alignItems: 'center',
246
+ },
247
+ });