@umituz/react-native-ai-generation-content 1.36.1 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/src/domains/creations/infrastructure/repositories/CreationsWriter.ts +124 -199
  3. package/src/domains/creations/presentation/components/GalleryResultPreview.tsx +88 -0
  4. package/src/domains/creations/presentation/hooks/useGalleryCallbacks.ts +127 -0
  5. package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +37 -123
  6. package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.factory.ts +0 -1
  7. package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +5 -231
  8. package/src/domains/generation/wizard/presentation/components/WizardContinueButton.tsx +73 -0
  9. package/src/domains/generation/wizard/presentation/components/WizardFlowContent.tsx +181 -0
  10. package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +18 -134
  11. package/src/domains/generation/wizard/presentation/components/step-renderers/renderPhotoUploadStep.tsx +52 -0
  12. package/src/domains/generation/wizard/presentation/components/step-renderers/renderSelectionStep.tsx +59 -0
  13. package/src/domains/generation/wizard/presentation/components/step-renderers/renderTextInputStep.tsx +62 -0
  14. package/src/domains/generation/wizard/presentation/hooks/useWizardFlowHandlers.ts +133 -0
  15. package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +1 -2
  16. package/src/domains/generation/wizard/presentation/screens/GenericPhotoUploadScreen.tsx +15 -83
  17. package/src/domains/generation/wizard/presentation/screens/SelectionScreen.tsx +55 -134
  18. package/src/domains/result-preview/presentation/components/GenerationErrorScreen.tsx +19 -122
  19. package/src/domains/result-preview/presentation/hooks/useResultActions.ts +16 -131
  20. package/src/domains/result-preview/presentation/utils/media-file-utils.ts +78 -0
  21. package/src/domains/scenarios/presentation/components/ScenarioContinueButton.tsx +76 -0
  22. package/src/domains/scenarios/presentation/hooks/useHierarchicalScenarios.ts +70 -0
  23. package/src/domains/scenarios/presentation/screens/HierarchicalScenarioListScreen.tsx +37 -170
  24. package/src/presentation/hooks/generation/moderation-handler.ts +77 -0
  25. package/src/presentation/hooks/generation/orchestrator.ts +33 -125
@@ -2,10 +2,6 @@ import React, { useState, useMemo, useCallback, useEffect, useRef } from "react"
2
2
  import { View, FlatList, RefreshControl, StyleSheet } from "react-native";
3
3
  import {
4
4
  useAppDesignTokens,
5
- useAlert,
6
- AlertType,
7
- AlertMode,
8
- useSharing,
9
5
  FilterSheet,
10
6
  ScreenLayout,
11
7
  useAppFocusEffect,
@@ -13,10 +9,9 @@ import {
13
9
  import { useCreations } from "../hooks/useCreations";
14
10
  import { useDeleteCreation } from "../hooks/useDeleteCreation";
15
11
  import { useGalleryFilters } from "../hooks/useGalleryFilters";
12
+ import { useGalleryCallbacks } from "../hooks/useGalleryCallbacks";
16
13
  import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
17
- import { ResultPreviewScreen } from "../../../result-preview/presentation/components/ResultPreviewScreen";
18
- import { StarRatingPicker } from "../../../result-preview/presentation/components/StarRatingPicker";
19
- import { useResultActions } from "../../../result-preview/presentation/hooks/useResultActions";
14
+ import { GalleryResultPreview } from "../components/GalleryResultPreview";
20
15
  import { usePendingJobs } from "../../../../presentation/hooks/use-pending-jobs";
21
16
  import { MEDIA_FILTER_OPTIONS, STATUS_FILTER_OPTIONS } from "../../domain/types/creation-filter";
22
17
  import { getPreviewUrl } from "../../domain/utils";
@@ -33,7 +28,6 @@ interface CreationsGalleryScreenProps {
33
28
  readonly onEmptyAction?: () => void;
34
29
  readonly emptyActionLabel?: string;
35
30
  readonly showFilter?: boolean;
36
- /** Show pending generation jobs badge in header */
37
31
  readonly showPendingJobs?: boolean;
38
32
  }
39
33
 
@@ -49,16 +43,13 @@ export function CreationsGalleryScreen({
49
43
  showPendingJobs = true,
50
44
  }: CreationsGalleryScreenProps) {
51
45
  const tokens = useAppDesignTokens();
52
- const { share } = useSharing();
53
- const alert = useAlert();
54
46
  const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
55
47
  const [showRatingPicker, setShowRatingPicker] = useState(false);
56
48
  const hasAutoSelectedRef = useRef(false);
57
49
 
58
50
  const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
59
-
60
- // Background jobs for pending generations
61
51
  const { jobs: pendingJobs } = usePendingJobs();
52
+ const deleteMutation = useDeleteCreation({ userId, repository });
62
53
 
63
54
  // Auto-select creation when initialCreationId is provided
64
55
  useEffect(() => {
@@ -71,16 +62,16 @@ export function CreationsGalleryScreen({
71
62
  }
72
63
  }, [initialCreationId, creations]);
73
64
 
74
- const deleteMutation = useDeleteCreation({ userId, repository });
75
-
76
- const selectedImageUrl = selectedCreation ? (getPreviewUrl(selectedCreation.output) || selectedCreation.uri) : undefined;
77
- const selectedVideoUrl = selectedCreation?.output?.videoUrl;
78
-
79
- const { isSharing, isSaving, handleDownload, handleShare } = useResultActions({
80
- imageUrl: selectedImageUrl,
81
- videoUrl: selectedVideoUrl,
82
- onSaveSuccess: () => alert.show(AlertType.SUCCESS, AlertMode.TOAST, t("result.saveSuccess"), t("result.saveSuccessMessage")),
83
- onSaveError: () => alert.show(AlertType.ERROR, AlertMode.TOAST, t("common.error"), t("result.saveError")),
65
+ const callbacks = useGalleryCallbacks({
66
+ userId,
67
+ repository,
68
+ config,
69
+ t,
70
+ deleteMutation,
71
+ refetch: async () => { await refetch(); },
72
+ setSelectedCreation,
73
+ setShowRatingPicker,
74
+ selectedCreation,
84
75
  });
85
76
 
86
77
  const statusOptions = config.filterConfig?.statusOptions ?? STATUS_FILTER_OPTIONS;
@@ -92,57 +83,6 @@ export function CreationsGalleryScreen({
92
83
 
93
84
  useAppFocusEffect(useCallback(() => { void refetch(); }, [refetch]));
94
85
 
95
- const handleShareCard = useCallback((c: Creation) => {
96
- void share(c.uri, { dialogTitle: t("common.share") });
97
- }, [share, t]);
98
-
99
- const handleDelete = useCallback((c: Creation) => {
100
- alert.show(AlertType.WARNING, AlertMode.MODAL, t(config.translations.deleteTitle), t(config.translations.deleteMessage), {
101
- actions: [
102
- { id: "cancel", label: t("common.cancel"), onPress: () => {} },
103
- { id: "delete", label: t("common.delete"), style: "destructive", onPress: async () => {
104
- await deleteMutation.mutateAsync(c.id);
105
- }}
106
- ]
107
- });
108
- }, [alert, config, deleteMutation, t]);
109
-
110
- const handleFavorite = useCallback((c: Creation, isFavorite: boolean) => {
111
- void (async () => {
112
- if (!userId) return;
113
- const success = await repository.updateFavorite(userId, c.id, isFavorite);
114
- if (success) void refetch();
115
- })();
116
- }, [userId, repository, refetch]);
117
-
118
- const handleCardPress = useCallback((item: Creation) => {
119
- setSelectedCreation(item);
120
- }, []);
121
-
122
- const handleBack = useCallback(() => {
123
- setSelectedCreation(null);
124
- }, []);
125
-
126
- const handleTryAgain = useCallback(() => {
127
- setSelectedCreation(null);
128
- }, []);
129
-
130
- const handleOpenRatingPicker = useCallback(() => {
131
- setShowRatingPicker(true);
132
- }, []);
133
-
134
- const handleSubmitRating = useCallback((rating: number, description: string) => {
135
- if (!userId || !selectedCreation) return;
136
- void (async () => {
137
- const success = await repository.rate(userId, selectedCreation.id, rating, description);
138
- if (success) {
139
- setSelectedCreation({ ...selectedCreation, rating, ratedAt: new Date() });
140
- alert.show(AlertType.SUCCESS, AlertMode.TOAST, t("result.rateSuccessTitle"), t("result.rateSuccessMessage"));
141
- void refetch();
142
- }
143
- })();
144
- }, [userId, selectedCreation, repository, alert, t, refetch]);
145
-
146
86
  const filterButtons = useMemo(() => {
147
87
  const buttons = [];
148
88
  if (showStatusFilter) {
@@ -168,10 +108,7 @@ export function CreationsGalleryScreen({
168
108
 
169
109
  const getScenarioTitle = useCallback((type: string): string => {
170
110
  const typeConfig = config.types?.find((tc) => tc.id === type);
171
- if (typeConfig?.labelKey) {
172
- return t(typeConfig.labelKey);
173
- }
174
- return type.split("_").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
111
+ return typeConfig?.labelKey ? t(typeConfig.labelKey) : type.split("_").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
175
112
  }, [config.types, t]);
176
113
 
177
114
  const renderItem = useCallback(({ item }: { item: Creation }) => (
@@ -179,24 +116,21 @@ export function CreationsGalleryScreen({
179
116
  creation={item}
180
117
  titleText={getScenarioTitle(item.type)}
181
118
  callbacks={{
182
- onPress: () => handleCardPress(item),
183
- onShare: async () => handleShareCard(item),
184
- onDelete: () => handleDelete(item),
185
- onFavorite: () => handleFavorite(item, !item.isFavorite),
119
+ onPress: () => callbacks.handleCardPress(item),
120
+ onShare: async () => callbacks.handleShareCard(item),
121
+ onDelete: () => callbacks.handleDelete(item),
122
+ onFavorite: () => callbacks.handleFavorite(item, !item.isFavorite),
186
123
  }}
187
124
  />
188
- ), [handleShareCard, handleDelete, handleFavorite, handleCardPress, getScenarioTitle]);
125
+ ), [callbacks, getScenarioTitle]);
189
126
 
190
- // Calculate active pending jobs count once
191
127
  const activePendingCount = useMemo(() => {
192
128
  if (!showPendingJobs) return 0;
193
129
  return pendingJobs.filter((j) => j.status === "processing" || j.status === "queued").length;
194
130
  }, [showPendingJobs, pendingJobs]);
195
131
 
196
132
  const renderHeader = useMemo(() => {
197
- const hasCreations = creations && creations.length > 0;
198
- if (!hasCreations && !isLoading) return null;
199
-
133
+ if (!creations?.length && !isLoading) return null;
200
134
  return (
201
135
  <View style={[styles.header, { backgroundColor: tokens.colors.surface, borderBottomColor: tokens.colors.border }]}>
202
136
  <GalleryHeader
@@ -227,45 +161,25 @@ export function CreationsGalleryScreen({
227
161
  />
228
162
  ), [isLoading, creations, filters.isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, filters.clearAllFilters]);
229
163
 
230
- // Show result preview when a creation is selected
164
+ const selectedImageUrl = selectedCreation ? (getPreviewUrl(selectedCreation.output) || selectedCreation.uri) : undefined;
165
+ const selectedVideoUrl = selectedCreation?.output?.videoUrl;
231
166
  const hasMediaToShow = selectedImageUrl || selectedVideoUrl;
167
+
232
168
  if (selectedCreation && hasMediaToShow) {
233
- const hasRating = selectedCreation.rating !== undefined && selectedCreation.rating !== null;
234
169
  return (
235
- <>
236
- <ResultPreviewScreen
237
- imageUrl={selectedVideoUrl ? undefined : selectedImageUrl}
238
- videoUrl={selectedVideoUrl}
239
- isSaving={isSaving}
240
- isSharing={isSharing}
241
- onDownload={handleDownload}
242
- onShare={handleShare}
243
- onTryAgain={handleTryAgain}
244
- onNavigateBack={handleBack}
245
- onRate={handleOpenRatingPicker}
246
- hideLabel
247
- iconOnly
248
- showTryAgain
249
- showRating={!hasRating}
250
- translations={{
251
- title: t(config.translations.title),
252
- saveButton: t("result.saveButton"),
253
- saving: t("result.saving"),
254
- shareButton: t("result.shareButton"),
255
- sharing: t("result.sharing"),
256
- tryAnother: t("result.tryAnother"),
257
- }}
258
- />
259
- <StarRatingPicker
260
- visible={showRatingPicker}
261
- onClose={() => setShowRatingPicker(false)}
262
- onRate={handleSubmitRating}
263
- title={t("result.rateTitle")}
264
- submitLabel={t("common.submit")}
265
- cancelLabel={t("common.cancel")}
266
- descriptionPlaceholder={t("result.feedbackPlaceholder")}
267
- />
268
- </>
170
+ <GalleryResultPreview
171
+ selectedCreation={selectedCreation}
172
+ imageUrl={selectedVideoUrl ? undefined : selectedImageUrl}
173
+ videoUrl={selectedVideoUrl}
174
+ showRatingPicker={showRatingPicker}
175
+ config={config}
176
+ t={t}
177
+ onBack={callbacks.handleBack}
178
+ onTryAgain={callbacks.handleTryAgain}
179
+ onRate={callbacks.handleOpenRatingPicker}
180
+ onSubmitRating={callbacks.handleSubmitRating}
181
+ onCloseRating={() => setShowRatingPicker(false)}
182
+ />
269
183
  );
270
184
  }
271
185
 
@@ -277,7 +191,7 @@ export function CreationsGalleryScreen({
277
191
  keyExtractor={(item) => item.id}
278
192
  ListHeaderComponent={renderHeader}
279
193
  ListEmptyComponent={renderEmpty}
280
- contentContainerStyle={[styles.listContent, (!filters.filtered || filters.filtered.length === 0) && styles.emptyContent]}
194
+ contentContainerStyle={[styles.listContent, (!filters.filtered?.length) && styles.emptyContent]}
281
195
  showsVerticalScrollIndicator={false}
282
196
  refreshControl={<RefreshControl refreshing={isLoading} onRefresh={() => void refetch()} tintColor={tokens.colors.primary} />}
283
197
  />
@@ -17,7 +17,6 @@ export type { WizardStrategy } from "./wizard-strategy.types";
17
17
 
18
18
  export interface CreateWizardStrategyOptions {
19
19
  readonly scenario: WizardScenarioData;
20
- readonly wizardData: Record<string, unknown>;
21
20
  readonly collectionName?: string;
22
21
  }
23
22
 
@@ -4,33 +4,13 @@
4
4
  * Supports both scenario object and scenarioId (resolved from registry)
5
5
  */
6
6
 
7
- import React, { useMemo, useCallback, useEffect, useRef, useState } from "react";
8
- import { View, StyleSheet } from "react-native";
9
- import {
10
- useAppDesignTokens,
11
- useAlert,
12
- AlertType,
13
- AlertMode,
14
- } from "@umituz/react-native-design-system";
15
- import { useFlow } from "../../../infrastructure/flow/useFlow";
16
- import {
17
- StepType,
18
- type StepDefinition,
19
- } from "../../../../../domain/entities/flow-config.types";
7
+ import React, { useMemo } from "react";
8
+ import type { StepType } from "../../../../../domain/entities/flow-config.types";
20
9
  import type { WizardFeatureConfig } from "../../domain/entities/wizard-config.types";
21
- import { buildFlowStepsFromWizard } from "../../infrastructure/builders/dynamic-step-builder";
22
- import {
23
- useWizardGeneration,
24
- type WizardScenarioData,
25
- } from "../hooks/useWizardGeneration";
10
+ import type { WizardScenarioData } from "../hooks/useWizardGeneration";
26
11
  import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
27
- import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
28
- import type { Creation } from "../../../../creations/domain/entities/Creation";
29
- import { createCreationsRepository } from "../../../../creations";
30
- import { useResultActions } from "../../../../result-preview/presentation/hooks/useResultActions";
31
12
  import { validateScenario } from "../utilities/validateScenario";
32
- import { WizardStepRenderer } from "./WizardStepRenderer";
33
- import { StarRatingPicker } from "../../../../result-preview/presentation/components/StarRatingPicker";
13
+ import { WizardFlowContent } from "./WizardFlowContent";
34
14
  import {
35
15
  getConfiguredScenario,
36
16
  getDefaultOutputType,
@@ -83,9 +63,6 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = (props) => {
83
63
  renderResult,
84
64
  } = props;
85
65
 
86
- const tokens = useAppDesignTokens();
87
- const alert = useAlert();
88
-
89
66
  // Resolve scenario from prop or registry
90
67
  const scenario = useMemo<WizardScenarioData | undefined>(() => {
91
68
  if (scenarioProp) {
@@ -118,10 +95,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = (props) => {
118
95
  return undefined;
119
96
  }, [scenarioProp, scenarioId]);
120
97
 
121
- const validatedScenario = useMemo(
122
- () => validateScenario(scenario),
123
- [scenario],
124
- );
98
+ const validatedScenario = useMemo(() => validateScenario(scenario), [scenario]);
125
99
 
126
100
  return (
127
101
  <WizardFlowContent
@@ -139,209 +113,9 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = (props) => {
139
113
  onBack={onBack}
140
114
  onTryAgain={onTryAgain}
141
115
  t={t}
142
- tokens={tokens}
143
- alert={alert}
144
116
  renderPreview={renderPreview}
145
117
  renderGenerating={renderGenerating}
146
118
  renderResult={renderResult}
147
119
  />
148
120
  );
149
121
  };
150
-
151
- interface WizardFlowContentProps
152
- extends Omit<GenericWizardFlowProps, "scenarioId" | "translations"> {
153
- readonly validatedScenario: WizardScenarioData;
154
- readonly tokens: ReturnType<typeof useAppDesignTokens>;
155
- readonly alert: ReturnType<typeof useAlert>;
156
- }
157
-
158
- const WizardFlowContent: React.FC<WizardFlowContentProps> = (props) => {
159
- const {
160
- featureConfig,
161
- scenario,
162
- validatedScenario,
163
- userId,
164
- alertMessages,
165
- skipResultStep = false,
166
- onStepChange,
167
- onGenerationStart,
168
- onGenerationComplete,
169
- onGenerationError,
170
- onCreditsExhausted,
171
- onBack,
172
- onTryAgain,
173
- t,
174
- tokens,
175
- alert,
176
- renderPreview,
177
- renderGenerating,
178
- renderResult,
179
- } = props;
180
-
181
- const [currentCreation, setCurrentCreation] = useState<Creation | null>(null);
182
- const [showRatingPicker, setShowRatingPicker] = useState(false);
183
- const [hasRated, setHasRated] = useState(false);
184
- const prevStepIdRef = useRef<string | undefined>(undefined);
185
- const repository = useMemo(() => createCreationsRepository("creations"), []);
186
-
187
- const flowSteps = useMemo<StepDefinition[]>(
188
- () =>
189
- buildFlowStepsFromWizard(featureConfig, {
190
- includePreview: true,
191
- includeGenerating: true,
192
- includeResult: !skipResultStep,
193
- }),
194
- [featureConfig, skipResultStep],
195
- );
196
-
197
- const flow = useFlow({ steps: flowSteps, initialStepIndex: 0 });
198
- const {
199
- currentStep,
200
- currentStepIndex,
201
- customData,
202
- generationProgress,
203
- generationResult,
204
- nextStep,
205
- previousStep,
206
- setCustomData,
207
- setResult,
208
- } = flow;
209
-
210
- const resultImageUrl =
211
- currentCreation?.output?.imageUrl || currentCreation?.uri || "";
212
- const resultVideoUrl = currentCreation?.output?.videoUrl || "";
213
- const { isSaving, isSharing, handleDownload, handleShare } = useResultActions(
214
- { imageUrl: resultImageUrl, videoUrl: resultVideoUrl },
215
- );
216
-
217
- const handleGenerationComplete = useCallback(
218
- (result: unknown) => {
219
- if (typeof __DEV__ !== "undefined" && __DEV__) {
220
- console.log("[WizardFlowContent] Generation completed");
221
- }
222
- setResult(result);
223
- setCurrentCreation(result as Creation);
224
- onGenerationComplete?.(result);
225
- if (!skipResultStep) nextStep();
226
- },
227
- [setResult, nextStep, onGenerationComplete, skipResultStep],
228
- );
229
-
230
- useWizardGeneration({
231
- scenario: validatedScenario,
232
- wizardData: customData,
233
- userId,
234
- isGeneratingStep: currentStep?.type === StepType.GENERATING,
235
- alertMessages,
236
- onSuccess: handleGenerationComplete,
237
- onError: onGenerationError,
238
- onCreditsExhausted,
239
- });
240
-
241
- useEffect(() => {
242
- if (currentStep && onStepChange && prevStepIdRef.current !== currentStep.id) {
243
- prevStepIdRef.current = currentStep.id;
244
- onStepChange(currentStep.id, currentStep.type);
245
- }
246
- }, [currentStep, onStepChange]);
247
-
248
- const handleDismissGenerating = useCallback(() => {
249
- if (typeof __DEV__ !== "undefined" && __DEV__) {
250
- console.log("[WizardFlowContent] Dismissing - generation continues");
251
- }
252
- alert.show(
253
- AlertType.INFO,
254
- AlertMode.TOAST,
255
- t("generator.backgroundTitle"),
256
- t("generator.backgroundMessage"),
257
- );
258
- onBack?.();
259
- }, [alert, t, onBack]);
260
-
261
- const handleBack = useCallback(() => {
262
- if (currentStepIndex === 0) onBack?.();
263
- else previousStep();
264
- }, [currentStepIndex, previousStep, onBack]);
265
-
266
- const handleNextStep = useCallback(() => {
267
- const nextStepDef = flowSteps[currentStepIndex + 1];
268
- if (nextStepDef?.type === StepType.GENERATING && onGenerationStart) {
269
- onGenerationStart(customData, nextStep);
270
- return;
271
- }
272
- nextStep();
273
- }, [currentStepIndex, flowSteps, customData, onGenerationStart, nextStep]);
274
-
275
- const handlePhotoContinue = useCallback(
276
- (stepId: string, image: UploadedImage) => {
277
- setCustomData(stepId, image);
278
- handleNextStep();
279
- },
280
- [setCustomData, handleNextStep],
281
- );
282
-
283
- const handleSubmitRating = useCallback(
284
- async (rating: number, description: string) => {
285
- if (!currentCreation?.id || !userId) return;
286
- const success = await repository.rate(
287
- userId,
288
- currentCreation.id,
289
- rating,
290
- description,
291
- );
292
- if (success) {
293
- setHasRated(true);
294
- alert.show(
295
- AlertType.SUCCESS,
296
- AlertMode.TOAST,
297
- t("result.rateSuccessTitle"),
298
- t("result.rateSuccessMessage"),
299
- );
300
- }
301
- setShowRatingPicker(false);
302
- },
303
- [currentCreation, userId, repository, alert, t],
304
- );
305
-
306
- return (
307
- <View
308
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
309
- >
310
- <WizardStepRenderer
311
- step={currentStep}
312
- scenario={scenario}
313
- customData={customData}
314
- generationProgress={generationProgress}
315
- generationResult={generationResult}
316
- isSaving={isSaving}
317
- isSharing={isSharing}
318
- showRating={Boolean(userId) && !hasRated}
319
- onNext={handleNextStep}
320
- onBack={handleBack}
321
- onPhotoContinue={handlePhotoContinue}
322
- onDownload={handleDownload}
323
- onShare={handleShare}
324
- onRate={() => setShowRatingPicker(true)}
325
- onTryAgain={onTryAgain}
326
- onDismissGenerating={handleDismissGenerating}
327
- t={t}
328
- renderPreview={renderPreview}
329
- renderGenerating={renderGenerating}
330
- renderResult={renderResult}
331
- />
332
- <StarRatingPicker
333
- visible={showRatingPicker}
334
- onClose={() => setShowRatingPicker(false)}
335
- onRate={handleSubmitRating}
336
- title={t("result.rateTitle")}
337
- submitLabel={t("common.submit")}
338
- cancelLabel={t("common.cancel")}
339
- descriptionPlaceholder={t("result.feedbackPlaceholder")}
340
- />
341
- </View>
342
- );
343
- };
344
-
345
- const styles = StyleSheet.create({
346
- container: { flex: 1 },
347
- });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Wizard Continue Button Component
3
+ * Reusable continue button for wizard screens
4
+ */
5
+
6
+ import React from "react";
7
+ import { TouchableOpacity, StyleSheet } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ type IconName,
13
+ } from "@umituz/react-native-design-system";
14
+
15
+ export interface WizardContinueButtonProps {
16
+ readonly canContinue: boolean;
17
+ readonly onPress: () => void;
18
+ readonly label: string;
19
+ readonly icon?: IconName;
20
+ }
21
+
22
+ export function WizardContinueButton({
23
+ canContinue,
24
+ onPress,
25
+ label,
26
+ icon = "chevron-forward-outline",
27
+ }: WizardContinueButtonProps) {
28
+ const tokens = useAppDesignTokens();
29
+
30
+ return (
31
+ <TouchableOpacity
32
+ onPress={onPress}
33
+ disabled={!canContinue}
34
+ activeOpacity={0.7}
35
+ style={[
36
+ styles.button,
37
+ {
38
+ backgroundColor: canContinue ? tokens.colors.primary : tokens.colors.surfaceVariant,
39
+ opacity: canContinue ? 1 : 0.5,
40
+ paddingHorizontal: tokens.spacing.md,
41
+ paddingVertical: tokens.spacing.xs,
42
+ borderRadius: tokens.borders.radius.full,
43
+ },
44
+ ]}
45
+ >
46
+ <AtomicText
47
+ type="bodyMedium"
48
+ style={[
49
+ styles.text,
50
+ { color: canContinue ? tokens.colors.onPrimary : tokens.colors.textSecondary },
51
+ ]}
52
+ >
53
+ {label}
54
+ </AtomicText>
55
+ <AtomicIcon
56
+ name={icon}
57
+ size="sm"
58
+ color={canContinue ? "onPrimary" : "textSecondary"}
59
+ />
60
+ </TouchableOpacity>
61
+ );
62
+ }
63
+
64
+ const styles = StyleSheet.create({
65
+ button: {
66
+ flexDirection: "row",
67
+ alignItems: "center",
68
+ },
69
+ text: {
70
+ fontWeight: "800",
71
+ marginRight: 4,
72
+ },
73
+ });