@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
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
calculateRetryDelay,
|
|
4
|
+
isRetryableError,
|
|
5
|
+
withRetry,
|
|
6
|
+
DEFAULT_RETRY_CONFIG,
|
|
7
|
+
sleep,
|
|
8
|
+
} from "./retry";
|
|
9
|
+
import { HarkenApiError, HarkenNetworkError } from "./errors";
|
|
10
|
+
|
|
11
|
+
describe("calculateRetryDelay", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Mock Math.random for predictable jitter
|
|
14
|
+
vi.spyOn(Math, "random").mockReturnValue(0.5);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("calculates exponential backoff correctly", () => {
|
|
22
|
+
const config = { ...DEFAULT_RETRY_CONFIG, jitter: 0 };
|
|
23
|
+
|
|
24
|
+
expect(calculateRetryDelay(0, config)).toBe(1000); // 1000 * 2^0 = 1000
|
|
25
|
+
expect(calculateRetryDelay(1, config)).toBe(2000); // 1000 * 2^1 = 2000
|
|
26
|
+
expect(calculateRetryDelay(2, config)).toBe(4000); // 1000 * 2^2 = 4000
|
|
27
|
+
expect(calculateRetryDelay(3, config)).toBe(8000); // 1000 * 2^3 = 8000
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("caps delay at maxDelay", () => {
|
|
31
|
+
const config = { ...DEFAULT_RETRY_CONFIG, jitter: 0, maxDelay: 5000 };
|
|
32
|
+
|
|
33
|
+
expect(calculateRetryDelay(0, config)).toBe(1000);
|
|
34
|
+
expect(calculateRetryDelay(3, config)).toBe(5000); // capped
|
|
35
|
+
expect(calculateRetryDelay(10, config)).toBe(5000); // capped
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("adds jitter within expected range", () => {
|
|
39
|
+
const config = { ...DEFAULT_RETRY_CONFIG, jitter: 0.1 };
|
|
40
|
+
|
|
41
|
+
// With random = 0.5, jitter should be 0 (centered)
|
|
42
|
+
// For attempt 0: base = 1000, jitterRange = 100, jitter = 0
|
|
43
|
+
const delay = calculateRetryDelay(0, config);
|
|
44
|
+
expect(delay).toBe(1000);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("respects Retry-After header when provided", () => {
|
|
48
|
+
const config = DEFAULT_RETRY_CONFIG;
|
|
49
|
+
|
|
50
|
+
// 5 seconds should be 5000ms
|
|
51
|
+
expect(calculateRetryDelay(0, config, 5)).toBe(5000);
|
|
52
|
+
expect(calculateRetryDelay(2, config, 10)).toBe(10000);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("caps Retry-After at maxDelay", () => {
|
|
56
|
+
const config = { ...DEFAULT_RETRY_CONFIG, maxDelay: 5000 };
|
|
57
|
+
|
|
58
|
+
// 60 seconds should be capped to 5000ms
|
|
59
|
+
expect(calculateRetryDelay(0, config, 60)).toBe(5000);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("ignores zero or negative Retry-After", () => {
|
|
63
|
+
const config = { ...DEFAULT_RETRY_CONFIG, jitter: 0 };
|
|
64
|
+
|
|
65
|
+
// Should fall back to exponential backoff
|
|
66
|
+
expect(calculateRetryDelay(0, config, 0)).toBe(1000);
|
|
67
|
+
expect(calculateRetryDelay(0, config, -5)).toBe(1000);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("isRetryableError", () => {
|
|
72
|
+
it("returns true for HarkenNetworkError", () => {
|
|
73
|
+
const error = new HarkenNetworkError("Network failed");
|
|
74
|
+
expect(isRetryableError(error)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns true for HarkenApiError with 429 status", () => {
|
|
78
|
+
const error = new HarkenApiError(429, {
|
|
79
|
+
error: { code: "rate_limited", message: "Too many requests" },
|
|
80
|
+
});
|
|
81
|
+
expect(isRetryableError(error)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("returns true for HarkenApiError with 5xx status", () => {
|
|
85
|
+
const error500 = new HarkenApiError(500, {
|
|
86
|
+
error: { code: "internal_error", message: "Server error" },
|
|
87
|
+
});
|
|
88
|
+
const error503 = new HarkenApiError(503, {
|
|
89
|
+
error: { code: "service_unavailable", message: "Service unavailable" },
|
|
90
|
+
});
|
|
91
|
+
expect(isRetryableError(error500)).toBe(true);
|
|
92
|
+
expect(isRetryableError(error503)).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns false for HarkenApiError with 4xx status (except 429)", () => {
|
|
96
|
+
const error400 = new HarkenApiError(400, {
|
|
97
|
+
error: { code: "validation_error", message: "Invalid input" },
|
|
98
|
+
});
|
|
99
|
+
const error401 = new HarkenApiError(401, {
|
|
100
|
+
error: { code: "unauthorized", message: "Unauthorized" },
|
|
101
|
+
});
|
|
102
|
+
const error404 = new HarkenApiError(404, {
|
|
103
|
+
error: { code: "not_found", message: "Not found" },
|
|
104
|
+
});
|
|
105
|
+
expect(isRetryableError(error400)).toBe(false);
|
|
106
|
+
expect(isRetryableError(error401)).toBe(false);
|
|
107
|
+
expect(isRetryableError(error404)).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("returns false for generic Error", () => {
|
|
111
|
+
expect(isRetryableError(new Error("Generic error"))).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("returns false for non-Error values", () => {
|
|
115
|
+
expect(isRetryableError(null)).toBe(false);
|
|
116
|
+
expect(isRetryableError(undefined)).toBe(false);
|
|
117
|
+
expect(isRetryableError("error string")).toBe(false);
|
|
118
|
+
expect(isRetryableError({ code: "error" })).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("sleep", () => {
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
vi.useFakeTimers();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
afterEach(() => {
|
|
128
|
+
vi.useRealTimers();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("resolves after specified delay", async () => {
|
|
132
|
+
const promise = sleep(1000);
|
|
133
|
+
vi.advanceTimersByTime(1000);
|
|
134
|
+
await expect(promise).resolves.toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("withRetry", () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
vi.useFakeTimers();
|
|
141
|
+
vi.spyOn(Math, "random").mockReturnValue(0.5);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
vi.useRealTimers();
|
|
146
|
+
vi.restoreAllMocks();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("returns result on first successful attempt", async () => {
|
|
150
|
+
const fn = vi.fn().mockResolvedValue("success");
|
|
151
|
+
|
|
152
|
+
const result = await withRetry(fn);
|
|
153
|
+
|
|
154
|
+
expect(result).toBe("success");
|
|
155
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("retries on retryable error and succeeds", async () => {
|
|
159
|
+
const fn = vi
|
|
160
|
+
.fn()
|
|
161
|
+
.mockRejectedValueOnce(new HarkenNetworkError("Network failed"))
|
|
162
|
+
.mockResolvedValue("success");
|
|
163
|
+
|
|
164
|
+
const promise = withRetry(fn, { maxRetries: 3 });
|
|
165
|
+
|
|
166
|
+
// First call fails immediately
|
|
167
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
168
|
+
|
|
169
|
+
// Wait for retry delay (1000ms for attempt 0)
|
|
170
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
171
|
+
|
|
172
|
+
const result = await promise;
|
|
173
|
+
expect(result).toBe("success");
|
|
174
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("throws after max retries exceeded", async () => {
|
|
178
|
+
const networkError = new HarkenNetworkError("Network failed");
|
|
179
|
+
const fn = vi.fn().mockRejectedValue(networkError);
|
|
180
|
+
|
|
181
|
+
// Attach rejection handler immediately to avoid unhandled rejection warning
|
|
182
|
+
let caughtError: unknown;
|
|
183
|
+
const promise = withRetry(fn, { maxRetries: 2, jitter: 0 }).catch((e) => {
|
|
184
|
+
caughtError = e;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Initial attempt + retries with delays
|
|
188
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
189
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
190
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
191
|
+
|
|
192
|
+
await promise;
|
|
193
|
+
|
|
194
|
+
expect(caughtError).toBe(networkError);
|
|
195
|
+
expect(fn).toHaveBeenCalledTimes(3); // 1 initial + 2 retries
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("does not retry non-retryable errors", async () => {
|
|
199
|
+
const validationError = new HarkenApiError(400, {
|
|
200
|
+
error: { code: "validation_error", message: "Invalid input" },
|
|
201
|
+
});
|
|
202
|
+
const fn = vi.fn().mockRejectedValue(validationError);
|
|
203
|
+
|
|
204
|
+
await expect(withRetry(fn, { maxRetries: 3 })).rejects.toThrow(validationError);
|
|
205
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("uses default config when none provided", async () => {
|
|
209
|
+
const fn = vi.fn().mockResolvedValue("success");
|
|
210
|
+
|
|
211
|
+
const result = await withRetry(fn);
|
|
212
|
+
|
|
213
|
+
expect(result).toBe("success");
|
|
214
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("merges partial config with defaults", async () => {
|
|
218
|
+
const fn = vi
|
|
219
|
+
.fn()
|
|
220
|
+
.mockRejectedValueOnce(new HarkenNetworkError("Network failed"))
|
|
221
|
+
.mockResolvedValue("success");
|
|
222
|
+
|
|
223
|
+
const promise = withRetry(fn, { baseDelay: 500 });
|
|
224
|
+
|
|
225
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
226
|
+
// Should use custom baseDelay of 500ms
|
|
227
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
228
|
+
|
|
229
|
+
const result = await promise;
|
|
230
|
+
expect(result).toBe("success");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("respects Retry-After from HarkenApiError", async () => {
|
|
234
|
+
const rateLimitError = new HarkenApiError(
|
|
235
|
+
429,
|
|
236
|
+
{ error: { code: "rate_limited", message: "Too many requests" } },
|
|
237
|
+
{ retryAfter: 5 }
|
|
238
|
+
);
|
|
239
|
+
const fn = vi.fn().mockRejectedValueOnce(rateLimitError).mockResolvedValue("success");
|
|
240
|
+
|
|
241
|
+
const promise = withRetry(fn, { maxRetries: 3, jitter: 0 });
|
|
242
|
+
|
|
243
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
244
|
+
// Should wait 5000ms (Retry-After: 5 seconds)
|
|
245
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
246
|
+
|
|
247
|
+
const result = await promise;
|
|
248
|
+
expect(result).toBe("success");
|
|
249
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
250
|
+
});
|
|
251
|
+
});
|
package/src/api/retry.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HarkenApiError, HarkenNetworkError } from
|
|
1
|
+
import { HarkenApiError, HarkenNetworkError } from "./errors";
|
|
2
2
|
|
|
3
3
|
export interface RetryConfig {
|
|
4
4
|
/** Maximum number of retry attempts (default: 3) */
|
|
@@ -45,9 +45,7 @@ export function calculateRetryDelay(
|
|
|
45
45
|
/**
|
|
46
46
|
* Check if an error is retryable.
|
|
47
47
|
*/
|
|
48
|
-
export function isRetryableError(
|
|
49
|
-
error: unknown
|
|
50
|
-
): error is HarkenApiError | HarkenNetworkError {
|
|
48
|
+
export function isRetryableError(error: unknown): error is HarkenApiError | HarkenNetworkError {
|
|
51
49
|
if (error instanceof HarkenApiError) {
|
|
52
50
|
return error.isRetryable;
|
|
53
51
|
}
|
|
@@ -86,8 +84,7 @@ export async function withRetry<T>(
|
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
// Calculate delay, respecting Retry-After header if present
|
|
89
|
-
const retryAfter =
|
|
90
|
-
error instanceof HarkenApiError ? error.retryAfter : undefined;
|
|
87
|
+
const retryAfter = error instanceof HarkenApiError ? error.retryAfter : undefined;
|
|
91
88
|
|
|
92
89
|
const delay = calculateRetryDelay(attempt, fullConfig, retryAfter);
|
|
93
90
|
await sleep(delay);
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import React, { useState, useCallback } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from
|
|
9
|
-
import
|
|
10
|
-
import type {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import { useAttachmentPicker } from '../hooks/useAttachmentPicker';
|
|
19
|
-
import type { AttachmentSourceConfig } from '../hooks/useAttachmentPicker';
|
|
20
|
-
import { AttachmentGrid } from '../components/AttachmentGrid';
|
|
21
|
-
import { AttachmentPicker } from '../components/AttachmentPicker';
|
|
22
|
-
|
|
23
|
-
type FeedbackSubmissionResponse = components['schemas']['FeedbackSubmissionResponse'];
|
|
1
|
+
import React, { useState, useCallback } from "react";
|
|
2
|
+
import { View, KeyboardAvoidingView, Platform, ScrollView, Alert } from "react-native";
|
|
3
|
+
import type { ViewStyle, StyleProp } from "react-native";
|
|
4
|
+
import type { components } from "../types/index.js";
|
|
5
|
+
import { useHarkenTheme, useFeedback } from "../hooks";
|
|
6
|
+
import { ThemedText } from "../components/ThemedText";
|
|
7
|
+
import { ThemedTextInput } from "../components/ThemedTextInput";
|
|
8
|
+
import { ThemedButton } from "../components/ThemedButton";
|
|
9
|
+
import { CategorySelector, DEFAULT_CATEGORIES } from "../components/CategorySelector";
|
|
10
|
+
import type { CategoryOption } from "../components/CategorySelector";
|
|
11
|
+
import type { FeedbackCategory } from "../types";
|
|
12
|
+
import { useAttachmentPicker } from "../hooks/useAttachmentPicker";
|
|
13
|
+
import type { AttachmentSourceConfig } from "../hooks/useAttachmentPicker";
|
|
14
|
+
import { AttachmentGrid } from "../components/AttachmentGrid";
|
|
15
|
+
import { AttachmentPicker } from "../components/AttachmentPicker";
|
|
16
|
+
|
|
17
|
+
type FeedbackSubmissionResponse = components["schemas"]["FeedbackSubmissionResponse"];
|
|
24
18
|
|
|
25
19
|
export interface FeedbackSheetProps {
|
|
26
20
|
/** Called when feedback is successfully submitted */
|
|
@@ -30,7 +24,7 @@ export interface FeedbackSheetProps {
|
|
|
30
24
|
/** Called when user cancels/dismisses the form */
|
|
31
25
|
onCancel?: () => void;
|
|
32
26
|
|
|
33
|
-
/** Title text */
|
|
27
|
+
/** Title text. Set to empty string to hide title section entirely. */
|
|
34
28
|
title?: string;
|
|
35
29
|
/** Placeholder text for message input */
|
|
36
30
|
placeholder?: string;
|
|
@@ -67,10 +61,21 @@ export interface FeedbackSheetProps {
|
|
|
67
61
|
/** Whether to clear form on success. @default true */
|
|
68
62
|
clearOnSuccess?: boolean;
|
|
69
63
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Layout mode for the container.
|
|
66
|
+
* - 'flex': Uses flex: 1 (default, requires parent with explicit height)
|
|
67
|
+
* - 'auto': Content determines height (for bottom sheet modal embedding)
|
|
68
|
+
*/
|
|
69
|
+
layout?: "flex" | "auto";
|
|
70
|
+
|
|
71
|
+
/** Container style override (outer KeyboardAvoidingView) */
|
|
72
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
73
|
+
/** Content style override (inner ScrollView content) */
|
|
74
|
+
contentStyle?: StyleProp<ViewStyle>;
|
|
75
|
+
/**
|
|
76
|
+
* @deprecated Use `contentStyle` instead
|
|
77
|
+
*/
|
|
78
|
+
formStyle?: StyleProp<ViewStyle>;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
/**
|
|
@@ -81,13 +86,26 @@ export interface FeedbackSheetProps {
|
|
|
81
86
|
*
|
|
82
87
|
* For the version without attachment dependencies, import from the main entry point.
|
|
83
88
|
*
|
|
89
|
+
* Uses the following theme tokens:
|
|
90
|
+
* - `colors.formBackground` for background
|
|
91
|
+
* - `spacing.formPadding` for padding
|
|
92
|
+
* - `spacing.sectionGap` for section gaps
|
|
93
|
+
* - `radii.form` for border radius
|
|
94
|
+
*
|
|
84
95
|
* @example
|
|
85
96
|
* ```tsx
|
|
86
|
-
* import { FeedbackSheet } from '@harkenapp/sdk-react-native
|
|
97
|
+
* import { FeedbackSheet } from '@harkenapp/sdk-react-native';
|
|
87
98
|
*
|
|
88
99
|
* // Minimal usage with attachments
|
|
89
100
|
* <FeedbackSheet onSuccess={() => navigation.goBack()} />
|
|
90
101
|
*
|
|
102
|
+
* // For bottom sheet modal embedding
|
|
103
|
+
* <FeedbackSheet
|
|
104
|
+
* layout="auto"
|
|
105
|
+
* title=""
|
|
106
|
+
* onSuccess={() => closeModal()}
|
|
107
|
+
* />
|
|
108
|
+
*
|
|
91
109
|
* // With customization
|
|
92
110
|
* <FeedbackSheet
|
|
93
111
|
* title="Report a Bug"
|
|
@@ -112,10 +130,10 @@ export function FeedbackSheet({
|
|
|
112
130
|
onSuccess,
|
|
113
131
|
onError,
|
|
114
132
|
onCancel,
|
|
115
|
-
title =
|
|
116
|
-
placeholder =
|
|
117
|
-
submitLabel =
|
|
118
|
-
cancelLabel =
|
|
133
|
+
title = "Send Feedback",
|
|
134
|
+
placeholder = "What would you like to share?",
|
|
135
|
+
submitLabel = "Submit",
|
|
136
|
+
cancelLabel = "Cancel",
|
|
119
137
|
categories = DEFAULT_CATEGORIES,
|
|
120
138
|
requireCategory = false,
|
|
121
139
|
minMessageLength = 1,
|
|
@@ -123,15 +141,17 @@ export function FeedbackSheet({
|
|
|
123
141
|
enableAttachments = true,
|
|
124
142
|
maxAttachments = 5,
|
|
125
143
|
attachmentSources,
|
|
126
|
-
successMessage =
|
|
144
|
+
successMessage = "Thank you for your feedback!",
|
|
127
145
|
showSuccessAlert = true,
|
|
128
146
|
clearOnSuccess = true,
|
|
147
|
+
layout = "flex",
|
|
129
148
|
containerStyle,
|
|
149
|
+
contentStyle,
|
|
130
150
|
formStyle,
|
|
131
151
|
}: FeedbackSheetProps): React.JSX.Element {
|
|
132
152
|
const theme = useHarkenTheme();
|
|
133
|
-
const {
|
|
134
|
-
|
|
153
|
+
const { form } = theme.components;
|
|
154
|
+
const { submitFeedback, isSubmitting, error, clearError, isInitializing } = useFeedback();
|
|
135
155
|
const {
|
|
136
156
|
attachments,
|
|
137
157
|
removeAttachment,
|
|
@@ -143,18 +163,17 @@ export function FeedbackSheet({
|
|
|
143
163
|
enabledSourceCount,
|
|
144
164
|
} = useAttachmentPicker(attachmentSources);
|
|
145
165
|
|
|
146
|
-
const [message, setMessage] = useState(
|
|
166
|
+
const [message, setMessage] = useState("");
|
|
147
167
|
const [category, setCategory] = useState<FeedbackCategory | null>(null);
|
|
148
168
|
|
|
149
169
|
const trimmedMessage = message.trim();
|
|
150
170
|
const isMessageValid =
|
|
151
|
-
trimmedMessage.length >= minMessageLength &&
|
|
152
|
-
trimmedMessage.length <= maxMessageLength;
|
|
171
|
+
trimmedMessage.length >= minMessageLength && trimmedMessage.length <= maxMessageLength;
|
|
153
172
|
const isCategoryValid = !requireCategory || category !== null;
|
|
154
173
|
const canSubmit = isMessageValid && isCategoryValid && !isSubmitting && !isInitializing;
|
|
155
174
|
|
|
156
175
|
const resetForm = useCallback(() => {
|
|
157
|
-
setMessage(
|
|
176
|
+
setMessage("");
|
|
158
177
|
setCategory(null);
|
|
159
178
|
clearError();
|
|
160
179
|
// Note: We don't clear attachments since they may still be uploading
|
|
@@ -169,18 +188,19 @@ export function FeedbackSheet({
|
|
|
169
188
|
try {
|
|
170
189
|
const result = await submitFeedback({
|
|
171
190
|
message: trimmedMessage,
|
|
172
|
-
category: category ??
|
|
191
|
+
category: category ?? "other",
|
|
173
192
|
attachments: enableAttachments ? getAttachmentIds() : undefined,
|
|
174
193
|
});
|
|
175
194
|
|
|
176
|
-
const uploadNote =
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
const uploadNote =
|
|
196
|
+
enableAttachments && hasActiveUploads
|
|
197
|
+
? "\n\nAttachments are still uploading in the background."
|
|
198
|
+
: "";
|
|
179
199
|
|
|
180
200
|
if (showSuccessAlert && successMessage) {
|
|
181
|
-
Alert.alert(
|
|
201
|
+
Alert.alert("Success", `${successMessage}${uploadNote}`, [
|
|
182
202
|
{
|
|
183
|
-
text:
|
|
203
|
+
text: "OK",
|
|
184
204
|
onPress: () => {
|
|
185
205
|
if (clearOnSuccess) {
|
|
186
206
|
resetForm();
|
|
@@ -197,8 +217,8 @@ export function FeedbackSheet({
|
|
|
197
217
|
}
|
|
198
218
|
} catch (e) {
|
|
199
219
|
const errorMessage =
|
|
200
|
-
e instanceof Error ? e.message :
|
|
201
|
-
Alert.alert(
|
|
220
|
+
e instanceof Error ? e.message : "Failed to submit feedback. Please try again.";
|
|
221
|
+
Alert.alert("Submission Failed", errorMessage);
|
|
202
222
|
onError?.(e instanceof Error ? e : new Error(errorMessage));
|
|
203
223
|
}
|
|
204
224
|
}, [
|
|
@@ -224,21 +244,25 @@ export function FeedbackSheet({
|
|
|
224
244
|
}, [resetForm, onCancel]);
|
|
225
245
|
|
|
226
246
|
const baseContainerStyle: ViewStyle = {
|
|
227
|
-
flex: 1,
|
|
228
|
-
backgroundColor:
|
|
247
|
+
...(layout === "flex" ? { flex: 1 } : {}),
|
|
248
|
+
backgroundColor: form.background,
|
|
249
|
+
borderRadius: form.radius,
|
|
229
250
|
};
|
|
230
251
|
|
|
231
|
-
const
|
|
232
|
-
flexGrow: 1,
|
|
233
|
-
padding:
|
|
252
|
+
const scrollContentStyle: ViewStyle = {
|
|
253
|
+
...(layout === "flex" ? { flexGrow: 1 } : {}),
|
|
254
|
+
padding: form.padding,
|
|
234
255
|
};
|
|
235
256
|
|
|
236
257
|
const sectionStyle: ViewStyle = {
|
|
237
|
-
marginBottom:
|
|
258
|
+
marginBottom: form.sectionGap,
|
|
238
259
|
};
|
|
239
260
|
|
|
261
|
+
// Support deprecated formStyle prop
|
|
262
|
+
const effectiveContentStyle = contentStyle ?? formStyle;
|
|
263
|
+
|
|
240
264
|
const buttonRowStyle: ViewStyle = {
|
|
241
|
-
flexDirection:
|
|
265
|
+
flexDirection: "row",
|
|
242
266
|
gap: theme.spacing.sm,
|
|
243
267
|
marginTop: theme.spacing.md,
|
|
244
268
|
};
|
|
@@ -248,7 +272,13 @@ export function FeedbackSheet({
|
|
|
248
272
|
|
|
249
273
|
if (isInitializing) {
|
|
250
274
|
return (
|
|
251
|
-
<View
|
|
275
|
+
<View
|
|
276
|
+
style={[
|
|
277
|
+
baseContainerStyle,
|
|
278
|
+
containerStyle,
|
|
279
|
+
{ justifyContent: "center", alignItems: "center" },
|
|
280
|
+
]}
|
|
281
|
+
>
|
|
252
282
|
<ThemedText variant="body" secondary>
|
|
253
283
|
Initializing...
|
|
254
284
|
</ThemedText>
|
|
@@ -259,26 +289,24 @@ export function FeedbackSheet({
|
|
|
259
289
|
return (
|
|
260
290
|
<>
|
|
261
291
|
<KeyboardAvoidingView
|
|
262
|
-
behavior={Platform.OS ===
|
|
292
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
263
293
|
style={[baseContainerStyle, containerStyle]}
|
|
264
294
|
>
|
|
265
295
|
<ScrollView
|
|
266
|
-
contentContainerStyle={[
|
|
296
|
+
contentContainerStyle={[scrollContentStyle, effectiveContentStyle]}
|
|
267
297
|
keyboardShouldPersistTaps="handled"
|
|
268
298
|
>
|
|
269
|
-
{/* Title */}
|
|
270
|
-
|
|
271
|
-
<
|
|
272
|
-
|
|
299
|
+
{/* Title - only render if provided */}
|
|
300
|
+
{title ? (
|
|
301
|
+
<View style={sectionStyle}>
|
|
302
|
+
<ThemedText variant="title">{title}</ThemedText>
|
|
303
|
+
</View>
|
|
304
|
+
) : null}
|
|
273
305
|
|
|
274
306
|
{/* Category selector */}
|
|
275
307
|
<View style={sectionStyle}>
|
|
276
|
-
<ThemedText
|
|
277
|
-
|
|
278
|
-
secondary
|
|
279
|
-
style={{ marginBottom: theme.spacing.sm }}
|
|
280
|
-
>
|
|
281
|
-
Category{requireCategory ? '' : ' (optional)'}
|
|
308
|
+
<ThemedText variant="label" secondary style={{ marginBottom: theme.spacing.sm }}>
|
|
309
|
+
Category{requireCategory ? "" : " (optional)"}
|
|
282
310
|
</ThemedText>
|
|
283
311
|
<CategorySelector
|
|
284
312
|
value={category}
|
|
@@ -290,11 +318,7 @@ export function FeedbackSheet({
|
|
|
290
318
|
|
|
291
319
|
{/* Message input */}
|
|
292
320
|
<View style={sectionStyle}>
|
|
293
|
-
<ThemedText
|
|
294
|
-
variant="label"
|
|
295
|
-
secondary
|
|
296
|
-
style={{ marginBottom: theme.spacing.sm }}
|
|
297
|
-
>
|
|
321
|
+
<ThemedText variant="label" secondary style={{ marginBottom: theme.spacing.sm }}>
|
|
298
322
|
Message
|
|
299
323
|
</ThemedText>
|
|
300
324
|
<ThemedTextInput
|
|
@@ -316,7 +340,7 @@ export function FeedbackSheet({
|
|
|
316
340
|
? theme.colors.error
|
|
317
341
|
: theme.colors.textSecondary
|
|
318
342
|
}
|
|
319
|
-
style={{ marginTop: theme.spacing.xs, textAlign:
|
|
343
|
+
style={{ marginTop: theme.spacing.xs, textAlign: "right" }}
|
|
320
344
|
>
|
|
321
345
|
{characterCount}/{maxMessageLength}
|
|
322
346
|
</ThemedText>
|
|
@@ -326,11 +350,7 @@ export function FeedbackSheet({
|
|
|
326
350
|
{/* Attachments */}
|
|
327
351
|
{enableAttachments && (
|
|
328
352
|
<View style={sectionStyle}>
|
|
329
|
-
<ThemedText
|
|
330
|
-
variant="label"
|
|
331
|
-
secondary
|
|
332
|
-
style={{ marginBottom: theme.spacing.sm }}
|
|
333
|
-
>
|
|
353
|
+
<ThemedText variant="label" secondary style={{ marginBottom: theme.spacing.sm }}>
|
|
334
354
|
Attachments
|
|
335
355
|
</ThemedText>
|
|
336
356
|
<AttachmentGrid
|
|
@@ -384,7 +404,7 @@ export function FeedbackSheet({
|
|
|
384
404
|
<ThemedText
|
|
385
405
|
variant="caption"
|
|
386
406
|
color={theme.colors.primary}
|
|
387
|
-
style={{ textAlign:
|
|
407
|
+
style={{ textAlign: "center" }}
|
|
388
408
|
>
|
|
389
409
|
Uploads in progress - you can still submit now
|
|
390
410
|
</ThemedText>
|
package/src/attachments/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ export {
|
|
|
38
38
|
// Domain types
|
|
39
39
|
UploadPhase,
|
|
40
40
|
DEFAULT_UPLOAD_RETRY_CONFIG,
|
|
41
|
-
} from
|
|
41
|
+
} from "../index";
|
|
42
42
|
|
|
43
43
|
export type {
|
|
44
44
|
// Attachment hook types
|
|
@@ -67,4 +67,4 @@ export type {
|
|
|
67
67
|
QueueStatus,
|
|
68
68
|
UploadProgress,
|
|
69
69
|
UploadRetryConfig,
|
|
70
|
-
} from
|
|
70
|
+
} from "../index";
|