@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,127 @@
1
+ import React from 'react';
2
+ import {
3
+ Pressable,
4
+ ActivityIndicator,
5
+ StyleSheet,
6
+ } from 'react-native';
7
+ import type { PressableProps, ViewStyle, StyleProp } from 'react-native';
8
+ import { useHarkenTheme } from '../hooks';
9
+ import { ThemedText } from './ThemedText';
10
+
11
+ export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
12
+
13
+ export interface ThemedButtonProps extends Omit<PressableProps, 'children' | 'style'> {
14
+ /** Button text */
15
+ title: string;
16
+ /** Button variant */
17
+ variant?: ButtonVariant;
18
+ /** Loading state */
19
+ loading?: boolean;
20
+ /** Full width button */
21
+ fullWidth?: boolean;
22
+ /**
23
+ * Additional styles for the button container.
24
+ * Note: Function styles are not supported; use static StyleProp<ViewStyle>.
25
+ */
26
+ style?: StyleProp<ViewStyle>;
27
+ }
28
+
29
+ /**
30
+ * Themed button component with Harken styling.
31
+ */
32
+ export function ThemedButton({
33
+ title,
34
+ variant = 'primary',
35
+ loading = false,
36
+ fullWidth = false,
37
+ disabled,
38
+ style,
39
+ ...props
40
+ }: ThemedButtonProps): React.JSX.Element {
41
+ const theme = useHarkenTheme();
42
+
43
+ const getBackgroundColor = (pressed: boolean): string => {
44
+ if (disabled) {
45
+ return variant === 'primary'
46
+ ? theme.colors.border
47
+ : 'transparent';
48
+ }
49
+
50
+ switch (variant) {
51
+ case 'primary':
52
+ return pressed ? theme.colors.primaryPressed : theme.colors.primary;
53
+ case 'secondary':
54
+ return pressed ? theme.colors.border : theme.colors.backgroundSecondary;
55
+ case 'ghost':
56
+ return pressed ? theme.colors.backgroundSecondary : 'transparent';
57
+ }
58
+ };
59
+
60
+ const getTextColor = (): string => {
61
+ if (disabled) {
62
+ return theme.colors.textPlaceholder;
63
+ }
64
+
65
+ switch (variant) {
66
+ case 'primary':
67
+ return theme.colors.textOnPrimary;
68
+ case 'secondary':
69
+ case 'ghost':
70
+ return theme.colors.text;
71
+ }
72
+ };
73
+
74
+ const getBorderColor = (): string => {
75
+ switch (variant) {
76
+ case 'secondary':
77
+ return theme.colors.border;
78
+ default:
79
+ return 'transparent';
80
+ }
81
+ };
82
+
83
+ // Flatten the style prop to handle arrays and registered styles
84
+ const flattenedStyle = style ? StyleSheet.flatten(style) : undefined;
85
+
86
+ return (
87
+ <Pressable
88
+ disabled={disabled || loading}
89
+ style={({ pressed }) => {
90
+ const baseStyle: ViewStyle = {
91
+ backgroundColor: getBackgroundColor(pressed),
92
+ borderWidth: variant === 'secondary' ? 1 : 0,
93
+ borderColor: getBorderColor(),
94
+ borderRadius: theme.radii.md,
95
+ paddingVertical: theme.spacing.sm + 4,
96
+ paddingHorizontal: theme.spacing.md,
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ flexDirection: 'row',
100
+ minHeight: 48,
101
+ opacity: disabled ? 0.6 : 1,
102
+ };
103
+
104
+ if (fullWidth) {
105
+ baseStyle.width = '100%';
106
+ }
107
+
108
+ return [baseStyle, flattenedStyle];
109
+ }}
110
+ {...props}
111
+ >
112
+ {loading ? (
113
+ <ActivityIndicator
114
+ color={getTextColor()}
115
+ size="small"
116
+ />
117
+ ) : (
118
+ <ThemedText
119
+ variant="label"
120
+ color={getTextColor()}
121
+ >
122
+ {title}
123
+ </ThemedText>
124
+ )}
125
+ </Pressable>
126
+ );
127
+ }
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { Text } from 'react-native';
3
+ import type { TextProps, TextStyle } from 'react-native';
4
+ import { useHarkenTheme } from '../hooks';
5
+
6
+ export type TextVariant = 'title' | 'body' | 'label' | 'caption';
7
+
8
+ export interface ThemedTextProps extends TextProps {
9
+ /** Text variant determining size and weight */
10
+ variant?: TextVariant;
11
+ /** Text color override (defaults to theme text color) */
12
+ color?: string;
13
+ /** Whether to use secondary text color */
14
+ secondary?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Themed text component that uses Harken theme typography.
19
+ */
20
+ export function ThemedText({
21
+ variant = 'body',
22
+ color,
23
+ secondary = false,
24
+ style,
25
+ children,
26
+ ...props
27
+ }: ThemedTextProps): React.JSX.Element {
28
+ const theme = useHarkenTheme();
29
+
30
+ const variantStyles: Record<TextVariant, TextStyle> = {
31
+ title: {
32
+ fontSize: theme.typography.titleSize,
33
+ lineHeight: theme.typography.titleSize * theme.typography.titleLineHeight,
34
+ fontWeight: theme.typography.titleWeight,
35
+ fontFamily: theme.typography.fontFamilyHeading ?? theme.typography.fontFamily,
36
+ },
37
+ body: {
38
+ fontSize: theme.typography.bodySize,
39
+ lineHeight: theme.typography.bodySize * theme.typography.bodyLineHeight,
40
+ fontWeight: theme.typography.bodyWeight,
41
+ fontFamily: theme.typography.fontFamily,
42
+ },
43
+ label: {
44
+ fontSize: theme.typography.labelSize,
45
+ fontWeight: theme.typography.labelWeight,
46
+ fontFamily: theme.typography.fontFamily,
47
+ },
48
+ caption: {
49
+ fontSize: theme.typography.captionSize,
50
+ fontWeight: theme.typography.captionWeight,
51
+ fontFamily: theme.typography.fontFamily,
52
+ },
53
+ };
54
+
55
+ const textColor = color ?? (secondary ? theme.colors.textSecondary : theme.colors.text);
56
+
57
+ return (
58
+ <Text
59
+ style={[variantStyles[variant], { color: textColor }, style]}
60
+ {...props}
61
+ >
62
+ {children}
63
+ </Text>
64
+ );
65
+ }
@@ -0,0 +1,65 @@
1
+ import React, { useState } from 'react';
2
+ import { TextInput, View } from 'react-native';
3
+ import type { TextInputProps, ViewStyle, TextStyle, StyleProp } from 'react-native';
4
+ import { useHarkenTheme } from '../hooks';
5
+
6
+ export interface ThemedTextInputProps extends TextInputProps {
7
+ /** Error state */
8
+ error?: boolean;
9
+ /** Container style override */
10
+ containerStyle?: StyleProp<ViewStyle>;
11
+ }
12
+
13
+ /**
14
+ * Themed text input component with Harken styling.
15
+ */
16
+ export function ThemedTextInput({
17
+ error = false,
18
+ containerStyle,
19
+ style,
20
+ onFocus,
21
+ onBlur,
22
+ ...props
23
+ }: ThemedTextInputProps): React.JSX.Element {
24
+ const theme = useHarkenTheme();
25
+ const [isFocused, setIsFocused] = useState(false);
26
+
27
+ const getBorderColor = () => {
28
+ if (error) return theme.colors.error;
29
+ if (isFocused) return theme.colors.borderFocused;
30
+ return theme.colors.border;
31
+ };
32
+
33
+ const inputStyle: TextStyle = {
34
+ fontSize: theme.typography.bodySize,
35
+ fontFamily: theme.typography.fontFamily,
36
+ color: theme.colors.text,
37
+ padding: theme.spacing.md,
38
+ minHeight: 44,
39
+ };
40
+
41
+ const containerStyles: ViewStyle = {
42
+ backgroundColor: theme.colors.backgroundSecondary,
43
+ borderWidth: 1,
44
+ borderColor: getBorderColor(),
45
+ borderRadius: theme.radii.md,
46
+ };
47
+
48
+ return (
49
+ <View style={[containerStyles, containerStyle]}>
50
+ <TextInput
51
+ style={[inputStyle, style]}
52
+ placeholderTextColor={theme.colors.textPlaceholder}
53
+ onFocus={(e) => {
54
+ setIsFocused(true);
55
+ onFocus?.(e);
56
+ }}
57
+ onBlur={(e) => {
58
+ setIsFocused(false);
59
+ onBlur?.(e);
60
+ }}
61
+ {...props}
62
+ />
63
+ </View>
64
+ );
65
+ }
@@ -0,0 +1,440 @@
1
+ import React from 'react';
2
+ import { View, Pressable, ActivityIndicator, StyleSheet } from 'react-native';
3
+ import type { ViewStyle, StyleProp } from 'react-native';
4
+ import { useHarkenTheme } from '../hooks';
5
+ import { ThemedText } from './ThemedText';
6
+ import { UploadPhase } from '../domain';
7
+
8
+ /**
9
+ * Customizable labels for upload status states.
10
+ */
11
+ export interface UploadStatusLabels {
12
+ /** Label for retry button (default: "Retry") */
13
+ retry?: string;
14
+ /** Label for remove/cancel button (default: "Remove") */
15
+ remove?: string;
16
+ /** Label for cancel during upload (default: "Cancel") */
17
+ cancel?: string;
18
+ /** Label while confirming (default: "Confirming...") */
19
+ confirming?: string;
20
+ /** Label while queued (default: "Waiting...") */
21
+ waiting?: string;
22
+ /** Default error message (default: "Upload failed") */
23
+ uploadFailed?: string;
24
+ }
25
+
26
+ export interface UploadStatusOverlayProps {
27
+ /** Current upload phase */
28
+ phase: UploadPhase;
29
+ /** Upload progress (0.0 - 1.0) */
30
+ progress: number;
31
+ /** Error message if failed */
32
+ error?: string;
33
+ /** Callback when retry is pressed */
34
+ onRetry?: () => void;
35
+ /** Callback when remove/cancel is pressed */
36
+ onRemove?: () => void;
37
+ /** Additional container style */
38
+ style?: StyleProp<ViewStyle>;
39
+ /** Custom labels for status text */
40
+ labels?: UploadStatusLabels;
41
+ /** Custom progress renderer */
42
+ renderProgress?: (progress: number) => React.ReactNode;
43
+ /** Custom error renderer */
44
+ renderError?: (
45
+ error: string,
46
+ onRetry?: () => void,
47
+ onRemove?: () => void
48
+ ) => React.ReactNode;
49
+ /** Custom success/completed renderer */
50
+ renderSuccess?: (onRemove?: () => void) => React.ReactNode;
51
+ }
52
+
53
+ /**
54
+ * Overlay component showing upload status on attachments.
55
+ *
56
+ * Shows:
57
+ * - Progress bar during upload
58
+ * - Spinner during confirmation
59
+ * - Checkmark when complete
60
+ * - Error with retry button when failed
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * // Basic usage
65
+ * <UploadStatusOverlay
66
+ * phase={attachment.phase}
67
+ * progress={attachment.progress}
68
+ * onRetry={() => retryAttachment(attachment.attachmentId)}
69
+ * onRemove={() => removeAttachment(attachment.attachmentId)}
70
+ * />
71
+ *
72
+ * // With custom labels
73
+ * <UploadStatusOverlay
74
+ * phase={phase}
75
+ * progress={progress}
76
+ * labels={{
77
+ * retry: 'Try Again',
78
+ * remove: 'Delete',
79
+ * confirming: 'Processing...',
80
+ * }}
81
+ * />
82
+ *
83
+ * // With custom progress renderer
84
+ * <UploadStatusOverlay
85
+ * phase={phase}
86
+ * progress={progress}
87
+ * renderProgress={(p) => <CustomProgressBar value={p} />}
88
+ * />
89
+ * ```
90
+ */
91
+ export function UploadStatusOverlay({
92
+ phase,
93
+ progress,
94
+ error,
95
+ onRetry,
96
+ onRemove,
97
+ style,
98
+ labels,
99
+ renderProgress,
100
+ renderError,
101
+ renderSuccess,
102
+ }: UploadStatusOverlayProps): React.JSX.Element | null {
103
+ const theme = useHarkenTheme();
104
+
105
+ // Merge labels with defaults
106
+ const resolvedLabels: Required<UploadStatusLabels> = {
107
+ retry: labels?.retry ?? 'Retry',
108
+ remove: labels?.remove ?? 'Remove',
109
+ cancel: labels?.cancel ?? 'Cancel',
110
+ confirming: labels?.confirming ?? 'Confirming...',
111
+ waiting: labels?.waiting ?? 'Waiting...',
112
+ uploadFailed: labels?.uploadFailed ?? 'Upload failed',
113
+ };
114
+
115
+ // Completed state - just show a subtle checkmark
116
+ if (phase === UploadPhase.COMPLETED) {
117
+ if (renderSuccess) {
118
+ return (
119
+ <View style={[styles.overlay, style]}>{renderSuccess(onRemove)}</View>
120
+ );
121
+ }
122
+
123
+ return (
124
+ <View style={[styles.overlay, styles.completedOverlay, style]}>
125
+ <View
126
+ style={[
127
+ styles.badge,
128
+ {
129
+ backgroundColor: theme.colors.success,
130
+ borderRadius: theme.radii.full,
131
+ },
132
+ ]}
133
+ >
134
+ <ThemedText style={styles.badgeIcon}>✓</ThemedText>
135
+ </View>
136
+ {onRemove && (
137
+ <Pressable
138
+ onPress={onRemove}
139
+ style={[
140
+ styles.removeButton,
141
+ {
142
+ backgroundColor: theme.colors.background,
143
+ borderRadius: theme.radii.full,
144
+ },
145
+ ]}
146
+ >
147
+ <ThemedText style={styles.removeIcon}>×</ThemedText>
148
+ </Pressable>
149
+ )}
150
+ </View>
151
+ );
152
+ }
153
+
154
+ // Failed state - show error and retry
155
+ if (phase === UploadPhase.FAILED) {
156
+ const errorMessage = error ?? resolvedLabels.uploadFailed;
157
+
158
+ if (renderError) {
159
+ return (
160
+ <View style={[styles.overlay, style]}>
161
+ {renderError(errorMessage, onRetry, onRemove)}
162
+ </View>
163
+ );
164
+ }
165
+
166
+ return (
167
+ <View
168
+ style={[
169
+ styles.overlay,
170
+ styles.fullOverlay,
171
+ { backgroundColor: theme.colors.overlayDark },
172
+ style,
173
+ ]}
174
+ >
175
+ <ThemedText style={styles.errorIcon}>⚠️</ThemedText>
176
+ <ThemedText style={styles.errorText} numberOfLines={2}>
177
+ {errorMessage}
178
+ </ThemedText>
179
+ <View style={styles.buttonRow}>
180
+ {onRetry && (
181
+ <Pressable
182
+ onPress={onRetry}
183
+ style={[
184
+ styles.actionButton,
185
+ {
186
+ backgroundColor: theme.colors.primary,
187
+ borderRadius: theme.radii.sm,
188
+ },
189
+ ]}
190
+ >
191
+ <ThemedText style={styles.actionButtonText}>
192
+ {resolvedLabels.retry}
193
+ </ThemedText>
194
+ </Pressable>
195
+ )}
196
+ {onRemove && (
197
+ <Pressable
198
+ onPress={onRemove}
199
+ style={[
200
+ styles.actionButton,
201
+ {
202
+ backgroundColor: theme.colors.error,
203
+ borderRadius: theme.radii.sm,
204
+ },
205
+ ]}
206
+ >
207
+ <ThemedText style={styles.actionButtonText}>
208
+ {resolvedLabels.remove}
209
+ </ThemedText>
210
+ </Pressable>
211
+ )}
212
+ </View>
213
+ </View>
214
+ );
215
+ }
216
+
217
+ // Uploading state - show progress bar
218
+ if (phase === UploadPhase.UPLOADING) {
219
+ const progressPercent = Math.round(progress * 100);
220
+
221
+ if (renderProgress) {
222
+ return (
223
+ <View
224
+ style={[
225
+ styles.overlay,
226
+ styles.fullOverlay,
227
+ { backgroundColor: theme.colors.overlay },
228
+ style,
229
+ ]}
230
+ >
231
+ {renderProgress(progress)}
232
+ </View>
233
+ );
234
+ }
235
+
236
+ return (
237
+ <View
238
+ style={[
239
+ styles.overlay,
240
+ styles.fullOverlay,
241
+ { backgroundColor: theme.colors.overlay },
242
+ style,
243
+ ]}
244
+ >
245
+ <ThemedText style={styles.progressText}>{progressPercent}%</ThemedText>
246
+ <View
247
+ style={[
248
+ styles.progressBarContainer,
249
+ {
250
+ backgroundColor: 'rgba(255,255,255,0.3)',
251
+ borderRadius: theme.radii.sm,
252
+ },
253
+ ]}
254
+ >
255
+ <View
256
+ style={[
257
+ styles.progressBarFill,
258
+ {
259
+ width: `${progressPercent}%`,
260
+ backgroundColor: theme.colors.primary,
261
+ borderRadius: theme.radii.sm,
262
+ },
263
+ ]}
264
+ />
265
+ </View>
266
+ {onRemove && (
267
+ <Pressable
268
+ onPress={onRemove}
269
+ style={[
270
+ styles.cancelButton,
271
+ {
272
+ borderRadius: theme.radii.sm,
273
+ },
274
+ ]}
275
+ >
276
+ <ThemedText style={styles.cancelText}>
277
+ {resolvedLabels.cancel}
278
+ </ThemedText>
279
+ </Pressable>
280
+ )}
281
+ </View>
282
+ );
283
+ }
284
+
285
+ // Confirming state - show spinner
286
+ if (phase === UploadPhase.CONFIRMING) {
287
+ return (
288
+ <View
289
+ style={[
290
+ styles.overlay,
291
+ styles.fullOverlay,
292
+ { backgroundColor: theme.colors.overlay },
293
+ style,
294
+ ]}
295
+ >
296
+ <ActivityIndicator color="#fff" size="small" />
297
+ <ThemedText style={styles.confirmingText}>
298
+ {resolvedLabels.confirming}
299
+ </ThemedText>
300
+ </View>
301
+ );
302
+ }
303
+
304
+ // Queued state - show waiting indicator
305
+ if (phase === UploadPhase.QUEUED) {
306
+ return (
307
+ <View
308
+ style={[
309
+ styles.overlay,
310
+ styles.fullOverlay,
311
+ { backgroundColor: theme.colors.overlay },
312
+ style,
313
+ ]}
314
+ >
315
+ <ActivityIndicator color="#fff" size="small" />
316
+ <ThemedText style={styles.queuedText}>
317
+ {resolvedLabels.waiting}
318
+ </ThemedText>
319
+ {onRemove && (
320
+ <Pressable
321
+ onPress={onRemove}
322
+ style={[
323
+ styles.cancelButton,
324
+ {
325
+ borderRadius: theme.radii.sm,
326
+ },
327
+ ]}
328
+ >
329
+ <ThemedText style={styles.cancelText}>
330
+ {resolvedLabels.cancel}
331
+ </ThemedText>
332
+ </Pressable>
333
+ )}
334
+ </View>
335
+ );
336
+ }
337
+
338
+ return null;
339
+ }
340
+
341
+ const styles = StyleSheet.create({
342
+ overlay: {
343
+ ...StyleSheet.absoluteFillObject,
344
+ },
345
+ fullOverlay: {
346
+ alignItems: 'center',
347
+ justifyContent: 'center',
348
+ padding: 8,
349
+ },
350
+ completedOverlay: {
351
+ alignItems: 'flex-end',
352
+ justifyContent: 'space-between',
353
+ flexDirection: 'row',
354
+ padding: 4,
355
+ },
356
+ badge: {
357
+ width: 24,
358
+ height: 24,
359
+ alignItems: 'center',
360
+ justifyContent: 'center',
361
+ },
362
+ badgeIcon: {
363
+ color: '#fff',
364
+ fontSize: 14,
365
+ fontWeight: '700',
366
+ },
367
+ removeButton: {
368
+ width: 24,
369
+ height: 24,
370
+ alignItems: 'center',
371
+ justifyContent: 'center',
372
+ shadowColor: '#000',
373
+ shadowOffset: { width: 0, height: 1 },
374
+ shadowOpacity: 0.2,
375
+ shadowRadius: 2,
376
+ elevation: 2,
377
+ },
378
+ removeIcon: {
379
+ fontSize: 18,
380
+ fontWeight: '700',
381
+ lineHeight: 20,
382
+ },
383
+ progressText: {
384
+ color: '#fff',
385
+ fontSize: 16,
386
+ fontWeight: '600',
387
+ marginBottom: 8,
388
+ },
389
+ progressBarContainer: {
390
+ width: '80%',
391
+ height: 6,
392
+ overflow: 'hidden',
393
+ },
394
+ progressBarFill: {
395
+ height: '100%',
396
+ },
397
+ cancelButton: {
398
+ marginTop: 8,
399
+ paddingHorizontal: 12,
400
+ paddingVertical: 4,
401
+ backgroundColor: 'rgba(255,255,255,0.2)',
402
+ },
403
+ cancelText: {
404
+ color: '#fff',
405
+ fontSize: 12,
406
+ },
407
+ confirmingText: {
408
+ color: '#fff',
409
+ fontSize: 12,
410
+ marginTop: 4,
411
+ },
412
+ queuedText: {
413
+ color: '#fff',
414
+ fontSize: 12,
415
+ marginTop: 4,
416
+ },
417
+ errorIcon: {
418
+ fontSize: 24,
419
+ marginBottom: 4,
420
+ },
421
+ errorText: {
422
+ color: '#fff',
423
+ fontSize: 11,
424
+ textAlign: 'center',
425
+ marginBottom: 8,
426
+ },
427
+ buttonRow: {
428
+ flexDirection: 'row',
429
+ gap: 8,
430
+ },
431
+ actionButton: {
432
+ paddingHorizontal: 12,
433
+ paddingVertical: 6,
434
+ },
435
+ actionButtonText: {
436
+ color: '#fff',
437
+ fontSize: 12,
438
+ fontWeight: '600',
439
+ },
440
+ });