@umituz/react-native-ai-generation-content 1.36.2 → 1.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/domains/creations/infrastructure/repositories/CreationsWriter.ts +124 -199
- package/src/domains/creations/presentation/components/GalleryResultPreview.tsx +88 -0
- package/src/domains/creations/presentation/hooks/useGalleryCallbacks.ts +127 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +37 -123
- package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +5 -231
- package/src/domains/generation/wizard/presentation/components/WizardContinueButton.tsx +73 -0
- package/src/domains/generation/wizard/presentation/components/WizardFlowContent.tsx +181 -0
- package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +18 -134
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderPhotoUploadStep.tsx +52 -0
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderSelectionStep.tsx +59 -0
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderTextInputStep.tsx +62 -0
- package/src/domains/generation/wizard/presentation/hooks/useWizardFlowHandlers.ts +133 -0
- package/src/domains/generation/wizard/presentation/screens/GenericPhotoUploadScreen.tsx +15 -83
- package/src/domains/generation/wizard/presentation/screens/SelectionScreen.tsx +55 -134
- package/src/domains/result-preview/presentation/components/GenerationErrorScreen.tsx +19 -122
- package/src/domains/result-preview/presentation/hooks/useResultActions.ts +16 -131
- package/src/domains/scenarios/presentation/components/ScenarioContinueButton.tsx +76 -0
- package/src/domains/scenarios/presentation/hooks/useHierarchicalScenarios.ts +70 -0
- package/src/domains/scenarios/presentation/screens/HierarchicalScenarioListScreen.tsx +37 -170
- package/src/presentation/hooks/generation/moderation-handler.ts +77 -0
- package/src/presentation/hooks/generation/orchestrator.ts +33 -125
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generic Photo Upload Screen
|
|
3
3
|
* Used by wizard domain for ANY photo upload step
|
|
4
|
-
* NO feature-specific concepts (no partner, couple, etc.)
|
|
5
|
-
* Works for: couple features, face-swap, image-to-video, ANY photo upload need
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
6
|
import React, { useMemo } from "react";
|
|
9
|
-
import { View,
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
10
8
|
import {
|
|
11
9
|
useAppDesignTokens,
|
|
12
10
|
ScreenLayout,
|
|
13
11
|
AtomicText,
|
|
14
|
-
AtomicIcon,
|
|
15
12
|
NavigationHeader,
|
|
16
13
|
InfoGrid,
|
|
17
14
|
type DesignTokens,
|
|
@@ -20,6 +17,7 @@ import {
|
|
|
20
17
|
import { PhotoUploadCard } from "../../../../../presentation/components";
|
|
21
18
|
import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
|
|
22
19
|
import { usePhotoUploadState } from "../hooks/usePhotoUploadState";
|
|
20
|
+
import { WizardContinueButton } from "../components/WizardContinueButton";
|
|
23
21
|
|
|
24
22
|
export interface PhotoUploadScreenTranslations {
|
|
25
23
|
readonly title: string;
|
|
@@ -51,10 +49,7 @@ export interface PhotoUploadScreenProps {
|
|
|
51
49
|
readonly stepId?: string;
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
const DEFAULT_CONFIG: PhotoUploadScreenConfig = {
|
|
55
|
-
showPhotoTips: true,
|
|
56
|
-
maxFileSizeMB: 10,
|
|
57
|
-
};
|
|
52
|
+
const DEFAULT_CONFIG: PhotoUploadScreenConfig = { showPhotoTips: true, maxFileSizeMB: 10 };
|
|
58
53
|
|
|
59
54
|
export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
60
55
|
translations,
|
|
@@ -66,7 +61,6 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
|
66
61
|
stepId,
|
|
67
62
|
}) => {
|
|
68
63
|
const tokens = useAppDesignTokens();
|
|
69
|
-
|
|
70
64
|
const { image, handlePickImage, canContinue } = usePhotoUploadState({
|
|
71
65
|
config: { maxFileSizeMB: config.maxFileSizeMB },
|
|
72
66
|
translations: {
|
|
@@ -87,7 +81,6 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
|
87
81
|
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
88
82
|
const showPhotoTips = config.showPhotoTips ?? true;
|
|
89
83
|
|
|
90
|
-
// Build photo tips items from translations
|
|
91
84
|
const photoTipsItems: InfoGridItem[] = useMemo(() => {
|
|
92
85
|
const tipKeys = [
|
|
93
86
|
{ key: "photoUpload.tips.clearFace", icon: "happy-outline" },
|
|
@@ -95,10 +88,7 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
|
95
88
|
{ key: "photoUpload.tips.recentPhoto", icon: "time-outline" },
|
|
96
89
|
{ key: "photoUpload.tips.noFilters", icon: "image-outline" },
|
|
97
90
|
];
|
|
98
|
-
return tipKeys.map(({ key, icon }) => ({
|
|
99
|
-
text: t(key),
|
|
100
|
-
icon,
|
|
101
|
-
}));
|
|
91
|
+
return tipKeys.map(({ key, icon }) => ({ text: t(key), icon }));
|
|
102
92
|
}, [t]);
|
|
103
93
|
|
|
104
94
|
return (
|
|
@@ -107,33 +97,11 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
|
107
97
|
title={translations.title}
|
|
108
98
|
onBackPress={onBack}
|
|
109
99
|
rightElement={
|
|
110
|
-
<
|
|
100
|
+
<WizardContinueButton
|
|
101
|
+
canContinue={canContinue && !!image}
|
|
111
102
|
onPress={handleContinuePress}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
style={[
|
|
115
|
-
styles.continueButton,
|
|
116
|
-
{
|
|
117
|
-
backgroundColor: canContinue && image ? tokens.colors.primary : tokens.colors.surfaceVariant,
|
|
118
|
-
opacity: canContinue && image ? 1 : 0.5,
|
|
119
|
-
},
|
|
120
|
-
]}
|
|
121
|
-
>
|
|
122
|
-
<AtomicText
|
|
123
|
-
type="bodyMedium"
|
|
124
|
-
style={[
|
|
125
|
-
styles.continueText,
|
|
126
|
-
{ color: canContinue && image ? tokens.colors.onPrimary : tokens.colors.textSecondary },
|
|
127
|
-
]}
|
|
128
|
-
>
|
|
129
|
-
{translations.continue}
|
|
130
|
-
</AtomicText>
|
|
131
|
-
<AtomicIcon
|
|
132
|
-
name="chevron-forward-outline"
|
|
133
|
-
size="sm"
|
|
134
|
-
color={canContinue && image ? "onPrimary" : "textSecondary"}
|
|
135
|
-
/>
|
|
136
|
-
</TouchableOpacity>
|
|
103
|
+
label={translations.continue}
|
|
104
|
+
/>
|
|
137
105
|
}
|
|
138
106
|
/>
|
|
139
107
|
<ScreenLayout
|
|
@@ -148,15 +116,9 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
|
148
116
|
{translations.subtitle}
|
|
149
117
|
</AtomicText>
|
|
150
118
|
|
|
151
|
-
{/* Photo Tips - InfoGrid version */}
|
|
152
119
|
{showPhotoTips && (
|
|
153
120
|
<View style={styles.tipsContainer}>
|
|
154
|
-
<InfoGrid
|
|
155
|
-
items={photoTipsItems}
|
|
156
|
-
columns={2}
|
|
157
|
-
title={t("photoUpload.tips.title")}
|
|
158
|
-
headerIcon="bulb-outline"
|
|
159
|
-
/>
|
|
121
|
+
<InfoGrid items={photoTipsItems} columns={2} title={t("photoUpload.tips.title")} headerIcon="bulb-outline" />
|
|
160
122
|
</View>
|
|
161
123
|
)}
|
|
162
124
|
|
|
@@ -175,10 +137,7 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
|
175
137
|
|
|
176
138
|
{translations.aiDisclosure && (
|
|
177
139
|
<View style={styles.disclosureContainer}>
|
|
178
|
-
<AtomicText
|
|
179
|
-
type="labelSmall"
|
|
180
|
-
style={[styles.disclosureText, { color: tokens.colors.textSecondary }]}
|
|
181
|
-
>
|
|
140
|
+
<AtomicText type="labelSmall" style={[styles.disclosureText, { color: tokens.colors.textSecondary }]}>
|
|
182
141
|
{translations.aiDisclosure}
|
|
183
142
|
</AtomicText>
|
|
184
143
|
</View>
|
|
@@ -190,33 +149,10 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
|
190
149
|
|
|
191
150
|
const createStyles = (tokens: DesignTokens) =>
|
|
192
151
|
StyleSheet.create({
|
|
193
|
-
container: {
|
|
194
|
-
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
paddingBottom: 40,
|
|
198
|
-
},
|
|
199
|
-
subtitle: {
|
|
200
|
-
fontSize: 16,
|
|
201
|
-
textAlign: "center",
|
|
202
|
-
marginHorizontal: 24,
|
|
203
|
-
marginBottom: 24,
|
|
204
|
-
},
|
|
205
|
-
tipsContainer: {
|
|
206
|
-
marginHorizontal: 24,
|
|
207
|
-
marginBottom: 20,
|
|
208
|
-
},
|
|
209
|
-
continueButton: {
|
|
210
|
-
flexDirection: "row",
|
|
211
|
-
alignItems: "center",
|
|
212
|
-
paddingHorizontal: tokens.spacing.md,
|
|
213
|
-
paddingVertical: tokens.spacing.xs,
|
|
214
|
-
borderRadius: tokens.borders.radius.full,
|
|
215
|
-
},
|
|
216
|
-
continueText: {
|
|
217
|
-
fontWeight: "800",
|
|
218
|
-
marginRight: 4,
|
|
219
|
-
},
|
|
152
|
+
container: { flex: 1 },
|
|
153
|
+
scrollContent: { paddingBottom: 40 },
|
|
154
|
+
subtitle: { fontSize: 16, textAlign: "center", marginHorizontal: 24, marginBottom: 24 },
|
|
155
|
+
tipsContainer: { marginHorizontal: 24, marginBottom: 20 },
|
|
220
156
|
disclosureContainer: {
|
|
221
157
|
marginTop: 24,
|
|
222
158
|
marginHorizontal: 24,
|
|
@@ -224,9 +160,5 @@ const createStyles = (tokens: DesignTokens) =>
|
|
|
224
160
|
borderRadius: 12,
|
|
225
161
|
backgroundColor: tokens.colors.surfaceVariant + "40",
|
|
226
162
|
},
|
|
227
|
-
disclosureText: {
|
|
228
|
-
textAlign: "center",
|
|
229
|
-
lineHeight: 18,
|
|
230
|
-
opacity: 0.8,
|
|
231
|
-
},
|
|
163
|
+
disclosureText: { textAlign: "center", lineHeight: 18, opacity: 0.8 },
|
|
232
164
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SelectionScreen
|
|
3
3
|
* Generic selection step for wizard flows (duration, style, etc.)
|
|
4
|
-
* Uses design system: NavigationHeader + ScreenLayout
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import React, { useState, useCallback, useMemo } from "react";
|
|
@@ -14,7 +13,8 @@ import {
|
|
|
14
13
|
NavigationHeader,
|
|
15
14
|
type DesignTokens,
|
|
16
15
|
} from "@umituz/react-native-design-system";
|
|
17
|
-
import type { SelectionScreenProps } from "./SelectionScreen.types";
|
|
16
|
+
import type { SelectionScreenProps, SelectionOption } from "./SelectionScreen.types";
|
|
17
|
+
import { WizardContinueButton } from "../components/WizardContinueButton";
|
|
18
18
|
|
|
19
19
|
export type {
|
|
20
20
|
SelectionOption,
|
|
@@ -41,129 +41,74 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
|
|
|
41
41
|
|
|
42
42
|
const isMultiSelect = config?.multiSelect ?? false;
|
|
43
43
|
const isRequired = config?.required ?? true;
|
|
44
|
-
|
|
45
44
|
const canContinue = isRequired
|
|
46
|
-
? isMultiSelect
|
|
47
|
-
? (selected as string[]).length > 0
|
|
48
|
-
: selected !== ""
|
|
45
|
+
? isMultiSelect ? (selected as string[]).length > 0 : selected !== ""
|
|
49
46
|
: true;
|
|
50
47
|
|
|
51
|
-
const handleSelect = useCallback(
|
|
52
|
-
(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
setSelected(optionId);
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
[isMultiSelect],
|
|
65
|
-
);
|
|
48
|
+
const handleSelect = useCallback((optionId: string) => {
|
|
49
|
+
if (isMultiSelect) {
|
|
50
|
+
setSelected((prev) => {
|
|
51
|
+
const arr = prev as string[];
|
|
52
|
+
return arr.includes(optionId) ? arr.filter((id) => id !== optionId) : [...arr, optionId];
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
setSelected(optionId);
|
|
56
|
+
}
|
|
57
|
+
}, [isMultiSelect]);
|
|
66
58
|
|
|
67
59
|
const handleContinue = useCallback(() => {
|
|
68
|
-
if (canContinue)
|
|
69
|
-
onContinue(selected);
|
|
70
|
-
}
|
|
60
|
+
if (canContinue) onContinue(selected);
|
|
71
61
|
}, [canContinue, selected, onContinue]);
|
|
72
62
|
|
|
73
63
|
const isOptionSelected = useCallback(
|
|
74
|
-
(optionId: string): boolean =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
return selected === optionId;
|
|
79
|
-
},
|
|
80
|
-
[isMultiSelect, selected],
|
|
64
|
+
(optionId: string): boolean =>
|
|
65
|
+
isMultiSelect ? (selected as string[]).includes(optionId) : selected === optionId,
|
|
66
|
+
[isMultiSelect, selected]
|
|
81
67
|
);
|
|
82
68
|
|
|
83
69
|
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
84
70
|
|
|
71
|
+
const renderOption = (option: SelectionOption) => {
|
|
72
|
+
const isSelected = isOptionSelected(option.id);
|
|
73
|
+
return (
|
|
74
|
+
<TouchableOpacity
|
|
75
|
+
key={option.id}
|
|
76
|
+
style={[
|
|
77
|
+
styles.optionCard,
|
|
78
|
+
{
|
|
79
|
+
backgroundColor: isSelected ? tokens.colors.primaryContainer : tokens.colors.backgroundSecondary,
|
|
80
|
+
borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
|
|
81
|
+
},
|
|
82
|
+
]}
|
|
83
|
+
onPress={() => handleSelect(option.id)}
|
|
84
|
+
activeOpacity={0.7}
|
|
85
|
+
>
|
|
86
|
+
{option.icon ? <AtomicIcon name={option.icon} size="lg" color={isSelected ? "primary" : "textSecondary"} /> : null}
|
|
87
|
+
<AtomicText type="labelLarge" color={isSelected ? "primary" : "textPrimary"} style={styles.optionLabel}>
|
|
88
|
+
{option.label}
|
|
89
|
+
</AtomicText>
|
|
90
|
+
{isSelected ? (
|
|
91
|
+
<View style={[styles.checkmark, { backgroundColor: tokens.colors.primary }]}>
|
|
92
|
+
<AtomicIcon name="checkmark" size="xs" color="onPrimary" />
|
|
93
|
+
</View>
|
|
94
|
+
) : null}
|
|
95
|
+
</TouchableOpacity>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
85
99
|
return (
|
|
86
100
|
<View style={{ flex: 1, backgroundColor: tokens.colors.backgroundPrimary }}>
|
|
87
101
|
<NavigationHeader
|
|
88
102
|
title=""
|
|
89
103
|
onBackPress={onBack}
|
|
90
104
|
rightElement={
|
|
91
|
-
<
|
|
92
|
-
onPress={handleContinue}
|
|
93
|
-
activeOpacity={0.7}
|
|
94
|
-
disabled={!canContinue}
|
|
95
|
-
style={[
|
|
96
|
-
styles.continueButton,
|
|
97
|
-
{
|
|
98
|
-
backgroundColor: canContinue ? tokens.colors.primary : tokens.colors.surfaceVariant,
|
|
99
|
-
opacity: canContinue ? 1 : 0.5,
|
|
100
|
-
},
|
|
101
|
-
]}
|
|
102
|
-
>
|
|
103
|
-
<AtomicText
|
|
104
|
-
type="bodyMedium"
|
|
105
|
-
style={[
|
|
106
|
-
styles.continueText,
|
|
107
|
-
{ color: canContinue ? tokens.colors.onPrimary : tokens.colors.textSecondary },
|
|
108
|
-
]}
|
|
109
|
-
>
|
|
110
|
-
{translations.continueButton}
|
|
111
|
-
</AtomicText>
|
|
112
|
-
<AtomicIcon
|
|
113
|
-
name="arrow-forward"
|
|
114
|
-
size="sm"
|
|
115
|
-
color={canContinue ? "onPrimary" : "textSecondary"}
|
|
116
|
-
/>
|
|
117
|
-
</TouchableOpacity>
|
|
105
|
+
<WizardContinueButton canContinue={canContinue} onPress={handleContinue} label={translations.continueButton} icon="arrow-forward" />
|
|
118
106
|
}
|
|
119
107
|
/>
|
|
120
|
-
<ScreenLayout
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
contentContainerStyle={styles.scrollContent}
|
|
125
|
-
>
|
|
126
|
-
<AtomicText type="headlineMedium" color="textPrimary" style={styles.title}>
|
|
127
|
-
{translations.title}
|
|
128
|
-
</AtomicText>
|
|
129
|
-
|
|
130
|
-
{translations.subtitle ? (
|
|
131
|
-
<AtomicText type="bodyMedium" color="textSecondary" style={styles.subtitle}>
|
|
132
|
-
{translations.subtitle}
|
|
133
|
-
</AtomicText>
|
|
134
|
-
) : null}
|
|
135
|
-
|
|
136
|
-
<View style={styles.optionsGrid}>
|
|
137
|
-
{options.map((option) => {
|
|
138
|
-
const isSelected = isOptionSelected(option.id);
|
|
139
|
-
return (
|
|
140
|
-
<TouchableOpacity
|
|
141
|
-
key={option.id}
|
|
142
|
-
style={[
|
|
143
|
-
styles.optionCard,
|
|
144
|
-
{
|
|
145
|
-
backgroundColor: isSelected ? tokens.colors.primaryContainer : tokens.colors.backgroundSecondary,
|
|
146
|
-
borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
|
|
147
|
-
},
|
|
148
|
-
]}
|
|
149
|
-
onPress={() => handleSelect(option.id)}
|
|
150
|
-
activeOpacity={0.7}
|
|
151
|
-
>
|
|
152
|
-
{option.icon ? (
|
|
153
|
-
<AtomicIcon name={option.icon} size="lg" color={isSelected ? "primary" : "textSecondary"} />
|
|
154
|
-
) : null}
|
|
155
|
-
<AtomicText type="labelLarge" color={isSelected ? "primary" : "textPrimary"} style={styles.optionLabel}>
|
|
156
|
-
{option.label}
|
|
157
|
-
</AtomicText>
|
|
158
|
-
{isSelected ? (
|
|
159
|
-
<View style={[styles.checkmark, { backgroundColor: tokens.colors.primary }]}>
|
|
160
|
-
<AtomicIcon name="checkmark" size="xs" color="onPrimary" />
|
|
161
|
-
</View>
|
|
162
|
-
) : null}
|
|
163
|
-
</TouchableOpacity>
|
|
164
|
-
);
|
|
165
|
-
})}
|
|
166
|
-
</View>
|
|
108
|
+
<ScreenLayout scrollable={true} edges={["left", "right"]} hideScrollIndicator={true} contentContainerStyle={styles.scrollContent}>
|
|
109
|
+
<AtomicText type="headlineMedium" color="textPrimary" style={styles.title}>{translations.title}</AtomicText>
|
|
110
|
+
{translations.subtitle ? <AtomicText type="bodyMedium" color="textSecondary" style={styles.subtitle}>{translations.subtitle}</AtomicText> : null}
|
|
111
|
+
<View style={styles.optionsGrid}>{options.map(renderOption)}</View>
|
|
167
112
|
</ScreenLayout>
|
|
168
113
|
</View>
|
|
169
114
|
);
|
|
@@ -171,21 +116,10 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
|
|
|
171
116
|
|
|
172
117
|
const createStyles = (tokens: DesignTokens) =>
|
|
173
118
|
StyleSheet.create({
|
|
174
|
-
scrollContent: {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
title: {
|
|
179
|
-
marginBottom: tokens.spacing.sm,
|
|
180
|
-
},
|
|
181
|
-
subtitle: {
|
|
182
|
-
marginBottom: tokens.spacing.lg,
|
|
183
|
-
},
|
|
184
|
-
optionsGrid: {
|
|
185
|
-
flexDirection: "row",
|
|
186
|
-
flexWrap: "wrap",
|
|
187
|
-
gap: tokens.spacing.sm,
|
|
188
|
-
},
|
|
119
|
+
scrollContent: { paddingHorizontal: tokens.spacing.lg, paddingBottom: 40 },
|
|
120
|
+
title: { marginBottom: tokens.spacing.sm },
|
|
121
|
+
subtitle: { marginBottom: tokens.spacing.lg },
|
|
122
|
+
optionsGrid: { flexDirection: "row", flexWrap: "wrap", gap: tokens.spacing.sm },
|
|
189
123
|
optionCard: {
|
|
190
124
|
flex: 1,
|
|
191
125
|
minWidth: "45%",
|
|
@@ -197,9 +131,7 @@ const createStyles = (tokens: DesignTokens) =>
|
|
|
197
131
|
gap: tokens.spacing.xs,
|
|
198
132
|
position: "relative",
|
|
199
133
|
},
|
|
200
|
-
optionLabel: {
|
|
201
|
-
textAlign: "center",
|
|
202
|
-
},
|
|
134
|
+
optionLabel: { textAlign: "center" },
|
|
203
135
|
checkmark: {
|
|
204
136
|
position: "absolute",
|
|
205
137
|
top: tokens.spacing.xs,
|
|
@@ -210,15 +142,4 @@ const createStyles = (tokens: DesignTokens) =>
|
|
|
210
142
|
alignItems: "center",
|
|
211
143
|
justifyContent: "center",
|
|
212
144
|
},
|
|
213
|
-
continueButton: {
|
|
214
|
-
flexDirection: "row",
|
|
215
|
-
alignItems: "center",
|
|
216
|
-
paddingHorizontal: tokens.spacing.md,
|
|
217
|
-
paddingVertical: tokens.spacing.xs,
|
|
218
|
-
borderRadius: tokens.borders.radius.full,
|
|
219
|
-
},
|
|
220
|
-
continueText: {
|
|
221
|
-
fontWeight: "800",
|
|
222
|
-
marginRight: 4,
|
|
223
|
-
},
|
|
224
145
|
});
|
|
@@ -14,59 +14,31 @@ import {
|
|
|
14
14
|
type DesignTokens,
|
|
15
15
|
} from "@umituz/react-native-design-system";
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Error screen translations
|
|
19
|
-
*/
|
|
20
17
|
export interface GenerationErrorTranslations {
|
|
21
|
-
/** Error title */
|
|
22
18
|
readonly title: string;
|
|
23
|
-
/** Try again button */
|
|
24
19
|
readonly tryAgain: string;
|
|
25
|
-
/** Choose another button */
|
|
26
20
|
readonly chooseAnother: string;
|
|
27
|
-
/** Credit info message */
|
|
28
21
|
readonly noCreditCharged: string;
|
|
29
22
|
}
|
|
30
23
|
|
|
31
|
-
/**
|
|
32
|
-
* Error screen config
|
|
33
|
-
*/
|
|
34
24
|
export interface GenerationErrorConfig {
|
|
35
|
-
/** Show credit info message */
|
|
36
25
|
readonly showCreditInfo?: boolean;
|
|
37
|
-
/** Icon name */
|
|
38
26
|
readonly iconName?: string;
|
|
39
|
-
/** Icon size */
|
|
40
27
|
readonly iconSize?: number;
|
|
41
28
|
}
|
|
42
29
|
|
|
43
|
-
/**
|
|
44
|
-
* Error screen props
|
|
45
|
-
*/
|
|
46
30
|
export interface GenerationErrorScreenProps {
|
|
47
|
-
/** Error message to display */
|
|
48
31
|
readonly errorMessage?: string;
|
|
49
|
-
/** Translations */
|
|
50
32
|
readonly translations: GenerationErrorTranslations;
|
|
51
|
-
/** Configuration */
|
|
52
33
|
readonly config?: GenerationErrorConfig;
|
|
53
|
-
/** Try again callback */
|
|
54
34
|
readonly onTryAgain: () => void;
|
|
55
|
-
/** Choose another scenario callback */
|
|
56
35
|
readonly onChooseAnother: () => void;
|
|
57
|
-
/** Optional premium button text */
|
|
58
36
|
readonly premiumButtonText?: string;
|
|
59
|
-
/** Optional premium button callback */
|
|
60
37
|
readonly onGoPremium?: () => void;
|
|
61
|
-
/** Optional custom style */
|
|
62
38
|
readonly style?: StyleProp<ViewStyle>;
|
|
63
39
|
}
|
|
64
40
|
|
|
65
|
-
const DEFAULT_CONFIG: GenerationErrorConfig = {
|
|
66
|
-
showCreditInfo: true,
|
|
67
|
-
iconName: "alert-circle",
|
|
68
|
-
iconSize: 56,
|
|
69
|
-
};
|
|
41
|
+
const DEFAULT_CONFIG: GenerationErrorConfig = { showCreditInfo: true, iconName: "alert-circle", iconSize: 56 };
|
|
70
42
|
|
|
71
43
|
export const GenerationErrorScreen: React.FC<GenerationErrorScreenProps> = ({
|
|
72
44
|
errorMessage,
|
|
@@ -83,43 +55,19 @@ export const GenerationErrorScreen: React.FC<GenerationErrorScreenProps> = ({
|
|
|
83
55
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
84
56
|
|
|
85
57
|
return (
|
|
86
|
-
<View
|
|
87
|
-
|
|
88
|
-
styles.container,
|
|
89
|
-
{ backgroundColor: tokens.colors.backgroundPrimary },
|
|
90
|
-
style,
|
|
91
|
-
]}
|
|
92
|
-
>
|
|
93
|
-
<ScreenLayout
|
|
94
|
-
scrollable={false}
|
|
95
|
-
edges={["top", "bottom"]}
|
|
96
|
-
backgroundColor="transparent"
|
|
97
|
-
>
|
|
58
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }, style]}>
|
|
59
|
+
<ScreenLayout scrollable={false} edges={["top", "bottom"]} backgroundColor="transparent">
|
|
98
60
|
<View style={styles.content}>
|
|
99
61
|
<View style={styles.iconContainer}>
|
|
100
|
-
<AtomicIcon
|
|
101
|
-
name={mergedConfig.iconName || "alert-circle"}
|
|
102
|
-
size={mergedConfig.iconSize || 56}
|
|
103
|
-
customColor={tokens.colors.error}
|
|
104
|
-
/>
|
|
62
|
+
<AtomicIcon name={mergedConfig.iconName || "alert-circle"} size={mergedConfig.iconSize || 56} customColor={tokens.colors.error} />
|
|
105
63
|
</View>
|
|
106
|
-
|
|
107
64
|
<AtomicText style={styles.title}>{translations.title}</AtomicText>
|
|
108
|
-
|
|
109
|
-
<AtomicText style={styles.message}>
|
|
110
|
-
{errorMessage || translations.title}
|
|
111
|
-
</AtomicText>
|
|
65
|
+
<AtomicText style={styles.message}>{errorMessage || translations.title}</AtomicText>
|
|
112
66
|
|
|
113
67
|
{mergedConfig.showCreditInfo && (
|
|
114
68
|
<View style={styles.infoContainer}>
|
|
115
|
-
<AtomicIcon
|
|
116
|
-
|
|
117
|
-
size={16}
|
|
118
|
-
customColor={tokens.colors.textSecondary}
|
|
119
|
-
/>
|
|
120
|
-
<AtomicText style={styles.infoText}>
|
|
121
|
-
{translations.noCreditCharged}
|
|
122
|
-
</AtomicText>
|
|
69
|
+
<AtomicIcon name="information-circle-outline" size={16} customColor={tokens.colors.textSecondary} />
|
|
70
|
+
<AtomicText style={styles.infoText}>{translations.noCreditCharged}</AtomicText>
|
|
123
71
|
</View>
|
|
124
72
|
)}
|
|
125
73
|
|
|
@@ -127,15 +75,8 @@ export const GenerationErrorScreen: React.FC<GenerationErrorScreenProps> = ({
|
|
|
127
75
|
|
|
128
76
|
<View style={styles.actionsContainer}>
|
|
129
77
|
{onGoPremium && premiumButtonText && (
|
|
130
|
-
<AtomicButton
|
|
131
|
-
title={premiumButtonText}
|
|
132
|
-
onPress={onGoPremium}
|
|
133
|
-
variant="primary"
|
|
134
|
-
size="lg"
|
|
135
|
-
fullWidth
|
|
136
|
-
/>
|
|
78
|
+
<AtomicButton title={premiumButtonText} onPress={onGoPremium} variant="primary" size="lg" fullWidth />
|
|
137
79
|
)}
|
|
138
|
-
|
|
139
80
|
<AtomicButton
|
|
140
81
|
title={translations.tryAgain}
|
|
141
82
|
onPress={onTryAgain}
|
|
@@ -143,15 +84,8 @@ export const GenerationErrorScreen: React.FC<GenerationErrorScreenProps> = ({
|
|
|
143
84
|
size="lg"
|
|
144
85
|
fullWidth
|
|
145
86
|
/>
|
|
146
|
-
|
|
147
87
|
<View style={styles.secondaryButtonContainer}>
|
|
148
|
-
<AtomicButton
|
|
149
|
-
title={translations.chooseAnother}
|
|
150
|
-
onPress={onChooseAnother}
|
|
151
|
-
variant="text"
|
|
152
|
-
size="md"
|
|
153
|
-
fullWidth
|
|
154
|
-
/>
|
|
88
|
+
<AtomicButton title={translations.chooseAnother} onPress={onChooseAnother} variant="text" size="md" fullWidth />
|
|
155
89
|
</View>
|
|
156
90
|
</View>
|
|
157
91
|
</View>
|
|
@@ -162,51 +96,14 @@ export const GenerationErrorScreen: React.FC<GenerationErrorScreenProps> = ({
|
|
|
162
96
|
|
|
163
97
|
const createStyles = (tokens: DesignTokens) =>
|
|
164
98
|
StyleSheet.create({
|
|
165
|
-
container: {
|
|
166
|
-
|
|
167
|
-
},
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
},
|
|
175
|
-
iconContainer: {
|
|
176
|
-
marginBottom: 24,
|
|
177
|
-
},
|
|
178
|
-
title: {
|
|
179
|
-
...tokens.typography.headingMedium,
|
|
180
|
-
color: tokens.colors.textPrimary,
|
|
181
|
-
fontWeight: "700",
|
|
182
|
-
marginBottom: 12,
|
|
183
|
-
textAlign: "center",
|
|
184
|
-
},
|
|
185
|
-
message: {
|
|
186
|
-
...tokens.typography.bodyMedium,
|
|
187
|
-
color: tokens.colors.textSecondary,
|
|
188
|
-
textAlign: "center",
|
|
189
|
-
lineHeight: 22,
|
|
190
|
-
marginBottom: 24,
|
|
191
|
-
},
|
|
192
|
-
infoContainer: {
|
|
193
|
-
flexDirection: "row",
|
|
194
|
-
alignItems: "center",
|
|
195
|
-
gap: 6,
|
|
196
|
-
opacity: 0.7,
|
|
197
|
-
},
|
|
198
|
-
infoText: {
|
|
199
|
-
...tokens.typography.bodySmall,
|
|
200
|
-
color: tokens.colors.textSecondary,
|
|
201
|
-
},
|
|
202
|
-
spacer: {
|
|
203
|
-
flex: 1,
|
|
204
|
-
},
|
|
205
|
-
actionsContainer: {
|
|
206
|
-
width: "100%",
|
|
207
|
-
gap: 16,
|
|
208
|
-
},
|
|
209
|
-
secondaryButtonContainer: {
|
|
210
|
-
marginTop: 4,
|
|
211
|
-
},
|
|
99
|
+
container: { flex: 1 },
|
|
100
|
+
content: { flex: 1, alignItems: "center", paddingHorizontal: 32, paddingTop: 80, paddingBottom: 40 },
|
|
101
|
+
iconContainer: { marginBottom: 24 },
|
|
102
|
+
title: { ...tokens.typography.headingMedium, color: tokens.colors.textPrimary, fontWeight: "700", marginBottom: 12, textAlign: "center" },
|
|
103
|
+
message: { ...tokens.typography.bodyMedium, color: tokens.colors.textSecondary, textAlign: "center", lineHeight: 22, marginBottom: 24 },
|
|
104
|
+
infoContainer: { flexDirection: "row", alignItems: "center", gap: 6, opacity: 0.7 },
|
|
105
|
+
infoText: { ...tokens.typography.bodySmall, color: tokens.colors.textSecondary },
|
|
106
|
+
spacer: { flex: 1 },
|
|
107
|
+
actionsContainer: { width: "100%", gap: 16 },
|
|
108
|
+
secondaryButtonContainer: { marginTop: 4 },
|
|
212
109
|
});
|