diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index aaed35a..1482286 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -328,6 +328,28 @@ export default function TabLayout() { tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 }} /> + + {/* 购买权益页面 */} + null, // 隐藏底部标签栏 + headerShown: false, // 隐藏导航栏 + tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 + }} + /> + + {/* 设置页面 */} + null, // 隐藏底部标签栏 + headerShown: false, // 隐藏导航栏 + tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 + }} + /> ); } diff --git a/app/(tabs)/owner.tsx b/app/(tabs)/owner.tsx index 9b6a75f..b1d34cc 100644 --- a/app/(tabs)/owner.tsx +++ b/app/(tabs)/owner.tsx @@ -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() { {/* 资源数据 */} - + router.push({ + pathname: '/rights', + params: { credit: userInfoDetails?.remain_points } + })} + style={styles.resourceContainer} + > {t("generalSetting.premium", { ns: "personal" })} {t("generalSetting.unlock", { ns: "personal" })} - + {/* 分类 */} diff --git a/app/(tabs)/privacy-policy.tsx b/app/(tabs)/privacy-policy.tsx index ac573ed..b55f37c 100644 --- a/app/(tabs)/privacy-policy.tsx +++ b/app/(tabs)/privacy-policy.tsx @@ -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({} as Policy); + const insets = useSafeAreaInsets(); useEffect(() => { const loadArticle = async () => { fetchApi(`/system-config/policy/privacy_policy`).then((res: any) => { @@ -26,7 +28,7 @@ const PrivacyPolicy = () => { } return ( - + Settings @@ -36,14 +38,9 @@ const PrivacyPolicy = () => { - + + {article.content} + @@ -86,6 +83,7 @@ const styles = StyleSheet.create({ }, modalContent: { flex: 1, + paddingHorizontal: 8 }, modalText: { fontSize: 16, diff --git a/app/(tabs)/rights.tsx b/app/(tabs)/rights.tsx new file mode 100644 index 0000000..f175b94 --- /dev/null +++ b/app/(tabs)/rights.tsx @@ -0,0 +1,245 @@ +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 { useTranslation } from 'react-i18next'; +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 { t } = useTranslation(); + // 获取路由参数 + const { credit } = useLocalSearchParams<{ + credit: string; + }>(); + // 普通用户,会员 + const [userType, setUserType] = useState<'normal' | 'premium'>('normal'); + // 选择权益方式 + const [payType, setPayType] = useState(''); + + // 用户协议弹窗打开 + const [showTerms, setShowTerms] = useState(false); + + // 调接口获取支付信息 + const [premiumPay, setPremiumPay] = useState(); + const [loading, setLoading] = useState(false); + const getPAy = async () => { + setLoading(true); + const payInfo = await fetchApi(`/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 ( + + + {/* 导航栏 */} + + { router.push('/owner') }} style={{ padding: 16 }}> + + + + {t('rights.title', { ns: 'personal' })} + + 123 + + {/* 会员卡 */} + + {userType === 'normal' ? ( + + ) : ( + + )} + + + + {t('rights.purchase', { ns: 'personal' })} + + + + {credit} + + + + + + {/* 会员信息 */} + + {/* 切换按钮 */} + + { setUserType("normal") }} + style={[styles.switchButtonItem, { backgroundColor: userType === 'normal' ? "#FFB645" : "#fff", borderColor: userType === 'normal' ? "#FFB645" : "#E2793F" }]} + > + {t('rights.free', { ns: 'personal' })} + + { setUserType("premium") }} + style={[styles.switchButtonItem, { backgroundColor: userType === 'premium' ? "#E2793F" : "#fff", borderColor: userType === 'premium' ? "#E2793F" : "#E2793F" }]} + > + {t('rights.premium', { ns: 'personal' })} + + + {/* 普通权益 */} + + {/* 会员权益 */} + + + {/* 会员权益信息 */} + + + + + {/* 付费按钮 */} + + { + setUserType('premium'); + }} + activeOpacity={0.8} + > + + {t('rights.subscribe', { ns: 'personal' })} + + + { + setShowTerms(true); + }} + activeOpacity={0.8} + > + + {t('rights.terms', { ns: 'personal' })} + + + + {/* 协议弹窗 */} + + + ); +} + +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 + } +}); diff --git a/app/(tabs)/setting.tsx b/app/(tabs)/setting.tsx new file mode 100644 index 0000000..5c679fc --- /dev/null +++ b/app/(tabs)/setting.tsx @@ -0,0 +1,502 @@ +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 * as SecureStore from 'expo-secure-store'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Linking, Platform, 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(); + // 判断当前语言环境 + let language = ""; + const getLanguage = async () => { + if (Platform.OS === 'web') { + language = localStorage.getItem('i18nextLng') || ""; + } else { + language = await SecureStore.getItemAsync('i18nextLng') || ""; + } + } + + 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
({} as Address); + + // 获取当前位置 + const getCurrentLocation = async () => { + setIsLoading(true); + setIsRefreshing(true); + + try { + // 1. 首先检查当前权限状态 -- 获取当前的位置权限 + let currentStatus = await getLocationPermission(); + + // 2. 如果没有权限,则跳过获取位置 + if (!currentStatus) { + return; + // const newStatus = await requestLocationPermission(); + // setLocationEnabled(newStatus); + // currentStatus = newStatus; + + // if (!currentStatus) { + // // alert('需要位置权限才能继续'); + // return; + // } + } + + // 3. 确保位置服务已启用 + const isEnabled = await Location.hasServicesEnabledAsync(); + if (!isEnabled) { + alert(t('permission.locationPermissionRequired', { ns: 'common' })); + return; + } + // 4. 获取当前位置 + const location = await Location.getCurrentPositionAsync({ + accuracy: Location.Accuracy.High, // 使用高精度 + timeInterval: 10000, // 可选:最大等待时间(毫秒) + }); + + // 地理位置逆编码 + const address = await reverseGeocode(location.coords.latitude, location.coords.longitude); + // 5. 更新位置状态 + if (address) { + setCurrentLocation(address); + } + + return location; + } catch (error: any) { + if (error.code === 'TIMEOUT') { + alert(t('permission.timeout', { ns: 'common' })); + } else { + alert(t('permission.notLocation', { ns: 'common' }) + error.message || t('permission.notError', { ns: 'common' })); + } + 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)) => { + setLocationEnabled(res); + }) + // 媒体库权限 + getPermissions().then((res: boolean | ((prevState: boolean) => boolean)) => { + setAlbumEnabled(res); + }) + // 通知权限 + checkNotificationPermission().then((res: boolean | ((prevState: boolean) => boolean)) => { + setNotificationsEnabled(res); + }) + } + }, [modalVisible]) + + // 获取语言环境 + useEffect(() => { + getLanguage(); + }, []) + + return ( + + + e.stopPropagation()}> + + { router.push('/owner') }}> + + + {t('generalSetting.allTitle', { ns: 'personal' })} + × + + + {/* 用户信息 */} + + {/* 升级版本 */} + {/* + {t('generalSetting.subscription', { ns: 'personal' })} + + + {t('generalSetting.subscriptionTitle', { ns: 'personal' })} + {t('generalSetting.subscriptionText', { ns: 'personal' })} + + { + + }} + > + + {t('generalSetting.upgrade', { ns: 'personal' })} + + + + */} + {/* 消息通知 */} + {/* + {t('permission.pushNotification', { ns: 'personal' })} + + + {t('permission.pushNotification', { ns: 'personal' })} + + + + */} + {/* 权限信息 */} + + {t('permission.permissionManagement', { ns: 'personal' })} + + {/* 相册权限 */} + + {t('permission.galleryAccess', { ns: 'personal' })} + + + {/* 分割线 */} + + {/* 位置权限 */} + + + {t('permission.locationPermission', { ns: 'personal' })} + + + + + + + {t('permission.pushNotification', { ns: 'personal' })} + + + + {/* 相册成片权限 */} + {/* + + Opus Permission + + + */} + + + {/* 账号 */} + {/* + Account + + + + Notifications + + + + + + Delete Account + + + + */} + {/* 协议 */} + + {t('lcenses.title', { ns: 'personal' })} + + { setModalType('privacy'); setPrivacyModalVisible(true) }} > + {t('lcenses.privacyPolicy', { ns: 'personal' })} + + + + { setModalType('terms'); setPrivacyModalVisible(true) }} > + {t('lcenses.applyPermission', { ns: 'personal' })} + + + + { setModalType('user'); setPrivacyModalVisible(true) }} > + {t('lcenses.userAgreement', { ns: 'personal' })} + + + + { setModalType('ai'); setPrivacyModalVisible(true) }} > + {t('lcenses.aiPolicy', { ns: 'personal' })} + + + + { setLcensesModalVisible(true) }} > + {t('lcenses.qualification', { ns: 'personal' })} + + + + Linking.openURL("https://beian.miit.gov.cn/")} > + {t('lcenses.ICP', { ns: 'personal' })}沪ICP备2025133004号-2A + + + + + {/* 其他信息 */} + + {t('generalSetting.otherInformation', { ns: 'personal' })} + + Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} > + {t('generalSetting.contactUs', { ns: 'personal' })} + {/* */} + + + + {t('generalSetting.version', { ns: 'personal' })} + {"0.5.0"} + + + + {/* 退出 */} + + {t('generalSetting.logout', { ns: 'personal' })} + + + {/* 注销账号 */} + setDeleteModalVisible(true)}> + {t('generalSetting.deleteAccount', { ns: 'personal' })} + + + + + + + + + + ); +}; + +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 ( + + ) +} +export default Setting; \ No newline at end of file diff --git a/assets/icons/svg/blackStar.svg b/assets/icons/svg/blackStar.svg new file mode 100644 index 0000000..c2c076f --- /dev/null +++ b/assets/icons/svg/blackStar.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/get.svg b/assets/icons/svg/get.svg new file mode 100644 index 0000000..1fd3a95 --- /dev/null +++ b/assets/icons/svg/get.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/proCard.svg b/assets/icons/svg/proCard.svg new file mode 100644 index 0000000..834fb22 --- /dev/null +++ b/assets/icons/svg/proCard.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/svg/rightCard.svg b/assets/icons/svg/rightCard.svg new file mode 100644 index 0000000..80ed82e --- /dev/null +++ b/assets/icons/svg/rightCard.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/svg/whiteStart.svg b/assets/icons/svg/whiteStart.svg new file mode 100644 index 0000000..17fc045 --- /dev/null +++ b/assets/icons/svg/whiteStart.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/png/owner/normal.png b/assets/images/png/owner/normal.png new file mode 100644 index 0000000..302d82e Binary files /dev/null and b/assets/images/png/owner/normal.png differ diff --git a/assets/images/png/owner/pro.png b/assets/images/png/owner/pro.png new file mode 100644 index 0000000..e1ad44c Binary files /dev/null and b/assets/images/png/owner/pro.png differ diff --git a/assets/images/png/placeholder.png b/assets/images/png/placeholder.png index 5371d4a..0565ebd 100644 Binary files a/assets/images/png/placeholder.png and b/assets/images/png/placeholder.png differ diff --git a/components/owner/album.tsx b/components/owner/album.tsx index e89b393..ff94e61 100644 --- a/components/owner/album.tsx +++ b/components/owner/album.tsx @@ -11,10 +11,9 @@ interface CategoryProps { const AlbumComponent = ({ setModalVisible, style }: CategoryProps) => { const { t } = useTranslation(); const router = useRouter(); - return ( - { router.push("/download") }}> + {t('generalSetting.album', { ns: 'personal' })} @@ -22,7 +21,8 @@ const AlbumComponent = ({ setModalVisible, style }: CategoryProps) => { { - setModalVisible(true); + // setModalVisible(true); + router.push('/setting'); }} activeOpacity={0.7} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} diff --git a/components/owner/rights/normal.tsx b/components/owner/rights/normal.tsx new file mode 100644 index 0000000..2fde55e --- /dev/null +++ b/components/owner/rights/normal.tsx @@ -0,0 +1,74 @@ +import GetSvg from "@/assets/icons/svg/get.svg"; +import { ThemedText } from "@/components/ThemedText"; +import { useTranslation } from "react-i18next"; +import { StyleProp, StyleSheet, TouchableOpacity, View, ViewStyle } from "react-native"; + +interface Props { + setUserType: (type: 'normal' | 'premium') => void; + style?: StyleProp; +} + +const Normal = (props: Props) => { + const { setUserType } = props; + const { t } = useTranslation(); + + return ( + + + + + + {t('rights.100Bonus', { ns: 'personal' })} + + {t('rights.100BonusText', { ns: 'personal' })} + + + + + {t('rights.10G', { ns: 'personal' })} + + {t('rights.10GText', { ns: 'personal' })} + + + { + setUserType('premium'); + }} + activeOpacity={0.8} + > + + {t('rights.purchase', { ns: 'personal' })} + + + + ); +} + +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", + } +}); \ No newline at end of file diff --git a/components/owner/rights/premium.tsx b/components/owner/rights/premium.tsx new file mode 100644 index 0000000..634f993 --- /dev/null +++ b/components/owner/rights/premium.tsx @@ -0,0 +1,127 @@ +import BlackStarSvg from '@/assets/icons/svg/blackStar.svg'; +import { ThemedText } from "@/components/ThemedText"; +import { useTranslation } from 'react-i18next'; +import { ScrollView, StyleProp, StyleSheet, TouchableOpacity, View, ViewStyle } from "react-native"; +import { maxDiscountProduct } from './utils'; + +interface Props { + style?: StyleProp; + 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 + const { t } = useTranslation(); + + return ( + + + {loading + ? + + {t('loading', { ns: 'common' })} + + : + premiumPay?.map((item: PayItem) => { + return { + 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} + > + + + + {t('rights.bestValue', { ns: 'personal' })} + + + + + {item.product_code?.split('_')[item.product_code?.split('_')?.length - 1]} + + + $ {item.unit_price.amount} + + + + $ {item.discount_amount.amount} + + + + }) + } + + + + {t('rights.cancelAnytimeBeforeRenewal', { ns: 'personal' })} + + setShowTerms(true)}> + {t('rights.terms', { ns: 'personal' })} + + + + ); +} + +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' + } +}); \ No newline at end of file diff --git a/components/owner/rights/proRights.tsx b/components/owner/rights/proRights.tsx new file mode 100644 index 0000000..d7b926f --- /dev/null +++ b/components/owner/rights/proRights.tsx @@ -0,0 +1,49 @@ +import GetSvg from "@/assets/icons/svg/get.svg"; +import { ThemedText } from "@/components/ThemedText"; +import { useTranslation } from "react-i18next"; +import { StyleProp, StyleSheet, View, ViewStyle } from "react-native"; +const ProRights = (props: { style?: StyleProp }) => { + const { style } = props; + const { t } = useTranslation(); + return ( + + + {t('rights.proTitle', { ns: 'personal' })} + + + + + {t('rights.noAd', { ns: 'personal' })} + + {t('rights.noAdText', { ns: 'personal' })} + + + + + {t('rights.bonus', { ns: 'personal' })} + + {t('rights.bonusText', { ns: 'personal' })} + + + + + {t('rights.storage', { ns: 'personal' })} + + {t('rights.storageText', { ns: 'personal' })} + + + ); +} + +const styles = StyleSheet.create({ + proRights: { + padding: 16, + gap: 8 + }, + itemContent: { + display: "flex", + flexDirection: "column", + } +}) + +export default ProRights; diff --git a/components/owner/rights/utils.ts b/components/owner/rights/utils.ts new file mode 100644 index 0000000..b7e16c8 --- /dev/null +++ b/components/owner/rights/utils.ts @@ -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; + }); +} \ No newline at end of file diff --git a/i18n/locales/en/common.json b/i18n/locales/en/common.json index 8a8d96e..025beb5 100644 --- a/i18n/locales/en/common.json +++ b/i18n/locales/en/common.json @@ -111,5 +111,11 @@ "required": "You must agree to the Terms and Privacy Policy" } }, - "loading": "Loading..." + "loading": "Loading...", + "permission": { + "locationPermissionRequired": "Location permission is required, please enable location service in settings", + "timeout": "Location timeout, please check network and location service", + "notLocation": "Unable to get your location: ", + "notError": "Unknown error" + } } \ No newline at end of file diff --git a/i18n/locales/en/personal.json b/i18n/locales/en/personal.json index 0c5b742..d1f4bd4 100644 --- a/i18n/locales/en/personal.json +++ b/i18n/locales/en/personal.json @@ -88,5 +88,27 @@ "unlock": "Unlock more memory magic", "delete": "Are you sure you want to delete your account?", "cancel": "Cancel" + }, + "rights": { + "title": "Subscription", + "premium": "Pro", + "purchase": "Purchase", + "free": "Free", + "subscribe": "Subscribe", + "terms": "Terms", + "100Bonus": "Enjoy 100 Bonus Credits Every Month", + "100BonusText": "Generate more memory pictures & videos and explore your past.", + "10G": "10GB of Cloud Storage", + "10GText": "Safely store your cherished photos, videos, and generated memories.", + "goPremium": "Go Premium", + "bestValue": "Best Value", + "cancelAnytimeBeforeRenewal": "Cancel anytime before renewal. Learn more", + "proTitle": "Enjoy MemoWake Pro Benefits", + "noAd": "No advertisements", + "noAdText": "There are no advertisements, so you can use the product with peace of mind.", + "bonus": "Enjoy 100 Bonus Credits Every Month", + "bonusText": "Generate more memory pictures & videos and explore your past.", + "storage": "10GB of Cloud Storage", + "storageText": "Safely store your cherished photos, videos, and generated memories." } } \ No newline at end of file diff --git a/i18n/locales/zh/common.json b/i18n/locales/zh/common.json index 9f4c00f..17eb407 100644 --- a/i18n/locales/zh/common.json +++ b/i18n/locales/zh/common.json @@ -110,5 +110,11 @@ "required": "您必须同意服务条款和隐私政策" } }, - "loading": "加载中..." + "loading": "加载中...", + "permission": { + "locationPermissionRequired": "位置权限被拒绝,请在设置中启用位置服务", + "timeout": "获取位置超时,请检查网络和位置服务", + "notLocation": "无法获取您的位置: ", + "notError": "未知错误" + } } \ No newline at end of file diff --git a/i18n/locales/zh/personal.json b/i18n/locales/zh/personal.json index c1be2df..c2c740e 100644 --- a/i18n/locales/zh/personal.json +++ b/i18n/locales/zh/personal.json @@ -88,5 +88,27 @@ "unlock": "解锁更多记忆魔法", "delete": "确定要注销账号吗?", "cancel": "取消" + }, + "rights": { + "title": "权益", + "purchase": "购买", + "free": "免费用户", + "premium": "会员", + "subscribe": "订阅", + "terms": "用户协议", + "100Bonus": "每月享受100积分", + "100BonusText": "生成更多记忆照片和视频,探索你的过去。", + "10G": "10GB的云存储", + "10GText": "安全存储你的珍贵照片、视频和生成的记忆。", + "goPremium": "升级至会员", + "bestValue": "最佳值", + "cancelAnytimeBeforeRenewal": "在续订前随时取消。了解更多", + "proTitle": "享受MemoWake Pro权益", + "noAd": "无广告", + "noAdText": "没有广告,所以你可以安心使用产品。", + "bonus": "每月享受100积分", + "bonusText": "生成更多记忆照片和视频,探索你的过去。", + "storage": "10GB的云存储", + "storageText": "安全存储你的珍贵照片、视频和生成的记忆。" } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1a876dc..21a7ef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1d563ba..f0c441a 100644 --- a/package.json +++ b/package.json @@ -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",