@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.
- package/README.md +67 -0
- package/app.plugin.cjs +135 -0
- package/app.plugin.js +1 -0
- package/dist/api/client.d.ts +67 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +163 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/errors.d.ts +46 -0
- package/dist/api/errors.d.ts.map +1 -0
- package/dist/api/errors.js +72 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +20 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/retry.d.ts +29 -0
- package/dist/api/retry.d.ts.map +1 -0
- package/dist/api/retry.js +74 -0
- package/dist/api/retry.js.map +1 -0
- package/dist/attachments/FeedbackSheet.d.ts +88 -0
- package/dist/attachments/FeedbackSheet.d.ts.map +1 -0
- package/dist/attachments/FeedbackSheet.js +250 -0
- package/dist/attachments/FeedbackSheet.js.map +1 -0
- package/dist/attachments/index.d.ts +20 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +40 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/components/AttachmentGrid.d.ts +94 -0
- package/dist/components/AttachmentGrid.d.ts.map +1 -0
- package/dist/components/AttachmentGrid.js +132 -0
- package/dist/components/AttachmentGrid.js.map +1 -0
- package/dist/components/AttachmentPicker.d.ts +98 -0
- package/dist/components/AttachmentPicker.d.ts.map +1 -0
- package/dist/components/AttachmentPicker.js +297 -0
- package/dist/components/AttachmentPicker.js.map +1 -0
- package/dist/components/AttachmentPreview.d.ts +78 -0
- package/dist/components/AttachmentPreview.d.ts.map +1 -0
- package/dist/components/AttachmentPreview.js +133 -0
- package/dist/components/AttachmentPreview.js.map +1 -0
- package/dist/components/CategorySelector.d.ts +77 -0
- package/dist/components/CategorySelector.d.ts.map +1 -0
- package/dist/components/CategorySelector.js +117 -0
- package/dist/components/CategorySelector.js.map +1 -0
- package/dist/components/FeedbackForm.d.ts +50 -0
- package/dist/components/FeedbackForm.d.ts.map +1 -0
- package/dist/components/FeedbackForm.js +141 -0
- package/dist/components/FeedbackForm.js.map +1 -0
- package/dist/components/FeedbackSheet.d.ts +75 -0
- package/dist/components/FeedbackSheet.d.ts.map +1 -0
- package/dist/components/FeedbackSheet.js +215 -0
- package/dist/components/FeedbackSheet.js.map +1 -0
- package/dist/components/ThemedButton.d.ts +23 -0
- package/dist/components/ThemedButton.d.ts.map +1 -0
- package/dist/components/ThemedButton.js +77 -0
- package/dist/components/ThemedButton.js.map +1 -0
- package/dist/components/ThemedText.d.ts +16 -0
- package/dist/components/ThemedText.d.ts.map +1 -0
- package/dist/components/ThemedText.js +44 -0
- package/dist/components/ThemedText.js.map +1 -0
- package/dist/components/ThemedTextInput.d.ts +13 -0
- package/dist/components/ThemedTextInput.d.ts.map +1 -0
- package/dist/components/ThemedTextInput.js +76 -0
- package/dist/components/ThemedTextInput.js.map +1 -0
- package/dist/components/UploadStatusOverlay.d.ts +82 -0
- package/dist/components/UploadStatusOverlay.d.ts.map +1 -0
- package/dist/components/UploadStatusOverlay.js +319 -0
- package/dist/components/UploadStatusOverlay.js.map +1 -0
- package/dist/components/index.d.ts +19 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +28 -0
- package/dist/components/index.js.map +1 -0
- package/dist/context/HarkenContext.d.ts +62 -0
- package/dist/context/HarkenContext.d.ts.map +1 -0
- package/dist/context/HarkenContext.js +128 -0
- package/dist/context/HarkenContext.js.map +1 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +7 -0
- package/dist/context/index.js.map +1 -0
- package/dist/domain/index.d.ts +3 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +7 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/upload-queue.d.ts +116 -0
- package/dist/domain/upload-queue.d.ts.map +1 -0
- package/dist/domain/upload-queue.js +34 -0
- package/dist/domain/upload-queue.js.map +1 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +16 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useAnonymousId.d.ts +28 -0
- package/dist/hooks/useAnonymousId.d.ts.map +1 -0
- package/dist/hooks/useAnonymousId.js +59 -0
- package/dist/hooks/useAnonymousId.js.map +1 -0
- package/dist/hooks/useAttachmentPicker.d.ts +84 -0
- package/dist/hooks/useAttachmentPicker.d.ts.map +1 -0
- package/dist/hooks/useAttachmentPicker.js +181 -0
- package/dist/hooks/useAttachmentPicker.js.map +1 -0
- package/dist/hooks/useAttachmentStatus.d.ts +51 -0
- package/dist/hooks/useAttachmentStatus.d.ts.map +1 -0
- package/dist/hooks/useAttachmentStatus.js +69 -0
- package/dist/hooks/useAttachmentStatus.js.map +1 -0
- package/dist/hooks/useAttachmentUpload.d.ts +101 -0
- package/dist/hooks/useAttachmentUpload.d.ts.map +1 -0
- package/dist/hooks/useAttachmentUpload.js +293 -0
- package/dist/hooks/useAttachmentUpload.js.map +1 -0
- package/dist/hooks/useFeedback.d.ts +55 -0
- package/dist/hooks/useFeedback.d.ts.map +1 -0
- package/dist/hooks/useFeedback.js +96 -0
- package/dist/hooks/useFeedback.js.map +1 -0
- package/dist/hooks/useHarkenContext.d.ts +25 -0
- package/dist/hooks/useHarkenContext.d.ts.map +1 -0
- package/dist/hooks/useHarkenContext.js +35 -0
- package/dist/hooks/useHarkenContext.js.map +1 -0
- package/dist/hooks/useHarkenTheme.d.ts +26 -0
- package/dist/hooks/useHarkenTheme.d.ts.map +1 -0
- package/dist/hooks/useHarkenTheme.js +36 -0
- package/dist/hooks/useHarkenTheme.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +9 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/uploadQueueService.d.ts +193 -0
- package/dist/services/uploadQueueService.d.ts.map +1 -0
- package/dist/services/uploadQueueService.js +623 -0
- package/dist/services/uploadQueueService.js.map +1 -0
- package/dist/services/uploadQueueStorage.d.ts +30 -0
- package/dist/services/uploadQueueStorage.d.ts.map +1 -0
- package/dist/services/uploadQueueStorage.js +77 -0
- package/dist/services/uploadQueueStorage.js.map +1 -0
- package/dist/storage/IdentityStore.d.ts +38 -0
- package/dist/storage/IdentityStore.d.ts.map +1 -0
- package/dist/storage/IdentityStore.js +83 -0
- package/dist/storage/IdentityStore.js.map +1 -0
- package/dist/storage/SecureStoreAdapter.d.ts +28 -0
- package/dist/storage/SecureStoreAdapter.d.ts.map +1 -0
- package/dist/storage/SecureStoreAdapter.js +52 -0
- package/dist/storage/SecureStoreAdapter.js.map +1 -0
- package/dist/storage/defaultStorage.d.ts +20 -0
- package/dist/storage/defaultStorage.d.ts.map +1 -0
- package/dist/storage/defaultStorage.js +131 -0
- package/dist/storage/defaultStorage.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/types.d.ts +32 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +11 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/theme/defaults.d.ts +43 -0
- package/dist/theme/defaults.d.ts.map +1 -0
- package/dist/theme/defaults.js +128 -0
- package/dist/theme/defaults.js.map +1 -0
- package/dist/theme/index.d.ts +3 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +14 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/types.d.ts +136 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +3 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/types/config.d.ts +100 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/openapi.d.ts +601 -0
- package/dist/types/openapi.d.ts.map +1 -0
- package/dist/types/openapi.js +7 -0
- package/dist/types/openapi.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/uuid.d.ts +10 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/dist/utils/uuid.js +60 -0
- package/dist/utils/uuid.js.map +1 -0
- package/package.json +124 -0
- package/src/@types/expo-file-system-legacy.d.ts +13 -0
- package/src/api/client.ts +250 -0
- package/src/api/errors.ts +84 -0
- package/src/api/index.ts +15 -0
- package/src/api/retry.ts +99 -0
- package/src/attachments/FeedbackSheet.tsx +400 -0
- package/src/attachments/index.ts +70 -0
- package/src/components/AttachmentGrid.tsx +247 -0
- package/src/components/AttachmentPicker.tsx +391 -0
- package/src/components/AttachmentPreview.tsx +210 -0
- package/src/components/CategorySelector.tsx +174 -0
- package/src/components/FeedbackForm.tsx +216 -0
- package/src/components/FeedbackSheet.tsx +321 -0
- package/src/components/ThemedButton.tsx +127 -0
- package/src/components/ThemedText.tsx +65 -0
- package/src/components/ThemedTextInput.tsx +65 -0
- package/src/components/UploadStatusOverlay.tsx +440 -0
- package/src/components/index.ts +39 -0
- package/src/context/HarkenContext.tsx +129 -0
- package/src/context/index.ts +2 -0
- package/src/domain/index.ts +12 -0
- package/src/domain/upload-queue.ts +131 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useAnonymousId.ts +68 -0
- package/src/hooks/useAttachmentPicker.ts +243 -0
- package/src/hooks/useAttachmentStatus.ts +86 -0
- package/src/hooks/useAttachmentUpload.ts +370 -0
- package/src/hooks/useFeedback.ts +139 -0
- package/src/hooks/useHarkenContext.ts +35 -0
- package/src/hooks/useHarkenTheme.ts +36 -0
- package/src/index.ts +168 -0
- package/src/services/index.ts +11 -0
- package/src/services/uploadQueueService.ts +727 -0
- package/src/services/uploadQueueStorage.ts +78 -0
- package/src/storage/IdentityStore.ts +89 -0
- package/src/storage/SecureStoreAdapter.ts +59 -0
- package/src/storage/defaultStorage.ts +109 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/types.ts +34 -0
- package/src/theme/defaults.ts +151 -0
- package/src/theme/index.ts +23 -0
- package/src/theme/types.ts +157 -0
- package/src/types/config.ts +112 -0
- package/src/types/index.ts +10 -0
- package/src/types/openapi.ts +601 -0
- package/src/utils/index.ts +1 -0
- 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,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
|
+
}
|