feat: 权益页面

This commit is contained in:
jinyaqiu 2025-07-24 16:45:29 +08:00
parent a18c2f7592
commit 12fce0021a
20 changed files with 1235 additions and 18 deletions

View File

@ -328,6 +328,28 @@ export default function TabLayout() {
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 购买权益页面 */}
<Tabs.Screen
name="rights"
options={{
title: 'rights',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 设置页面 */}
<Tabs.Screen
name="setting"
options={{
title: 'setting',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
</Tabs >
);
}

View File

@ -15,7 +15,7 @@ import { CountData, UserInfoDetails } from '@/types/user';
import { useRouter } from 'expo-router';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FlatList, StyleSheet, View } from 'react-native';
import { FlatList, StyleSheet, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function OwnerPage() {
@ -86,13 +86,19 @@ export default function OwnerPage() {
<AlbumComponent setModalVisible={setModalVisible} />
{/* 资源数据 */}
<View style={styles.resourceContainer}>
<TouchableOpacity
onPress={() => router.push({
pathname: '/rights',
params: { credit: userInfoDetails?.remain_points }
})}
style={styles.resourceContainer}
>
<View style={{ gap: 4 }}>
<ThemedText style={styles.text}>{t("generalSetting.premium", { ns: "personal" })}</ThemedText>
<ThemedText style={styles.secondText}>{t("generalSetting.unlock", { ns: "personal" })}</ThemedText>
</View>
<ComeinSvg width={24} height={24} />
</View>
</TouchableOpacity>
{/* 分类 */}
<View style={{ marginHorizontal: -16, marginBottom: -16 }}>
<CarouselComponent data={userInfoDetails?.material_counter} />

View File

@ -2,10 +2,12 @@ import { fetchApi } from "@/lib/server-api-util";
import { Policy } from "@/types/personal-info";
import { useEffect, useState } from "react";
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
import RenderHtml from 'react-native-render-html';
import Markdown from 'react-native-markdown-display';
import { useSafeAreaInsets } from "react-native-safe-area-context";
const PrivacyPolicy = () => {
const [article, setArticle] = useState<Policy>({} as Policy);
const insets = useSafeAreaInsets();
useEffect(() => {
const loadArticle = async () => {
fetchApi<Policy>(`/system-config/policy/privacy_policy`).then((res: any) => {
@ -26,7 +28,7 @@ const PrivacyPolicy = () => {
}
return (
<View style={styles.centeredView}>
<View style={[styles.centeredView, { paddingTop: insets.top, marginBottom: insets.bottom }]}>
<View style={styles.modalView}>
<View style={styles.modalHeader}>
<Text style={{ opacity: 0 }}>Settings</Text>
@ -36,14 +38,9 @@ const PrivacyPolicy = () => {
</TouchableOpacity>
</View>
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
<RenderHtml
source={{ html: article.content }}
tagsStyles={{
p: { fontSize: 16, lineHeight: 24 },
strong: { fontWeight: 'bold' },
em: { fontStyle: 'italic' },
}}
/>
<Markdown>
{article.content}
</Markdown>
</ScrollView>
</View>
</View>
@ -86,6 +83,7 @@ const styles = StyleSheet.create({
},
modalContent: {
flex: 1,
paddingHorizontal: 8
},
modalText: {
fontSize: 16,

243
app/(tabs)/rights.tsx Normal file
View File

@ -0,0 +1,243 @@
import ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg';
import StarSvg from '@/assets/icons/svg/whiteStart.svg';
import PrivacyModal from '@/components/owner/qualification/privacy';
import Normal from '@/components/owner/rights/normal';
import Premium, { PayItem } from '@/components/owner/rights/premium';
import ProRights from '@/components/owner/rights/proRights';
import { maxDiscountProduct } from '@/components/owner/rights/utils';
import { ThemedText } from '@/components/ThemedText';
import { fetchApi } from '@/lib/server-api-util';
import { useLocalSearchParams, useRouter } from "expo-router";
import { useEffect, useState } from 'react';
import { Image, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function Rights() {
const insets = useSafeAreaInsets();
const router = useRouter();
// 获取路由参数
const { credit } = useLocalSearchParams<{
credit: string;
}>();
// 普通用户,会员
const [userType, setUserType] = useState<'normal' | 'premium'>('normal');
// 选择权益方式
const [payType, setPayType] = useState<string>('');
// 用户协议弹窗打开
const [showTerms, setShowTerms] = useState<boolean>(false);
// 调接口获取支付信息
const [premiumPay, setPremiumPay] = useState<PayItem[]>();
const [loading, setLoading] = useState<boolean>(false);
const getPAy = async () => {
setLoading(true);
const payInfo = await fetchApi<PayItem[]>(`/order/product-items?product_type=Membership`)
let bestValue = maxDiscountProduct(payInfo)
setPayType(bestValue?.product_code)
setPremiumPay([bestValue, ...payInfo?.filter((item) => item.product_code !== bestValue?.product_code)]);
setLoading(false);
}
useEffect(() => {
getPAy();
}, []);
return (
<View style={{ flex: 1 }}>
<ScrollView style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom + 80 }]}>
{/* 导航栏 */}
<View
style={styles.header}
>
<TouchableOpacity onPress={() => { router.push('/owner') }} style={{ padding: 16 }}>
<ReturnArrowSvg />
</TouchableOpacity>
<ThemedText style={styles.headerTitle}>
Subscription
</ThemedText>
<ThemedText className='opacity-0'>123</ThemedText>
</View>
{/* 会员卡 */}
<View style={styles.card}>
{userType === 'normal' ? (
<Image source={require('@/assets/images/png/owner/normal.png')} style={{ height: 150, objectFit: 'cover', width: '100%' }} />
) : (
<Image source={require('@/assets/images/png/owner/pro.png')} style={{ height: 150, objectFit: 'cover', width: '100%' }} />
)}
<View style={styles.cardContent}>
<View style={styles.cardinfo}>
<ThemedText style={styles.cardTitle}>
Purchase
</ThemedText>
<View style={styles.cardPoints}>
<StarSvg />
<ThemedText style={styles.cardPointsText}>{credit}</ThemedText>
</View>
</View>
</View>
</View>
{/* 会员信息 */}
<View style={styles.info}>
{/* 切换按钮 */}
<View style={styles.switchButton}>
<TouchableOpacity
onPress={() => { setUserType("normal") }}
style={[styles.switchButtonItem, { backgroundColor: userType === 'normal' ? "#FFB645" : "#fff", borderColor: userType === 'normal' ? "#FFB645" : "#E2793F" }]}
>
<ThemedText style={{ color: userType === 'normal' ? "#fff" : "#E2793F" }}>Free</ThemedText>
</TouchableOpacity>
<TouchableOpacity
onPress={() => { setUserType("premium") }}
style={[styles.switchButtonItem, { backgroundColor: userType === 'premium' ? "#E2793F" : "#fff", borderColor: userType === 'premium' ? "#E2793F" : "#E2793F" }]}
>
<ThemedText style={{ color: userType === 'premium' ? "#fff" : "#E2793F" }}>Pro</ThemedText>
</TouchableOpacity>
</View>
{/* 普通权益 */}
<Normal setUserType={setUserType} style={{ display: userType === 'normal' ? "flex" : "none" }} />
{/* 会员权益 */}
<Premium setPayType={setPayType} setShowTerms={setShowTerms} payType={payType} premiumPay={premiumPay} loading={loading} style={{ display: userType === 'normal' ? "none" : "flex" }} />
</View>
{/* 会员权益信息 */}
<View style={{ flex: 1, marginBottom: 80 }}>
<ProRights style={{ display: userType === 'normal' ? "none" : "flex" }} />
</View>
</ScrollView>
{/* 付费按钮 */}
<View style={{
padding: 16,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#eee',
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
display: userType === 'normal' ? "none" : "flex"
}}>
<TouchableOpacity
style={styles.goPay}
onPress={async () => {
setUserType('premium');
}}
activeOpacity={0.8}
>
<ThemedText style={{ color: '#fff', fontWeight: '700', fontSize: 14 }}>
Subscribe Yearly
</ThemedText>
</TouchableOpacity>
<TouchableOpacity
onPress={async () => {
setShowTerms(true);
}}
activeOpacity={0.8}
>
<ThemedText style={{ color: '#AC7E35', fontWeight: '400', fontSize: 11, textDecorationLine: 'underline', textAlign: 'center' }}>
Terms Privacy
</ThemedText>
</TouchableOpacity>
</View>
{/* 协议弹窗 */}
<PrivacyModal modalVisible={showTerms} setModalVisible={setShowTerms} type={"user"} />
</View>
);
}
const styles = StyleSheet.create({
goPay: {
backgroundColor: '#E2793F',
borderRadius: 24,
paddingVertical: 10,
display: "flex",
alignItems: "center",
width: "100%",
},
switchButton: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 16,
marginBottom: 16
},
switchButtonItem: {
width: "48%",
borderRadius: 24,
paddingVertical: 6,
display: "flex",
alignItems: "center",
borderWidth: 1
},
info: {
marginHorizontal: 16,
marginVertical: 16,
padding: 16,
borderRadius: 12,
backgroundColor: '#fff',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 5,
elevation: 5,
},
container: {
flex: 1,
backgroundColor: 'white',
},
header: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginVertical: 16,
},
headerTitle: {
fontSize: 20,
fontWeight: '700',
color: '#4C320C',
},
card: {
marginHorizontal: 16,
marginVertical: 16,
backgroundColor: '#FFB645',
borderRadius: 12,
},
cardContent: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
padding: 16,
justifyContent: 'space-between'
},
cardinfo: {
alignItems: 'flex-end',
},
cardTitle: {
fontSize: 12,
fontWeight: '700',
color: '#E2793F',
backgroundColor: '#fff',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 20,
textAlign: 'center',
marginBottom: 24
},
cardPoints: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 4
},
cardPointsText: {
fontSize: 32,
fontWeight: '700',
color: '#4C320C',
lineHeight: 32
}
});

495
app/(tabs)/setting.tsx Normal file
View File

@ -0,0 +1,495 @@
import DeleteSvg from '@/assets/icons/svg/delete.svg';
import LogoutSvg from '@/assets/icons/svg/logout.svg';
import ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg';
import RightArrowSvg from '@/assets/icons/svg/rightArrow.svg';
import DeleteModal from '@/components/owner/delete';
import LcensesModal from '@/components/owner/qualification/lcenses';
import PrivacyModal from '@/components/owner/qualification/privacy';
import CustomSwitch from '@/components/owner/switch';
import UserInfo from '@/components/owner/userInfo';
import { checkNotificationPermission, getLocationPermission, getPermissions, requestLocationPermission, requestMediaLibraryPermission, requestNotificationPermission, reverseGeocode } from '@/components/owner/utils';
import { ThemedText } from '@/components/ThemedText';
import { useAuth } from '@/contexts/auth-context';
import { fetchApi } from '@/lib/server-api-util';
import { Address, User } from '@/types/user';
import * as Location from 'expo-location';
import { useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Linking, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context";
const Setting = (props: { modalVisible: boolean, setModalVisible: (visible: boolean) => void, userInfo: User }) => {
const { modalVisible, setModalVisible, userInfo } = props;
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const [modalType, setModalType] = useState<'ai' | 'terms' | 'privacy' | 'user'>('ai');
// 协议弹窗
const [privacyModalVisible, setPrivacyModalVisible] = useState(false);
// 许可证弹窗
const [lcensesModalVisible, setLcensesModalVisible] = useState(false);
// 删除弹窗
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const { logout } = useAuth();
const router = useRouter();
// 打开设置
const openAppSettings = () => {
Linking.openSettings();
};
// 通知消息权限开关
const [notificationsEnabled, setNotificationsEnabled] = useState(false);
const toggleNotifications = async () => {
if (notificationsEnabled) {
// 引导去设置关闭权限
openAppSettings()
} else {
requestNotificationPermission()
.then((granted: boolean | ((prevState: boolean) => boolean)) => {
setNotificationsEnabled(granted);
});
setModalVisible(false);
}
};
// 相册权限
const [albumEnabled, setAlbumEnabled] = useState(false);
const toggleAlbum = async () => {
if (albumEnabled) {
// 引导去设置关闭权限
openAppSettings()
} else {
requestMediaLibraryPermission()
.then((granted: boolean | ((prevState: boolean) => boolean)) => {
setAlbumEnabled(granted);
});
setModalVisible(false);
}
}
// 位置权限
const [locationEnabled, setLocationEnabled] = useState(false);
// 位置权限更改
const toggleLocation = async () => {
if (locationEnabled) {
// 如果权限已开启,点击则引导用户去设置关闭
openAppSettings();
} else {
requestLocationPermission()
.then((granted: boolean | ((prevState: boolean) => boolean)) => {
setLocationEnabled(granted);
});
setModalVisible(false);
}
};
// 正在获取位置信息
const [isLoading, setIsLoading] = useState(false);
// 动画开启
const [isRefreshing, setIsRefreshing] = useState(false);
// 当前位置状态
const [currentLocation, setCurrentLocation] = useState<Address>({} as Address);
// 获取当前位置
const getCurrentLocation = async () => {
setIsLoading(true);
setIsRefreshing(true);
try {
// 1. 首先检查当前权限状态 -- 获取当前的位置权限
let currentStatus = await getLocationPermission();
console.log('当前权限状态:', currentStatus);
// 2. 如果没有权限,则跳过获取位置
if (!currentStatus) {
console.log('没有权限,跳过获取位置')
return;
// const newStatus = await requestLocationPermission();
// setLocationEnabled(newStatus);
// currentStatus = newStatus;
// if (!currentStatus) {
// // alert('需要位置权限才能继续');
// return;
// }
}
// 3. 确保位置服务已启用
const isEnabled = await Location.hasServicesEnabledAsync();
if (!isEnabled) {
alert('请先启用位置服务');
return;
}
console.log('位置服务已启用');
// 4. 获取当前位置
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High, // 使用高精度
timeInterval: 10000, // 可选:最大等待时间(毫秒)
});
console.log('位置:', location);
// 地理位置逆编码
const address = await reverseGeocode(location.coords.latitude, location.coords.longitude);
// 5. 更新位置状态
if (address) {
setCurrentLocation(address);
}
return location;
} catch (error: any) {
if (error.code === 'PERMISSION_DENIED' || error.code === 'PERMISSION_DENIED_ERROR') {
alert('位置权限被拒绝,请在设置中启用位置服务');
} else if (error.code === 'TIMEOUT') {
alert('获取位置超时,请检查网络和位置服务');
} else {
alert(`无法获取您的位置: ${error.message || '未知错误'}`);
}
throw error; // 重新抛出错误以便上层处理
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
};
// 退出登录
const handleLogout = () => {
fetchApi("/iam/logout", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
})
.then(async (res) => {
await logout();
setModalVisible(false);
router.replace('/login');
})
.catch(() => {
console.error("jwt has expired.");
});
};
// 检查是否有权限
useEffect(() => {
if (modalVisible) {
// 位置权限
getLocationPermission().then((res: boolean | ((prevState: boolean) => boolean)) => {
console.log('位置权限:', res);
setLocationEnabled(res);
})
// 媒体库权限
getPermissions().then((res: boolean | ((prevState: boolean) => boolean)) => {
console.log('媒体库权限:', res);
setAlbumEnabled(res);
})
// 通知权限
checkNotificationPermission().then((res: boolean | ((prevState: boolean) => boolean)) => {
console.log('通知权限:', res);
setNotificationsEnabled(res);
})
}
}, [modalVisible])
return (
<View style={{ flex: 1, paddingTop: insets.top, marginBottom: insets.bottom }}>
<Pressable
style={styles.centeredView}
>
<Pressable
style={styles.modalView}
onPress={(e) => e.stopPropagation()}>
<View style={styles.modalHeader}>
<TouchableOpacity onPress={() => { router.push('/owner') }}>
<ReturnArrowSvg />
</TouchableOpacity>
<Text style={styles.modalTitle}>{t('generalSetting.allTitle', { ns: 'personal' })}</Text>
<Text style={{ opacity: 0 }}>×</Text>
</View>
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
{/* 用户信息 */}
<UserInfo
userInfo={userInfo}
setModalVisible={setModalVisible}
modalVisible={modalVisible}
setCurrentLocation={setCurrentLocation}
getCurrentLocation={getCurrentLocation}
isLoading={isLoading}
isRefreshing={isRefreshing}
currentLocation={currentLocation}
/>
{/* 升级版本 */}
{/* <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.subscription', { ns: 'personal' })}</ThemedText>
<View style={styles.premium}>
<View>
<ThemedText style={styles.itemText}>{t('generalSetting.subscriptionTitle', { ns: 'personal' })}</ThemedText>
<ThemedText style={{ color: '#AC7E35', fontSize: 12 }}>{t('generalSetting.subscriptionText', { ns: 'personal' })}</ThemedText>
</View>
<TouchableOpacity
style={styles.upgradeButton}
onPress={async () => {
}}
>
<Text style={styles.upgradeButtonText}>
{t('generalSetting.upgrade', { ns: 'personal' })}
</Text>
</TouchableOpacity>
</View>
</View> */}
{/* 消息通知 */}
{/* <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
<View style={styles.premium}>
<View>
<ThemedText style={styles.itemText}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
</View>
<CustomSwitch
isEnabled={notificationsEnabled}
toggleSwitch={toggleNotifications}
/>
</View>
</View> */}
{/* 权限信息 */}
<View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('permission.permissionManagement', { ns: 'personal' })}</ThemedText>
<View style={styles.content}>
{/* 相册权限 */}
<View style={styles.item}>
<ThemedText style={styles.itemText}>{t('permission.galleryAccess', { ns: 'personal' })}</ThemedText>
<CustomSwitch
isEnabled={albumEnabled}
toggleSwitch={toggleAlbum}
/>
</View>
{/* 分割线 */}
<Divider />
{/* 位置权限 */}
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
</View>
<CustomSwitch
isEnabled={locationEnabled}
toggleSwitch={toggleLocation}
/>
</View>
<Divider />
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
</View>
<CustomSwitch
isEnabled={notificationsEnabled}
toggleSwitch={toggleNotifications}
/>
</View>
{/* 相册成片权限 */}
{/* <View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>Opus Permission</ThemedText>
</View>
<CustomSwitch
isEnabled={albumEnabled}
toggleSwitch={toggleAlbum}
/>
</View> */}
</View>
</View>
{/* 账号 */}
{/* <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>Account</ThemedText>
<View style={styles.content}>
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>Notifications</ThemedText>
</View>
<CustomSwitch
isEnabled={notificationsEnabled}
toggleSwitch={toggleNotifications}
/>
</View>
<Divider />
<View style={styles.item}>
<ThemedText style={styles.itemText}>Delete Account</ThemedText>
<DeleteSvg />
</View>
</View>
</View> */}
{/* 协议 */}
<View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('lcenses.title', { ns: 'personal' })}</ThemedText>
<View style={styles.content}>
<TouchableOpacity style={styles.item} onPress={() => { setModalType('privacy'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.privacyPolicy', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('terms'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.applyPermission', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('user'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.userAgreement', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('ai'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.aiPolicy', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setLcensesModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.qualification', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => Linking.openURL("https://beian.miit.gov.cn/")} >
<ThemedText style={styles.itemText}>{t('lcenses.ICP', { ns: 'personal' })}ICP备2023032876号-4</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
</View>
</View>
{/* 其他信息 */}
<View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.otherInformation', { ns: 'personal' })}</ThemedText>
<View style={styles.content}>
<TouchableOpacity style={styles.item} onPress={() => Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} >
<ThemedText style={styles.itemText}>{t('generalSetting.contactUs', { ns: 'personal' })}</ThemedText>
{/* <RightArrowSvg /> */}
</TouchableOpacity>
<Divider />
<View style={styles.item}>
<ThemedText style={styles.itemText}>{t('generalSetting.version', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText}>{"0.5.0"}</ThemedText>
</View>
</View>
</View>
{/* 退出 */}
<TouchableOpacity style={[styles.premium, { marginVertical: 8 }]} onPress={handleLogout}>
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.logout', { ns: 'personal' })}</ThemedText>
<LogoutSvg />
</TouchableOpacity>
{/* 注销账号 */}
<TouchableOpacity style={[styles.premium, { marginVertical: 8 }]} onPress={() => setDeleteModalVisible(true)}>
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.deleteAccount', { ns: 'personal' })}</ThemedText>
<DeleteSvg />
</TouchableOpacity>
</ScrollView>
</Pressable>
</Pressable>
<PrivacyModal modalVisible={privacyModalVisible} setModalVisible={setPrivacyModalVisible} type={modalType} />
<LcensesModal modalVisible={lcensesModalVisible} setModalVisible={setLcensesModalVisible} />
<DeleteModal modalVisible={deleteModalVisible} setModalVisible={setDeleteModalVisible} setSettingModalVisible={setModalVisible} />
</View>
);
};
const styles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(0,0,0,0.5)',
},
modalView: {
width: '100%',
height: '100%',
backgroundColor: 'white',
paddingHorizontal: 16,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#4C320C',
},
closeButton: {
fontSize: 28,
color: '#4C320C',
padding: 10,
},
modalContent: {
flex: 1,
},
modalText: {
fontSize: 16,
color: '#4C320C',
},
premium: {
backgroundColor: "#FAF9F6",
padding: 16,
borderRadius: 24,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
content: {
flex: 1,
flexDirection: 'column',
gap: 4,
backgroundColor: '#FAF9F6',
borderRadius: 24,
paddingVertical: 8
},
item: {
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
itemText: {
fontSize: 14,
fontWeight: '600',
color: '#4C320C',
},
upgradeButton: {
backgroundColor: '#E2793F',
borderRadius: 20,
paddingHorizontal: 16,
paddingVertical: 8,
},
upgradeButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: "600"
},
switchContainer: {
width: 50,
height: 30,
borderRadius: 15,
justifyContent: 'center',
paddingHorizontal: 2,
},
switchOn: {
backgroundColor: '#E2793F',
alignItems: 'flex-end',
},
switchOff: {
backgroundColor: '#E5E5E5',
alignItems: 'flex-start',
},
switchCircle: {
width: 26,
height: 26,
borderRadius: 13,
},
switchCircleOn: {
backgroundColor: 'white',
},
switchCircleOff: {
backgroundColor: '#A5A5A5',
},
});
const Divider = () => {
return (
<View className='w-full h-[1px] bg-[#B5977F]'></View>
)
}
export default Setting;

View File

@ -0,0 +1,3 @@
<svg width="12" height="15" viewBox="0 0 12 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 0C6 0 6.33105 5.36245 7.62054 6.97432C8.91004 8.58619 12 9 12 9C12 9 8.91004 9.41381 7.62054 11.0257C6.33105 12.6375 6 15 6 15C6 15 5.66895 12.6375 4.37946 11.0257C3.08996 9.41381 0 9 0 9C0 9 3.08996 8.58619 4.37946 6.97432C5.66895 5.36245 6 0 6 0Z" fill="#4C320C"/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

3
assets/icons/svg/get.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 4.00001L4.33357 7L11 1" stroke="#FFB645" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 220 B

View File

@ -0,0 +1,62 @@
<svg width="365" height="140" viewBox="0 0 365 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_3223_1003" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="365" height="140">
<rect width="365" height="140" rx="26" fill="#FFB645"/>
</mask>
<g mask="url(#mask0_3223_1003)">
<g filter="url(#filter0_i_3223_1003)">
<path d="M61.4295 45.6599C60.8472 41.3476 66.5965 39.3378 68.8277 43.0737L71.3125 47.2342C73.0132 50.082 77.2417 49.7415 78.4648 46.6582L88.4108 21.5851C89.7463 18.2183 94.5115 18.2183 95.8471 21.5851L105.793 46.6582C107.016 49.7415 111.245 50.082 112.945 47.2342L115.43 43.0737C117.661 39.3378 123.411 41.3476 122.828 45.6599L120.386 63.7507C120.118 65.735 118.424 67.2155 116.422 67.2155H67.8363C65.8341 67.2155 64.1402 65.735 63.8723 63.7507L61.4295 45.6599Z" fill="#FFF8DE"/>
</g>
<circle cx="59.1272" cy="28.4803" r="9.12722" fill="white" stroke="#FFB645" stroke-width="4"/>
<circle cx="92.2844" cy="16.1272" r="9.12722" fill="#E2793F" stroke="#FFB645" stroke-width="4"/>
<circle cx="125.441" cy="28.4803" r="9.12722" fill="white" stroke="#FFB645" stroke-width="4"/>
<path d="M49.5755 61.4144C50.4475 58.5036 57.1744 61.1692 60.4288 62.8658L52.5625 66.8182C50.6468 66.7604 48.7034 64.3251 49.5755 61.4144Z" fill="#FFDBA3"/>
<path d="M51.0981 61.8437C51.5796 59.3845 56.005 61.8494 58.1575 63.3893L54.2174 65.2282C52.977 65.1247 50.6165 64.3029 51.0981 61.8437Z" fill="#AC7E35"/>
<path d="M133.568 60.0186C132.303 57.2559 126.008 60.8237 123.019 62.9529L131.355 65.7827C133.244 65.4612 134.833 62.7813 133.568 60.0186Z" fill="#FFDBA3"/>
<path d="M132.118 60.6536C131.302 58.2843 127.259 61.3359 125.339 63.1579L129.495 64.436C130.709 64.1624 132.934 63.0229 132.118 60.6536Z" fill="#AC7E35"/>
<path d="M27.1969 92.9743C56.4427 42.319 129.557 42.3191 158.803 92.9744L178.751 127.526C207.997 178.181 171.44 241.5 112.948 241.5H73.0518C14.5601 241.5 -21.9971 178.181 7.24869 127.526L27.1969 92.9743Z" fill="#FFD18A"/>
<rect x="88.5132" y="89.6838" width="2.99145" height="4.18803" rx="1.49573" transform="rotate(-180 88.5132 89.6838)" fill="#4C320C"/>
<rect x="99.2822" y="89.6838" width="2.99145" height="4.18803" rx="1.49573" transform="rotate(-180 99.2822 89.6838)" fill="#4C320C"/>
<path d="M50.1157 120.318C70.7994 90.2494 115.202 90.2493 135.886 120.318L159.443 154.565C183.198 189.1 158.474 236.115 116.558 236.115H69.4435C27.5268 236.115 2.8027 189.1 26.5585 154.565L50.1157 120.318Z" fill="#FFF8DE"/>
<g filter="url(#filter1_i_3223_1003)">
<ellipse cx="134.581" cy="115.111" rx="49.0598" ry="35" fill="#FFF8DE"/>
</g>
<g filter="url(#filter2_i_3223_1003)">
<ellipse cx="51.1196" cy="115.111" rx="48.7607" ry="35" fill="#FFF8DE"/>
</g>
<ellipse cx="92.7008" cy="97.7608" rx="3.58974" ry="2.69231" transform="rotate(180 92.7008 97.7608)" fill="#FFB8B9"/>
<ellipse cx="8.5474" cy="3.40976" rx="8.5474" ry="3.40976" transform="matrix(1 0 0 -1 108.647 142)" fill="#FFD38D"/>
<ellipse cx="65.5473" cy="138.59" rx="8.5474" ry="3.40976" transform="rotate(-180 65.5473 138.59)" fill="#FFD38D"/>
<path d="M91.9591 101.026C92.2223 100.57 92.8803 100.57 93.1434 101.026L93.7356 102.051C93.9988 102.507 93.6698 103.077 93.1434 103.077H91.9591C91.4328 103.077 91.1038 102.507 91.367 102.051L91.9591 101.026Z" fill="#4C320C"/>
</g>
<defs>
<filter id="filter0_i_3223_1003" x="61.3882" y="19.0601" width="61.4814" height="48.1555" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="21"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.886275 0 0 0 0 0.47451 0 0 0 0 0.247059 0 0 0 1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3223_1003"/>
</filter>
<filter id="filter1_i_3223_1003" x="80.1369" y="80.1111" width="103.504" height="71.7949" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="-5.38462" dy="1.79487"/>
<feGaussianBlur stdDeviation="4.9359"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713726 0 0 0 0 0.270588 0 0 0 1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3223_1003"/>
</filter>
<filter id="filter2_i_3223_1003" x="2.35889" y="80.1111" width="103.504" height="70" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="5.98291"/>
<feGaussianBlur stdDeviation="3.2906"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713974 0 0 0 0 0.272498 0 0 0 1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3223_1003"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,47 @@
<svg width="365" height="140" viewBox="0 0 365 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_3223_1545" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="365" height="140">
<rect width="365" height="140" rx="26" fill="#FFB645"/>
</mask>
<g mask="url(#mask0_3223_1545)">
<path d="M49.5755 61.4144C50.4475 58.5036 57.1744 61.1692 60.4288 62.8658L52.5625 66.8182C50.6468 66.7604 48.7034 64.3251 49.5755 61.4144Z" fill="#FFDBA3"/>
<path d="M51.0981 61.8437C51.5796 59.3845 56.005 61.8494 58.1575 63.3893L54.2174 65.2282C52.977 65.1247 50.6165 64.3029 51.0981 61.8437Z" fill="#AC7E35"/>
<path d="M133.568 60.0186C132.303 57.2559 126.008 60.8237 123.019 62.9529L131.355 65.7827C133.244 65.4612 134.833 62.7813 133.568 60.0186Z" fill="#FFDBA3"/>
<path d="M132.118 60.6536C131.302 58.2843 127.259 61.3359 125.339 63.1579L129.495 64.436C130.709 64.1624 132.934 63.0229 132.118 60.6536Z" fill="#AC7E35"/>
<path d="M27.1969 92.9743C56.4427 42.319 129.557 42.3191 158.803 92.9744L178.751 127.526C207.997 178.181 171.44 241.5 112.948 241.5H73.0518C14.5601 241.5 -21.9971 178.181 7.24869 127.526L27.1969 92.9743Z" fill="#FFD18A"/>
<rect x="88.5132" y="89.6838" width="2.99145" height="4.18803" rx="1.49573" transform="rotate(-180 88.5132 89.6838)" fill="#4C320C"/>
<rect x="99.2822" y="89.6838" width="2.99145" height="4.18803" rx="1.49573" transform="rotate(-180 99.2822 89.6838)" fill="#4C320C"/>
<path d="M50.1157 120.318C70.7994 90.2494 115.202 90.2493 135.886 120.318L159.443 154.565C183.198 189.1 158.474 236.115 116.558 236.115H69.4435C27.5268 236.115 2.8027 189.1 26.5585 154.565L50.1157 120.318Z" fill="#FFF8DE"/>
<g filter="url(#filter0_i_3223_1545)">
<ellipse cx="134.581" cy="115.111" rx="49.0598" ry="35" fill="#FFF8DE"/>
</g>
<g filter="url(#filter1_i_3223_1545)">
<ellipse cx="51.1196" cy="115.111" rx="48.7607" ry="35" fill="#FFF8DE"/>
</g>
<ellipse cx="92.7008" cy="97.7608" rx="3.58974" ry="2.69231" transform="rotate(180 92.7008 97.7608)" fill="#FFB8B9"/>
<ellipse cx="8.5474" cy="3.40976" rx="8.5474" ry="3.40976" transform="matrix(1 0 0 -1 108.647 142)" fill="#FFD38D"/>
<ellipse cx="65.5473" cy="138.59" rx="8.5474" ry="3.40976" transform="rotate(-180 65.5473 138.59)" fill="#FFD38D"/>
<path d="M91.9591 101.026C92.2223 100.57 92.8803 100.57 93.1434 101.026L93.7356 102.051C93.9988 102.507 93.6698 103.077 93.1434 103.077H91.9591C91.4328 103.077 91.1038 102.507 91.367 102.051L91.9591 101.026Z" fill="#4C320C"/>
</g>
<defs>
<filter id="filter0_i_3223_1545" x="80.1369" y="80.1111" width="103.504" height="71.7949" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="-5.38462" dy="1.79487"/>
<feGaussianBlur stdDeviation="4.9359"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713726 0 0 0 0 0.270588 0 0 0 1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3223_1545"/>
</filter>
<filter id="filter1_i_3223_1545" x="2.35889" y="80.1111" width="103.504" height="70" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="5.98291"/>
<feGaussianBlur stdDeviation="3.2906"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713974 0 0 0 0 0.272498 0 0 0 1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3223_1545"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,3 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 0C13 0 13.7173 9.29492 16.5112 12.0888C19.3051 14.8827 26 15.6 26 15.6C26 15.6 19.3051 16.3173 16.5112 19.1112C13.7173 21.9051 13 26 13 26C13 26 12.2827 21.9051 9.48882 19.1112C6.69492 16.3173 0 15.6 0 15.6C0 15.6 6.69492 14.8827 9.48882 12.0888C12.2827 9.29492 13 0 13 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 B

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -11,10 +11,9 @@ interface CategoryProps {
const AlbumComponent = ({ setModalVisible, style }: CategoryProps) => {
const { t } = useTranslation();
const router = useRouter();
return (
<View style={[styles.container, style]}>
<TouchableOpacity style={{ flex: 3 }} onPress={() => { router.push("/download") }}>
<TouchableOpacity style={{ flex: 3 }}>
<ThemedText style={styles.text}>{t('generalSetting.album', { ns: 'personal' })}</ThemedText>
</TouchableOpacity>
<TouchableOpacity style={{ flex: 3 }}>
@ -22,7 +21,8 @@ const AlbumComponent = ({ setModalVisible, style }: CategoryProps) => {
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setModalVisible(true);
// setModalVisible(true);
router.push('/setting');
}}
activeOpacity={0.7}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}

View File

@ -0,0 +1,72 @@
import GetSvg from "@/assets/icons/svg/get.svg";
import { ThemedText } from "@/components/ThemedText";
import { StyleProp, StyleSheet, TouchableOpacity, View, ViewStyle } from "react-native";
interface Props {
setUserType: (type: 'normal' | 'premium') => void;
style?: StyleProp<ViewStyle>;
}
const Normal = (props: Props) => {
const { setUserType } = props;
return (
<View style={[styles.normalInfo, props.style]}>
<View style={styles.normalItem}>
<View style={styles.normalItemContent}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<GetSvg style={{ marginTop: 8 }} />
<ThemedText style={{ fontSize: 12, fontWeight: '500', color: "#4C320C" }}>Enjoy 100 Bonus Credits Every Month</ThemedText>
</View>
<ThemedText style={{ fontSize: 10, color: "#AC7E35", marginLeft: 20 }}>Generate more memory pictures & videos and explore your past.</ThemedText>
</View>
<View style={styles.normalItemContent}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<GetSvg style={{ marginTop: 8 }} />
<ThemedText style={{ fontSize: 12, fontWeight: '500', color: "#4C320C" }}>10GB of Cloud Storage</ThemedText>
</View>
<ThemedText style={{ fontSize: 10, color: "#AC7E35", marginLeft: 20 }}>Safely store your cherished photos, videos, and generated memories.</ThemedText>
</View>
</View>
<TouchableOpacity
style={styles.goPro}
onPress={async () => {
setUserType('premium');
}}
activeOpacity={0.8}
>
<ThemedText style={{ color: '#fff', fontWeight: '700', fontSize: 14 }}>
Go Premium
</ThemedText>
</TouchableOpacity>
</View>
);
}
export default Normal;
const styles = StyleSheet.create({
goPro: {
backgroundColor: '#E2793F',
borderRadius: 24,
paddingVertical: 6,
display: "flex",
alignItems: "center",
width: "100%",
},
normalInfo: {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 16
},
normalItem: {
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: 16
},
normalItemContent: {
display: "flex",
flexDirection: "column",
}
});

View File

@ -0,0 +1,125 @@
import BlackStarSvg from '@/assets/icons/svg/blackStar.svg';
import { ThemedText } from "@/components/ThemedText";
import { ScrollView, StyleProp, StyleSheet, TouchableOpacity, View, ViewStyle } from "react-native";
import { maxDiscountProduct } from './utils';
interface Props {
style?: StyleProp<ViewStyle>;
payType: string;
setPayType: (type: string) => void;
premiumPay: any;
loading: boolean;
setShowTerms: (visible: boolean) => void;
}
export interface PayItem {
id: number;
product_id: number;
product_type: string;
product_code: string;
product_name: string;
unit_price: {
amount: string;
currency: string;
},
discount_amount: {
amount: string;
currency: string;
}
}
const Premium = (props: Props) => {
const { style, payType, setPayType, premiumPay, loading, setShowTerms } = props;
const bestValue = maxDiscountProduct(premiumPay)?.product_code
return (
<View style={[styles.proInfo, style]}>
<ScrollView
contentContainerStyle={{ gap: 16 }}
showsVerticalScrollIndicator={false}
horizontal={true}
showsHorizontalScrollIndicator={false}
>
{loading
?
<ThemedText style={{ fontSize: 12, color: "#4C320C", fontWeight: "700", width: "100%", textAlign: "center" }}>
Loading...
</ThemedText>
:
premiumPay?.map((item: PayItem) => {
return <TouchableOpacity
onPress={async () => {
setPayType(item?.product_code);
}}
style={[styles.yearly, { borderColor: payType === item?.product_code ? '#FFB645' : '#E1E1E1', opacity: payType === item?.product_code ? 1 : 0.5 }]}
activeOpacity={0.8}
>
<View style={[styles.title, { opacity: item?.product_code === bestValue ? 1 : 0 }]}>
<BlackStarSvg />
<ThemedText style={[styles.titleText, { fontSize: 14 }]}>
Best Value
</ThemedText>
<BlackStarSvg />
</View>
<ThemedText style={[styles.titleText, { fontSize: 16 }]}>
{item.product_code?.split('_')[item.product_code?.split('_')?.length - 1]}
</ThemedText>
<ThemedText style={[styles.titleText, { fontSize: 32, lineHeight: 32 }]}>
$ {item.unit_price.amount}
</ThemedText>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<ThemedText style={[styles.titleText, { fontSize: 12, color: "#AC7E35", textDecorationLine: 'line-through' }]}>
$ {item.discount_amount.amount}
</ThemedText>
</View>
</TouchableOpacity>
})
}
</ScrollView>
<View style={{ flexDirection: 'row', gap: 8, marginLeft: 4 }}>
<ThemedText style={{ color: '#AC7E35', fontSize: 10 }}>
Cancel anytime before renewal. Learn more
</ThemedText>
<ThemedText style={{ color: '#E2793F', fontSize: 10, textDecorationLine: 'underline' }} onPress={() => setShowTerms(true)}>
Terms & Conditions
</ThemedText>
</View>
</View>
);
}
export default Premium;
const styles = StyleSheet.create({
proInfo: {
borderRadius: 24,
display: "flex",
width: "100%",
},
yearly: {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 16,
borderColor: "#FFB645",
borderWidth: 2,
borderRadius: 24,
width: 200,
paddingBottom: 16
},
title: {
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 8,
backgroundColor: "#FFB645",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 4,
width: "100%"
},
titleText: {
color: '#4C320C',
fontWeight: '700'
}
});

View File

@ -0,0 +1,47 @@
import GetSvg from "@/assets/icons/svg/get.svg";
import { ThemedText } from "@/components/ThemedText";
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
const ProRights = (props: { style?: StyleProp<ViewStyle> }) => {
const { style } = props;
return (
<View style={[styles.proRights, style]}>
<ThemedText style={{ fontSize: 12, color: "#4C320C", fontWeight: "700", width: "100%", textAlign: "center" }}>
Enjoy MemoWake Pro Benefits
</ThemedText>
<View style={{ display: "flex", flexDirection: "column", }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<GetSvg style={{ marginTop: 8 }} />
<ThemedText style={{ fontSize: 12, fontWeight: '500', color: "#4C320C" }}>no advertisement</ThemedText>
</View>
<ThemedText style={{ fontSize: 10, color: "#AC7E35", marginLeft: 20 }}>There are no advertisements, so you can use the product with peace of mind.</ThemedText>
</View>
<View style={styles.itemContent}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<GetSvg style={{ marginTop: 8 }} />
<ThemedText style={{ fontSize: 12, fontWeight: '500', color: "#4C320C" }}>Enjoy 1000 Bonus Credits Every Month</ThemedText>
</View>
<ThemedText style={{ fontSize: 10, color: "#AC7E35", marginLeft: 20 }}>Generate more memory pictures & videos and explore your past.</ThemedText>
</View>
<View style={styles.itemContent}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<GetSvg style={{ marginTop: 8 }} />
<ThemedText style={{ fontSize: 12, fontWeight: '500', color: "#4C320C" }}>100GB of Cloud Storage</ThemedText>
</View>
<ThemedText style={{ fontSize: 10, color: "#AC7E35", marginLeft: 20 }}>Safely store your cherished photos, videos, and generated memories.</ThemedText>
</View>
</View>
);
}
const styles = StyleSheet.create({
proRights: {
padding: 16,
gap: 8
},
itemContent: {
display: "flex",
flexDirection: "column",
}
})
export default ProRights;

View File

@ -0,0 +1,12 @@
import { PayItem } from "./premium";
// 使用 reduce 方法获取 discount_amount 的 amount 值最大的对象
export const maxDiscountProduct = (products: PayItem[]) => {
return products?.reduce((max, current) => {
// 将 amount 转换为数字进行比较
const maxAmount = parseFloat(max.discount_amount.amount);
const currentAmount = parseFloat(current.discount_amount.amount);
return currentAmount > maxAmount ? current : max;
});
}

78
package-lock.json generated
View File

@ -57,6 +57,7 @@
"react-native": "0.79.5",
"react-native-gesture-handler": "~2.24.0",
"react-native-linear-gradient": "^2.8.3",
"react-native-markdown-display": "^7.0.2",
"react-native-modal": "^14.0.0-rc.1",
"react-native-picker-select": "^9.3.1",
"react-native-progress": "^5.0.1",
@ -12471,6 +12472,15 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/linkify-it": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
"license": "MIT",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -12704,6 +12714,37 @@
"tmpl": "1.0.5"
}
},
"node_modules/markdown-it": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"entities": "~2.0.0",
"linkify-it": "^2.0.0",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/markdown-it/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/markdown-it/node_modules/entities": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
"license": "BSD-2-Clause"
},
"node_modules/marky": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
@ -12725,6 +12766,12 @@
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
"license": "MIT"
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -14964,6 +15011,15 @@
"react-native": "*"
}
},
"node_modules/react-native-fit-image": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz",
"integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==",
"license": "Beerware",
"dependencies": {
"prop-types": "^15.5.10"
}
},
"node_modules/react-native-gesture-handler": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.24.0.tgz",
@ -14999,6 +15055,22 @@
"react-native": "*"
}
},
"node_modules/react-native-markdown-display": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz",
"integrity": "sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ==",
"license": "MIT",
"dependencies": {
"css-to-react-native": "^3.0.0",
"markdown-it": "^10.0.0",
"prop-types": "^15.7.2",
"react-native-fit-image": "^1.5.5"
},
"peerDependencies": {
"react": ">=16.2.0",
"react-native": ">=0.50.4"
}
},
"node_modules/react-native-modal": {
"version": "14.0.0-rc.1",
"resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-14.0.0-rc.1.tgz",
@ -17510,6 +17582,12 @@
"node": "*"
}
},
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"license": "MIT"
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",

View File

@ -63,6 +63,7 @@
"react-native": "0.79.5",
"react-native-gesture-handler": "~2.24.0",
"react-native-linear-gradient": "^2.8.3",
"react-native-markdown-display": "^7.0.2",
"react-native-modal": "^14.0.0-rc.1",
"react-native-picker-select": "^9.3.1",
"react-native-progress": "^5.0.1",
@ -75,10 +76,10 @@
"react-native-svg": "^15.11.2",
"react-native-toast-message": "^2.3.0",
"react-native-uuid": "^2.0.3",
"react-native-view-shot": "4.0.3",
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5",
"react-redux": "^9.2.0",
"react-native-view-shot": "4.0.3"
"react-redux": "^9.2.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",