@umituz/react-native-ai-generation-content 1.27.21 → 1.27.23

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 (23) hide show
  1. package/package.json +1 -1
  2. package/src/domains/generation/wizard/configs/text-to-image.config.ts +14 -19
  3. package/src/domains/generation/wizard/index.ts +6 -1
  4. package/src/domains/generation/wizard/presentation/components/WizardFlow.types.ts +34 -0
  5. package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +78 -1
  6. package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.utils.ts +24 -1
  7. package/src/domains/generation/wizard/presentation/components/step-renderers/index.ts +8 -0
  8. package/src/domains/generation/wizard/presentation/components/step-renderers/renderGeneratingStep.tsx +24 -0
  9. package/src/domains/generation/wizard/presentation/components/step-renderers/renderPreviewStep.tsx +41 -0
  10. package/src/domains/generation/wizard/presentation/components/step-renderers/renderResultStep.tsx +69 -0
  11. package/src/domains/generation/wizard/presentation/screens/SelectionScreen.styles.ts +63 -0
  12. package/src/domains/generation/wizard/presentation/screens/SelectionScreen.tsx +148 -0
  13. package/src/domains/generation/wizard/presentation/screens/SelectionScreen.types.ts +32 -0
  14. package/src/domains/generation/wizard/presentation/screens/TextInputScreen.styles.ts +48 -0
  15. package/src/domains/generation/wizard/presentation/screens/TextInputScreen.tsx +131 -0
  16. package/src/domains/generation/wizard/presentation/screens/TextInputScreen.types.ts +28 -0
  17. package/src/domains/generation/wizard/presentation/screens/index.ts +13 -0
  18. package/src/features/image-to-video/presentation/index.ts +4 -0
  19. package/src/features/image-to-video/presentation/screens/ImageToVideoWizardFlow.tsx +94 -0
  20. package/src/features/text-to-image/presentation/index.ts +3 -1
  21. package/src/features/text-to-image/presentation/screens/TextToImageWizardFlow.tsx +94 -0
  22. package/src/features/text-to-video/presentation/index.ts +2 -0
  23. package/src/features/text-to-video/presentation/screens/TextToVideoWizardFlow.tsx +94 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.27.21",
3
+ "version": "1.27.23",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -3,27 +3,22 @@
3
3
  * Config-driven wizard steps for text-to-image generation
4
4
  */
5
5
 
6
- import type { WizardFeatureConfig } from "../domain/entities/wizard-config.types";
6
+ import type { WizardFeatureConfig, TextInputStepConfig } from "../domain/entities/wizard-config.types";
7
+
8
+ const promptStep: TextInputStepConfig = {
9
+ id: "prompt",
10
+ type: "text_input",
11
+ required: true,
12
+ titleKey: "text2image.wizard.prompt.title",
13
+ subtitleKey: "text2image.wizard.prompt.subtitle",
14
+ placeholderKey: "text2image.wizard.prompt.placeholder",
15
+ minLength: 3,
16
+ maxLength: 1000,
17
+ multiline: true,
18
+ };
7
19
 
8
20
  export const TEXT_TO_IMAGE_WIZARD_CONFIG: WizardFeatureConfig = {
9
21
  id: "text-to-image",
10
22
  name: "Text to Image",
11
- steps: [
12
- {
13
- id: "prompt",
14
- type: "text_input",
15
- required: true,
16
- placeholderKey: "textToImage.promptPlaceholder",
17
- minLength: 3,
18
- maxLength: 1000,
19
- multiline: true,
20
- },
21
- {
22
- id: "style",
23
- type: "selection",
24
- selectionType: "style",
25
- options: [],
26
- required: false,
27
- },
28
- ],
23
+ steps: [promptStep],
29
24
  };
@@ -50,7 +50,12 @@ export { GenericWizardFlow } from "./presentation/components";
50
50
  export type { GenericWizardFlowProps } from "./presentation/components";
51
51
 
52
52
  // Presentation - Screens
53
- export { GeneratingScreen } from "./presentation/screens";
53
+ export { GeneratingScreen, TextInputScreen } from "./presentation/screens";
54
+ export type {
55
+ TextInputScreenTranslations,
56
+ TextInputScreenConfig,
57
+ TextInputScreenProps,
58
+ } from "./presentation/screens";
54
59
 
55
60
  // Feature Configs
56
61
  export * from "./configs";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared types for Wizard Flow components
3
+ */
4
+
5
+ import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
6
+
7
+ export interface BaseWizardFlowProps {
8
+ /** Model ID for generation */
9
+ readonly model: string;
10
+ /** User ID for saving creations */
11
+ readonly userId?: string;
12
+ /** Is user authenticated (registered, not anonymous) */
13
+ readonly isAuthenticated: boolean;
14
+ /** Does user have premium subscription */
15
+ readonly hasPremium: boolean;
16
+ /** User's credit balance */
17
+ readonly creditBalance: number;
18
+ /** Are credits loaded */
19
+ readonly isCreditsLoaded: boolean;
20
+ /** Show auth modal with callback */
21
+ readonly onShowAuthModal: (callback: () => void) => void;
22
+ /** Show paywall */
23
+ readonly onShowPaywall: () => void;
24
+ /** Called when generation completes */
25
+ readonly onGenerationComplete?: () => void;
26
+ /** Called on generation error */
27
+ readonly onGenerationError?: (error: string) => void;
28
+ /** Called when back is pressed on first step */
29
+ readonly onBack: () => void;
30
+ /** Translation function */
31
+ readonly t: (key: string) => string;
32
+ /** Alert messages for error handling */
33
+ readonly alertMessages?: AlertMessages;
34
+ }
@@ -3,9 +3,11 @@ import { extractMediaUrl, getMediaTypeFromUrl } from "@umituz/react-native-desig
3
3
  import { StepType } from "../../../../../domain/entities/flow-config.types";
4
4
  import { GenericPhotoUploadScreen } from "../screens/GenericPhotoUploadScreen";
5
5
  import { GeneratingScreen } from "../screens/GeneratingScreen";
6
+ import { TextInputScreen } from "../screens/TextInputScreen";
7
+ import { SelectionScreen } from "../screens/SelectionScreen";
6
8
  import { ScenarioPreviewScreen } from "../../../../scenarios/presentation/screens/ScenarioPreviewScreen";
7
9
  import { ResultPreviewScreen } from "../../../../result-preview/presentation/components/ResultPreviewScreen";
8
- import { getWizardStepConfig, getUploadedImage } from "./WizardStepRenderer.utils";
10
+ import { getWizardStepConfig, getTextInputConfig, getSelectionConfig, getUploadedImage } from "./WizardStepRenderer.utils";
9
11
  import type { WizardStepRendererProps } from "./WizardStepRenderer.types";
10
12
 
11
13
  export type { WizardStepRendererProps } from "./WizardStepRenderer.types";
@@ -127,6 +129,81 @@ export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
127
129
  );
128
130
  }
129
131
 
132
+ case StepType.TEXT_INPUT: {
133
+ const textConfig = getTextInputConfig(step.config);
134
+ const titleKey = textConfig?.titleKey ?? `wizard.steps.${step.id}.title`;
135
+ const subtitleKey = textConfig?.subtitleKey ?? `wizard.steps.${step.id}.subtitle`;
136
+ const placeholderKey = textConfig?.placeholderKey ?? `wizard.steps.${step.id}.placeholder`;
137
+ const existingData = customData[step.id];
138
+ const existingText = typeof existingData === "string"
139
+ ? existingData
140
+ : typeof existingData === "object" && existingData !== null && "text" in existingData
141
+ ? String((existingData as { text: string }).text)
142
+ : "";
143
+
144
+ return (
145
+ <TextInputScreen
146
+ stepId={step.id}
147
+ translations={{
148
+ title: t(titleKey),
149
+ subtitle: subtitleKey ? t(subtitleKey) : undefined,
150
+ placeholder: t(placeholderKey),
151
+ continueButton: t("common.continue"),
152
+ backButton: t("common.back"),
153
+ examplesTitle: t("textInput.examplesTitle"),
154
+ }}
155
+ config={{
156
+ minLength: textConfig?.minLength ?? 3,
157
+ maxLength: textConfig?.maxLength ?? 1000,
158
+ multiline: textConfig?.multiline ?? true,
159
+ }}
160
+ initialValue={existingText}
161
+ onBack={onBack}
162
+ onContinue={(text) => {
163
+ // Store text in a structure compatible with existing handlers
164
+ onPhotoContinue(step.id, { uri: text, text, previewUrl: "" } as any);
165
+ }}
166
+ />
167
+ );
168
+ }
169
+
170
+ case StepType.FEATURE_SELECTION: {
171
+ const selectionConfig = getSelectionConfig(step.config);
172
+ const titleKey = selectionConfig?.titleKey ?? `wizard.steps.${step.id}.title`;
173
+ const subtitleKey = selectionConfig?.subtitleKey ?? `wizard.steps.${step.id}.subtitle`;
174
+ const existingValue = customData[step.id] as string | string[] | undefined;
175
+
176
+ const options = selectionConfig?.options ?? [];
177
+
178
+ return (
179
+ <SelectionScreen
180
+ stepId={step.id}
181
+ translations={{
182
+ title: t(titleKey),
183
+ subtitle: subtitleKey ? t(subtitleKey) : undefined,
184
+ continueButton: t("common.continue"),
185
+ backButton: t("common.back"),
186
+ }}
187
+ options={options.map((opt) => ({
188
+ id: opt.id,
189
+ label: opt.label,
190
+ icon: opt.icon,
191
+ value: opt.value,
192
+ }))}
193
+ config={{
194
+ multiSelect: selectionConfig?.multiSelect ?? false,
195
+ required: step.required ?? true,
196
+ }}
197
+ initialValue={existingValue}
198
+ onBack={onBack}
199
+ onContinue={(value) => {
200
+ // Store selection value
201
+ onPhotoContinue(step.id, { uri: String(value), selection: value, previewUrl: "" } as any);
202
+ }}
203
+ />
204
+ );
205
+ }
206
+
130
207
  default:
131
208
  if (typeof __DEV__ !== "undefined" && __DEV__) {
132
209
  console.warn("[WizardStepRenderer] Unhandled step type", { stepType: step.type });
@@ -1,4 +1,9 @@
1
- import type { WizardStepConfig } from "../../domain/entities/wizard-config.types";
1
+ import type {
2
+ WizardStepConfig,
3
+ TextInputStepConfig,
4
+ PhotoUploadStepConfig,
5
+ SelectionStepConfig,
6
+ } from "../../domain/entities/wizard-config.types";
2
7
  import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
3
8
 
4
9
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -19,7 +24,25 @@ export function getWizardStepConfig(config: unknown): WizardStepConfig | undefin
19
24
  return undefined;
20
25
  }
21
26
 
27
+ export function getTextInputConfig(config: unknown): TextInputStepConfig | undefined {
28
+ if (!isRecord(config)) return undefined;
29
+ if (config.type === "text_input") return config as unknown as TextInputStepConfig;
30
+ return undefined;
31
+ }
32
+
33
+ export function getPhotoUploadConfig(config: unknown): PhotoUploadStepConfig | undefined {
34
+ if (!isRecord(config)) return undefined;
35
+ if (config.type === "photo_upload") return config as unknown as PhotoUploadStepConfig;
36
+ return undefined;
37
+ }
38
+
22
39
  export function getUploadedImage(data: unknown): UploadedImage | undefined {
23
40
  if (isUploadedImage(data)) return data;
24
41
  return undefined;
25
42
  }
43
+
44
+ export function getSelectionConfig(config: unknown): SelectionStepConfig | undefined {
45
+ if (!isRecord(config)) return undefined;
46
+ if (config.type === "selection") return config as unknown as SelectionStepConfig;
47
+ return undefined;
48
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Step Renderers
3
+ * Separated renderer functions for each wizard step type
4
+ */
5
+
6
+ export { renderPreviewStep, type PreviewStepProps } from "./renderPreviewStep";
7
+ export { renderGeneratingStep, type GeneratingStepProps } from "./renderGeneratingStep";
8
+ export { renderResultStep, type ResultStepProps } from "./renderResultStep";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Generating Step Renderer
3
+ */
4
+
5
+ import React from "react";
6
+ import { GeneratingScreen } from "../../screens/GeneratingScreen";
7
+ import type { WizardScenarioData } from "../../hooks/useWizardGeneration";
8
+
9
+ export interface GeneratingStepProps {
10
+ readonly progress: number;
11
+ readonly scenario: WizardScenarioData | undefined;
12
+ readonly t: (key: string) => string;
13
+ readonly renderGenerating?: (progress: number) => React.ReactElement | null;
14
+ }
15
+
16
+ export function renderGeneratingStep({
17
+ progress,
18
+ scenario,
19
+ t,
20
+ renderGenerating,
21
+ }: GeneratingStepProps): React.ReactElement | null {
22
+ if (renderGenerating) return renderGenerating(progress);
23
+ return <GeneratingScreen progress={progress} scenario={scenario} t={t} />;
24
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Preview Step Renderer
3
+ */
4
+
5
+ import React from "react";
6
+ import { ScenarioPreviewScreen } from "../../../../../scenarios/presentation/screens/ScenarioPreviewScreen";
7
+ import type { ScenarioData } from "../../../../../scenarios/domain/scenario.types";
8
+ import type { StepDefinition } from "../../../../../../domain/entities/flow-config.types";
9
+
10
+ export interface PreviewStepProps {
11
+ readonly step: StepDefinition;
12
+ readonly scenario: ScenarioData | undefined;
13
+ readonly onNext: () => void;
14
+ readonly onBack: () => void;
15
+ readonly t: (key: string) => string;
16
+ readonly renderPreview?: (onContinue: () => void) => React.ReactElement | null;
17
+ }
18
+
19
+ export function renderPreviewStep({
20
+ scenario,
21
+ onNext,
22
+ onBack,
23
+ t,
24
+ renderPreview,
25
+ }: PreviewStepProps): React.ReactElement | null {
26
+ if (renderPreview) return renderPreview(onNext);
27
+ if (!scenario) return null;
28
+
29
+ return (
30
+ <ScenarioPreviewScreen
31
+ scenario={scenario}
32
+ translations={{
33
+ continueButton: t("common.continue"),
34
+ whatToExpect: t("scenarioPreview.whatToExpect"),
35
+ }}
36
+ onContinue={onNext}
37
+ onBack={onBack}
38
+ t={t}
39
+ />
40
+ );
41
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Result Step Renderer
3
+ */
4
+
5
+ import React from "react";
6
+ import { extractMediaUrl, getMediaTypeFromUrl } from "@umituz/react-native-design-system";
7
+ import { ResultPreviewScreen } from "../../../../../result-preview/presentation/components/ResultPreviewScreen";
8
+
9
+ export interface ResultStepProps {
10
+ readonly generationResult: unknown;
11
+ readonly isSaving: boolean;
12
+ readonly isSharing: boolean;
13
+ readonly showRating: boolean;
14
+ readonly onDownload: () => void;
15
+ readonly onShare: () => void;
16
+ readonly onRate?: () => void;
17
+ readonly onTryAgain?: () => void;
18
+ readonly onBack: () => void;
19
+ readonly t: (key: string) => string;
20
+ readonly renderResult?: (result: unknown) => React.ReactElement | null;
21
+ }
22
+
23
+ export function renderResultStep({
24
+ generationResult,
25
+ isSaving,
26
+ isSharing,
27
+ showRating,
28
+ onDownload,
29
+ onShare,
30
+ onRate,
31
+ onTryAgain,
32
+ onBack,
33
+ t,
34
+ renderResult,
35
+ }: ResultStepProps): React.ReactElement | null {
36
+ if (renderResult) return renderResult(generationResult);
37
+
38
+ const media = extractMediaUrl(generationResult);
39
+ if (!media) return null;
40
+
41
+ const isVideo = media.isVideo || getMediaTypeFromUrl(media.url) === "video";
42
+ const handleTryAgain = onTryAgain ?? onBack;
43
+
44
+ return (
45
+ <ResultPreviewScreen
46
+ imageUrl={isVideo ? undefined : media.url}
47
+ videoUrl={isVideo ? media.url : undefined}
48
+ isSaving={isSaving}
49
+ isSharing={isSharing}
50
+ onDownload={onDownload}
51
+ onShare={onShare}
52
+ onRate={onRate}
53
+ onTryAgain={handleTryAgain}
54
+ onNavigateBack={handleTryAgain}
55
+ hideLabel
56
+ iconOnly
57
+ showTryAgain
58
+ showRating={showRating}
59
+ translations={{
60
+ title: t("generation.result.title"),
61
+ saveButton: t("generation.result.save"),
62
+ saving: t("generation.result.saving"),
63
+ shareButton: t("generation.result.share"),
64
+ sharing: t("generation.result.sharing"),
65
+ tryAnother: t("generation.result.tryAnother"),
66
+ }}
67
+ />
68
+ );
69
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * SelectionScreen Styles
3
+ */
4
+
5
+ import { StyleSheet } from "react-native";
6
+
7
+ export const styles = StyleSheet.create({
8
+ container: {
9
+ flex: 1,
10
+ },
11
+ header: {
12
+ flexDirection: "row",
13
+ alignItems: "center",
14
+ paddingVertical: 8,
15
+ },
16
+ backButtonContent: {
17
+ flexDirection: "row",
18
+ alignItems: "center",
19
+ },
20
+ backButtonText: {
21
+ marginLeft: 4,
22
+ },
23
+ scrollView: {
24
+ flex: 1,
25
+ },
26
+ title: {
27
+ marginBottom: 8,
28
+ },
29
+ optionsGrid: {
30
+ flexDirection: "row",
31
+ flexWrap: "wrap",
32
+ gap: 12,
33
+ },
34
+ optionCard: {
35
+ flex: 1,
36
+ minWidth: "45%",
37
+ padding: 16,
38
+ borderWidth: 2,
39
+ alignItems: "center",
40
+ position: "relative",
41
+ },
42
+ optionLabel: {
43
+ marginTop: 8,
44
+ textAlign: "center",
45
+ },
46
+ checkmark: {
47
+ position: "absolute",
48
+ top: 8,
49
+ right: 8,
50
+ width: 20,
51
+ height: 20,
52
+ borderRadius: 10,
53
+ alignItems: "center",
54
+ justifyContent: "center",
55
+ },
56
+ footer: {
57
+ borderTopWidth: 1,
58
+ borderTopColor: "rgba(0,0,0,0.1)",
59
+ },
60
+ continueButton: {
61
+ width: "100%",
62
+ },
63
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * SelectionScreen
3
+ * Generic selection step for wizard flows (duration, style, etc.)
4
+ */
5
+
6
+ import React, { useState, useCallback } from "react";
7
+ import { View, ScrollView, TouchableOpacity } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicButton,
11
+ AtomicIcon,
12
+ useAppDesignTokens,
13
+ } from "@umituz/react-native-design-system";
14
+ import { styles } from "./SelectionScreen.styles";
15
+ import type { SelectionScreenProps } from "./SelectionScreen.types";
16
+
17
+ export type {
18
+ SelectionOption,
19
+ SelectionScreenTranslations,
20
+ SelectionScreenConfig,
21
+ SelectionScreenProps,
22
+ } from "./SelectionScreen.types";
23
+
24
+ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
25
+ stepId: _stepId,
26
+ translations,
27
+ options,
28
+ config,
29
+ initialValue,
30
+ onBack,
31
+ onContinue,
32
+ }) => {
33
+ const tokens = useAppDesignTokens();
34
+ const [selected, setSelected] = useState<string | string[]>(() => {
35
+ if (initialValue) return initialValue;
36
+ if (config?.multiSelect) return [];
37
+ return "";
38
+ });
39
+
40
+ const isMultiSelect = config?.multiSelect ?? false;
41
+ const isRequired = config?.required ?? true;
42
+
43
+ const canContinue = isRequired
44
+ ? isMultiSelect
45
+ ? (selected as string[]).length > 0
46
+ : selected !== ""
47
+ : true;
48
+
49
+ const handleSelect = useCallback(
50
+ (optionId: string) => {
51
+ if (isMultiSelect) {
52
+ setSelected((prev) => {
53
+ const arr = prev as string[];
54
+ return arr.includes(optionId)
55
+ ? arr.filter((id) => id !== optionId)
56
+ : [...arr, optionId];
57
+ });
58
+ } else {
59
+ setSelected(optionId);
60
+ }
61
+ },
62
+ [isMultiSelect],
63
+ );
64
+
65
+ const handleContinue = useCallback(() => {
66
+ if (canContinue) {
67
+ onContinue(selected);
68
+ }
69
+ }, [canContinue, selected, onContinue]);
70
+
71
+ const isOptionSelected = useCallback(
72
+ (optionId: string): boolean => {
73
+ if (isMultiSelect) {
74
+ return (selected as string[]).includes(optionId);
75
+ }
76
+ return selected === optionId;
77
+ },
78
+ [isMultiSelect, selected],
79
+ );
80
+
81
+ return (
82
+ <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
83
+ <View style={[styles.header, { paddingHorizontal: tokens.spacing.md }]}>
84
+ <AtomicButton variant="text" size="sm" onPress={onBack}>
85
+ <View style={styles.backButtonContent}>
86
+ <AtomicIcon name="arrow-back" size="sm" color="textPrimary" />
87
+ {translations.backButton ? (
88
+ <AtomicText type="labelMedium" color="textPrimary" style={styles.backButtonText}>
89
+ {translations.backButton}
90
+ </AtomicText>
91
+ ) : null}
92
+ </View>
93
+ </AtomicButton>
94
+ </View>
95
+
96
+ <ScrollView style={styles.scrollView} contentContainerStyle={{ padding: tokens.spacing.md }}>
97
+ <AtomicText type="headlineMedium" color="textPrimary" style={styles.title}>
98
+ {translations.title}
99
+ </AtomicText>
100
+
101
+ {translations.subtitle ? (
102
+ <AtomicText type="bodyMedium" color="textSecondary" style={{ marginBottom: tokens.spacing.lg }}>
103
+ {translations.subtitle}
104
+ </AtomicText>
105
+ ) : null}
106
+
107
+ <View style={styles.optionsGrid}>
108
+ {options.map((option) => {
109
+ const isSelected = isOptionSelected(option.id);
110
+ return (
111
+ <TouchableOpacity
112
+ key={option.id}
113
+ style={[
114
+ styles.optionCard,
115
+ {
116
+ backgroundColor: isSelected ? tokens.colors.primaryContainer : tokens.colors.backgroundSecondary,
117
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
118
+ borderRadius: tokens.borders.radius.md,
119
+ },
120
+ ]}
121
+ onPress={() => handleSelect(option.id)}
122
+ activeOpacity={0.7}
123
+ >
124
+ {option.icon ? (
125
+ <AtomicIcon name={option.icon} size="lg" color={isSelected ? "primary" : "textSecondary"} />
126
+ ) : null}
127
+ <AtomicText type="labelLarge" color={isSelected ? "primary" : "textPrimary"} style={styles.optionLabel}>
128
+ {option.label}
129
+ </AtomicText>
130
+ {isSelected ? (
131
+ <View style={[styles.checkmark, { backgroundColor: tokens.colors.primary }]}>
132
+ <AtomicIcon name="checkmark" size="xs" color="onPrimary" />
133
+ </View>
134
+ ) : null}
135
+ </TouchableOpacity>
136
+ );
137
+ })}
138
+ </View>
139
+ </ScrollView>
140
+
141
+ <View style={[styles.footer, { padding: tokens.spacing.md }]}>
142
+ <AtomicButton variant="primary" size="lg" onPress={handleContinue} disabled={!canContinue} style={styles.continueButton}>
143
+ {translations.continueButton}
144
+ </AtomicButton>
145
+ </View>
146
+ </View>
147
+ );
148
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * SelectionScreen Types
3
+ */
4
+
5
+ export interface SelectionOption {
6
+ readonly id: string;
7
+ readonly label: string;
8
+ readonly icon?: string;
9
+ readonly value: unknown;
10
+ }
11
+
12
+ export interface SelectionScreenTranslations {
13
+ readonly title: string;
14
+ readonly subtitle?: string;
15
+ readonly continueButton: string;
16
+ readonly backButton?: string;
17
+ }
18
+
19
+ export interface SelectionScreenConfig {
20
+ readonly multiSelect?: boolean;
21
+ readonly required?: boolean;
22
+ }
23
+
24
+ export interface SelectionScreenProps {
25
+ readonly stepId: string;
26
+ readonly translations: SelectionScreenTranslations;
27
+ readonly options: readonly SelectionOption[];
28
+ readonly config?: SelectionScreenConfig;
29
+ readonly initialValue?: string | string[];
30
+ readonly onBack: () => void;
31
+ readonly onContinue: (selectedValue: string | string[]) => void;
32
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * TextInputScreen Styles
3
+ */
4
+
5
+ import { StyleSheet } from "react-native";
6
+
7
+ export const styles = StyleSheet.create({
8
+ container: {
9
+ flex: 1,
10
+ },
11
+ header: {
12
+ flexDirection: "row",
13
+ alignItems: "center",
14
+ paddingVertical: 8,
15
+ },
16
+ backButtonContent: {
17
+ flexDirection: "row",
18
+ alignItems: "center",
19
+ },
20
+ backButtonText: {
21
+ marginLeft: 4,
22
+ },
23
+ scrollView: {
24
+ flex: 1,
25
+ },
26
+ title: {
27
+ marginBottom: 8,
28
+ },
29
+ inputContainer: {
30
+ borderWidth: 1,
31
+ padding: 12,
32
+ },
33
+ textInput: {
34
+ fontSize: 16,
35
+ lineHeight: 24,
36
+ },
37
+ charCount: {
38
+ textAlign: "right",
39
+ marginTop: 4,
40
+ },
41
+ footer: {
42
+ borderTopWidth: 1,
43
+ borderTopColor: "rgba(0,0,0,0.1)",
44
+ },
45
+ continueButton: {
46
+ width: "100%",
47
+ },
48
+ });
@@ -0,0 +1,131 @@
1
+ /**
2
+ * TextInputScreen
3
+ * Generic text input step for wizard flows
4
+ */
5
+
6
+ import React, { useState, useCallback } from "react";
7
+ import { View, ScrollView, TextInput } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicButton,
11
+ AtomicIcon,
12
+ useAppDesignTokens,
13
+ } from "@umituz/react-native-design-system";
14
+ import { styles } from "./TextInputScreen.styles";
15
+ import type { TextInputScreenProps } from "./TextInputScreen.types";
16
+
17
+ export type {
18
+ TextInputScreenTranslations,
19
+ TextInputScreenConfig,
20
+ TextInputScreenProps,
21
+ } from "./TextInputScreen.types";
22
+
23
+ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
24
+ stepId: _stepId,
25
+ translations,
26
+ config,
27
+ examplePrompts = [],
28
+ initialValue = "",
29
+ onBack,
30
+ onContinue,
31
+ }) => {
32
+ const tokens = useAppDesignTokens();
33
+ const [text, setText] = useState(initialValue);
34
+
35
+ const minLength = config?.minLength ?? 3;
36
+ const maxLength = config?.maxLength ?? 1000;
37
+ const canContinue = text.trim().length >= minLength;
38
+
39
+ const handleContinue = useCallback(() => {
40
+ if (canContinue) {
41
+ onContinue(text.trim());
42
+ }
43
+ }, [canContinue, text, onContinue]);
44
+
45
+ const handleExampleSelect = useCallback((example: string) => {
46
+ setText(example);
47
+ }, []);
48
+
49
+ return (
50
+ <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
51
+ <View style={[styles.header, { paddingHorizontal: tokens.spacing.md }]}>
52
+ <AtomicButton variant="text" size="sm" onPress={onBack}>
53
+ <View style={styles.backButtonContent}>
54
+ <AtomicIcon name="arrow-back" size="sm" color="textPrimary" />
55
+ {translations.backButton ? (
56
+ <AtomicText type="labelMedium" color="textPrimary" style={styles.backButtonText}>
57
+ {translations.backButton}
58
+ </AtomicText>
59
+ ) : null}
60
+ </View>
61
+ </AtomicButton>
62
+ </View>
63
+
64
+ <ScrollView
65
+ style={styles.scrollView}
66
+ contentContainerStyle={{ padding: tokens.spacing.md }}
67
+ keyboardShouldPersistTaps="handled"
68
+ >
69
+ <AtomicText type="headlineMedium" color="textPrimary" style={styles.title}>
70
+ {translations.title}
71
+ </AtomicText>
72
+
73
+ {translations.subtitle ? (
74
+ <AtomicText type="bodyMedium" color="textSecondary" style={{ marginBottom: tokens.spacing.lg }}>
75
+ {translations.subtitle}
76
+ </AtomicText>
77
+ ) : null}
78
+
79
+ <View
80
+ style={[
81
+ styles.inputContainer,
82
+ {
83
+ backgroundColor: tokens.colors.backgroundSecondary,
84
+ borderRadius: tokens.borders.radius.md,
85
+ borderColor: tokens.colors.border,
86
+ },
87
+ ]}
88
+ >
89
+ <TextInput
90
+ style={[styles.textInput, { color: tokens.colors.textPrimary, minHeight: config?.multiline ? 120 : 48 }]}
91
+ placeholder={translations.placeholder}
92
+ placeholderTextColor={tokens.colors.textTertiary}
93
+ value={text}
94
+ onChangeText={setText}
95
+ multiline={config?.multiline ?? true}
96
+ maxLength={maxLength}
97
+ textAlignVertical="top"
98
+ />
99
+ <AtomicText type="bodySmall" color="textTertiary" style={styles.charCount}>
100
+ {text.length}/{maxLength}
101
+ </AtomicText>
102
+ </View>
103
+
104
+ {examplePrompts.length > 0 && translations.examplesTitle ? (
105
+ <View style={{ marginTop: tokens.spacing.lg }}>
106
+ <AtomicText type="labelLarge" color="textSecondary" style={{ marginBottom: tokens.spacing.sm }}>
107
+ {translations.examplesTitle}
108
+ </AtomicText>
109
+ {examplePrompts.slice(0, 4).map((example, index) => (
110
+ <AtomicButton
111
+ key={index}
112
+ variant="outline"
113
+ size="sm"
114
+ onPress={() => handleExampleSelect(example)}
115
+ style={{ marginBottom: tokens.spacing.xs }}
116
+ >
117
+ {example.length > 50 ? `${example.slice(0, 50)}...` : example}
118
+ </AtomicButton>
119
+ ))}
120
+ </View>
121
+ ) : null}
122
+ </ScrollView>
123
+
124
+ <View style={[styles.footer, { padding: tokens.spacing.md }]}>
125
+ <AtomicButton variant="primary" size="lg" onPress={handleContinue} disabled={!canContinue} style={styles.continueButton}>
126
+ {translations.continueButton}
127
+ </AtomicButton>
128
+ </View>
129
+ </View>
130
+ );
131
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * TextInputScreen Types
3
+ */
4
+
5
+ export interface TextInputScreenTranslations {
6
+ readonly title: string;
7
+ readonly subtitle?: string;
8
+ readonly placeholder: string;
9
+ readonly continueButton: string;
10
+ readonly backButton?: string;
11
+ readonly examplesTitle?: string;
12
+ }
13
+
14
+ export interface TextInputScreenConfig {
15
+ readonly minLength?: number;
16
+ readonly maxLength?: number;
17
+ readonly multiline?: boolean;
18
+ }
19
+
20
+ export interface TextInputScreenProps {
21
+ readonly stepId: string;
22
+ readonly translations: TextInputScreenTranslations;
23
+ readonly config?: TextInputScreenConfig;
24
+ readonly examplePrompts?: string[];
25
+ readonly initialValue?: string;
26
+ readonly onBack: () => void;
27
+ readonly onContinue: (text: string) => void;
28
+ }
@@ -5,3 +5,16 @@ export type {
5
5
  PhotoUploadScreenConfig,
6
6
  PhotoUploadScreenProps,
7
7
  } from "./GenericPhotoUploadScreen";
8
+ export { TextInputScreen } from "./TextInputScreen";
9
+ export type {
10
+ TextInputScreenTranslations,
11
+ TextInputScreenConfig,
12
+ TextInputScreenProps,
13
+ } from "./TextInputScreen";
14
+ export { SelectionScreen } from "./SelectionScreen";
15
+ export type {
16
+ SelectionOption,
17
+ SelectionScreenTranslations,
18
+ SelectionScreenConfig,
19
+ SelectionScreenProps,
20
+ } from "./SelectionScreen";
@@ -3,3 +3,7 @@ export * from "./hooks";
3
3
 
4
4
  // Components
5
5
  export * from "./components";
6
+
7
+ // Screens
8
+ export { ImageToVideoWizardFlow } from "./screens/ImageToVideoWizardFlow";
9
+ export type { ImageToVideoWizardFlowProps } from "./screens/ImageToVideoWizardFlow";
@@ -0,0 +1,94 @@
1
+ /**
2
+ * ImageToVideoWizardFlow
3
+ * Step-based wizard flow for image-to-video generation
4
+ */
5
+
6
+ import React, { useMemo, useCallback, useEffect, useRef } from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
9
+ import { GenericWizardFlow } from "../../../../domains/generation/wizard/presentation/components";
10
+ import { IMAGE_TO_VIDEO_WIZARD_CONFIG } from "../../../../domains/generation/wizard/configs";
11
+ import type { WizardScenarioData } from "../../../../domains/generation/wizard/presentation/hooks/useWizardGeneration";
12
+ import type { BaseWizardFlowProps } from "../../../../domains/generation/wizard/presentation/components/WizardFlow.types";
13
+ import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
14
+
15
+ const AutoSkipPreview: React.FC<{ onContinue: () => void }> = ({ onContinue }) => {
16
+ const hasContinued = useRef(false);
17
+ useEffect(() => {
18
+ if (!hasContinued.current) {
19
+ hasContinued.current = true;
20
+ onContinue();
21
+ }
22
+ }, [onContinue]);
23
+ return null;
24
+ };
25
+
26
+ export type ImageToVideoWizardFlowProps = BaseWizardFlowProps;
27
+
28
+ export const ImageToVideoWizardFlow: React.FC<ImageToVideoWizardFlowProps> = (props) => {
29
+ const {
30
+ model,
31
+ userId,
32
+ isAuthenticated,
33
+ hasPremium,
34
+ creditBalance,
35
+ isCreditsLoaded,
36
+ onShowAuthModal,
37
+ onShowPaywall,
38
+ onGenerationComplete,
39
+ onGenerationError,
40
+ onBack,
41
+ t,
42
+ alertMessages,
43
+ } = props;
44
+
45
+ const tokens = useAppDesignTokens();
46
+
47
+ const scenario: WizardScenarioData = useMemo(
48
+ () => ({ id: "image-to-video", outputType: "video", model, title: t("image2video.title") }),
49
+ [model, t],
50
+ );
51
+
52
+ const defaultAlerts = useMemo<AlertMessages>(
53
+ () => ({
54
+ networkError: t("common.errors.network"),
55
+ policyViolation: t("common.errors.policy"),
56
+ saveFailed: t("common.errors.saveFailed"),
57
+ creditFailed: t("common.errors.creditFailed"),
58
+ unknown: t("common.errors.unknown"),
59
+ }),
60
+ [t],
61
+ );
62
+
63
+ const handleGenerationStart = useCallback(
64
+ (_data: Record<string, unknown>, proceed: () => void) => {
65
+ if (!isAuthenticated) { onShowAuthModal(proceed); return; }
66
+ if (!hasPremium && isCreditsLoaded && creditBalance < 1) { onShowPaywall(); return; }
67
+ proceed();
68
+ },
69
+ [isAuthenticated, hasPremium, creditBalance, isCreditsLoaded, onShowAuthModal, onShowPaywall],
70
+ );
71
+
72
+ return (
73
+ <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
74
+ <GenericWizardFlow
75
+ featureConfig={IMAGE_TO_VIDEO_WIZARD_CONFIG}
76
+ scenario={scenario}
77
+ userId={userId}
78
+ alertMessages={alertMessages ?? defaultAlerts}
79
+ onGenerationStart={handleGenerationStart}
80
+ onGenerationComplete={onGenerationComplete}
81
+ onGenerationError={onGenerationError}
82
+ onCreditsExhausted={onShowPaywall}
83
+ onBack={onBack}
84
+ onTryAgain={onBack}
85
+ t={t}
86
+ renderPreview={(onContinue) => <AutoSkipPreview onContinue={onContinue} />}
87
+ />
88
+ </View>
89
+ );
90
+ };
91
+
92
+ const styles = StyleSheet.create({
93
+ container: { flex: 1 },
94
+ });
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Text-to-Image Presentation Layer
3
- * Hooks and components exports
3
+ * Hooks, components, and screens exports
4
4
  */
5
5
 
6
6
  export * from "./hooks";
7
7
  export * from "./components";
8
+ export { TextToImageWizardFlow } from "./screens/TextToImageWizardFlow";
9
+ export type { TextToImageWizardFlowProps } from "./screens/TextToImageWizardFlow";
@@ -0,0 +1,94 @@
1
+ /**
2
+ * TextToImageWizardFlow
3
+ * Step-based wizard flow for text-to-image generation
4
+ */
5
+
6
+ import React, { useMemo, useCallback, useEffect, useRef } from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
9
+ import { GenericWizardFlow } from "../../../../domains/generation/wizard/presentation/components";
10
+ import { TEXT_TO_IMAGE_WIZARD_CONFIG } from "../../../../domains/generation/wizard/configs";
11
+ import type { WizardScenarioData } from "../../../../domains/generation/wizard/presentation/hooks/useWizardGeneration";
12
+ import type { BaseWizardFlowProps } from "../../../../domains/generation/wizard/presentation/components/WizardFlow.types";
13
+ import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
14
+
15
+ const AutoSkipPreview: React.FC<{ onContinue: () => void }> = ({ onContinue }) => {
16
+ const hasContinued = useRef(false);
17
+ useEffect(() => {
18
+ if (!hasContinued.current) {
19
+ hasContinued.current = true;
20
+ onContinue();
21
+ }
22
+ }, [onContinue]);
23
+ return null;
24
+ };
25
+
26
+ export type TextToImageWizardFlowProps = BaseWizardFlowProps;
27
+
28
+ export const TextToImageWizardFlow: React.FC<TextToImageWizardFlowProps> = (props) => {
29
+ const {
30
+ model,
31
+ userId,
32
+ isAuthenticated,
33
+ hasPremium,
34
+ creditBalance,
35
+ isCreditsLoaded,
36
+ onShowAuthModal,
37
+ onShowPaywall,
38
+ onGenerationComplete,
39
+ onGenerationError,
40
+ onBack,
41
+ t,
42
+ alertMessages,
43
+ } = props;
44
+
45
+ const tokens = useAppDesignTokens();
46
+
47
+ const scenario: WizardScenarioData = useMemo(
48
+ () => ({ id: "text-to-image", outputType: "image", model, title: t("text2image.title") }),
49
+ [model, t],
50
+ );
51
+
52
+ const defaultAlerts = useMemo<AlertMessages>(
53
+ () => ({
54
+ networkError: t("common.errors.network"),
55
+ policyViolation: t("common.errors.policy"),
56
+ saveFailed: t("common.errors.saveFailed"),
57
+ creditFailed: t("common.errors.creditFailed"),
58
+ unknown: t("common.errors.unknown"),
59
+ }),
60
+ [t],
61
+ );
62
+
63
+ const handleGenerationStart = useCallback(
64
+ (_data: Record<string, unknown>, proceed: () => void) => {
65
+ if (!isAuthenticated) { onShowAuthModal(proceed); return; }
66
+ if (!hasPremium && isCreditsLoaded && creditBalance < 1) { onShowPaywall(); return; }
67
+ proceed();
68
+ },
69
+ [isAuthenticated, hasPremium, creditBalance, isCreditsLoaded, onShowAuthModal, onShowPaywall],
70
+ );
71
+
72
+ return (
73
+ <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
74
+ <GenericWizardFlow
75
+ featureConfig={TEXT_TO_IMAGE_WIZARD_CONFIG}
76
+ scenario={scenario}
77
+ userId={userId}
78
+ alertMessages={alertMessages ?? defaultAlerts}
79
+ onGenerationStart={handleGenerationStart}
80
+ onGenerationComplete={onGenerationComplete}
81
+ onGenerationError={onGenerationError}
82
+ onCreditsExhausted={onShowPaywall}
83
+ onBack={onBack}
84
+ onTryAgain={onBack}
85
+ t={t}
86
+ renderPreview={(onContinue) => <AutoSkipPreview onContinue={onContinue} />}
87
+ />
88
+ </View>
89
+ );
90
+ };
91
+
92
+ const styles = StyleSheet.create({
93
+ container: { flex: 1 },
94
+ });
@@ -5,3 +5,5 @@
5
5
 
6
6
  export * from "./hooks";
7
7
  export * from "./components";
8
+ export { TextToVideoWizardFlow } from "./screens/TextToVideoWizardFlow";
9
+ export type { TextToVideoWizardFlowProps } from "./screens/TextToVideoWizardFlow";
@@ -0,0 +1,94 @@
1
+ /**
2
+ * TextToVideoWizardFlow
3
+ * Step-based wizard flow for text-to-video generation
4
+ */
5
+
6
+ import React, { useMemo, useCallback, useEffect, useRef } from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
9
+ import { GenericWizardFlow } from "../../../../domains/generation/wizard/presentation/components";
10
+ import { TEXT_TO_VIDEO_WIZARD_CONFIG } from "../../../../domains/generation/wizard/configs";
11
+ import type { WizardScenarioData } from "../../../../domains/generation/wizard/presentation/hooks/useWizardGeneration";
12
+ import type { BaseWizardFlowProps } from "../../../../domains/generation/wizard/presentation/components/WizardFlow.types";
13
+ import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
14
+
15
+ const AutoSkipPreview: React.FC<{ onContinue: () => void }> = ({ onContinue }) => {
16
+ const hasContinued = useRef(false);
17
+ useEffect(() => {
18
+ if (!hasContinued.current) {
19
+ hasContinued.current = true;
20
+ onContinue();
21
+ }
22
+ }, [onContinue]);
23
+ return null;
24
+ };
25
+
26
+ export type TextToVideoWizardFlowProps = BaseWizardFlowProps;
27
+
28
+ export const TextToVideoWizardFlow: React.FC<TextToVideoWizardFlowProps> = (props) => {
29
+ const {
30
+ model,
31
+ userId,
32
+ isAuthenticated,
33
+ hasPremium,
34
+ creditBalance,
35
+ isCreditsLoaded,
36
+ onShowAuthModal,
37
+ onShowPaywall,
38
+ onGenerationComplete,
39
+ onGenerationError,
40
+ onBack,
41
+ t,
42
+ alertMessages,
43
+ } = props;
44
+
45
+ const tokens = useAppDesignTokens();
46
+
47
+ const scenario: WizardScenarioData = useMemo(
48
+ () => ({ id: "text-to-video", outputType: "video", model, title: t("text2video.title") }),
49
+ [model, t],
50
+ );
51
+
52
+ const defaultAlerts = useMemo<AlertMessages>(
53
+ () => ({
54
+ networkError: t("common.errors.network"),
55
+ policyViolation: t("common.errors.policy"),
56
+ saveFailed: t("common.errors.saveFailed"),
57
+ creditFailed: t("common.errors.creditFailed"),
58
+ unknown: t("common.errors.unknown"),
59
+ }),
60
+ [t],
61
+ );
62
+
63
+ const handleGenerationStart = useCallback(
64
+ (_data: Record<string, unknown>, proceed: () => void) => {
65
+ if (!isAuthenticated) { onShowAuthModal(proceed); return; }
66
+ if (!hasPremium && isCreditsLoaded && creditBalance < 1) { onShowPaywall(); return; }
67
+ proceed();
68
+ },
69
+ [isAuthenticated, hasPremium, creditBalance, isCreditsLoaded, onShowAuthModal, onShowPaywall],
70
+ );
71
+
72
+ return (
73
+ <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
74
+ <GenericWizardFlow
75
+ featureConfig={TEXT_TO_VIDEO_WIZARD_CONFIG}
76
+ scenario={scenario}
77
+ userId={userId}
78
+ alertMessages={alertMessages ?? defaultAlerts}
79
+ onGenerationStart={handleGenerationStart}
80
+ onGenerationComplete={onGenerationComplete}
81
+ onGenerationError={onGenerationError}
82
+ onCreditsExhausted={onShowPaywall}
83
+ onBack={onBack}
84
+ onTryAgain={onBack}
85
+ t={t}
86
+ renderPreview={(onContinue) => <AutoSkipPreview onContinue={onContinue} />}
87
+ />
88
+ </View>
89
+ );
90
+ };
91
+
92
+ const styles = StyleSheet.create({
93
+ container: { flex: 1 },
94
+ });