@umituz/react-native-ai-generation-content 1.17.264 → 1.17.266
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 -1
- package/src/features/love-message/infrastructure/persistence/PartnerProfileRepository.ts +8 -16
- package/src/features/partner-upload/domain/types.ts +59 -0
- package/src/features/partner-upload/index.ts +30 -0
- package/src/features/partner-upload/presentation/components/PartnerInfoInput.tsx +112 -0
- package/src/features/partner-upload/presentation/components/PhotoTips.tsx +53 -0
- package/src/features/partner-upload/presentation/components/index.ts +4 -0
- package/src/features/partner-upload/presentation/hooks/index.ts +7 -0
- package/src/features/partner-upload/presentation/hooks/usePartnerStep.ts +113 -0
- package/src/features/partner-upload/presentation/screens/PartnerStepScreen.tsx +208 -0
- package/src/features/partner-upload/presentation/screens/index.ts +6 -0
- package/src/index.ts +1 -0
- package/src/infrastructure/utils/feature-utils.ts +6 -1
- package/src/presentation/hooks/useGenerationCallbacksBuilder.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.266",
|
|
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",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"expo-haptics": "^15.0.8",
|
|
78
78
|
"expo-image": "^3.0.11",
|
|
79
79
|
"expo-image-manipulator": "^14.0.8",
|
|
80
|
+
"expo-image-picker": "^17.0.10",
|
|
80
81
|
"expo-linear-gradient": "^15.0.8",
|
|
81
82
|
"expo-localization": "^17.0.8",
|
|
82
83
|
"expo-media-library": "^18.2.1",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles persistence of partner profile data
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
7
7
|
import { PartnerProfile } from "../../domain/types";
|
|
8
8
|
|
|
9
9
|
const PARTNER_PROFILE_STORAGE_KEY = "love_message_partner_profile";
|
|
@@ -14,12 +14,9 @@ export const PartnerProfileRepository = {
|
|
|
14
14
|
*/
|
|
15
15
|
getProfile: async (): Promise<PartnerProfile | null> => {
|
|
16
16
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
);
|
|
21
|
-
if (result.success && result.data) {
|
|
22
|
-
return JSON.parse(result.data) as PartnerProfile;
|
|
17
|
+
const data = await AsyncStorage.getItem(PARTNER_PROFILE_STORAGE_KEY);
|
|
18
|
+
if (data) {
|
|
19
|
+
return JSON.parse(data) as PartnerProfile;
|
|
23
20
|
}
|
|
24
21
|
return null;
|
|
25
22
|
} catch {
|
|
@@ -32,11 +29,8 @@ export const PartnerProfileRepository = {
|
|
|
32
29
|
*/
|
|
33
30
|
saveProfile: async (profile: PartnerProfile): Promise<boolean> => {
|
|
34
31
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
JSON.stringify(profile),
|
|
38
|
-
);
|
|
39
|
-
return result.success;
|
|
32
|
+
await AsyncStorage.setItem(PARTNER_PROFILE_STORAGE_KEY, JSON.stringify(profile));
|
|
33
|
+
return true;
|
|
40
34
|
} catch {
|
|
41
35
|
return false;
|
|
42
36
|
}
|
|
@@ -47,10 +41,8 @@ export const PartnerProfileRepository = {
|
|
|
47
41
|
*/
|
|
48
42
|
clearProfile: async (): Promise<boolean> => {
|
|
49
43
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
);
|
|
53
|
-
return result.success;
|
|
44
|
+
await AsyncStorage.removeItem(PARTNER_PROFILE_STORAGE_KEY);
|
|
45
|
+
return true;
|
|
54
46
|
} catch {
|
|
55
47
|
return false;
|
|
56
48
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partner Upload Types
|
|
3
|
+
* Generic partner/photo upload feature types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface UploadedImage {
|
|
7
|
+
readonly uri: string;
|
|
8
|
+
readonly base64?: string;
|
|
9
|
+
readonly previewUrl: string;
|
|
10
|
+
readonly width?: number;
|
|
11
|
+
readonly height?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PartnerStepConfig {
|
|
15
|
+
readonly titleKey: string;
|
|
16
|
+
readonly subtitleKey: string;
|
|
17
|
+
readonly showFaceDetection?: boolean;
|
|
18
|
+
readonly showNameInput?: boolean;
|
|
19
|
+
readonly showPhotoTips?: boolean;
|
|
20
|
+
readonly maxNameLength?: number;
|
|
21
|
+
readonly namePlaceholderKey?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PartnerStepTranslations {
|
|
25
|
+
readonly tapToUpload: string;
|
|
26
|
+
readonly selectPhoto: string;
|
|
27
|
+
readonly change: string;
|
|
28
|
+
readonly analyzing: string;
|
|
29
|
+
readonly continue: string;
|
|
30
|
+
readonly faceDetectionLabel?: string;
|
|
31
|
+
readonly namePlaceholder?: string;
|
|
32
|
+
readonly photoTip1?: string;
|
|
33
|
+
readonly photoTip2?: string;
|
|
34
|
+
readonly photoTip3?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PhotoTipsConfig {
|
|
38
|
+
readonly tips: readonly string[];
|
|
39
|
+
readonly icon?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DEFAULT_PARTNER_STEP_CONFIG: PartnerStepConfig = {
|
|
43
|
+
titleKey: "partner.upload.title",
|
|
44
|
+
subtitleKey: "partner.upload.subtitle",
|
|
45
|
+
showFaceDetection: false,
|
|
46
|
+
showNameInput: true,
|
|
47
|
+
showPhotoTips: true,
|
|
48
|
+
maxNameLength: 50,
|
|
49
|
+
namePlaceholderKey: "partner.upload.namePlaceholder",
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
export const DEFAULT_PHOTO_TIPS: PhotoTipsConfig = {
|
|
53
|
+
tips: [
|
|
54
|
+
"photoTips.tip1",
|
|
55
|
+
"photoTips.tip2",
|
|
56
|
+
"photoTips.tip3",
|
|
57
|
+
],
|
|
58
|
+
icon: "lightbulb",
|
|
59
|
+
} as const;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partner Upload Feature
|
|
3
|
+
* Generic partner/photo upload screens and components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
UploadedImage,
|
|
8
|
+
PartnerStepConfig,
|
|
9
|
+
PartnerStepTranslations,
|
|
10
|
+
PhotoTipsConfig,
|
|
11
|
+
} from "./domain/types";
|
|
12
|
+
export { DEFAULT_PARTNER_STEP_CONFIG, DEFAULT_PHOTO_TIPS } from "./domain/types";
|
|
13
|
+
|
|
14
|
+
export { PhotoTips, PartnerInfoInput } from "./presentation/components";
|
|
15
|
+
export type { PhotoTipsProps, PhotoTipConfig, PartnerInfoInputProps } from "./presentation/components";
|
|
16
|
+
|
|
17
|
+
export { usePartnerStep } from "./presentation/hooks";
|
|
18
|
+
export type {
|
|
19
|
+
UsePartnerStepConfig,
|
|
20
|
+
UsePartnerStepTranslations,
|
|
21
|
+
UsePartnerStepOptions,
|
|
22
|
+
UsePartnerStepReturn,
|
|
23
|
+
} from "./presentation/hooks";
|
|
24
|
+
|
|
25
|
+
export { PartnerStepScreen } from "./presentation/screens";
|
|
26
|
+
export type {
|
|
27
|
+
PartnerStepScreenProps,
|
|
28
|
+
PartnerStepScreenTranslations,
|
|
29
|
+
PartnerStepScreenConfig,
|
|
30
|
+
} from "./presentation/screens";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PartnerInfoInput Component
|
|
3
|
+
* Name and optional description input for partner
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicInput, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
|
|
10
|
+
export interface PartnerInfoInputProps {
|
|
11
|
+
readonly t: (key: string) => string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly onNameChange: (name: string) => void;
|
|
14
|
+
readonly description?: string;
|
|
15
|
+
readonly onDescriptionChange?: (description: string) => void;
|
|
16
|
+
readonly showName?: boolean;
|
|
17
|
+
readonly showDescription?: boolean;
|
|
18
|
+
readonly maxNameLength?: number;
|
|
19
|
+
readonly maxDescriptionLength?: number;
|
|
20
|
+
readonly nameLabelKey?: string;
|
|
21
|
+
readonly namePlaceholderKey?: string;
|
|
22
|
+
readonly descriptionLabelKey?: string;
|
|
23
|
+
readonly descriptionPlaceholderKey?: string;
|
|
24
|
+
readonly optionalKey?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const PartnerInfoInput: React.FC<PartnerInfoInputProps> = ({
|
|
28
|
+
t,
|
|
29
|
+
name,
|
|
30
|
+
onNameChange,
|
|
31
|
+
description = "",
|
|
32
|
+
onDescriptionChange,
|
|
33
|
+
showName = false,
|
|
34
|
+
showDescription = false,
|
|
35
|
+
maxNameLength = 30,
|
|
36
|
+
maxDescriptionLength = 200,
|
|
37
|
+
nameLabelKey = "photoUpload.nameLabel",
|
|
38
|
+
namePlaceholderKey = "photoUpload.namePlaceholder",
|
|
39
|
+
descriptionLabelKey = "photoUpload.descriptionLabel",
|
|
40
|
+
descriptionPlaceholderKey = "photoUpload.descriptionPlaceholder",
|
|
41
|
+
optionalKey = "common.optional",
|
|
42
|
+
}) => {
|
|
43
|
+
const tokens = useAppDesignTokens();
|
|
44
|
+
|
|
45
|
+
const styles = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
StyleSheet.create({
|
|
48
|
+
container: {
|
|
49
|
+
paddingHorizontal: 24,
|
|
50
|
+
gap: 16,
|
|
51
|
+
},
|
|
52
|
+
optionalLabel: {
|
|
53
|
+
flexDirection: "row",
|
|
54
|
+
alignItems: "center",
|
|
55
|
+
gap: 8,
|
|
56
|
+
marginBottom: 8,
|
|
57
|
+
},
|
|
58
|
+
optional: {
|
|
59
|
+
fontSize: 12,
|
|
60
|
+
color: tokens.colors.textTertiary,
|
|
61
|
+
fontStyle: "italic",
|
|
62
|
+
},
|
|
63
|
+
label: {
|
|
64
|
+
fontSize: 14,
|
|
65
|
+
fontWeight: "600",
|
|
66
|
+
color: tokens.colors.textSecondary,
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
[tokens],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (!showName && !showDescription) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View style={styles.container}>
|
|
78
|
+
{showName && (
|
|
79
|
+
<AtomicInput
|
|
80
|
+
label={t(nameLabelKey)}
|
|
81
|
+
value={name}
|
|
82
|
+
onChangeText={onNameChange}
|
|
83
|
+
placeholder={t(namePlaceholderKey)}
|
|
84
|
+
maxLength={maxNameLength}
|
|
85
|
+
showCharacterCount
|
|
86
|
+
variant="outlined"
|
|
87
|
+
size="md"
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{showDescription && onDescriptionChange && (
|
|
92
|
+
<View>
|
|
93
|
+
<View style={styles.optionalLabel}>
|
|
94
|
+
<AtomicText style={styles.label}>{t(descriptionLabelKey)}</AtomicText>
|
|
95
|
+
<AtomicText style={styles.optional}>({t(optionalKey)})</AtomicText>
|
|
96
|
+
</View>
|
|
97
|
+
<AtomicInput
|
|
98
|
+
value={description}
|
|
99
|
+
onChangeText={onDescriptionChange}
|
|
100
|
+
placeholder={t(descriptionPlaceholderKey)}
|
|
101
|
+
multiline
|
|
102
|
+
numberOfLines={3}
|
|
103
|
+
maxLength={maxDescriptionLength}
|
|
104
|
+
showCharacterCount
|
|
105
|
+
variant="outlined"
|
|
106
|
+
inputStyle={{ minHeight: 100, textAlignVertical: "top" }}
|
|
107
|
+
/>
|
|
108
|
+
</View>
|
|
109
|
+
)}
|
|
110
|
+
</View>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhotoTips Component
|
|
3
|
+
* Displays photo upload tips in a grid layout
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { InfoGrid, type InfoGridItem } from "@umituz/react-native-design-system";
|
|
8
|
+
|
|
9
|
+
export interface PhotoTipConfig {
|
|
10
|
+
readonly icon: string;
|
|
11
|
+
readonly textKey: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PhotoTipsProps {
|
|
15
|
+
readonly t: (key: string) => string;
|
|
16
|
+
readonly titleKey?: string;
|
|
17
|
+
readonly headerIcon?: string;
|
|
18
|
+
readonly tips?: readonly PhotoTipConfig[];
|
|
19
|
+
readonly style?: object;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_TIPS: readonly PhotoTipConfig[] = [
|
|
23
|
+
{ icon: "sunny-outline", textKey: "photoUpload.tips.lighting" },
|
|
24
|
+
{ icon: "person-outline", textKey: "photoUpload.tips.faceForward" },
|
|
25
|
+
{ icon: "eye-outline", textKey: "photoUpload.tips.clearFace" },
|
|
26
|
+
{ icon: "diamond-outline", textKey: "photoUpload.tips.goodQuality" },
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
export const PhotoTips: React.FC<PhotoTipsProps> = ({
|
|
30
|
+
t,
|
|
31
|
+
titleKey = "photoUpload.tips.title",
|
|
32
|
+
headerIcon = "bulb",
|
|
33
|
+
tips = DEFAULT_TIPS,
|
|
34
|
+
style,
|
|
35
|
+
}) => {
|
|
36
|
+
const gridItems: InfoGridItem[] = useMemo(
|
|
37
|
+
() =>
|
|
38
|
+
tips.map((tip) => ({
|
|
39
|
+
icon: tip.icon,
|
|
40
|
+
text: t(tip.textKey),
|
|
41
|
+
})),
|
|
42
|
+
[tips, t],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<InfoGrid
|
|
47
|
+
title={t(titleKey)}
|
|
48
|
+
headerIcon={headerIcon}
|
|
49
|
+
items={gridItems}
|
|
50
|
+
style={style ?? { marginHorizontal: 24, marginBottom: 20 }}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePartnerStep Hook
|
|
3
|
+
* Manages partner photo upload step logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback } from "react";
|
|
7
|
+
import { Alert } from "react-native";
|
|
8
|
+
import * as ImagePicker from "expo-image-picker";
|
|
9
|
+
import * as FileSystem from "expo-file-system";
|
|
10
|
+
import type { UploadedImage } from "../../domain/types";
|
|
11
|
+
|
|
12
|
+
export interface UsePartnerStepConfig {
|
|
13
|
+
readonly maxFileSizeMB?: number;
|
|
14
|
+
readonly imageQuality?: number;
|
|
15
|
+
readonly allowsEditing?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UsePartnerStepTranslations {
|
|
19
|
+
readonly fileTooLarge: string;
|
|
20
|
+
readonly maxFileSize: string;
|
|
21
|
+
readonly error: string;
|
|
22
|
+
readonly uploadFailed: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UsePartnerStepOptions {
|
|
26
|
+
readonly initialName?: string;
|
|
27
|
+
readonly config?: UsePartnerStepConfig;
|
|
28
|
+
readonly translations: UsePartnerStepTranslations;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PartnerStepState {
|
|
32
|
+
image: UploadedImage | null;
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DEFAULT_CONFIG: UsePartnerStepConfig = {
|
|
38
|
+
maxFileSizeMB: 10,
|
|
39
|
+
imageQuality: 0.7,
|
|
40
|
+
allowsEditing: true,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const usePartnerStep = (options: UsePartnerStepOptions) => {
|
|
44
|
+
const { initialName = "", config = DEFAULT_CONFIG, translations } = options;
|
|
45
|
+
|
|
46
|
+
const [state, setState] = useState<PartnerStepState>({
|
|
47
|
+
image: null,
|
|
48
|
+
name: initialName,
|
|
49
|
+
description: "",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const setName = useCallback((name: string) => {
|
|
53
|
+
setState((prev) => ({ ...prev, name }));
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const setDescription = useCallback((description: string) => {
|
|
57
|
+
setState((prev) => ({ ...prev, description }));
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const handlePickImage = useCallback(async () => {
|
|
61
|
+
try {
|
|
62
|
+
const maxFileSizeMB = config.maxFileSizeMB ?? 10;
|
|
63
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
64
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
65
|
+
allowsEditing: config.allowsEditing ?? true,
|
|
66
|
+
quality: config.imageQuality ?? 0.7,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (result.canceled || !result.assets?.[0]) return;
|
|
70
|
+
|
|
71
|
+
const asset = result.assets[0];
|
|
72
|
+
|
|
73
|
+
if (asset.fileSize && asset.fileSize > maxFileSizeMB * 1024 * 1024) {
|
|
74
|
+
Alert.alert(
|
|
75
|
+
translations.fileTooLarge,
|
|
76
|
+
translations.maxFileSize.replace("{size}", String(maxFileSizeMB)),
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const base64 = await FileSystem.readAsStringAsync(asset.uri, {
|
|
82
|
+
encoding: "base64",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const uploadedImage: UploadedImage = {
|
|
86
|
+
uri: asset.uri,
|
|
87
|
+
previewUrl: asset.uri,
|
|
88
|
+
base64: `data:image/jpeg;base64,${base64}`,
|
|
89
|
+
width: asset.width,
|
|
90
|
+
height: asset.height,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
setState((prev) => ({
|
|
94
|
+
...prev,
|
|
95
|
+
image: uploadedImage,
|
|
96
|
+
}));
|
|
97
|
+
} catch {
|
|
98
|
+
Alert.alert(translations.error, translations.uploadFailed);
|
|
99
|
+
}
|
|
100
|
+
}, [config, translations]);
|
|
101
|
+
|
|
102
|
+
const canContinue = state.image !== null;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
...state,
|
|
106
|
+
setName,
|
|
107
|
+
setDescription,
|
|
108
|
+
handlePickImage,
|
|
109
|
+
canContinue,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type UsePartnerStepReturn = ReturnType<typeof usePartnerStep>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PartnerStepScreen
|
|
3
|
+
* Generic partner/photo upload screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
useAppDesignTokens,
|
|
10
|
+
ScreenLayout,
|
|
11
|
+
AtomicText,
|
|
12
|
+
AtomicIcon,
|
|
13
|
+
NavigationHeader,
|
|
14
|
+
type DesignTokens,
|
|
15
|
+
} from "@umituz/react-native-design-system";
|
|
16
|
+
import { PhotoUploadCard } from "../../../../presentation/components";
|
|
17
|
+
import { FaceDetectionToggle } from "../../../../domains/face-detection";
|
|
18
|
+
import { PhotoTips } from "../components/PhotoTips";
|
|
19
|
+
import { PartnerInfoInput } from "../components/PartnerInfoInput";
|
|
20
|
+
import { usePartnerStep } from "../hooks/usePartnerStep";
|
|
21
|
+
import type { UploadedImage } from "../../domain/types";
|
|
22
|
+
|
|
23
|
+
export interface PartnerStepScreenTranslations {
|
|
24
|
+
readonly title: string;
|
|
25
|
+
readonly subtitle: string;
|
|
26
|
+
readonly continue: string;
|
|
27
|
+
readonly tapToUpload: string;
|
|
28
|
+
readonly selectPhoto: string;
|
|
29
|
+
readonly change: string;
|
|
30
|
+
readonly analyzing: string;
|
|
31
|
+
readonly faceDetectionLabel?: string;
|
|
32
|
+
readonly fileTooLarge: string;
|
|
33
|
+
readonly maxFileSize: string;
|
|
34
|
+
readonly error: string;
|
|
35
|
+
readonly uploadFailed: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PartnerStepScreenConfig {
|
|
39
|
+
readonly showFaceDetection?: boolean;
|
|
40
|
+
readonly showNameInput?: boolean;
|
|
41
|
+
readonly showPhotoTips?: boolean;
|
|
42
|
+
readonly maxFileSizeMB?: number;
|
|
43
|
+
readonly maxNameLength?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PartnerStepScreenProps {
|
|
47
|
+
readonly translations: PartnerStepScreenTranslations;
|
|
48
|
+
readonly t: (key: string) => string;
|
|
49
|
+
readonly initialName?: string;
|
|
50
|
+
readonly config?: PartnerStepScreenConfig;
|
|
51
|
+
readonly faceDetectionEnabled?: boolean;
|
|
52
|
+
readonly onFaceDetectionToggle?: (enabled: boolean) => void;
|
|
53
|
+
readonly onBack: () => void;
|
|
54
|
+
readonly onContinue: (image: UploadedImage, name: string) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_CONFIG: PartnerStepScreenConfig = {
|
|
58
|
+
showFaceDetection: false,
|
|
59
|
+
showNameInput: false,
|
|
60
|
+
showPhotoTips: true,
|
|
61
|
+
maxFileSizeMB: 10,
|
|
62
|
+
maxNameLength: 30,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const PartnerStepScreen: React.FC<PartnerStepScreenProps> = ({
|
|
66
|
+
translations,
|
|
67
|
+
t,
|
|
68
|
+
initialName = "",
|
|
69
|
+
config = DEFAULT_CONFIG,
|
|
70
|
+
faceDetectionEnabled = false,
|
|
71
|
+
onFaceDetectionToggle,
|
|
72
|
+
onBack,
|
|
73
|
+
onContinue,
|
|
74
|
+
}) => {
|
|
75
|
+
const tokens = useAppDesignTokens();
|
|
76
|
+
|
|
77
|
+
const { image, name, setName, handlePickImage, canContinue } = usePartnerStep({
|
|
78
|
+
initialName,
|
|
79
|
+
config: { maxFileSizeMB: config.maxFileSizeMB },
|
|
80
|
+
translations: {
|
|
81
|
+
fileTooLarge: translations.fileTooLarge,
|
|
82
|
+
maxFileSize: translations.maxFileSize,
|
|
83
|
+
error: translations.error,
|
|
84
|
+
uploadFailed: translations.uploadFailed,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const handleContinuePress = () => {
|
|
89
|
+
if (!canContinue || !image) return;
|
|
90
|
+
onContinue(image, name);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
94
|
+
const showFaceDetection = config.showFaceDetection ?? false;
|
|
95
|
+
const showNameInput = config.showNameInput ?? false;
|
|
96
|
+
const showPhotoTips = config.showPhotoTips ?? true;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
|
|
100
|
+
<NavigationHeader
|
|
101
|
+
title={translations.title}
|
|
102
|
+
onBackPress={onBack}
|
|
103
|
+
rightElement={
|
|
104
|
+
<TouchableOpacity
|
|
105
|
+
onPress={handleContinuePress}
|
|
106
|
+
activeOpacity={0.7}
|
|
107
|
+
disabled={!canContinue || !image}
|
|
108
|
+
style={[
|
|
109
|
+
styles.continueButton,
|
|
110
|
+
{
|
|
111
|
+
backgroundColor: canContinue && image ? tokens.colors.primary : tokens.colors.surfaceVariant,
|
|
112
|
+
opacity: canContinue && image ? 1 : 0.5,
|
|
113
|
+
},
|
|
114
|
+
]}
|
|
115
|
+
>
|
|
116
|
+
<AtomicText
|
|
117
|
+
type="bodyMedium"
|
|
118
|
+
style={[
|
|
119
|
+
styles.continueText,
|
|
120
|
+
{ color: canContinue && image ? tokens.colors.onPrimary : tokens.colors.textSecondary },
|
|
121
|
+
]}
|
|
122
|
+
>
|
|
123
|
+
{translations.continue}
|
|
124
|
+
</AtomicText>
|
|
125
|
+
<AtomicIcon
|
|
126
|
+
name="arrow-forward"
|
|
127
|
+
size="sm"
|
|
128
|
+
color={canContinue && image ? "onPrimary" : "textSecondary"}
|
|
129
|
+
/>
|
|
130
|
+
</TouchableOpacity>
|
|
131
|
+
}
|
|
132
|
+
/>
|
|
133
|
+
<ScreenLayout
|
|
134
|
+
edges={["left", "right"]}
|
|
135
|
+
backgroundColor="transparent"
|
|
136
|
+
scrollable={true}
|
|
137
|
+
keyboardAvoiding={true}
|
|
138
|
+
contentContainerStyle={styles.scrollContent}
|
|
139
|
+
hideScrollIndicator={true}
|
|
140
|
+
>
|
|
141
|
+
<AtomicText style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
|
|
142
|
+
{translations.subtitle}
|
|
143
|
+
</AtomicText>
|
|
144
|
+
|
|
145
|
+
{showPhotoTips && <PhotoTips t={t} />}
|
|
146
|
+
|
|
147
|
+
{showFaceDetection && onFaceDetectionToggle && (
|
|
148
|
+
<FaceDetectionToggle
|
|
149
|
+
isEnabled={faceDetectionEnabled}
|
|
150
|
+
onToggle={onFaceDetectionToggle}
|
|
151
|
+
label={translations.faceDetectionLabel ?? ""}
|
|
152
|
+
hidden={true}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
<PhotoUploadCard
|
|
157
|
+
imageUri={image?.previewUrl || null}
|
|
158
|
+
onPress={handlePickImage}
|
|
159
|
+
isValidating={false}
|
|
160
|
+
isValid={null}
|
|
161
|
+
translations={{
|
|
162
|
+
tapToUpload: translations.tapToUpload,
|
|
163
|
+
selectPhoto: translations.selectPhoto,
|
|
164
|
+
change: translations.change,
|
|
165
|
+
analyzing: translations.analyzing,
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
{showNameInput && (
|
|
170
|
+
<PartnerInfoInput
|
|
171
|
+
t={t}
|
|
172
|
+
name={name}
|
|
173
|
+
onNameChange={setName}
|
|
174
|
+
showName={true}
|
|
175
|
+
maxNameLength={config.maxNameLength}
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
</ScreenLayout>
|
|
179
|
+
</View>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const createStyles = (tokens: DesignTokens) =>
|
|
184
|
+
StyleSheet.create({
|
|
185
|
+
container: {
|
|
186
|
+
flex: 1,
|
|
187
|
+
},
|
|
188
|
+
scrollContent: {
|
|
189
|
+
paddingBottom: 40,
|
|
190
|
+
},
|
|
191
|
+
subtitle: {
|
|
192
|
+
fontSize: 16,
|
|
193
|
+
textAlign: "center",
|
|
194
|
+
marginHorizontal: 24,
|
|
195
|
+
marginBottom: 24,
|
|
196
|
+
},
|
|
197
|
+
continueButton: {
|
|
198
|
+
flexDirection: "row",
|
|
199
|
+
alignItems: "center",
|
|
200
|
+
paddingHorizontal: tokens.spacing.md,
|
|
201
|
+
paddingVertical: tokens.spacing.xs,
|
|
202
|
+
borderRadius: tokens.borders.radius.full,
|
|
203
|
+
},
|
|
204
|
+
continueText: {
|
|
205
|
+
fontWeight: "800",
|
|
206
|
+
marginRight: 4,
|
|
207
|
+
},
|
|
208
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -152,6 +152,7 @@ export * from "./features/hd-touch-up";
|
|
|
152
152
|
export * from "./features/meme-generator";
|
|
153
153
|
export * from "./features/couple-future";
|
|
154
154
|
export * from "./features/love-message";
|
|
155
|
+
export * from "./features/partner-upload";
|
|
155
156
|
export * from "./infrastructure/orchestration";
|
|
156
157
|
|
|
157
158
|
// Result Preview Domain
|
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
* Uses ONLY configured app services - no alternatives
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import * as FileSystem from "expo-file-system";
|
|
7
7
|
import { getAuthService, getCreditService, getPaywallService, isAppServicesConfigured } from "../config/app-services.config";
|
|
8
8
|
|
|
9
|
+
async function readFileAsBase64(uri: string): Promise<string> {
|
|
10
|
+
const base64 = await FileSystem.readAsStringAsync(uri, { encoding: "base64" });
|
|
11
|
+
return `data:image/jpeg;base64,${base64}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
declare const __DEV__: boolean;
|
|
10
15
|
|
|
11
16
|
export type ImageSelector = () => Promise<string | null>;
|