@harkenapp/sdk-react-native 0.0.1-alpha.1 → 0.0.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -7
- package/app.plugin.cjs +12 -17
- package/dist/__mocks__/async-storage.d.ts +16 -0
- package/dist/__mocks__/async-storage.d.ts.map +1 -0
- package/dist/__mocks__/async-storage.js +39 -0
- package/dist/__mocks__/async-storage.js.map +1 -0
- package/dist/__mocks__/expo-document-picker.d.ts +26 -0
- package/dist/__mocks__/expo-document-picker.d.ts.map +1 -0
- package/dist/__mocks__/expo-document-picker.js +25 -0
- package/dist/__mocks__/expo-document-picker.js.map +1 -0
- package/dist/__mocks__/expo-file-system.d.ts +42 -0
- package/dist/__mocks__/expo-file-system.d.ts.map +1 -0
- package/dist/__mocks__/expo-file-system.js +37 -0
- package/dist/__mocks__/expo-file-system.js.map +1 -0
- package/dist/__mocks__/expo-image-picker.d.ts +30 -0
- package/dist/__mocks__/expo-image-picker.d.ts.map +1 -0
- package/dist/__mocks__/expo-image-picker.js +30 -0
- package/dist/__mocks__/expo-image-picker.js.map +1 -0
- package/dist/__mocks__/expo-secure-store.d.ts +15 -0
- package/dist/__mocks__/expo-secure-store.d.ts.map +1 -0
- package/dist/__mocks__/expo-secure-store.js +30 -0
- package/dist/__mocks__/expo-secure-store.js.map +1 -0
- package/dist/__mocks__/react-native.d.ts +73 -0
- package/dist/__mocks__/react-native.d.ts.map +1 -0
- package/dist/__mocks__/react-native.js +45 -0
- package/dist/__mocks__/react-native.js.map +1 -0
- package/dist/api/client.d.ts +8 -8
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +17 -19
- package/dist/api/client.js.map +1 -1
- package/dist/api/client.test.d.ts +2 -0
- package/dist/api/client.test.d.ts.map +1 -0
- package/dist/api/client.test.js +417 -0
- package/dist/api/client.test.js.map +1 -0
- package/dist/api/errors.d.ts +3 -3
- package/dist/api/errors.d.ts.map +1 -1
- package/dist/api/errors.js +3 -3
- package/dist/api/errors.js.map +1 -1
- package/dist/api/errors.test.d.ts +2 -0
- package/dist/api/errors.test.d.ts.map +1 -0
- package/dist/api/errors.test.js +155 -0
- package/dist/api/errors.test.js.map +1 -0
- package/dist/api/index.d.ts +6 -6
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/retry.d.ts +1 -1
- package/dist/api/retry.d.ts.map +1 -1
- package/dist/api/retry.js.map +1 -1
- package/dist/api/retry.test.d.ts +2 -0
- package/dist/api/retry.test.d.ts.map +1 -0
- package/dist/api/retry.test.js +193 -0
- package/dist/api/retry.test.js.map +1 -0
- package/dist/attachments/FeedbackSheet.d.ts +36 -13
- package/dist/attachments/FeedbackSheet.d.ts.map +1 -1
- package/dist/attachments/FeedbackSheet.js +50 -30
- package/dist/attachments/FeedbackSheet.js.map +1 -1
- package/dist/attachments/index.d.ts +2 -2
- package/dist/components/AttachmentGrid.d.ts +12 -4
- package/dist/components/AttachmentGrid.d.ts.map +1 -1
- package/dist/components/AttachmentGrid.js +44 -34
- package/dist/components/AttachmentGrid.js.map +1 -1
- package/dist/components/AttachmentPicker.d.ts +3 -3
- package/dist/components/AttachmentPicker.d.ts.map +1 -1
- package/dist/components/AttachmentPicker.js +34 -36
- package/dist/components/AttachmentPicker.js.map +1 -1
- package/dist/components/AttachmentPreview.d.ts +10 -4
- package/dist/components/AttachmentPreview.d.ts.map +1 -1
- package/dist/components/AttachmentPreview.js +48 -34
- package/dist/components/AttachmentPreview.js.map +1 -1
- package/dist/components/CategorySelector.d.ts +3 -3
- package/dist/components/CategorySelector.d.ts.map +1 -1
- package/dist/components/CategorySelector.js +21 -27
- package/dist/components/CategorySelector.js.map +1 -1
- package/dist/components/FeedbackForm.d.ts +3 -3
- package/dist/components/FeedbackForm.d.ts.map +1 -1
- package/dist/components/FeedbackForm.js +7 -8
- package/dist/components/FeedbackForm.js.map +1 -1
- package/dist/components/FeedbackSheet.d.ts +34 -11
- package/dist/components/FeedbackSheet.d.ts.map +1 -1
- package/dist/components/FeedbackSheet.js +46 -28
- package/dist/components/FeedbackSheet.js.map +1 -1
- package/dist/components/ThemedButton.d.ts +16 -5
- package/dist/components/ThemedButton.d.ts.map +1 -1
- package/dist/components/ThemedButton.js +38 -29
- package/dist/components/ThemedButton.js.map +1 -1
- package/dist/components/ThemedText.d.ts +3 -3
- package/dist/components/ThemedText.d.ts.map +1 -1
- package/dist/components/ThemedText.js +1 -1
- package/dist/components/ThemedText.js.map +1 -1
- package/dist/components/ThemedTextInput.d.ts +11 -2
- package/dist/components/ThemedTextInput.d.ts.map +1 -1
- package/dist/components/ThemedTextInput.js +19 -9
- package/dist/components/ThemedTextInput.js.map +1 -1
- package/dist/components/UploadStatusOverlay.d.ts +11 -3
- package/dist/components/UploadStatusOverlay.d.ts.map +1 -1
- package/dist/components/UploadStatusOverlay.js +59 -76
- package/dist/components/UploadStatusOverlay.js.map +1 -1
- package/dist/components/index.d.ts +18 -18
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/context/HarkenContext.d.ts +20 -15
- package/dist/context/HarkenContext.d.ts.map +1 -1
- package/dist/context/HarkenContext.js +20 -17
- package/dist/context/HarkenContext.js.map +1 -1
- package/dist/context/index.d.ts +2 -2
- package/dist/domain/index.d.ts +2 -2
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js.map +1 -1
- package/dist/hooks/index.d.ts +5 -5
- package/dist/hooks/useAnonymousId.js +1 -1
- package/dist/hooks/useAnonymousId.test.d.ts +2 -0
- package/dist/hooks/useAnonymousId.test.d.ts.map +1 -0
- package/dist/hooks/useAnonymousId.test.js +154 -0
- package/dist/hooks/useAnonymousId.test.js.map +1 -0
- package/dist/hooks/useAttachmentPicker.d.ts +3 -3
- package/dist/hooks/useAttachmentPicker.js +7 -7
- package/dist/hooks/useAttachmentStatus.d.ts +1 -1
- package/dist/hooks/useAttachmentStatus.d.ts.map +1 -1
- package/dist/hooks/useAttachmentStatus.js.map +1 -1
- package/dist/hooks/useAttachmentUpload.d.ts +2 -2
- package/dist/hooks/useAttachmentUpload.d.ts.map +1 -1
- package/dist/hooks/useAttachmentUpload.js +5 -5
- package/dist/hooks/useAttachmentUpload.js.map +1 -1
- package/dist/hooks/useAttachmentUpload.test.d.ts +2 -0
- package/dist/hooks/useAttachmentUpload.test.d.ts.map +1 -0
- package/dist/hooks/useAttachmentUpload.test.js +542 -0
- package/dist/hooks/useAttachmentUpload.test.js.map +1 -0
- package/dist/hooks/useFeedback.d.ts +4 -4
- package/dist/hooks/useFeedback.d.ts.map +1 -1
- package/dist/hooks/useFeedback.js +3 -5
- package/dist/hooks/useFeedback.js.map +1 -1
- package/dist/hooks/useFeedback.test.d.ts +2 -0
- package/dist/hooks/useFeedback.test.d.ts.map +1 -0
- package/dist/hooks/useFeedback.test.js +299 -0
- package/dist/hooks/useFeedback.test.js.map +1 -0
- package/dist/hooks/useHarkenContext.d.ts +1 -1
- package/dist/hooks/useHarkenContext.js +1 -1
- package/dist/hooks/useHarkenTheme.d.ts +27 -3
- package/dist/hooks/useHarkenTheme.d.ts.map +1 -1
- package/dist/hooks/useHarkenTheme.js +26 -2
- package/dist/hooks/useHarkenTheme.js.map +1 -1
- package/dist/index.d.ts +28 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/index.d.ts +3 -3
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/uploadQueueService.d.ts +2 -2
- package/dist/services/uploadQueueService.d.ts.map +1 -1
- package/dist/services/uploadQueueService.js +16 -17
- package/dist/services/uploadQueueService.js.map +1 -1
- package/dist/services/uploadQueueService.test.d.ts +2 -0
- package/dist/services/uploadQueueService.test.d.ts.map +1 -0
- package/dist/services/uploadQueueService.test.js +426 -0
- package/dist/services/uploadQueueService.test.js.map +1 -0
- package/dist/services/uploadQueueStorage.d.ts +1 -1
- package/dist/services/uploadQueueStorage.d.ts.map +1 -1
- package/dist/services/uploadQueueStorage.js +4 -4
- package/dist/services/uploadQueueStorage.js.map +1 -1
- package/dist/services/uploadQueueStorage.test.d.ts +2 -0
- package/dist/services/uploadQueueStorage.test.d.ts.map +1 -0
- package/dist/services/uploadQueueStorage.test.js +200 -0
- package/dist/services/uploadQueueStorage.test.js.map +1 -0
- package/dist/storage/IdentityStore.d.ts +1 -1
- package/dist/storage/IdentityStore.d.ts.map +1 -1
- package/dist/storage/IdentityStore.js.map +1 -1
- package/dist/storage/IdentityStore.test.d.ts +2 -0
- package/dist/storage/IdentityStore.test.d.ts.map +1 -0
- package/dist/storage/IdentityStore.test.js +176 -0
- package/dist/storage/IdentityStore.test.js.map +1 -0
- package/dist/storage/SecureStoreAdapter.d.ts +1 -1
- package/dist/storage/SecureStoreAdapter.test.d.ts +2 -0
- package/dist/storage/SecureStoreAdapter.test.d.ts.map +1 -0
- package/dist/storage/SecureStoreAdapter.test.js +114 -0
- package/dist/storage/SecureStoreAdapter.test.js.map +1 -0
- package/dist/storage/defaultStorage.d.ts +1 -1
- package/dist/storage/defaultStorage.js +4 -4
- package/dist/storage/defaultStorage.test.d.ts +2 -0
- package/dist/storage/defaultStorage.test.d.ts.map +1 -0
- package/dist/storage/defaultStorage.test.js +159 -0
- package/dist/storage/defaultStorage.test.js.map +1 -0
- package/dist/storage/index.d.ts +5 -5
- package/dist/storage/types.js +1 -1
- package/dist/theme/defaults.d.ts +14 -3
- package/dist/theme/defaults.d.ts.map +1 -1
- package/dist/theme/defaults.js +58 -43
- package/dist/theme/defaults.js.map +1 -1
- package/dist/theme/index.d.ts +3 -2
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +4 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/resolver.d.ts +16 -0
- package/dist/theme/resolver.d.ts.map +1 -0
- package/dist/theme/resolver.js +375 -0
- package/dist/theme/resolver.js.map +1 -0
- package/dist/theme/resolver.test.d.ts +2 -0
- package/dist/theme/resolver.test.d.ts.map +1 -0
- package/dist/theme/resolver.test.js +344 -0
- package/dist/theme/resolver.test.js.map +1 -0
- package/dist/theme/types.d.ts +378 -5
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/types/config.d.ts +4 -4
- package/dist/types/index.d.ts +2 -2
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/uuid.d.ts.map +1 -1
- package/dist/utils/uuid.js +4 -5
- package/dist/utils/uuid.js.map +1 -1
- package/dist/utils/uuid.test.d.ts +2 -0
- package/dist/utils/uuid.test.d.ts.map +1 -0
- package/dist/utils/uuid.test.js +78 -0
- package/dist/utils/uuid.test.js.map +1 -0
- package/package.json +21 -13
- package/src/@types/expo-file-system-legacy.d.ts +3 -3
- package/src/__mocks__/async-storage.ts +46 -0
- package/src/__mocks__/expo-document-picker.ts +41 -0
- package/src/__mocks__/expo-file-system.ts +62 -0
- package/src/__mocks__/expo-image-picker.ts +48 -0
- package/src/__mocks__/expo-secure-store.ts +29 -0
- package/src/__mocks__/react-native.ts +46 -0
- package/src/api/client.test.ts +515 -0
- package/src/api/client.ts +45 -64
- package/src/api/errors.test.ts +193 -0
- package/src/api/errors.ts +7 -11
- package/src/api/index.ts +6 -10
- package/src/api/retry.test.ts +251 -0
- package/src/api/retry.ts +3 -6
- package/src/attachments/FeedbackSheet.tsx +100 -80
- package/src/attachments/index.ts +2 -2
- package/src/components/AttachmentGrid.tsx +54 -45
- package/src/components/AttachmentPicker.tsx +43 -54
- package/src/components/AttachmentPreview.tsx +51 -47
- package/src/components/CategorySelector.tsx +29 -35
- package/src/components/FeedbackForm.tsx +23 -35
- package/src/components/FeedbackSheet.tsx +89 -68
- package/src/components/ThemedButton.tsx +49 -47
- package/src/components/ThemedText.tsx +7 -10
- package/src/components/ThemedTextInput.tsx +23 -13
- package/src/components/UploadStatusOverlay.tsx +66 -89
- package/src/components/index.ts +18 -21
- package/src/context/HarkenContext.tsx +29 -28
- package/src/context/index.ts +2 -2
- package/src/domain/index.ts +2 -5
- package/src/domain/upload-queue.ts +5 -5
- package/src/hooks/index.ts +5 -5
- package/src/hooks/useAnonymousId.test.ts +189 -0
- package/src/hooks/useAnonymousId.ts +3 -3
- package/src/hooks/useAttachmentPicker.ts +12 -12
- package/src/hooks/useAttachmentStatus.ts +12 -16
- package/src/hooks/useAttachmentUpload.test.ts +632 -0
- package/src/hooks/useAttachmentUpload.ts +45 -54
- package/src/hooks/useFeedback.test.ts +376 -0
- package/src/hooks/useFeedback.ts +12 -14
- package/src/hooks/useHarkenContext.ts +4 -4
- package/src/hooks/useHarkenTheme.ts +30 -6
- package/src/index.ts +28 -52
- package/src/services/index.ts +3 -9
- package/src/services/uploadQueueService.test.ts +489 -0
- package/src/services/uploadQueueService.ts +40 -56
- package/src/services/uploadQueueStorage.test.ts +243 -0
- package/src/services/uploadQueueStorage.ts +7 -9
- package/src/storage/IdentityStore.test.ts +173 -0
- package/src/storage/IdentityStore.ts +4 -5
- package/src/storage/SecureStoreAdapter.test.ts +147 -0
- package/src/storage/SecureStoreAdapter.ts +1 -1
- package/src/storage/defaultStorage.test.ts +159 -0
- package/src/storage/defaultStorage.ts +6 -6
- package/src/storage/index.ts +5 -5
- package/src/storage/types.ts +1 -1
- package/src/theme/defaults.ts +75 -46
- package/src/theme/index.ts +15 -2
- package/src/theme/resolver.test.ts +411 -0
- package/src/theme/resolver.ts +446 -0
- package/src/theme/types.ts +453 -15
- package/src/types/config.ts +4 -4
- package/src/types/index.ts +2 -2
- package/src/utils/index.ts +1 -1
- package/src/utils/uuid.test.ts +85 -0
- package/src/utils/uuid.ts +4 -7
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
* Uploads happen in background via the singleton uploadQueueService.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState, useEffect, useCallback, useRef } from
|
|
9
|
-
import * as ImagePicker from
|
|
10
|
-
import * as DocumentPicker from
|
|
11
|
-
import * as FileSystem from
|
|
12
|
-
import { uploadQueueService } from
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
8
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
9
|
+
import * as ImagePicker from "expo-image-picker";
|
|
10
|
+
import * as DocumentPicker from "expo-document-picker";
|
|
11
|
+
import * as FileSystem from "expo-file-system/legacy";
|
|
12
|
+
import { uploadQueueService } from "../services";
|
|
13
|
+
import type { UploadProgress } from "../domain";
|
|
14
|
+
import { UploadPhase } from "../domain";
|
|
15
|
+
import { useHarkenContext } from "./useHarkenContext";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* State for a single attachment.
|
|
@@ -41,7 +42,7 @@ export interface UseAttachmentUploadResult {
|
|
|
41
42
|
attachments: AttachmentState[];
|
|
42
43
|
|
|
43
44
|
/** Pick image from camera or library */
|
|
44
|
-
pickImage: (source:
|
|
45
|
+
pickImage: (source: "camera" | "library") => Promise<AttachmentState | null>;
|
|
45
46
|
|
|
46
47
|
/** Pick document (images or PDFs) */
|
|
47
48
|
pickDocument: () => Promise<AttachmentState | null>;
|
|
@@ -118,9 +119,7 @@ export interface UseAttachmentUploadResult {
|
|
|
118
119
|
*/
|
|
119
120
|
export function useAttachmentUpload(): UseAttachmentUploadResult {
|
|
120
121
|
const { client, config } = useHarkenContext();
|
|
121
|
-
const [attachments, setAttachments] = useState<Map<string, AttachmentState>>(
|
|
122
|
-
new Map()
|
|
123
|
-
);
|
|
122
|
+
const [attachments, setAttachments] = useState<Map<string, AttachmentState>>(new Map());
|
|
124
123
|
|
|
125
124
|
// Track which attachment IDs this hook instance is managing
|
|
126
125
|
const attachmentIdsRef = useRef<Set<string>>(new Set());
|
|
@@ -142,26 +141,24 @@ export function useAttachmentUpload(): UseAttachmentUploadResult {
|
|
|
142
141
|
return;
|
|
143
142
|
}
|
|
144
143
|
|
|
145
|
-
const unsubProgress = uploadQueueService.onProgress(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
error: progress.error,
|
|
160
|
-
});
|
|
161
|
-
return next;
|
|
144
|
+
const unsubProgress = uploadQueueService.onProgress((progress: UploadProgress) => {
|
|
145
|
+
// Only track attachments we added
|
|
146
|
+
if (!attachmentIdsRef.current.has(progress.attachmentId)) return;
|
|
147
|
+
|
|
148
|
+
setAttachments((prev) => {
|
|
149
|
+
const existing = prev.get(progress.attachmentId);
|
|
150
|
+
if (!existing) return prev;
|
|
151
|
+
|
|
152
|
+
const next = new Map(prev);
|
|
153
|
+
next.set(progress.attachmentId, {
|
|
154
|
+
...existing,
|
|
155
|
+
phase: progress.phase,
|
|
156
|
+
progress: progress.progress,
|
|
157
|
+
error: progress.error,
|
|
162
158
|
});
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
return next;
|
|
160
|
+
});
|
|
161
|
+
});
|
|
165
162
|
|
|
166
163
|
return () => {
|
|
167
164
|
unsubProgress();
|
|
@@ -206,14 +203,14 @@ export function useAttachmentUpload(): UseAttachmentUploadResult {
|
|
|
206
203
|
* Pick an image from camera or photo library.
|
|
207
204
|
*/
|
|
208
205
|
const pickImage = useCallback(
|
|
209
|
-
async (source:
|
|
206
|
+
async (source: "camera" | "library"): Promise<AttachmentState | null> => {
|
|
210
207
|
const options: ImagePicker.ImagePickerOptions = {
|
|
211
|
-
mediaTypes: [
|
|
208
|
+
mediaTypes: ["images"],
|
|
212
209
|
quality: 0.8,
|
|
213
210
|
};
|
|
214
211
|
|
|
215
212
|
const result =
|
|
216
|
-
source ===
|
|
213
|
+
source === "camera"
|
|
217
214
|
? await ImagePicker.launchCameraAsync(options)
|
|
218
215
|
: await ImagePicker.launchImageLibraryAsync(options);
|
|
219
216
|
|
|
@@ -223,7 +220,7 @@ export function useAttachmentUpload(): UseAttachmentUploadResult {
|
|
|
223
220
|
|
|
224
221
|
const asset = result.assets[0];
|
|
225
222
|
const fileName = asset.fileName ?? `image_${Date.now()}.jpg`;
|
|
226
|
-
const mimeType = asset.mimeType ??
|
|
223
|
+
const mimeType = asset.mimeType ?? "image/jpeg";
|
|
227
224
|
|
|
228
225
|
// Get file size - use asset.fileSize if available, otherwise query filesystem
|
|
229
226
|
let fileSize = asset.fileSize;
|
|
@@ -247,7 +244,7 @@ export function useAttachmentUpload(): UseAttachmentUploadResult {
|
|
|
247
244
|
*/
|
|
248
245
|
const pickDocument = useCallback(async (): Promise<AttachmentState | null> => {
|
|
249
246
|
const result = await DocumentPicker.getDocumentAsync({
|
|
250
|
-
type: [
|
|
247
|
+
type: ["image/*", "application/pdf"],
|
|
251
248
|
copyToCacheDirectory: true,
|
|
252
249
|
});
|
|
253
250
|
|
|
@@ -266,7 +263,7 @@ export function useAttachmentUpload(): UseAttachmentUploadResult {
|
|
|
266
263
|
|
|
267
264
|
return addAttachment({
|
|
268
265
|
uri: asset.uri,
|
|
269
|
-
mimeType: asset.mimeType ??
|
|
266
|
+
mimeType: asset.mimeType ?? "application/octet-stream",
|
|
270
267
|
fileName: asset.name,
|
|
271
268
|
fileSize,
|
|
272
269
|
});
|
|
@@ -275,28 +272,22 @@ export function useAttachmentUpload(): UseAttachmentUploadResult {
|
|
|
275
272
|
/**
|
|
276
273
|
* Retry a failed attachment upload.
|
|
277
274
|
*/
|
|
278
|
-
const retryAttachment = useCallback(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
},
|
|
282
|
-
[]
|
|
283
|
-
);
|
|
275
|
+
const retryAttachment = useCallback(async (attachmentId: string): Promise<void> => {
|
|
276
|
+
await uploadQueueService.retryItem(attachmentId);
|
|
277
|
+
}, []);
|
|
284
278
|
|
|
285
279
|
/**
|
|
286
280
|
* Remove an attachment (cancels upload if in progress).
|
|
287
281
|
*/
|
|
288
|
-
const removeAttachment = useCallback(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
},
|
|
298
|
-
[]
|
|
299
|
-
);
|
|
282
|
+
const removeAttachment = useCallback(async (attachmentId: string): Promise<void> => {
|
|
283
|
+
await uploadQueueService.cancelItem(attachmentId);
|
|
284
|
+
attachmentIdsRef.current.delete(attachmentId);
|
|
285
|
+
setAttachments((prev) => {
|
|
286
|
+
const next = new Map(prev);
|
|
287
|
+
next.delete(attachmentId);
|
|
288
|
+
return next;
|
|
289
|
+
});
|
|
290
|
+
}, []);
|
|
300
291
|
|
|
301
292
|
/**
|
|
302
293
|
* Get all attachment IDs for feedback submission.
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
5
|
+
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
6
|
+
import { useFeedback } from "./useFeedback";
|
|
7
|
+
import { HarkenApiError, HarkenNetworkError } from "../api/errors";
|
|
8
|
+
import type { FeedbackCategory } from "../types";
|
|
9
|
+
|
|
10
|
+
// Mock dependencies
|
|
11
|
+
vi.mock("react-native", () => ({
|
|
12
|
+
Platform: { OS: "ios" },
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockSubmitFeedback = vi.fn();
|
|
16
|
+
|
|
17
|
+
vi.mock("../api/client", () => ({
|
|
18
|
+
HarkenClient: class MockHarkenClient {
|
|
19
|
+
submitFeedback = mockSubmitFeedback;
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("./useHarkenContext", () => ({
|
|
24
|
+
useHarkenContext: vi.fn(() => ({
|
|
25
|
+
config: {
|
|
26
|
+
publishableKey: "pk_test_123",
|
|
27
|
+
userToken: undefined,
|
|
28
|
+
apiBaseUrl: "https://api.harken.app",
|
|
29
|
+
},
|
|
30
|
+
})),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
import { useAnonymousId } from "./useAnonymousId";
|
|
34
|
+
|
|
35
|
+
vi.mock("./useAnonymousId", () => ({
|
|
36
|
+
useAnonymousId: vi.fn(() => ({
|
|
37
|
+
anonymousId: "test-anon-id-123",
|
|
38
|
+
isLoading: false,
|
|
39
|
+
})),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe("useFeedback", () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("payload construction", () => {
|
|
48
|
+
it("sends correct payload to client", async () => {
|
|
49
|
+
mockSubmitFeedback.mockResolvedValue({ id: "feedback_123" });
|
|
50
|
+
|
|
51
|
+
const { result } = renderHook(() => useFeedback());
|
|
52
|
+
|
|
53
|
+
await act(async () => {
|
|
54
|
+
await result.current.submitFeedback({
|
|
55
|
+
message: "Great app!",
|
|
56
|
+
category: "idea" as FeedbackCategory,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(mockSubmitFeedback).toHaveBeenCalledWith({
|
|
61
|
+
message: "Great app!",
|
|
62
|
+
category: "idea",
|
|
63
|
+
title: undefined,
|
|
64
|
+
anon_id: "test-anon-id-123",
|
|
65
|
+
metadata: { platform: "ios" },
|
|
66
|
+
attachments: undefined,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("includes title when provided", async () => {
|
|
71
|
+
mockSubmitFeedback.mockResolvedValue({ id: "feedback_123" });
|
|
72
|
+
|
|
73
|
+
const { result } = renderHook(() => useFeedback());
|
|
74
|
+
|
|
75
|
+
await act(async () => {
|
|
76
|
+
await result.current.submitFeedback({
|
|
77
|
+
message: "Bug description",
|
|
78
|
+
category: "bug" as FeedbackCategory,
|
|
79
|
+
title: "Login issue",
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(mockSubmitFeedback).toHaveBeenCalledWith(
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
title: "Login issue",
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("includes attachments when provided", async () => {
|
|
91
|
+
mockSubmitFeedback.mockResolvedValue({ id: "feedback_123" });
|
|
92
|
+
|
|
93
|
+
const { result } = renderHook(() => useFeedback());
|
|
94
|
+
|
|
95
|
+
await act(async () => {
|
|
96
|
+
await result.current.submitFeedback({
|
|
97
|
+
message: "See attached screenshot",
|
|
98
|
+
category: "bug" as FeedbackCategory,
|
|
99
|
+
attachments: ["att_123", "att_456"],
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(mockSubmitFeedback).toHaveBeenCalledWith(
|
|
104
|
+
expect.objectContaining({
|
|
105
|
+
attachments: ["att_123", "att_456"],
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("merges custom metadata with platform", async () => {
|
|
111
|
+
mockSubmitFeedback.mockResolvedValue({ id: "feedback_123" });
|
|
112
|
+
|
|
113
|
+
const { result } = renderHook(() => useFeedback());
|
|
114
|
+
|
|
115
|
+
await act(async () => {
|
|
116
|
+
await result.current.submitFeedback({
|
|
117
|
+
message: "Test",
|
|
118
|
+
category: "other" as FeedbackCategory,
|
|
119
|
+
metadata: { app_version: "1.2.3", screen: "settings" },
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(mockSubmitFeedback).toHaveBeenCalledWith(
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
metadata: {
|
|
126
|
+
platform: "ios",
|
|
127
|
+
app_version: "1.2.3",
|
|
128
|
+
screen: "settings",
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("loading state", () => {
|
|
136
|
+
it("sets isSubmitting true during submission", async () => {
|
|
137
|
+
let resolveSubmit: (value: unknown) => void;
|
|
138
|
+
const submitPromise = new Promise((resolve) => {
|
|
139
|
+
resolveSubmit = resolve;
|
|
140
|
+
});
|
|
141
|
+
mockSubmitFeedback.mockReturnValue(submitPromise);
|
|
142
|
+
|
|
143
|
+
const { result } = renderHook(() => useFeedback());
|
|
144
|
+
|
|
145
|
+
expect(result.current.isSubmitting).toBe(false);
|
|
146
|
+
|
|
147
|
+
let submitPromiseResult: Promise<unknown>;
|
|
148
|
+
act(() => {
|
|
149
|
+
submitPromiseResult = result.current.submitFeedback({
|
|
150
|
+
message: "Test",
|
|
151
|
+
category: "idea" as FeedbackCategory,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(result.current.isSubmitting).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await act(async () => {
|
|
160
|
+
resolveSubmit!({ id: "feedback_123" });
|
|
161
|
+
await submitPromiseResult;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(result.current.isSubmitting).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("sets isSubmitting false after error", async () => {
|
|
168
|
+
mockSubmitFeedback.mockRejectedValue(
|
|
169
|
+
new HarkenApiError(400, { error: { code: "validation_error", message: "Invalid" } })
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const { result } = renderHook(() => useFeedback());
|
|
173
|
+
|
|
174
|
+
await act(async () => {
|
|
175
|
+
try {
|
|
176
|
+
await result.current.submitFeedback({
|
|
177
|
+
message: "Test",
|
|
178
|
+
category: "idea" as FeedbackCategory,
|
|
179
|
+
});
|
|
180
|
+
} catch {
|
|
181
|
+
// Expected
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(result.current.isSubmitting).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("error handling", () => {
|
|
190
|
+
it("sets error state on API failure", async () => {
|
|
191
|
+
const apiError = new HarkenApiError(400, {
|
|
192
|
+
error: { code: "validation_error", message: "Message is required" },
|
|
193
|
+
});
|
|
194
|
+
mockSubmitFeedback.mockRejectedValue(apiError);
|
|
195
|
+
|
|
196
|
+
const { result } = renderHook(() => useFeedback());
|
|
197
|
+
|
|
198
|
+
await act(async () => {
|
|
199
|
+
try {
|
|
200
|
+
await result.current.submitFeedback({
|
|
201
|
+
message: "",
|
|
202
|
+
category: "idea" as FeedbackCategory,
|
|
203
|
+
});
|
|
204
|
+
} catch {
|
|
205
|
+
// Expected
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(result.current.error).toBe(apiError);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("sets error state on network failure", async () => {
|
|
213
|
+
const networkError = new HarkenNetworkError("Network request failed");
|
|
214
|
+
mockSubmitFeedback.mockRejectedValue(networkError);
|
|
215
|
+
|
|
216
|
+
const { result } = renderHook(() => useFeedback());
|
|
217
|
+
|
|
218
|
+
await act(async () => {
|
|
219
|
+
try {
|
|
220
|
+
await result.current.submitFeedback({
|
|
221
|
+
message: "Test",
|
|
222
|
+
category: "idea" as FeedbackCategory,
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
// Expected
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(result.current.error).toBe(networkError);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("wraps unknown errors in HarkenNetworkError", async () => {
|
|
233
|
+
mockSubmitFeedback.mockRejectedValue(new Error("Something went wrong"));
|
|
234
|
+
|
|
235
|
+
const { result } = renderHook(() => useFeedback());
|
|
236
|
+
|
|
237
|
+
await act(async () => {
|
|
238
|
+
try {
|
|
239
|
+
await result.current.submitFeedback({
|
|
240
|
+
message: "Test",
|
|
241
|
+
category: "idea" as FeedbackCategory,
|
|
242
|
+
});
|
|
243
|
+
} catch {
|
|
244
|
+
// Expected
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(result.current.error).toBeInstanceOf(HarkenNetworkError);
|
|
249
|
+
expect(result.current.error?.message).toBe("Something went wrong");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("clears error on successful submission", async () => {
|
|
253
|
+
// First, cause an error
|
|
254
|
+
mockSubmitFeedback.mockRejectedValueOnce(
|
|
255
|
+
new HarkenApiError(500, { error: { code: "server_error", message: "Error" } })
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const { result } = renderHook(() => useFeedback());
|
|
259
|
+
|
|
260
|
+
await act(async () => {
|
|
261
|
+
try {
|
|
262
|
+
await result.current.submitFeedback({
|
|
263
|
+
message: "Test",
|
|
264
|
+
category: "idea" as FeedbackCategory,
|
|
265
|
+
});
|
|
266
|
+
} catch {
|
|
267
|
+
// Expected
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(result.current.error).not.toBeNull();
|
|
272
|
+
|
|
273
|
+
// Now succeed
|
|
274
|
+
mockSubmitFeedback.mockResolvedValueOnce({ id: "feedback_123" });
|
|
275
|
+
|
|
276
|
+
await act(async () => {
|
|
277
|
+
await result.current.submitFeedback({
|
|
278
|
+
message: "Test",
|
|
279
|
+
category: "idea" as FeedbackCategory,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(result.current.error).toBeNull();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("clearError clears the error state", async () => {
|
|
287
|
+
mockSubmitFeedback.mockRejectedValue(
|
|
288
|
+
new HarkenApiError(400, { error: { code: "error", message: "Error" } })
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const { result } = renderHook(() => useFeedback());
|
|
292
|
+
|
|
293
|
+
await act(async () => {
|
|
294
|
+
try {
|
|
295
|
+
await result.current.submitFeedback({
|
|
296
|
+
message: "Test",
|
|
297
|
+
category: "idea" as FeedbackCategory,
|
|
298
|
+
});
|
|
299
|
+
} catch {
|
|
300
|
+
// Expected
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(result.current.error).not.toBeNull();
|
|
305
|
+
|
|
306
|
+
act(() => {
|
|
307
|
+
result.current.clearError();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(result.current.error).toBeNull();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("successful submission", () => {
|
|
315
|
+
it("returns response from client", async () => {
|
|
316
|
+
const expectedResponse = { id: "feedback_123", status: "received" };
|
|
317
|
+
mockSubmitFeedback.mockResolvedValue(expectedResponse);
|
|
318
|
+
|
|
319
|
+
const { result } = renderHook(() => useFeedback());
|
|
320
|
+
|
|
321
|
+
let response: unknown;
|
|
322
|
+
await act(async () => {
|
|
323
|
+
response = await result.current.submitFeedback({
|
|
324
|
+
message: "Great app!",
|
|
325
|
+
category: "praise" as FeedbackCategory,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(response).toEqual(expectedResponse);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("anonymousId requirement", () => {
|
|
334
|
+
it("throws when anonymousId is not available", async () => {
|
|
335
|
+
// Override mock to return null anonymousId
|
|
336
|
+
vi.mocked(useAnonymousId).mockReturnValue({
|
|
337
|
+
anonymousId: null,
|
|
338
|
+
isLoading: false,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const { result } = renderHook(() => useFeedback());
|
|
342
|
+
|
|
343
|
+
await expect(
|
|
344
|
+
act(async () => {
|
|
345
|
+
await result.current.submitFeedback({
|
|
346
|
+
message: "Test",
|
|
347
|
+
category: "idea" as FeedbackCategory,
|
|
348
|
+
});
|
|
349
|
+
})
|
|
350
|
+
).rejects.toThrow("Anonymous ID not yet initialized");
|
|
351
|
+
|
|
352
|
+
// Restore default mock
|
|
353
|
+
vi.mocked(useAnonymousId).mockReturnValue({
|
|
354
|
+
anonymousId: "test-anon-id-123",
|
|
355
|
+
isLoading: false,
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("exposes isInitializing from useAnonymousId", () => {
|
|
360
|
+
vi.mocked(useAnonymousId).mockReturnValue({
|
|
361
|
+
anonymousId: null,
|
|
362
|
+
isLoading: true,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const { result } = renderHook(() => useFeedback());
|
|
366
|
+
|
|
367
|
+
expect(result.current.isInitializing).toBe(true);
|
|
368
|
+
|
|
369
|
+
// Restore default mock
|
|
370
|
+
vi.mocked(useAnonymousId).mockReturnValue({
|
|
371
|
+
anonymousId: "test-anon-id-123",
|
|
372
|
+
isLoading: false,
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
package/src/hooks/useFeedback.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { useState, useCallback, useMemo } from
|
|
2
|
-
import { Platform } from
|
|
3
|
-
import type { components } from
|
|
4
|
-
import { useHarkenContext } from
|
|
5
|
-
import { useAnonymousId } from
|
|
6
|
-
import { HarkenClient } from
|
|
7
|
-
import { HarkenApiError, HarkenNetworkError } from
|
|
8
|
-
import type { FeedbackCategory, DeviceMetadata } from
|
|
1
|
+
import { useState, useCallback, useMemo } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import type { components } from "../types/index.js";
|
|
4
|
+
import { useHarkenContext } from "./useHarkenContext";
|
|
5
|
+
import { useAnonymousId } from "./useAnonymousId";
|
|
6
|
+
import { HarkenClient } from "../api/client";
|
|
7
|
+
import { HarkenApiError, HarkenNetworkError } from "../api/errors";
|
|
8
|
+
import type { FeedbackCategory, DeviceMetadata } from "../types";
|
|
9
9
|
|
|
10
|
-
type FeedbackSubmissionResponse = components[
|
|
10
|
+
type FeedbackSubmissionResponse = components["schemas"]["FeedbackSubmissionResponse"];
|
|
11
11
|
|
|
12
12
|
export interface SubmitFeedbackParams {
|
|
13
13
|
/** Feedback message content */
|
|
@@ -82,7 +82,7 @@ export function useFeedback(): UseFeedbackResult {
|
|
|
82
82
|
const submitFeedback = useCallback(
|
|
83
83
|
async (params: SubmitFeedbackParams): Promise<FeedbackSubmissionResponse> => {
|
|
84
84
|
if (!anonymousId) {
|
|
85
|
-
throw new Error(
|
|
85
|
+
throw new Error("Anonymous ID not yet initialized. Wait for isInitializing to be false.");
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
setIsSubmitting(true);
|
|
@@ -93,9 +93,7 @@ export function useFeedback(): UseFeedbackResult {
|
|
|
93
93
|
// Only set platform if it's a known value (ios, android)
|
|
94
94
|
// Other platforms (web, windows, macos) should be passed via metadata
|
|
95
95
|
const detectedPlatform =
|
|
96
|
-
Platform.OS ===
|
|
97
|
-
Platform.OS === 'android' ? 'android' :
|
|
98
|
-
undefined;
|
|
96
|
+
Platform.OS === "ios" ? "ios" : Platform.OS === "android" ? "android" : undefined;
|
|
99
97
|
|
|
100
98
|
const deviceMetadata: DeviceMetadata = {
|
|
101
99
|
...(detectedPlatform && { platform: detectedPlatform }),
|
|
@@ -117,7 +115,7 @@ export function useFeedback(): UseFeedbackResult {
|
|
|
117
115
|
e instanceof HarkenApiError || e instanceof HarkenNetworkError
|
|
118
116
|
? e
|
|
119
117
|
: new HarkenNetworkError(
|
|
120
|
-
e instanceof Error ? e.message :
|
|
118
|
+
e instanceof Error ? e.message : "Unknown error",
|
|
121
119
|
e instanceof Error ? e : undefined
|
|
122
120
|
);
|
|
123
121
|
setError(harkenError);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useContext } from
|
|
2
|
-
import { HarkenContext } from
|
|
3
|
-
import type { HarkenContextValue } from
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { HarkenContext } from "../context";
|
|
3
|
+
import type { HarkenContextValue } from "../context";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Hook to access the full Harken context.
|
|
@@ -28,7 +28,7 @@ export function useHarkenContext(): HarkenContextValue {
|
|
|
28
28
|
const context = useContext(HarkenContext);
|
|
29
29
|
|
|
30
30
|
if (!context) {
|
|
31
|
-
throw new Error(
|
|
31
|
+
throw new Error("useHarkenContext must be used within a HarkenProvider");
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
return context;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { useContext } from
|
|
2
|
-
import { HarkenContext } from
|
|
3
|
-
import type {
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { HarkenContext } from "../context";
|
|
3
|
+
import type { ResolvedHarkenTheme } from "../theme";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Hook to access the current Harken theme.
|
|
7
7
|
*
|
|
8
8
|
* Must be used within a HarkenProvider.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Returns a fully-resolved theme with all component tokens populated.
|
|
11
|
+
* You can access tokens in two ways:
|
|
12
|
+
* - Flat: `theme.colors.chipBackground`
|
|
13
|
+
* - Structured: `theme.components.chip.background`
|
|
14
|
+
*
|
|
15
|
+
* @returns The resolved theme object with all fallbacks applied
|
|
11
16
|
* @throws Error if used outside of HarkenProvider
|
|
12
17
|
*
|
|
13
18
|
* @example
|
|
@@ -24,12 +29,31 @@ import type { HarkenTheme } from '../theme';
|
|
|
24
29
|
* );
|
|
25
30
|
* }
|
|
26
31
|
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* // Using structured component tokens
|
|
36
|
+
* function ChipComponent() {
|
|
37
|
+
* const theme = useHarkenTheme();
|
|
38
|
+
* const { chip } = theme.components;
|
|
39
|
+
*
|
|
40
|
+
* return (
|
|
41
|
+
* <View style={{
|
|
42
|
+
* backgroundColor: chip.background,
|
|
43
|
+
* borderRadius: chip.radius,
|
|
44
|
+
* padding: chip.paddingVertical,
|
|
45
|
+
* }}>
|
|
46
|
+
* <Text style={{ color: chip.text }}>Label</Text>
|
|
47
|
+
* </View>
|
|
48
|
+
* );
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
27
51
|
*/
|
|
28
|
-
export function useHarkenTheme():
|
|
52
|
+
export function useHarkenTheme(): ResolvedHarkenTheme {
|
|
29
53
|
const context = useContext(HarkenContext);
|
|
30
54
|
|
|
31
55
|
if (!context) {
|
|
32
|
-
throw new Error(
|
|
56
|
+
throw new Error("useHarkenTheme must be used within a HarkenProvider");
|
|
33
57
|
}
|
|
34
58
|
|
|
35
59
|
return context.theme;
|