@umituz/react-native-ai-generation-content 1.61.39 → 1.61.40

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 (28) hide show
  1. package/package.json +1 -1
  2. package/src/domains/creations/infrastructure/repositories/CreationsWriter.ts +16 -213
  3. package/src/domains/creations/infrastructure/repositories/creations-operations.ts +128 -0
  4. package/src/domains/creations/infrastructure/repositories/creations-state-operations.ts +86 -0
  5. package/src/domains/creations/presentation/components/GalleryScreenHeader.tsx +54 -0
  6. package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +23 -56
  7. package/src/domains/creations/presentation/utils/filter-buttons.util.ts +75 -0
  8. package/src/domains/generation/wizard/presentation/screens/GeneratingScreen.tsx +36 -43
  9. package/src/domains/generation/wizard/presentation/screens/TextInputScreen.tsx +6 -39
  10. package/src/domains/scenarios/presentation/screens/ScenarioPreviewScreen.tsx +7 -25
  11. package/src/features/image-to-video/presentation/screens/ImageToVideoWizardFlow.tsx +26 -46
  12. package/src/features/shared/index.ts +6 -0
  13. package/src/features/shared/presentation/components/AutoSkipPreview.tsx +24 -0
  14. package/src/features/shared/presentation/components/index.ts +6 -0
  15. package/src/features/shared/presentation/utils/index.ts +14 -0
  16. package/src/features/shared/presentation/utils/wizard-flow.utils.ts +84 -0
  17. package/src/features/text-to-image/presentation/screens/TextToImageWizardFlow.tsx +26 -46
  18. package/src/features/text-to-video/presentation/screens/TextToVideoWizardFlow.tsx +26 -46
  19. package/src/infrastructure/logging/debug.util.ts +1 -1
  20. package/src/infrastructure/logging/index.ts +1 -1
  21. package/src/infrastructure/validation/advanced-validator.ts +97 -0
  22. package/src/infrastructure/validation/ai-validator.ts +77 -0
  23. package/src/infrastructure/validation/base-validator.ts +149 -0
  24. package/src/infrastructure/validation/entity-validator.ts +64 -0
  25. package/src/infrastructure/validation/input-validator.ts +37 -409
  26. package/src/infrastructure/validation/sanitizer.ts +43 -0
  27. package/src/presentation/components/buttons/ContinueButton.tsx +72 -0
  28. package/src/presentation/components/buttons/index.ts +1 -0
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Filter Buttons Utility
3
+ * Creates filter buttons for the gallery screen
4
+ */
5
+
6
+ export interface FilterButton {
7
+ readonly id: string;
8
+ readonly label: string;
9
+ readonly icon: string;
10
+ readonly isActive: boolean;
11
+ readonly onPress: () => void;
12
+ }
13
+
14
+ export interface FilterButtonsConfig {
15
+ readonly showStatusFilter: boolean;
16
+ readonly showMediaFilter: boolean;
17
+ readonly statusFilterActive: boolean;
18
+ readonly mediaFilterActive: boolean;
19
+ readonly statusFilterLabel: string;
20
+ readonly mediaFilterLabel: string;
21
+ readonly onStatusFilterPress: () => void;
22
+ readonly onMediaFilterPress: () => void;
23
+ }
24
+
25
+ /**
26
+ * Creates filter buttons array for gallery header
27
+ */
28
+ export function createFilterButtons(config: FilterButtonsConfig): FilterButton[] {
29
+ const buttons: FilterButton[] = [];
30
+
31
+ if (config.showStatusFilter) {
32
+ buttons.push({
33
+ id: "status",
34
+ label: config.statusFilterLabel,
35
+ icon: "list-outline",
36
+ isActive: config.statusFilterActive,
37
+ onPress: config.onStatusFilterPress,
38
+ });
39
+ }
40
+
41
+ if (config.showMediaFilter) {
42
+ buttons.push({
43
+ id: "media",
44
+ label: config.mediaFilterLabel,
45
+ icon: "grid-outline",
46
+ isActive: config.mediaFilterActive,
47
+ onPress: config.onMediaFilterPress,
48
+ });
49
+ }
50
+
51
+ return buttons;
52
+ }
53
+
54
+ /**
55
+ * Creates item title for creation card
56
+ */
57
+ export function createItemTitle(
58
+ item: { type: string; metadata?: Record<string, unknown> },
59
+ config: {
60
+ readonly types?: readonly { id: string; labelKey?: string }[];
61
+ readonly getCreationTitle?: (params: { type: string; metadata?: Record<string, unknown> }) => string;
62
+ },
63
+ t: (key: string) => string
64
+ ): string {
65
+ if (config.getCreationTitle) {
66
+ return config.getCreationTitle({ type: item.type, metadata: item.metadata });
67
+ }
68
+
69
+ const typeConfig = config.types?.find((tc) => tc.id === item.type);
70
+ if (typeConfig?.labelKey) {
71
+ return t(typeConfig.labelKey);
72
+ }
73
+
74
+ return item.type.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
75
+ }
@@ -7,7 +7,7 @@
7
7
 
8
8
  import React, { useMemo } from "react";
9
9
  import { View, StyleSheet, ActivityIndicator, TouchableOpacity } from "react-native";
10
- import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
10
+ import { useAppDesignTokens, AtomicText, ScreenLayout } from "@umituz/react-native-design-system";
11
11
  import { useGenerationPhase } from "../hooks/useGenerationPhase";
12
12
  import { IndeterminateProgressBar } from "../components/IndeterminateProgressBar";
13
13
 
@@ -25,7 +25,6 @@ export interface GeneratingScreenProps {
25
25
  };
26
26
  };
27
27
  readonly t: (key: string) => string;
28
- /** Called when user dismisses the screen - generation continues in background */
29
28
  readonly onDismiss?: () => void;
30
29
  }
31
30
 
@@ -38,13 +37,6 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
38
37
  const tokens = useAppDesignTokens();
39
38
  const phase = useGenerationPhase();
40
39
 
41
- if (typeof __DEV__ !== "undefined" && __DEV__) {
42
- console.log("[GeneratingScreen] Rendering", {
43
- phase,
44
- scenarioId: scenario?.id,
45
- });
46
- }
47
-
48
40
  const messages = useMemo(() => {
49
41
  const custom = scenario?.generatingMessages;
50
42
  return {
@@ -69,48 +61,49 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
69
61
  }, [phase, t, messages.waitMessage]);
70
62
 
71
63
  return (
72
- <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
73
- <View style={styles.content}>
74
- <ActivityIndicator size="large" color={tokens.colors.primary} />
64
+ <ScreenLayout backgroundColor={tokens.colors.backgroundPrimary}>
65
+ <View style={styles.container}>
66
+ <View style={styles.content}>
67
+ <ActivityIndicator size="large" color={tokens.colors.primary} />
75
68
 
76
- <AtomicText type="headlineMedium" style={styles.title}>
77
- {messages.title}
78
- </AtomicText>
69
+ <AtomicText type="headlineMedium" style={styles.title}>
70
+ {messages.title}
71
+ </AtomicText>
79
72
 
80
- <AtomicText type="bodyMedium" style={[styles.message, { color: tokens.colors.textSecondary }]}>
81
- {statusMessage}
82
- </AtomicText>
73
+ <AtomicText type="bodyMedium" style={[styles.message, { color: tokens.colors.textSecondary }]}>
74
+ {statusMessage}
75
+ </AtomicText>
83
76
 
84
- <View style={styles.progressContainer}>
85
- <IndeterminateProgressBar
86
- backgroundColor={tokens.colors.surfaceVariant}
87
- fillColor={tokens.colors.primary}
88
- />
89
- </View>
77
+ <View style={styles.progressContainer}>
78
+ <IndeterminateProgressBar
79
+ backgroundColor={tokens.colors.surfaceVariant}
80
+ fillColor={tokens.colors.primary}
81
+ />
82
+ </View>
83
+
84
+ {scenario && (
85
+ <AtomicText type="bodySmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
86
+ {scenario.title || scenario.id}
87
+ </AtomicText>
88
+ )}
90
89
 
91
- {scenario && (
92
90
  <AtomicText type="bodySmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
93
- {scenario.title || scenario.id}
91
+ {messages.hint}
94
92
  </AtomicText>
95
- )}
96
-
97
- <AtomicText type="bodySmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
98
- {messages.hint}
99
- </AtomicText>
100
93
 
101
- {/* Background hint - tap to dismiss */}
102
- {onDismiss && (
103
- <TouchableOpacity
104
- style={[styles.backgroundHintButton, { borderColor: tokens.colors.primary }]}
105
- onPress={onDismiss}
106
- >
107
- <AtomicText type="bodyLarge" style={[styles.backgroundHint, { color: tokens.colors.primary }]}>
108
- {messages.backgroundHint}
109
- </AtomicText>
110
- </TouchableOpacity>
111
- )}
94
+ {onDismiss && (
95
+ <TouchableOpacity
96
+ style={[styles.backgroundHintButton, { borderColor: tokens.colors.primary }]}
97
+ onPress={onDismiss}
98
+ >
99
+ <AtomicText type="bodyLarge" style={[styles.backgroundHint, { color: tokens.colors.primary }]}>
100
+ {messages.backgroundHint}
101
+ </AtomicText>
102
+ </TouchableOpacity>
103
+ )}
104
+ </View>
112
105
  </View>
113
- </View>
106
+ </ScreenLayout>
114
107
  );
115
108
  };
116
109
 
@@ -5,16 +5,16 @@
5
5
  */
6
6
 
7
7
  import React, { useState, useCallback, useMemo } from "react";
8
- import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
8
+ import { View, TextInput, StyleSheet } from "react-native";
9
9
  import {
10
10
  AtomicText,
11
11
  AtomicButton,
12
- AtomicIcon,
13
12
  useAppDesignTokens,
14
13
  ScreenLayout,
15
14
  NavigationHeader,
16
15
  type DesignTokens,
17
16
  } from "@umituz/react-native-design-system";
17
+ import { ContinueButton } from "#presentation/components/buttons";
18
18
  import type { TextInputScreenProps } from "./TextInputScreen.types";
19
19
 
20
20
  export type {
@@ -57,33 +57,11 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
57
57
  title=""
58
58
  onBackPress={onBack}
59
59
  rightElement={
60
- <TouchableOpacity
60
+ <ContinueButton
61
+ label={translations.continueButton}
62
+ canContinue={canContinue}
61
63
  onPress={handleContinue}
62
- activeOpacity={0.7}
63
- disabled={!canContinue}
64
- style={[
65
- styles.continueButton,
66
- {
67
- backgroundColor: canContinue ? tokens.colors.primary : tokens.colors.surfaceVariant,
68
- opacity: canContinue ? 1 : 0.5,
69
- },
70
- ]}
71
- >
72
- <AtomicText
73
- type="bodyMedium"
74
- style={[
75
- styles.continueText,
76
- { color: canContinue ? tokens.colors.onPrimary : tokens.colors.textSecondary },
77
- ]}
78
- >
79
- {translations.continueButton}
80
- </AtomicText>
81
- <AtomicIcon
82
- name="arrow-forward"
83
- size="sm"
84
- color={canContinue ? "onPrimary" : "textSecondary"}
85
- />
86
- </TouchableOpacity>
64
+ />
87
65
  }
88
66
  />
89
67
  <ScreenLayout
@@ -179,15 +157,4 @@ const createStyles = (tokens: DesignTokens) =>
179
157
  exampleButton: {
180
158
  marginBottom: tokens.spacing.xs,
181
159
  },
182
- continueButton: {
183
- flexDirection: "row",
184
- alignItems: "center",
185
- paddingHorizontal: tokens.spacing.md,
186
- paddingVertical: tokens.spacing.xs,
187
- borderRadius: tokens.borders.radius.full,
188
- },
189
- continueText: {
190
- fontWeight: "800",
191
- marginRight: 4,
192
- },
193
160
  });
@@ -4,16 +4,17 @@
4
4
  */
5
5
 
6
6
  import React, { useMemo } from "react";
7
- import { View, StyleSheet, TouchableOpacity } from "react-native";
7
+ import { View, StyleSheet } from "react-native";
8
8
  import {
9
9
  AtomicText,
10
+ AtomicIcon,
10
11
  useAppDesignTokens,
11
12
  ScreenLayout,
12
13
  type DesignTokens,
13
14
  HeroSection,
14
- AtomicIcon,
15
15
  NavigationHeader,
16
16
  } from "@umituz/react-native-design-system";
17
+ import { ContinueButton } from "#presentation/components/buttons";
17
18
  import type { ScenarioData } from "../../domain/scenario.types";
18
19
 
19
20
  export interface ScenarioPreviewTranslations {
@@ -45,30 +46,11 @@ export const ScenarioPreviewScreen: React.FC<ScenarioPreviewScreenProps> = ({
45
46
  title=""
46
47
  onBackPress={onBack}
47
48
  rightElement={
48
- <TouchableOpacity
49
+ <ContinueButton
50
+ label={translations.continueButton}
51
+ canContinue={true}
49
52
  onPress={onContinue}
50
- activeOpacity={0.7}
51
- style={{
52
- flexDirection: "row",
53
- alignItems: "center",
54
- backgroundColor: tokens.colors.primary,
55
- paddingHorizontal: tokens.spacing.md,
56
- paddingVertical: tokens.spacing.xs,
57
- borderRadius: tokens.borders.radius.full,
58
- }}
59
- >
60
- <AtomicText
61
- type="bodyMedium"
62
- style={{
63
- fontWeight: "800",
64
- color: tokens.colors.onPrimary,
65
- marginRight: 4,
66
- }}
67
- >
68
- {translations.continueButton}
69
- </AtomicText>
70
- <AtomicIcon name="arrow-forward" size="sm" color="onPrimary" />
71
- </TouchableOpacity>
53
+ />
72
54
  }
73
55
  />
74
56
  <ScreenLayout
@@ -3,26 +3,19 @@
3
3
  * Step-based wizard flow for image-to-video generation
4
4
  */
5
5
 
6
- import React, { useMemo, useCallback, useEffect, useRef } from "react";
6
+ import React, { useMemo } from "react";
7
7
  import { View, StyleSheet } from "react-native";
8
8
  import { useAppDesignTokens } from "@umituz/react-native-design-system";
9
9
  import { GenericWizardFlow } from "../../../../domains/generation/wizard/presentation/components";
10
10
  import { IMAGE_TO_VIDEO_WIZARD_CONFIG } from "../../../../domains/generation/wizard/configs";
11
11
  import { useAIFeatureGate } from "../../../../domains/access-control";
12
- import type { WizardScenarioData } from "../../../../domains/generation/wizard/presentation/hooks/useWizardGeneration";
12
+ import {
13
+ createDefaultAlerts,
14
+ createScenarioData,
15
+ useWizardFlowHandlers,
16
+ AutoSkipPreview,
17
+ } from "../../../shared";
13
18
  import type { BaseWizardFlowProps } from "../../../../domains/generation/wizard/presentation/components/WizardFlow.types";
14
- import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
15
-
16
- const AutoSkipPreview: React.FC<{ onContinue: () => void }> = ({ onContinue }) => {
17
- const hasContinued = useRef(false);
18
- useEffect(() => {
19
- if (!hasContinued.current) {
20
- hasContinued.current = true;
21
- onContinue();
22
- }
23
- }, [onContinue]);
24
- return null;
25
- };
26
19
 
27
20
  export type ImageToVideoWizardFlowProps = BaseWizardFlowProps;
28
21
 
@@ -41,46 +34,33 @@ export const ImageToVideoWizardFlow: React.FC<ImageToVideoWizardFlowProps> = (pr
41
34
 
42
35
  const tokens = useAppDesignTokens();
43
36
 
44
- // Centralized access control - handles offline, auth, credits, paywall
45
37
  const { requireFeature } = useAIFeatureGate({
46
38
  creditCost,
47
39
  onNetworkError,
48
40
  });
49
41
 
50
- const scenario: WizardScenarioData = useMemo(
51
- () => ({
52
- id: "image-to-video",
53
- outputType: "video",
54
- inputType: "single",
55
- model,
56
- title: t("image2video.title"),
57
- }),
58
- [model, t],
59
- );
60
-
61
- const defaultAlerts = useMemo<AlertMessages>(
62
- () => ({
63
- networkError: t("common.errors.network"),
64
- policyViolation: t("common.errors.policy"),
65
- saveFailed: t("common.errors.saveFailed"),
66
- creditFailed: t("common.errors.creditFailed"),
67
- unknown: t("common.errors.unknown"),
68
- }),
69
- [t],
42
+ const scenario = useMemo(
43
+ () =>
44
+ createScenarioData(
45
+ {
46
+ id: "image-to-video",
47
+ outputType: "video",
48
+ inputType: "single",
49
+ model,
50
+ titleKey: "image2video.title",
51
+ },
52
+ t
53
+ ),
54
+ [model, t]
70
55
  );
71
56
 
72
- const handleGenerationStart = useCallback(
73
- (_data: Record<string, unknown>, proceed: () => void) => {
74
- // Use centralized access control - checks offline, auth, credits
75
- requireFeature(proceed);
76
- },
77
- [requireFeature],
78
- );
57
+ const defaultAlerts = useMemo(() => createDefaultAlerts(t), [t]);
79
58
 
80
- const handleGenerationComplete = useCallback(() => {
81
- onGenerationComplete?.();
82
- onBack();
83
- }, [onGenerationComplete, onBack]);
59
+ const { handleGenerationStart, handleGenerationComplete } = useWizardFlowHandlers({
60
+ requireFeature,
61
+ onGenerationComplete,
62
+ onBack,
63
+ });
84
64
 
85
65
  return (
86
66
  <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared Features
3
+ */
4
+
5
+ export * from "./presentation/utils";
6
+ export * from "./presentation/components";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * AutoSkipPreview Component
3
+ * Automatically continues to the next step on mount
4
+ * Used in wizard flows to skip the preview step
5
+ */
6
+
7
+ import { useEffect, useRef } from "react";
8
+
9
+ export interface AutoSkipPreviewProps {
10
+ readonly onContinue: () => void;
11
+ }
12
+
13
+ export const AutoSkipPreview: React.FC<AutoSkipPreviewProps> = ({ onContinue }) => {
14
+ const hasContinued = useRef(false);
15
+
16
+ useEffect(() => {
17
+ if (!hasContinued.current) {
18
+ hasContinued.current = true;
19
+ onContinue();
20
+ }
21
+ }, [onContinue]);
22
+
23
+ return null;
24
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared Presentation Components
3
+ */
4
+
5
+ export { AutoSkipPreview } from "./AutoSkipPreview";
6
+ export type { AutoSkipPreviewProps } from "./AutoSkipPreview";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Wizard Flow Utilities
3
+ */
4
+
5
+ export {
6
+ createDefaultAlerts,
7
+ createScenarioData,
8
+ useWizardFlowHandlers,
9
+ type WizardFlowConfig,
10
+ type UseWizardFlowHandlersOptions,
11
+ } from "./wizard-flow.utils";
12
+
13
+ export { AutoSkipPreview } from "../components/AutoSkipPreview";
14
+ export type { AutoSkipPreviewProps } from "../components/AutoSkipPreview";
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Wizard Flow Utilities
3
+ * Common utilities for wizard flow screens
4
+ */
5
+
6
+ import { useCallback } from "react";
7
+ import type { WizardScenarioData } from "../../../../domains/generation/wizard/presentation/hooks/useWizardGeneration";
8
+ import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
9
+
10
+ /**
11
+ * Creates default alert messages for wizard flows
12
+ */
13
+ export function createDefaultAlerts(t: (key: string) => string): AlertMessages {
14
+ return {
15
+ networkError: t("common.errors.network"),
16
+ policyViolation: t("common.errors.policy"),
17
+ saveFailed: t("common.errors.saveFailed"),
18
+ creditFailed: t("common.errors.creditFailed"),
19
+ unknown: t("common.errors.unknown"),
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Configuration for wizard flow utilities
25
+ */
26
+ export interface WizardFlowConfig {
27
+ readonly id: string;
28
+ readonly outputType: "image" | "video";
29
+ readonly inputType: "text" | "single" | "dual";
30
+ readonly model: string;
31
+ readonly titleKey: string;
32
+ }
33
+
34
+ /**
35
+ * Creates wizard scenario data from config
36
+ */
37
+ export function createScenarioData(
38
+ config: WizardFlowConfig,
39
+ t: (key: string) => string
40
+ ): WizardScenarioData {
41
+ return {
42
+ id: config.id,
43
+ outputType: config.outputType,
44
+ inputType: config.inputType,
45
+ model: config.model,
46
+ title: t(config.titleKey),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Hook for wizard flow handlers
52
+ */
53
+ export interface UseWizardFlowHandlersOptions {
54
+ readonly requireFeature: (proceed: () => void) => void;
55
+ readonly onGenerationComplete?: () => void;
56
+ readonly onBack: () => void;
57
+ }
58
+
59
+ export function useWizardFlowHandlers({
60
+ requireFeature,
61
+ onGenerationComplete,
62
+ onBack,
63
+ }: UseWizardFlowHandlersOptions) {
64
+ const handleGenerationStart = useCallback(
65
+ (_data: Record<string, unknown>, proceed: () => void) => {
66
+ requireFeature(proceed);
67
+ },
68
+ [requireFeature]
69
+ );
70
+
71
+ const handleGenerationComplete = useCallback(() => {
72
+ onGenerationComplete?.();
73
+ onBack();
74
+ }, [onGenerationComplete, onBack]);
75
+
76
+ return {
77
+ handleGenerationStart,
78
+ handleGenerationComplete,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Wizard flow type definitions
84
+ */
@@ -3,26 +3,19 @@
3
3
  * Step-based wizard flow for text-to-image generation
4
4
  */
5
5
 
6
- import React, { useMemo, useCallback, useEffect, useRef } from "react";
6
+ import React, { useMemo } from "react";
7
7
  import { View, StyleSheet } from "react-native";
8
8
  import { useAppDesignTokens } from "@umituz/react-native-design-system";
9
9
  import { GenericWizardFlow } from "../../../../domains/generation/wizard/presentation/components";
10
10
  import { TEXT_TO_IMAGE_WIZARD_CONFIG } from "../../../../domains/generation/wizard/configs";
11
11
  import { useAIFeatureGate } from "../../../../domains/access-control";
12
- import type { WizardScenarioData } from "../../../../domains/generation/wizard/presentation/hooks/useWizardGeneration";
12
+ import {
13
+ createDefaultAlerts,
14
+ createScenarioData,
15
+ useWizardFlowHandlers,
16
+ AutoSkipPreview,
17
+ } from "../../../shared";
13
18
  import type { BaseWizardFlowProps } from "../../../../domains/generation/wizard/presentation/components/WizardFlow.types";
14
- import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
15
-
16
- const AutoSkipPreview: React.FC<{ onContinue: () => void }> = ({ onContinue }) => {
17
- const hasContinued = useRef(false);
18
- useEffect(() => {
19
- if (!hasContinued.current) {
20
- hasContinued.current = true;
21
- onContinue();
22
- }
23
- }, [onContinue]);
24
- return null;
25
- };
26
19
 
27
20
  export type TextToImageWizardFlowProps = BaseWizardFlowProps;
28
21
 
@@ -41,46 +34,33 @@ export const TextToImageWizardFlow: React.FC<TextToImageWizardFlowProps> = (prop
41
34
 
42
35
  const tokens = useAppDesignTokens();
43
36
 
44
- // Centralized access control - handles offline, auth, credits, paywall
45
37
  const { requireFeature } = useAIFeatureGate({
46
38
  creditCost,
47
39
  onNetworkError,
48
40
  });
49
41
 
50
- const scenario: WizardScenarioData = useMemo(
51
- () => ({
52
- id: "text-to-image",
53
- outputType: "image",
54
- inputType: "text",
55
- model,
56
- title: t("text2image.title"),
57
- }),
58
- [model, t],
59
- );
60
-
61
- const defaultAlerts = useMemo<AlertMessages>(
62
- () => ({
63
- networkError: t("common.errors.network"),
64
- policyViolation: t("common.errors.policy"),
65
- saveFailed: t("common.errors.saveFailed"),
66
- creditFailed: t("common.errors.creditFailed"),
67
- unknown: t("common.errors.unknown"),
68
- }),
69
- [t],
42
+ const scenario = useMemo(
43
+ () =>
44
+ createScenarioData(
45
+ {
46
+ id: "text-to-image",
47
+ outputType: "image",
48
+ inputType: "text",
49
+ model,
50
+ titleKey: "text2image.title",
51
+ },
52
+ t
53
+ ),
54
+ [model, t]
70
55
  );
71
56
 
72
- const handleGenerationStart = useCallback(
73
- (_data: Record<string, unknown>, proceed: () => void) => {
74
- // Use centralized access control - checks offline, auth, credits
75
- requireFeature(proceed);
76
- },
77
- [requireFeature],
78
- );
57
+ const defaultAlerts = useMemo(() => createDefaultAlerts(t), [t]);
79
58
 
80
- const handleGenerationComplete = useCallback(() => {
81
- onGenerationComplete?.();
82
- onBack();
83
- }, [onGenerationComplete, onBack]);
59
+ const { handleGenerationStart, handleGenerationComplete } = useWizardFlowHandlers({
60
+ requireFeature,
61
+ onGenerationComplete,
62
+ onBack,
63
+ });
84
64
 
85
65
  return (
86
66
  <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>