@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,39 @@
1
+ // Base themed components
2
+ export { ThemedText } from './ThemedText';
3
+ export type { ThemedTextProps, TextVariant } from './ThemedText';
4
+
5
+ export { ThemedTextInput } from './ThemedTextInput';
6
+ export type { ThemedTextInputProps } from './ThemedTextInput';
7
+
8
+ export { ThemedButton } from './ThemedButton';
9
+ export type { ThemedButtonProps, ButtonVariant } from './ThemedButton';
10
+
11
+ // Feedback components
12
+ export { CategorySelector, DEFAULT_CATEGORIES } from './CategorySelector';
13
+ export type { CategorySelectorProps, CategoryOption } from './CategorySelector';
14
+
15
+ export { FeedbackForm } from './FeedbackForm';
16
+ export type { FeedbackFormProps, FeedbackFormData } from './FeedbackForm';
17
+
18
+ // Note: FeedbackSheet is exported from the main entry point (comes from attachments module)
19
+ // to provide full attachment support by default.
20
+
21
+ // Attachment components
22
+ export { AttachmentPicker } from './AttachmentPicker';
23
+ export type {
24
+ AttachmentPickerProps,
25
+ AttachmentSource,
26
+ PickerOptionConfig,
27
+ } from './AttachmentPicker';
28
+
29
+ export { UploadStatusOverlay } from './UploadStatusOverlay';
30
+ export type {
31
+ UploadStatusOverlayProps,
32
+ UploadStatusLabels,
33
+ } from './UploadStatusOverlay';
34
+
35
+ export { AttachmentPreview } from './AttachmentPreview';
36
+ export type { AttachmentPreviewProps } from './AttachmentPreview';
37
+
38
+ export { AttachmentGrid } from './AttachmentGrid';
39
+ export type { AttachmentGridProps } from './AttachmentGrid';
@@ -0,0 +1,129 @@
1
+ import React, { createContext, useMemo } from 'react';
2
+ import { useColorScheme } from 'react-native';
3
+ import type { HarkenTheme, ThemeMode } from '../theme';
4
+ import { lightTheme, darkTheme, createTheme } from '../theme';
5
+ import type { HarkenConfig, HarkenProviderProps } from '../types';
6
+ import { IdentityStore, createDefaultStorage } from '../storage';
7
+ import { HarkenClient } from '../api/client';
8
+
9
+ /**
10
+ * Context value provided by HarkenProvider.
11
+ */
12
+ export interface HarkenContextValue {
13
+ /** The resolved theme based on mode and overrides */
14
+ theme: HarkenTheme;
15
+ /** Current theme mode */
16
+ themeMode: ThemeMode;
17
+ /** Whether dark mode is currently active */
18
+ isDarkMode: boolean;
19
+ /** SDK configuration */
20
+ config: HarkenConfig;
21
+ /** Identity store for anonymous ID management */
22
+ identityStore: IdentityStore;
23
+ /** API client instance */
24
+ client: HarkenClient;
25
+ }
26
+
27
+ /**
28
+ * React context for Harken SDK state.
29
+ * @internal
30
+ */
31
+ export const HarkenContext = createContext<HarkenContextValue | null>(null);
32
+
33
+ /**
34
+ * Provider component that configures the Harken SDK.
35
+ *
36
+ * Wrap your app with this provider to enable Harken feedback components.
37
+ * By default, uses expo-secure-store for persistent anonymous ID storage
38
+ * (falls back to in-memory storage if not available).
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * import { HarkenProvider, FeedbackSheet } from '@harkenapp/sdk-react-native';
43
+ *
44
+ * function App() {
45
+ * return (
46
+ * <HarkenProvider config={{ publishableKey: 'pk_live_xxxx' }}>
47
+ * <FeedbackSheet />
48
+ * </HarkenProvider>
49
+ * );
50
+ * }
51
+ * ```
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * // With custom storage implementation
56
+ * import { HarkenProvider, createSecureStoreAdapter } from '@harkenapp/sdk-react-native';
57
+ * import * as SecureStore from 'expo-secure-store';
58
+ *
59
+ * const storage = createSecureStoreAdapter(SecureStore);
60
+ *
61
+ * <HarkenProvider config={{ publishableKey: 'pk_live_xxxx' }} storage={storage}>
62
+ * <YourApp />
63
+ * </HarkenProvider>
64
+ * ```
65
+ */
66
+ export function HarkenProvider({
67
+ config,
68
+ themeMode = 'system',
69
+ lightTheme: lightOverrides,
70
+ darkTheme: darkOverrides,
71
+ storage,
72
+ children,
73
+ }: HarkenProviderProps): React.JSX.Element {
74
+ // Get system color scheme
75
+ const systemColorScheme = useColorScheme();
76
+
77
+ // Determine if dark mode should be active
78
+ const isDarkMode = useMemo(() => {
79
+ if (themeMode === 'dark') return true;
80
+ if (themeMode === 'light') return false;
81
+ // 'system' mode - follow device preference
82
+ return systemColorScheme === 'dark';
83
+ }, [themeMode, systemColorScheme]);
84
+
85
+ // Build the resolved theme
86
+ const theme = useMemo(() => {
87
+ const baseTheme = isDarkMode ? darkTheme : lightTheme;
88
+ const overrides = isDarkMode ? darkOverrides : lightOverrides;
89
+ return createTheme(baseTheme, overrides);
90
+ }, [isDarkMode, lightOverrides, darkOverrides]);
91
+
92
+ // Create identity store (memoized to persist across re-renders)
93
+ // Uses expo-secure-store by default if available, otherwise falls back to memory
94
+ const identityStore = useMemo(() => {
95
+ const storageImpl = storage ?? createDefaultStorage();
96
+ return new IdentityStore(storageImpl);
97
+ }, [storage]);
98
+
99
+ // Create API client (memoized)
100
+ const client = useMemo(() => {
101
+ return new HarkenClient({
102
+ publishableKey: config.publishableKey,
103
+ userToken: config.userToken,
104
+ baseUrl: config.apiBaseUrl,
105
+ });
106
+ }, [config.publishableKey, config.userToken, config.apiBaseUrl]);
107
+
108
+ // Note: Upload queue service initialization has been moved to the attachments module.
109
+ // When using attachments, the service is initialized when useAttachmentUpload is first called.
110
+
111
+ // Memoize the context value
112
+ const contextValue = useMemo<HarkenContextValue>(
113
+ () => ({
114
+ theme,
115
+ themeMode,
116
+ isDarkMode,
117
+ config,
118
+ identityStore,
119
+ client,
120
+ }),
121
+ [theme, themeMode, isDarkMode, config, identityStore, client]
122
+ );
123
+
124
+ return (
125
+ <HarkenContext.Provider value={contextValue}>
126
+ {children}
127
+ </HarkenContext.Provider>
128
+ );
129
+ }
@@ -0,0 +1,2 @@
1
+ export { HarkenProvider, HarkenContext } from './HarkenContext';
2
+ export type { HarkenContextValue } from './HarkenContext';
@@ -0,0 +1,12 @@
1
+ export {
2
+ UploadPhase,
3
+ DEFAULT_UPLOAD_RETRY_CONFIG,
4
+ } from './upload-queue';
5
+
6
+ export type {
7
+ QueueItem,
8
+ QueueStatus,
9
+ UploadProgress,
10
+ PersistedQueue,
11
+ UploadRetryConfig,
12
+ } from './upload-queue';
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Upload queue domain types.
3
+ *
4
+ * These types define the state machine for background attachment uploads.
5
+ */
6
+
7
+ /**
8
+ * Phases of the upload lifecycle.
9
+ */
10
+ export enum UploadPhase {
11
+ /** Waiting in queue to be processed */
12
+ QUEUED = 'queued',
13
+ /** Currently uploading to storage */
14
+ UPLOADING = 'uploading',
15
+ /** Upload complete, confirming with server */
16
+ CONFIRMING = 'confirming',
17
+ /** Successfully uploaded and confirmed */
18
+ COMPLETED = 'completed',
19
+ /** Failed after max retries */
20
+ FAILED = 'failed',
21
+ }
22
+
23
+ /**
24
+ * A single item in the upload queue.
25
+ */
26
+ export interface QueueItem {
27
+ /** Internal queue item ID */
28
+ id: string;
29
+ /** Server-assigned attachment ID */
30
+ attachmentId: string;
31
+ /** Local file URI (file://) */
32
+ localUri: string;
33
+ /** Presigned upload URL */
34
+ uploadUrl: string;
35
+ /** ISO timestamp when upload URL expires */
36
+ uploadExpiresAt: string;
37
+ /** MIME type (e.g., 'image/png') */
38
+ mimeType: string;
39
+ /** Original filename */
40
+ fileName: string;
41
+ /** File size in bytes */
42
+ fileSize: number;
43
+
44
+ // State
45
+ /** Current phase in upload lifecycle */
46
+ phase: UploadPhase;
47
+ /** Upload progress (0.0 - 1.0) */
48
+ progress: number;
49
+ /** Current attempt number (1-based) */
50
+ attemptNumber: number;
51
+ /** Maximum retry attempts */
52
+ maxAttempts: number;
53
+ /** Last error message if failed */
54
+ lastError?: string;
55
+
56
+ // Timestamps
57
+ /** ISO timestamp when item was queued */
58
+ createdAt: string;
59
+ /** ISO timestamp when upload started */
60
+ startedAt?: string;
61
+ /** ISO timestamp when completed or failed */
62
+ completedAt?: string;
63
+ /** ISO timestamp for scheduled retry (backoff) */
64
+ scheduledRetryAt?: string;
65
+ }
66
+
67
+ /**
68
+ * Summary of queue state.
69
+ */
70
+ export interface QueueStatus {
71
+ /** Total items in queue */
72
+ total: number;
73
+ /** Items waiting to be processed */
74
+ queued: number;
75
+ /** Items currently uploading or confirming */
76
+ uploading: number;
77
+ /** Successfully completed items */
78
+ completed: number;
79
+ /** Failed items (max retries exceeded) */
80
+ failed: number;
81
+ /** Whether queue is paused (e.g., offline) */
82
+ isPaused: boolean;
83
+ }
84
+
85
+ /**
86
+ * Progress update for a single attachment.
87
+ */
88
+ export interface UploadProgress {
89
+ /** Server-assigned attachment ID */
90
+ attachmentId: string;
91
+ /** Current phase */
92
+ phase: UploadPhase;
93
+ /** Upload progress (0.0 - 1.0) */
94
+ progress: number;
95
+ /** Error message if failed */
96
+ error?: string;
97
+ }
98
+
99
+ /**
100
+ * Persisted queue schema for AsyncStorage.
101
+ */
102
+ export interface PersistedQueue {
103
+ /** Schema version for migrations */
104
+ version: number;
105
+ /** Queue items */
106
+ items: QueueItem[];
107
+ }
108
+
109
+ /**
110
+ * Configuration for upload retry behavior.
111
+ */
112
+ export interface UploadRetryConfig {
113
+ /** Base delay in milliseconds (default: 2000) */
114
+ baseDelayMs: number;
115
+ /** Maximum delay in milliseconds (default: 60000) */
116
+ maxDelayMs: number;
117
+ /** Maximum number of attempts (default: 3) */
118
+ maxAttempts: number;
119
+ /** Random jitter in milliseconds (default: 1000) */
120
+ jitterMs: number;
121
+ }
122
+
123
+ /**
124
+ * Default retry configuration per feature spec D4.
125
+ */
126
+ export const DEFAULT_UPLOAD_RETRY_CONFIG: UploadRetryConfig = {
127
+ baseDelayMs: 2000,
128
+ maxDelayMs: 60000,
129
+ maxAttempts: 3,
130
+ jitterMs: 1000,
131
+ };
@@ -0,0 +1,10 @@
1
+ // Core hooks (no native module dependencies)
2
+ export { useHarkenTheme } from './useHarkenTheme';
3
+ export { useHarkenContext } from './useHarkenContext';
4
+ export { useAnonymousId } from './useAnonymousId';
5
+ export { useFeedback } from './useFeedback';
6
+ export type { SubmitFeedbackParams, UseFeedbackResult } from './useFeedback';
7
+
8
+ // Note: Attachment hooks (useAttachmentUpload, useAttachmentStatus) are
9
+ // exported from '@harkenapp/sdk-react-native/attachments' to avoid eager
10
+ // loading of native modules (expo-file-system, expo-image-picker, etc.)
@@ -0,0 +1,68 @@
1
+ import { useState, useEffect, useContext } from 'react';
2
+ import { HarkenContext } from '../context';
3
+
4
+ /**
5
+ * Hook to access the anonymous ID for the current installation.
6
+ *
7
+ * The anonymous ID is a stable UUID that persists across app sessions.
8
+ * It's generated once and stored securely on the device.
9
+ *
10
+ * @returns Object with the anonymous ID and loading state
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * function MyComponent() {
15
+ * const { anonymousId, isLoading } = useAnonymousId();
16
+ *
17
+ * if (isLoading) {
18
+ * return <Text>Loading...</Text>;
19
+ * }
20
+ *
21
+ * return <Text>ID: {anonymousId}</Text>;
22
+ * }
23
+ * ```
24
+ */
25
+ export function useAnonymousId(): {
26
+ /** The anonymous ID, or null while loading */
27
+ anonymousId: string | null;
28
+ /** True while the ID is being loaded from storage */
29
+ isLoading: boolean;
30
+ } {
31
+ const context = useContext(HarkenContext);
32
+
33
+ if (!context) {
34
+ throw new Error('useAnonymousId must be used within a HarkenProvider');
35
+ }
36
+
37
+ // Capture identityStore to satisfy TypeScript narrowing in useEffect
38
+ const { identityStore } = context;
39
+
40
+ const [anonymousId, setAnonymousId] = useState<string | null>(null);
41
+ const [isLoading, setIsLoading] = useState(true);
42
+
43
+ useEffect(() => {
44
+ let mounted = true;
45
+
46
+ async function loadAnonymousId() {
47
+ try {
48
+ const id = await identityStore.getAnonymousId();
49
+ if (mounted) {
50
+ setAnonymousId(id);
51
+ setIsLoading(false);
52
+ }
53
+ } catch {
54
+ if (mounted) {
55
+ setIsLoading(false);
56
+ }
57
+ }
58
+ }
59
+
60
+ void loadAnonymousId();
61
+
62
+ return () => {
63
+ mounted = false;
64
+ };
65
+ }, [identityStore]);
66
+
67
+ return { anonymousId, isLoading };
68
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Hook for managing the attachment picker with smart source selection.
3
+ *
4
+ * Provides configurable attachment sources and automatically handles:
5
+ * - Skipping the picker modal when only one source is enabled
6
+ * - Warning when no sources are enabled
7
+ */
8
+
9
+ import { useState, useCallback, useMemo, useEffect } from 'react';
10
+ import { useAttachmentUpload } from './useAttachmentUpload';
11
+ import type { UseAttachmentUploadResult } from './useAttachmentUpload';
12
+ import type { AttachmentPickerProps } from '../components/AttachmentPicker';
13
+
14
+ /**
15
+ * Configuration for which attachment sources are enabled.
16
+ */
17
+ export interface AttachmentSourceConfig {
18
+ /** Enable camera source. @default true */
19
+ camera?: boolean;
20
+ /** Enable photo library source. @default true */
21
+ library?: boolean;
22
+ /** Enable file/document picker source. @default true */
23
+ files?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Return type for useAttachmentPicker hook.
28
+ */
29
+ export interface UseAttachmentPickerResult extends UseAttachmentUploadResult {
30
+ /** Open the attachment picker (or directly invoke source if only one enabled) */
31
+ openPicker: () => void;
32
+
33
+ /** Whether the picker modal is visible */
34
+ isPickerVisible: boolean;
35
+
36
+ /** Close the picker modal */
37
+ closePicker: () => void;
38
+
39
+ /** Props to spread onto AttachmentPicker component */
40
+ pickerProps: Pick<
41
+ AttachmentPickerProps,
42
+ 'visible' | 'onClose' | 'onTakePhoto' | 'onPickFromLibrary' | 'onPickDocument' | 'options'
43
+ >;
44
+
45
+ /** Which sources are enabled */
46
+ enabledSources: {
47
+ camera: boolean;
48
+ library: boolean;
49
+ files: boolean;
50
+ };
51
+
52
+ /** Count of enabled sources */
53
+ enabledSourceCount: number;
54
+ }
55
+
56
+ /**
57
+ * Hook for managing the attachment picker with smart source selection.
58
+ *
59
+ * When only one source is enabled, calling `openPicker()` will skip the modal
60
+ * and directly open that source. When no sources are enabled, a warning is logged.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * // Allow only photo library
65
+ * const { openPicker, pickerProps, attachments } = useAttachmentPicker({
66
+ * camera: false,
67
+ * library: true,
68
+ * files: false,
69
+ * });
70
+ *
71
+ * // openPicker() will directly open photo library without showing modal
72
+ *
73
+ * return (
74
+ * <>
75
+ * <Button onPress={openPicker} title="Add Photo" />
76
+ * <AttachmentPicker {...pickerProps} />
77
+ * </>
78
+ * );
79
+ * ```
80
+ *
81
+ * @example
82
+ * ```tsx
83
+ * // With FeedbackSheet integration
84
+ * const {
85
+ * openPicker,
86
+ * pickerProps,
87
+ * attachments,
88
+ * removeAttachment,
89
+ * retryAttachment,
90
+ * } = useAttachmentPicker({
91
+ * camera: true,
92
+ * library: true,
93
+ * files: false, // Disable document picker
94
+ * });
95
+ * ```
96
+ */
97
+ export function useAttachmentPicker(
98
+ sourceConfig: AttachmentSourceConfig = {}
99
+ ): UseAttachmentPickerResult {
100
+ const {
101
+ camera: cameraEnabled = true,
102
+ library: libraryEnabled = true,
103
+ files: filesEnabled = true,
104
+ } = sourceConfig;
105
+
106
+ const [isPickerVisible, setIsPickerVisible] = useState(false);
107
+
108
+ const uploadResult = useAttachmentUpload();
109
+ const { pickImage, pickDocument } = uploadResult;
110
+
111
+ // Calculate enabled sources
112
+ const enabledSources = useMemo(
113
+ () => ({
114
+ camera: cameraEnabled,
115
+ library: libraryEnabled,
116
+ files: filesEnabled,
117
+ }),
118
+ [cameraEnabled, libraryEnabled, filesEnabled]
119
+ );
120
+
121
+ const enabledSourceCount = useMemo(() => {
122
+ let count = 0;
123
+ if (cameraEnabled) count++;
124
+ if (libraryEnabled) count++;
125
+ if (filesEnabled) count++;
126
+ return count;
127
+ }, [cameraEnabled, libraryEnabled, filesEnabled]);
128
+
129
+ // Auto-close picker if sources change to 0 or 1 while visible
130
+ // This prevents the iOS ActionSheet from showing with no/single options
131
+ useEffect(() => {
132
+ if (isPickerVisible && enabledSourceCount < 2) {
133
+ setIsPickerVisible(false);
134
+ }
135
+ }, [isPickerVisible, enabledSourceCount]);
136
+
137
+ // Handlers for each source
138
+ const handleTakePhoto = useCallback(async () => {
139
+ setIsPickerVisible(false);
140
+ try {
141
+ await pickImage('camera');
142
+ } catch (e) {
143
+ console.error('[Harken] Failed to take photo:', e);
144
+ }
145
+ }, [pickImage]);
146
+
147
+ const handlePickFromLibrary = useCallback(async () => {
148
+ setIsPickerVisible(false);
149
+ try {
150
+ await pickImage('library');
151
+ } catch (e) {
152
+ console.error('[Harken] Failed to pick from library:', e);
153
+ }
154
+ }, [pickImage]);
155
+
156
+ const handlePickDocument = useCallback(async () => {
157
+ setIsPickerVisible(false);
158
+ try {
159
+ await pickDocument();
160
+ } catch (e) {
161
+ console.error('[Harken] Failed to pick document:', e);
162
+ }
163
+ }, [pickDocument]);
164
+
165
+ const closePicker = useCallback(() => {
166
+ setIsPickerVisible(false);
167
+ }, []);
168
+
169
+ /**
170
+ * Smart picker opener:
171
+ * - 0 sources enabled: log warning, do nothing
172
+ * - 1 source enabled: directly invoke that source
173
+ * - 2+ sources enabled: show picker modal
174
+ */
175
+ const openPicker = useCallback(() => {
176
+ if (enabledSourceCount === 0) {
177
+ console.warn(
178
+ '[Harken] useAttachmentPicker: No attachment sources are enabled. ' +
179
+ 'Enable at least one of: camera, library, or files.'
180
+ );
181
+ return;
182
+ }
183
+
184
+ if (enabledSourceCount === 1) {
185
+ // Directly invoke the single enabled source
186
+ if (cameraEnabled) {
187
+ void handleTakePhoto();
188
+ } else if (libraryEnabled) {
189
+ void handlePickFromLibrary();
190
+ } else if (filesEnabled) {
191
+ void handlePickDocument();
192
+ }
193
+ return;
194
+ }
195
+
196
+ // Multiple sources enabled - show the picker
197
+ setIsPickerVisible(true);
198
+ }, [
199
+ enabledSourceCount,
200
+ cameraEnabled,
201
+ libraryEnabled,
202
+ filesEnabled,
203
+ handleTakePhoto,
204
+ handlePickFromLibrary,
205
+ handlePickDocument,
206
+ ]);
207
+
208
+ // Build picker props with hidden options for disabled sources
209
+ const pickerProps = useMemo(
210
+ () => ({
211
+ visible: isPickerVisible,
212
+ onClose: closePicker,
213
+ onTakePhoto: handleTakePhoto,
214
+ onPickFromLibrary: handlePickFromLibrary,
215
+ onPickDocument: handlePickDocument,
216
+ options: {
217
+ camera: { hidden: !cameraEnabled },
218
+ library: { hidden: !libraryEnabled },
219
+ document: { hidden: !filesEnabled },
220
+ },
221
+ }),
222
+ [
223
+ isPickerVisible,
224
+ closePicker,
225
+ handleTakePhoto,
226
+ handlePickFromLibrary,
227
+ handlePickDocument,
228
+ cameraEnabled,
229
+ libraryEnabled,
230
+ filesEnabled,
231
+ ]
232
+ );
233
+
234
+ return {
235
+ ...uploadResult,
236
+ openPicker,
237
+ isPickerVisible,
238
+ closePicker,
239
+ pickerProps,
240
+ enabledSources,
241
+ enabledSourceCount,
242
+ };
243
+ }