@umituz/react-native-ai-generation-content 1.27.22 → 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 (21) hide show
  1. package/package.json +1 -1
  2. package/src/domains/generation/wizard/presentation/components/WizardFlow.types.ts +34 -0
  3. package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +39 -1
  4. package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.utils.ts +7 -0
  5. package/src/domains/generation/wizard/presentation/components/step-renderers/index.ts +8 -0
  6. package/src/domains/generation/wizard/presentation/components/step-renderers/renderGeneratingStep.tsx +24 -0
  7. package/src/domains/generation/wizard/presentation/components/step-renderers/renderPreviewStep.tsx +41 -0
  8. package/src/domains/generation/wizard/presentation/components/step-renderers/renderResultStep.tsx +69 -0
  9. package/src/domains/generation/wizard/presentation/screens/SelectionScreen.styles.ts +63 -0
  10. package/src/domains/generation/wizard/presentation/screens/SelectionScreen.tsx +148 -0
  11. package/src/domains/generation/wizard/presentation/screens/SelectionScreen.types.ts +32 -0
  12. package/src/domains/generation/wizard/presentation/screens/TextInputScreen.styles.ts +48 -0
  13. package/src/domains/generation/wizard/presentation/screens/TextInputScreen.tsx +15 -103
  14. package/src/domains/generation/wizard/presentation/screens/TextInputScreen.types.ts +28 -0
  15. package/src/domains/generation/wizard/presentation/screens/index.ts +7 -0
  16. package/src/features/image-to-video/presentation/index.ts +4 -0
  17. package/src/features/image-to-video/presentation/screens/ImageToVideoWizardFlow.tsx +94 -0
  18. package/src/features/text-to-image/presentation/index.ts +3 -1
  19. package/src/features/text-to-image/presentation/screens/TextToImageWizardFlow.tsx +94 -0
  20. package/src/features/text-to-video/presentation/index.ts +2 -0
  21. 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.22",
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",
@@ -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
+ }
@@ -4,9 +4,10 @@ import { StepType } from "../../../../../domain/entities/flow-config.types";
4
4
  import { GenericPhotoUploadScreen } from "../screens/GenericPhotoUploadScreen";
5
5
  import { GeneratingScreen } from "../screens/GeneratingScreen";
6
6
  import { TextInputScreen } from "../screens/TextInputScreen";
7
+ import { SelectionScreen } from "../screens/SelectionScreen";
7
8
  import { ScenarioPreviewScreen } from "../../../../scenarios/presentation/screens/ScenarioPreviewScreen";
8
9
  import { ResultPreviewScreen } from "../../../../result-preview/presentation/components/ResultPreviewScreen";
9
- import { getWizardStepConfig, getTextInputConfig, getUploadedImage } from "./WizardStepRenderer.utils";
10
+ import { getWizardStepConfig, getTextInputConfig, getSelectionConfig, getUploadedImage } from "./WizardStepRenderer.utils";
10
11
  import type { WizardStepRendererProps } from "./WizardStepRenderer.types";
11
12
 
12
13
  export type { WizardStepRendererProps } from "./WizardStepRenderer.types";
@@ -166,6 +167,43 @@ export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
166
167
  );
167
168
  }
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
+
169
207
  default:
170
208
  if (typeof __DEV__ !== "undefined" && __DEV__) {
171
209
  console.warn("[WizardStepRenderer] Unhandled step type", { stepType: step.type });
@@ -2,6 +2,7 @@ import type {
2
2
  WizardStepConfig,
3
3
  TextInputStepConfig,
4
4
  PhotoUploadStepConfig,
5
+ SelectionStepConfig,
5
6
  } from "../../domain/entities/wizard-config.types";
6
7
  import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
7
8
 
@@ -39,3 +40,9 @@ export function getUploadedImage(data: unknown): UploadedImage | undefined {
39
40
  if (isUploadedImage(data)) return data;
40
41
  return undefined;
41
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
+ });
@@ -1,41 +1,24 @@
1
1
  /**
2
- * Text Input Screen
2
+ * TextInputScreen
3
3
  * Generic text input step for wizard flows
4
4
  */
5
5
 
6
6
  import React, { useState, useCallback } from "react";
7
- import { View, ScrollView, TextInput, StyleSheet } from "react-native";
7
+ import { View, ScrollView, TextInput } from "react-native";
8
8
  import {
9
9
  AtomicText,
10
10
  AtomicButton,
11
11
  AtomicIcon,
12
12
  useAppDesignTokens,
13
13
  } from "@umituz/react-native-design-system";
14
+ import { styles } from "./TextInputScreen.styles";
15
+ import type { TextInputScreenProps } from "./TextInputScreen.types";
14
16
 
15
- export interface TextInputScreenTranslations {
16
- readonly title: string;
17
- readonly subtitle?: string;
18
- readonly placeholder: string;
19
- readonly continueButton: string;
20
- readonly backButton?: string;
21
- readonly examplesTitle?: string;
22
- }
23
-
24
- export interface TextInputScreenConfig {
25
- readonly minLength?: number;
26
- readonly maxLength?: number;
27
- readonly multiline?: boolean;
28
- }
29
-
30
- export interface TextInputScreenProps {
31
- readonly stepId: string;
32
- readonly translations: TextInputScreenTranslations;
33
- readonly config?: TextInputScreenConfig;
34
- readonly examplePrompts?: string[];
35
- readonly initialValue?: string;
36
- readonly onBack: () => void;
37
- readonly onContinue: (text: string) => void;
38
- }
17
+ export type {
18
+ TextInputScreenTranslations,
19
+ TextInputScreenConfig,
20
+ TextInputScreenProps,
21
+ } from "./TextInputScreen.types";
39
22
 
40
23
  export const TextInputScreen: React.FC<TextInputScreenProps> = ({
41
24
  stepId: _stepId,
@@ -66,11 +49,7 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
66
49
  return (
67
50
  <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
68
51
  <View style={[styles.header, { paddingHorizontal: tokens.spacing.md }]}>
69
- <AtomicButton
70
- variant="text"
71
- size="sm"
72
- onPress={onBack}
73
- >
52
+ <AtomicButton variant="text" size="sm" onPress={onBack}>
74
53
  <View style={styles.backButtonContent}>
75
54
  <AtomicIcon name="arrow-back" size="sm" color="textPrimary" />
76
55
  {translations.backButton ? (
@@ -92,11 +71,7 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
92
71
  </AtomicText>
93
72
 
94
73
  {translations.subtitle ? (
95
- <AtomicText
96
- type="bodyMedium"
97
- color="textSecondary"
98
- style={{ marginBottom: tokens.spacing.lg }}
99
- >
74
+ <AtomicText type="bodyMedium" color="textSecondary" style={{ marginBottom: tokens.spacing.lg }}>
100
75
  {translations.subtitle}
101
76
  </AtomicText>
102
77
  ) : null}
@@ -112,13 +87,7 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
112
87
  ]}
113
88
  >
114
89
  <TextInput
115
- style={[
116
- styles.textInput,
117
- {
118
- color: tokens.colors.textPrimary,
119
- minHeight: config?.multiline ? 120 : 48,
120
- },
121
- ]}
90
+ style={[styles.textInput, { color: tokens.colors.textPrimary, minHeight: config?.multiline ? 120 : 48 }]}
122
91
  placeholder={translations.placeholder}
123
92
  placeholderTextColor={tokens.colors.textTertiary}
124
93
  value={text}
@@ -127,22 +96,14 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
127
96
  maxLength={maxLength}
128
97
  textAlignVertical="top"
129
98
  />
130
- <AtomicText
131
- type="bodySmall"
132
- color="textTertiary"
133
- style={styles.charCount}
134
- >
99
+ <AtomicText type="bodySmall" color="textTertiary" style={styles.charCount}>
135
100
  {text.length}/{maxLength}
136
101
  </AtomicText>
137
102
  </View>
138
103
 
139
104
  {examplePrompts.length > 0 && translations.examplesTitle ? (
140
105
  <View style={{ marginTop: tokens.spacing.lg }}>
141
- <AtomicText
142
- type="labelLarge"
143
- color="textSecondary"
144
- style={{ marginBottom: tokens.spacing.sm }}
145
- >
106
+ <AtomicText type="labelLarge" color="textSecondary" style={{ marginBottom: tokens.spacing.sm }}>
146
107
  {translations.examplesTitle}
147
108
  </AtomicText>
148
109
  {examplePrompts.slice(0, 4).map((example, index) => (
@@ -161,59 +122,10 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
161
122
  </ScrollView>
162
123
 
163
124
  <View style={[styles.footer, { padding: tokens.spacing.md }]}>
164
- <AtomicButton
165
- variant="primary"
166
- size="lg"
167
- onPress={handleContinue}
168
- disabled={!canContinue}
169
- style={styles.continueButton}
170
- >
125
+ <AtomicButton variant="primary" size="lg" onPress={handleContinue} disabled={!canContinue} style={styles.continueButton}>
171
126
  {translations.continueButton}
172
127
  </AtomicButton>
173
128
  </View>
174
129
  </View>
175
130
  );
176
131
  };
177
-
178
- const styles = StyleSheet.create({
179
- container: {
180
- flex: 1,
181
- },
182
- header: {
183
- flexDirection: "row",
184
- alignItems: "center",
185
- paddingVertical: 8,
186
- },
187
- backButtonContent: {
188
- flexDirection: "row",
189
- alignItems: "center",
190
- },
191
- backButtonText: {
192
- marginLeft: 4,
193
- },
194
- scrollView: {
195
- flex: 1,
196
- },
197
- title: {
198
- marginBottom: 8,
199
- },
200
- inputContainer: {
201
- borderWidth: 1,
202
- padding: 12,
203
- },
204
- textInput: {
205
- fontSize: 16,
206
- lineHeight: 24,
207
- },
208
- charCount: {
209
- textAlign: "right",
210
- marginTop: 4,
211
- },
212
- footer: {
213
- borderTopWidth: 1,
214
- borderTopColor: "rgba(0,0,0,0.1)",
215
- },
216
- continueButton: {
217
- width: "100%",
218
- },
219
- });
@@ -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
+ }
@@ -11,3 +11,10 @@ export type {
11
11
  TextInputScreenConfig,
12
12
  TextInputScreenProps,
13
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
+ });