@umituz/react-native-ai-generation-content 1.23.4 → 1.24.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/package.json +3 -2
- package/src/domain/entities/step-config.types.ts +133 -0
- package/src/domains/scenarios/configs/wizard-configs.ts +187 -0
- package/src/domains/scenarios/index.ts +8 -0
- package/src/domains/wizard/domain/entities/wizard-config.types.ts +214 -0
- package/src/domains/wizard/index.ts +62 -0
- package/src/domains/wizard/infrastructure/builders/dynamic-step-builder.ts +107 -0
- package/src/domains/wizard/infrastructure/renderers/step-renderer.tsx +106 -0
- package/src/domains/wizard/presentation/components/GenericWizardFlow.tsx +189 -0
- package/src/domains/wizard/presentation/hooks/usePhotoUploadState.ts +115 -0
- package/src/domains/wizard/presentation/screens/GenericPhotoUploadScreen.tsx +226 -0
- package/src/domains/wizard/presentation/steps/PhotoUploadStep.tsx +67 -0
- package/src/domains/wizard/presentation/steps/SelectionStep.tsx +244 -0
- package/src/domains/wizard/presentation/steps/TextInputStep.tsx +199 -0
- package/src/features/couple-future/domain/wizard-config.adapter.ts +76 -0
- package/src/features/couple-future/presentation/components/CoupleFutureWizard.tsx +45 -1
- package/src/index.ts +3 -0
- package/src/infrastructure/flow/step-builder.ts +226 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Step Builder
|
|
3
|
+
* Builds flow-compatible steps from wizard configurations
|
|
4
|
+
* Works with ANY feature - completely generic!
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { StepType } from "../../../../domain/entities/flow-config.types";
|
|
8
|
+
import type { StepDefinition } from "../../../../domain/entities/flow-config.types";
|
|
9
|
+
import type {
|
|
10
|
+
WizardFeatureConfig,
|
|
11
|
+
WizardStepConfig,
|
|
12
|
+
ScenarioBasedConfig,
|
|
13
|
+
buildWizardConfigFromScenario,
|
|
14
|
+
} from "../../domain/entities/wizard-config.types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert wizard step config to flow step definition
|
|
18
|
+
*/
|
|
19
|
+
const convertToFlowStep = (wizardStep: WizardStepConfig): StepDefinition => {
|
|
20
|
+
// Map wizard step types to flow step types
|
|
21
|
+
const typeMap: Record<string, StepType> = {
|
|
22
|
+
photo_upload: StepType.PARTNER_UPLOAD,
|
|
23
|
+
text_input: StepType.TEXT_INPUT,
|
|
24
|
+
selection: StepType.FEATURE_SELECTION,
|
|
25
|
+
preview: StepType.SCENARIO_PREVIEW,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
id: wizardStep.id,
|
|
30
|
+
type: typeMap[wizardStep.type] || StepType.CUSTOM,
|
|
31
|
+
required: wizardStep.required ?? true,
|
|
32
|
+
config: wizardStep, // Pass entire wizard config as config
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build flow steps from wizard feature config
|
|
38
|
+
* This is the ONLY function needed to convert any feature config to flow steps!
|
|
39
|
+
*/
|
|
40
|
+
export const buildFlowStepsFromWizard = (
|
|
41
|
+
featureConfig: WizardFeatureConfig,
|
|
42
|
+
options?: {
|
|
43
|
+
includePreview?: boolean;
|
|
44
|
+
includeGenerating?: boolean;
|
|
45
|
+
},
|
|
46
|
+
): StepDefinition[] => {
|
|
47
|
+
const steps: StepDefinition[] = [];
|
|
48
|
+
|
|
49
|
+
// Add scenario preview if requested
|
|
50
|
+
if (options?.includePreview) {
|
|
51
|
+
steps.push({
|
|
52
|
+
id: "SCENARIO_PREVIEW",
|
|
53
|
+
type: StepType.SCENARIO_PREVIEW,
|
|
54
|
+
required: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Convert all wizard steps to flow steps
|
|
59
|
+
featureConfig.steps.forEach((wizardStep) => {
|
|
60
|
+
if (wizardStep.enabled !== false) {
|
|
61
|
+
steps.push(convertToFlowStep(wizardStep));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Add generating step if requested
|
|
66
|
+
if (options?.includeGenerating) {
|
|
67
|
+
steps.push({
|
|
68
|
+
id: "GENERATING",
|
|
69
|
+
type: StepType.GENERATING,
|
|
70
|
+
required: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return steps;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get photo upload count from wizard config
|
|
79
|
+
*/
|
|
80
|
+
export const getPhotoUploadCount = (config: WizardFeatureConfig): number => {
|
|
81
|
+
return config.steps.filter((s) => s.type === "photo_upload").length;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get step config by ID
|
|
86
|
+
*/
|
|
87
|
+
export const getStepConfig = (
|
|
88
|
+
config: WizardFeatureConfig,
|
|
89
|
+
stepId: string,
|
|
90
|
+
): WizardStepConfig | undefined => {
|
|
91
|
+
return config.steps.find((s) => s.id === stepId);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Quick builder for common patterns
|
|
96
|
+
*/
|
|
97
|
+
export const quickBuildWizard = (
|
|
98
|
+
featureId: string,
|
|
99
|
+
scenarioConfig: ScenarioBasedConfig,
|
|
100
|
+
): StepDefinition[] => {
|
|
101
|
+
const { buildWizardConfigFromScenario } = require("../../domain/entities/wizard-config.types");
|
|
102
|
+
const wizardConfig = buildWizardConfigFromScenario(featureId, scenarioConfig);
|
|
103
|
+
return buildFlowStepsFromWizard(wizardConfig, {
|
|
104
|
+
includePreview: true,
|
|
105
|
+
includeGenerating: true,
|
|
106
|
+
});
|
|
107
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Step Renderer
|
|
3
|
+
* Renders any step type based on configuration
|
|
4
|
+
* NO feature-specific code! Works for ALL features!
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { StepType } from "../../../../domain/entities/flow-config.types";
|
|
9
|
+
import type { StepDefinition } from "../../../../domain/entities/flow-config.types";
|
|
10
|
+
import type {
|
|
11
|
+
WizardStepConfig,
|
|
12
|
+
PhotoUploadStepConfig,
|
|
13
|
+
TextInputStepConfig,
|
|
14
|
+
SelectionStepConfig,
|
|
15
|
+
} from "../../domain/entities/wizard-config.types";
|
|
16
|
+
|
|
17
|
+
// Import generic step components (will create these)
|
|
18
|
+
import { PhotoUploadStep } from "../../presentation/steps/PhotoUploadStep";
|
|
19
|
+
import { TextInputStep } from "../../presentation/steps/TextInputStep";
|
|
20
|
+
import { SelectionStep } from "../../presentation/steps/SelectionStep";
|
|
21
|
+
|
|
22
|
+
export interface StepRendererProps {
|
|
23
|
+
readonly step: StepDefinition;
|
|
24
|
+
readonly onContinue: (data: Record<string, unknown>) => void;
|
|
25
|
+
readonly onBack: () => void;
|
|
26
|
+
readonly t: (key: string) => string;
|
|
27
|
+
readonly translations?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render a step based on its type and configuration
|
|
32
|
+
* This is the ONLY renderer needed for ALL features!
|
|
33
|
+
*/
|
|
34
|
+
export const renderStep = (props: StepRendererProps): React.ReactElement | null => {
|
|
35
|
+
const { step, onContinue, onBack, t, translations } = props;
|
|
36
|
+
|
|
37
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
38
|
+
console.log("[StepRenderer] Rendering step", {
|
|
39
|
+
stepId: step.id,
|
|
40
|
+
stepType: step.type,
|
|
41
|
+
hasConfig: !!step.config,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const wizardConfig = step.config as WizardStepConfig | undefined;
|
|
46
|
+
|
|
47
|
+
switch (step.type) {
|
|
48
|
+
case StepType.PARTNER_UPLOAD: {
|
|
49
|
+
// This handles ALL photo uploads (couple, face-swap, image-to-video, etc.)
|
|
50
|
+
const photoConfig = wizardConfig as PhotoUploadStepConfig;
|
|
51
|
+
return (
|
|
52
|
+
<PhotoUploadStep
|
|
53
|
+
config={photoConfig}
|
|
54
|
+
onContinue={(imageData) => {
|
|
55
|
+
onContinue({ [`photo_${step.id}`]: imageData });
|
|
56
|
+
}}
|
|
57
|
+
onBack={onBack}
|
|
58
|
+
t={t}
|
|
59
|
+
translations={translations}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case StepType.TEXT_INPUT: {
|
|
65
|
+
const textConfig = wizardConfig as TextInputStepConfig;
|
|
66
|
+
return (
|
|
67
|
+
<TextInputStep
|
|
68
|
+
config={textConfig}
|
|
69
|
+
onContinue={(text) => {
|
|
70
|
+
onContinue({ text });
|
|
71
|
+
}}
|
|
72
|
+
onBack={onBack}
|
|
73
|
+
t={t}
|
|
74
|
+
translations={translations}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case StepType.FEATURE_SELECTION: {
|
|
80
|
+
const selectionConfig = wizardConfig as SelectionStepConfig;
|
|
81
|
+
return (
|
|
82
|
+
<SelectionStep
|
|
83
|
+
config={selectionConfig}
|
|
84
|
+
onContinue={(selected) => {
|
|
85
|
+
onContinue({ [`selection_${step.id}`]: selected });
|
|
86
|
+
}}
|
|
87
|
+
onBack={onBack}
|
|
88
|
+
t={t}
|
|
89
|
+
translations={translations}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case StepType.SCENARIO_PREVIEW:
|
|
95
|
+
case StepType.GENERATING:
|
|
96
|
+
case StepType.RESULT_PREVIEW:
|
|
97
|
+
// These are handled by parent wizard component
|
|
98
|
+
return null;
|
|
99
|
+
|
|
100
|
+
default:
|
|
101
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
102
|
+
console.warn("[StepRenderer] Unknown step type", step.type);
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Wizard Flow Component
|
|
3
|
+
* ONE wizard to rule them all!
|
|
4
|
+
*
|
|
5
|
+
* Works for:
|
|
6
|
+
* - Couple features (romantic-kiss, ai-hug, etc.)
|
|
7
|
+
* - Face swap
|
|
8
|
+
* - Image-to-video
|
|
9
|
+
* - Text-to-video
|
|
10
|
+
* - ANY future feature!
|
|
11
|
+
*
|
|
12
|
+
* NO feature-specific code here - everything driven by configuration!
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { useMemo, useCallback, useEffect } from "react";
|
|
16
|
+
import { View, StyleSheet } from "react-native";
|
|
17
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
18
|
+
import { useFlow } from "../../../../infrastructure/flow/useFlow";
|
|
19
|
+
import { StepType } from "../../../../domain/entities/flow-config.types";
|
|
20
|
+
import type { StepDefinition } from "../../../../domain/entities/flow-config.types";
|
|
21
|
+
import { renderStep } from "../../infrastructure/renderers/step-renderer";
|
|
22
|
+
import type { WizardFeatureConfig } from "../../domain/entities/wizard-config.types";
|
|
23
|
+
import { buildFlowStepsFromWizard } from "../../infrastructure/builders/dynamic-step-builder";
|
|
24
|
+
|
|
25
|
+
export interface GenericWizardFlowProps {
|
|
26
|
+
readonly featureConfig: WizardFeatureConfig;
|
|
27
|
+
readonly onStepChange?: (stepId: string, stepType: StepType | string) => void;
|
|
28
|
+
readonly onGenerationStart?: (data: Record<string, unknown>) => void;
|
|
29
|
+
readonly onBack?: () => void;
|
|
30
|
+
readonly t: (key: string) => string;
|
|
31
|
+
readonly translations?: Record<string, string>;
|
|
32
|
+
readonly renderPreview?: () => React.ReactElement | null;
|
|
33
|
+
readonly renderGenerating?: (progress: number) => React.ReactElement | null;
|
|
34
|
+
readonly renderResult?: (result: unknown) => React.ReactElement | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
38
|
+
featureConfig,
|
|
39
|
+
onStepChange,
|
|
40
|
+
onGenerationStart,
|
|
41
|
+
onBack,
|
|
42
|
+
t,
|
|
43
|
+
translations,
|
|
44
|
+
renderPreview,
|
|
45
|
+
renderGenerating,
|
|
46
|
+
renderResult,
|
|
47
|
+
}) => {
|
|
48
|
+
const tokens = useAppDesignTokens();
|
|
49
|
+
|
|
50
|
+
// Build flow steps from wizard config
|
|
51
|
+
const flowSteps = useMemo<StepDefinition[]>(() => {
|
|
52
|
+
return buildFlowStepsFromWizard(featureConfig, {
|
|
53
|
+
includePreview: !!renderPreview,
|
|
54
|
+
includeGenerating: !!renderGenerating,
|
|
55
|
+
});
|
|
56
|
+
}, [featureConfig, renderPreview, renderGenerating]);
|
|
57
|
+
|
|
58
|
+
// Initialize flow
|
|
59
|
+
const flow = useFlow({
|
|
60
|
+
steps: flowSteps,
|
|
61
|
+
initialStepIndex: 0,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// DEBUG logging
|
|
65
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
66
|
+
console.log("[GenericWizardFlow] Render", {
|
|
67
|
+
featureId: featureConfig.id,
|
|
68
|
+
currentStepId: flow.currentStep?.id,
|
|
69
|
+
currentStepType: flow.currentStep?.type,
|
|
70
|
+
stepIndex: flow.currentStepIndex,
|
|
71
|
+
totalSteps: flowSteps.length,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Notify parent when step changes
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (flow.currentStep && onStepChange) {
|
|
78
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
79
|
+
console.log("[GenericWizardFlow] Step changed", {
|
|
80
|
+
stepId: flow.currentStep.id,
|
|
81
|
+
stepType: flow.currentStep.type,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
onStepChange(flow.currentStep.id, flow.currentStep.type);
|
|
85
|
+
}
|
|
86
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
+
}, [flow.currentStep, flow.currentStepIndex]);
|
|
88
|
+
|
|
89
|
+
// Handle step continue
|
|
90
|
+
const handleStepContinue = useCallback(
|
|
91
|
+
(stepData: Record<string, unknown>) => {
|
|
92
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
93
|
+
console.log("[GenericWizardFlow] Step continue", {
|
|
94
|
+
stepId: flow.currentStep?.id,
|
|
95
|
+
data: stepData,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Store step data in custom data
|
|
100
|
+
Object.entries(stepData).forEach(([key, value]) => {
|
|
101
|
+
flow.setCustomData(key, value);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Check if this is the last step before generating
|
|
105
|
+
if (flow.currentStepIndex === flowSteps.length - 2) {
|
|
106
|
+
// Next step is GENERATING
|
|
107
|
+
if (onGenerationStart) {
|
|
108
|
+
onGenerationStart(flow.customData);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Move to next step
|
|
113
|
+
flow.nextStep();
|
|
114
|
+
},
|
|
115
|
+
[flow, flowSteps.length, onGenerationStart],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Handle back
|
|
119
|
+
const handleBack = useCallback(() => {
|
|
120
|
+
if (flow.currentStepIndex === 0) {
|
|
121
|
+
onBack?.();
|
|
122
|
+
} else {
|
|
123
|
+
flow.previousStep();
|
|
124
|
+
}
|
|
125
|
+
}, [flow, onBack]);
|
|
126
|
+
|
|
127
|
+
// Render current step
|
|
128
|
+
const renderCurrentStep = useCallback(() => {
|
|
129
|
+
const step = flow.currentStep;
|
|
130
|
+
if (!step) {
|
|
131
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
132
|
+
console.warn("[GenericWizardFlow] No current step!");
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
138
|
+
console.log("[GenericWizardFlow] Rendering step", {
|
|
139
|
+
stepId: step.id,
|
|
140
|
+
stepType: step.type,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Special steps with custom renderers
|
|
145
|
+
switch (step.type) {
|
|
146
|
+
case StepType.SCENARIO_PREVIEW:
|
|
147
|
+
return renderPreview?.() || null;
|
|
148
|
+
|
|
149
|
+
case StepType.GENERATING:
|
|
150
|
+
return renderGenerating?.(flow.generationProgress) || null;
|
|
151
|
+
|
|
152
|
+
case StepType.RESULT_PREVIEW:
|
|
153
|
+
return renderResult?.(flow.generationResult) || null;
|
|
154
|
+
|
|
155
|
+
default:
|
|
156
|
+
// Use generic step renderer
|
|
157
|
+
return renderStep({
|
|
158
|
+
step,
|
|
159
|
+
onContinue: handleStepContinue,
|
|
160
|
+
onBack: handleBack,
|
|
161
|
+
t,
|
|
162
|
+
translations,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}, [
|
|
166
|
+
flow.currentStep,
|
|
167
|
+
flow.generationProgress,
|
|
168
|
+
flow.generationResult,
|
|
169
|
+
renderPreview,
|
|
170
|
+
renderGenerating,
|
|
171
|
+
renderResult,
|
|
172
|
+
handleStepContinue,
|
|
173
|
+
handleBack,
|
|
174
|
+
t,
|
|
175
|
+
translations,
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
|
|
180
|
+
{renderCurrentStep()}
|
|
181
|
+
</View>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const styles = StyleSheet.create({
|
|
186
|
+
container: {
|
|
187
|
+
flex: 1,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Photo Upload State Hook
|
|
3
|
+
* Manages photo upload state for wizard steps
|
|
4
|
+
* NO feature-specific logic - works for ANY photo upload
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
8
|
+
import * as ImagePicker from "expo-image-picker";
|
|
9
|
+
import { Alert } from "react-native";
|
|
10
|
+
import type { UploadedImage } from "../../../../features/partner-upload/domain/types";
|
|
11
|
+
|
|
12
|
+
export interface PhotoUploadConfig {
|
|
13
|
+
readonly maxFileSizeMB?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PhotoUploadTranslations {
|
|
17
|
+
readonly fileTooLarge: string;
|
|
18
|
+
readonly maxFileSize: string;
|
|
19
|
+
readonly error: string;
|
|
20
|
+
readonly uploadFailed: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UsePhotoUploadStateProps {
|
|
24
|
+
readonly config?: PhotoUploadConfig;
|
|
25
|
+
readonly translations: PhotoUploadTranslations;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UsePhotoUploadStateReturn {
|
|
29
|
+
readonly image: UploadedImage | null;
|
|
30
|
+
readonly handlePickImage: () => Promise<void>;
|
|
31
|
+
readonly canContinue: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_MAX_FILE_SIZE_MB = 10;
|
|
35
|
+
|
|
36
|
+
export const usePhotoUploadState = ({
|
|
37
|
+
config,
|
|
38
|
+
translations,
|
|
39
|
+
}: UsePhotoUploadStateProps): UsePhotoUploadStateReturn => {
|
|
40
|
+
const [image, setImage] = useState<UploadedImage | null>(null);
|
|
41
|
+
|
|
42
|
+
const maxFileSizeMB = config?.maxFileSizeMB ?? DEFAULT_MAX_FILE_SIZE_MB;
|
|
43
|
+
|
|
44
|
+
const handlePickImage = useCallback(async () => {
|
|
45
|
+
try {
|
|
46
|
+
// Request permission
|
|
47
|
+
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
48
|
+
if (status !== "granted") {
|
|
49
|
+
Alert.alert(translations.error, "Permission to access media library is required");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Pick image
|
|
54
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
55
|
+
mediaTypes: "images" as any,
|
|
56
|
+
allowsEditing: true,
|
|
57
|
+
aspect: [1, 1],
|
|
58
|
+
quality: 0.8,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (result.canceled) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const selectedAsset = result.assets[0];
|
|
66
|
+
if (!selectedAsset) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check file size
|
|
71
|
+
const fileSize = selectedAsset.fileSize || 0;
|
|
72
|
+
const fileSizeMB = fileSize / (1024 * 1024);
|
|
73
|
+
|
|
74
|
+
if (fileSizeMB > maxFileSizeMB) {
|
|
75
|
+
Alert.alert(
|
|
76
|
+
translations.fileTooLarge,
|
|
77
|
+
translations.maxFileSize.replace("{size}", maxFileSizeMB.toString()),
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Create uploaded image object
|
|
83
|
+
const uploadedImage: UploadedImage = {
|
|
84
|
+
uri: selectedAsset.uri,
|
|
85
|
+
previewUrl: selectedAsset.uri,
|
|
86
|
+
width: selectedAsset.width,
|
|
87
|
+
height: selectedAsset.height,
|
|
88
|
+
fileSize,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
setImage(uploadedImage);
|
|
92
|
+
|
|
93
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
94
|
+
console.log("[usePhotoUploadState] Image selected", {
|
|
95
|
+
width: uploadedImage.width,
|
|
96
|
+
height: uploadedImage.height,
|
|
97
|
+
fileSizeMB: fileSizeMB.toFixed(2),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
102
|
+
console.error("[usePhotoUploadState] Error picking image", error);
|
|
103
|
+
}
|
|
104
|
+
Alert.alert(translations.error, translations.uploadFailed);
|
|
105
|
+
}
|
|
106
|
+
}, [maxFileSizeMB, translations]);
|
|
107
|
+
|
|
108
|
+
const canContinue = image !== null;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
image,
|
|
112
|
+
handlePickImage,
|
|
113
|
+
canContinue,
|
|
114
|
+
};
|
|
115
|
+
};
|