From 714001f6ac60c131a7d1e592d592310ac0f040aa Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Tue, 29 Jul 2025 12:31:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=8B=B9=E6=9E=9C=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://git.fairclip.cn/FairClip/memowake-front/pulls/15 Co-authored-by: jinyaqiu Co-committed-by: jinyaqiu --- app.json | 2 +- app/(tabs)/owner.tsx | 2 +- app/(tabs)/rights.tsx | 269 ++++++++++++++++++--- assets/icons/svg/choicePay.svg | 3 + components/owner/qualification/privacy.tsx | 33 ++- components/owner/rights/payType.tsx | 156 ++++++++++++ components/owner/rights/premium.tsx | 20 +- components/owner/rights/utils.ts | 96 +++++++- i18n/locales/en/personal.json | 15 +- i18n/locales/zh/personal.json | 15 +- package-lock.json | 60 +++++ package.json | 2 + types/personal-info.ts | 47 +++- 13 files changed, 672 insertions(+), 48 deletions(-) create mode 100644 assets/icons/svg/choicePay.svg create mode 100644 components/owner/rights/payType.tsx diff --git a/app.json b/app.json index 3b5b2de..426908f 100644 --- a/app.json +++ b/app.json @@ -46,7 +46,7 @@ "plugins": [ "expo-router", "expo-secure-store", - [ + [ "expo-background-task", { "minimumInterval": 15 diff --git a/app/(tabs)/owner.tsx b/app/(tabs)/owner.tsx index b1d34cc..3cc0eab 100644 --- a/app/(tabs)/owner.tsx +++ b/app/(tabs)/owner.tsx @@ -89,7 +89,7 @@ export default function OwnerPage() { router.push({ pathname: '/rights', - params: { credit: userInfoDetails?.remain_points } + params: { credit: userInfoDetails?.remain_points, pro: userInfoDetails?.membership_level } })} style={styles.resourceContainer} > diff --git a/app/(tabs)/rights.tsx b/app/(tabs)/rights.tsx index f175b94..1fb9eda 100644 --- a/app/(tabs)/rights.tsx +++ b/app/(tabs)/rights.tsx @@ -1,28 +1,49 @@ import ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg'; import StarSvg from '@/assets/icons/svg/whiteStart.svg'; +import CheckSvg from '@/assets/icons/svg/yes.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 { createOrder, createPayment, getPAy, isOrderExpired, payFailure, payProcessing, paySuccess } from '@/components/owner/rights/utils'; import { ThemedText } from '@/components/ThemedText'; -import { fetchApi } from '@/lib/server-api-util'; +import { CreateOrder } from '@/types/personal-info'; +import { ErrorCode, getAvailablePurchases, getPurchaseHistories, ProductPurchase, useIAP } from 'expo-iap'; 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 { ActivityIndicator, 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 { + connected, + products, + subscriptions, + currentPurchase, + currentPurchaseError, + requestProducts, + requestPurchase, + finishTransaction, + validateReceipt, + } = useIAP(); + // 用户勾选协议 + const [agree, setAgree] = useState(false); + // 用户选择购买的loading + const [confirmLoading, setConfirmLoading] = useState(false); + // 选择购买方式 + const [payChoice, setPayChoice] = useState<'ApplePay'>('ApplePay'); // 获取路由参数 - const { credit } = useLocalSearchParams<{ + const { credit, pro } = useLocalSearchParams<{ credit: string; + pro: string; }>(); // 普通用户,会员 const [userType, setUserType] = useState<'normal' | 'premium'>('normal'); + // 选择权益方式 const [payType, setPayType] = useState(''); @@ -32,26 +53,154 @@ export default function Rights() { // 调接口获取支付信息 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); - } + + // 查看历史订单 + const fetchPurchaseHistory = async () => { + try { + const purchaseHistories = await getPurchaseHistories(); + console.log('Purchase history fetched:', purchaseHistories); + return purchaseHistories + } catch (error) { + console.error('Failed to fetch purchase history:', error); + } + }; + + // 恢复购买 + const restorePurchases = async () => { + try { + const purchases = await getAvailablePurchases(); + console.log('Available purchases:', purchases); + // Process and validate restored purchases + for (const purchase of purchases) { + await validateAndGrantPurchase(purchase); + } + alert(t('personal:rights.restoreSuccess')); + } catch (error) { + console.error('Restore failed:', error); + } + }; + + // 处理购买 + const handlePurchase = async (sku: string, transaction_id: string) => { + try { + // 支付中 + await payProcessing(transaction_id, "") + const res = await requestPurchase({ + request: { + ios: { + sku: payType, + andDangerouslyFinishTransactionAutomaticallyIOS: false, + }, + }, + }); + console.log('Purchase success:', res); + + // 支付成功 + await paySuccess(transaction_id, res?.transactionId || "") + } catch (error: any) { + console.log('Purchase failed:', error); + // 支付失败 + payFailure(transaction_id, ErrorCode[error?.code as keyof typeof ErrorCode || "E_UNKNOWN"]) + } + }; + + // 获取苹果订单信息 useEffect(() => { - getPAy(); + if (!connected) return; + + const initializeStore = async () => { + try { + await requestProducts({ skus: ["MEMBERSHIP_PRO_QUARTERLY", "MEMBERSHIP_PRO_YEARLY", "MEMBERSHIP_PRO_MONTH"], type: 'subs' }); + } catch (error) { + console.error('Failed to initialize store:', error); + } + }; + + initializeStore(); + }, [connected]); + + // 初始化获取产品项 + useEffect(() => { + setLoading(true); + getPAy().then(({ bestValue, payInfo }) => { + setPayType(bestValue?.product_code) + setPremiumPay([bestValue, ...payInfo?.filter((item) => item.product_code !== bestValue?.product_code)]); + setLoading(false); + }).catch(() => { + setLoading(false); + }) }, []); + // 用户确认购买时,进行 创建订单,创建支付 接口调用 + const confirmPurchase = async () => { + if (!agree) { + alert(t('personal:rights.agreementError')); + return + } + setConfirmLoading(true); + const history = await fetchPurchaseHistory() + const historyIds = history?.filter((item: any) => isOrderExpired(item?.expirationDateIos))?.map((i) => { return i?.id }) + if (historyIds?.includes(payType)) { + setConfirmLoading(false); + setTimeout(() => { + alert(t('personal:rights.againError')); + }, 0); + return + } + + try { + // 创建订单 + createOrder(premiumPay?.filter((item) => item.product_code === payType)?.[0]?.id || 1, 1).then((res: CreateOrder) => { + // 创建支付 + createPayment(res?.id || "", payChoice).then(async (res) => { + // 苹果支付 + await handlePurchase(payType, res?.transaction_id || "") + setConfirmLoading(false); + }).catch((err) => { + console.log("createPayment", err); + setConfirmLoading(false); + }) + }).catch((err) => { + console.log("createOrder", err); + setConfirmLoading(false); + }) + } catch (error) { + console.log("confirmPurchase", error); + setConfirmLoading(false); + } + }; + + useEffect(() => { + if (pro === "Pro") { + setUserType('premium') + } else { + setUserType('normal') + } + }, [pro]) + + useEffect(() => { + fetchPurchaseHistory() + }, []) + return ( + {/* 整个页面的中间添加一个loading */} + {confirmLoading && ( + + + + + {t('personal:rights.confirmLoading')} + + + + )} {/* 导航栏 */} - { router.push('/owner') }} style={{ padding: 16 }}> + { router.push('/owner'); setConfirmLoading(false) }} style={{ padding: 16 }}> @@ -99,8 +248,10 @@ export default function Rights() { {/* 普通权益 */} {/* 会员权益 */} - + + {/* 支付方式 */} + {/* */} {/* 会员权益信息 */} @@ -109,6 +260,7 @@ export default function Rights() { {/* 付费按钮 */} + + { setAgree(!agree) }} activeOpacity={0.8}> + + {agree && } + + + + + {t('personal:rights.agreement')} + + { + setShowTerms(true); + }} + activeOpacity={0.8} + > + + {t('personal:rights.membership')} + + + + { - setUserType('premium'); + confirmPurchase() }} activeOpacity={0.8} > @@ -129,24 +303,62 @@ export default function Rights() { {t('rights.subscribe', { ns: 'personal' })} - { - setShowTerms(true); - }} - activeOpacity={0.8} - > - - {t('rights.terms', { ns: 'personal' })} - - + {/* 协议弹窗 */} - + ); } const styles = StyleSheet.create({ + agree: { + width: 15, + height: 15, + borderRadius: 15, + alignItems: 'center', + justifyContent: 'center', + borderColor: '#AC7E35', + borderWidth: 1, + }, + loadingContent: { + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#fff', + borderRadius: 12, + padding: 16, + }, + loadingContainer: { + justifyContent: 'center', + alignItems: 'center', + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + zIndex: 9, + backgroundColor: 'rgba(255, 255, 255, 0.5)', + }, + payChoice: { + width: 20, + height: 20, + borderRadius: 15, + alignItems: 'center', + justifyContent: 'center', + }, + paymentMethod: { + marginHorizontal: 16, + marginVertical: 16, + borderRadius: 12, + backgroundColor: '#fff', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 5, + elevation: 5, + }, goPay: { backgroundColor: '#E2793F', borderRadius: 24, @@ -243,3 +455,6 @@ const styles = StyleSheet.create({ lineHeight: 32 } }); +function validateAndGrantPurchase(purchase: ProductPurchase) { + throw new Error('Function not implemented.'); +} diff --git a/assets/icons/svg/choicePay.svg b/assets/icons/svg/choicePay.svg new file mode 100644 index 0000000..cd6758a --- /dev/null +++ b/assets/icons/svg/choicePay.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/owner/qualification/privacy.tsx b/components/owner/qualification/privacy.tsx index 3540069..1d95722 100644 --- a/components/owner/qualification/privacy.tsx +++ b/components/owner/qualification/privacy.tsx @@ -3,10 +3,25 @@ import { Policy } from '@/types/personal-info'; import React, { useEffect, useState } from 'react'; import { Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import RenderHtml from 'react-native-render-html'; +import { useSafeAreaInsets } from "react-native-safe-area-context"; -const PrivacyModal = (props: { modalVisible: boolean, setModalVisible: (visible: boolean) => void, type: string }) => { - const { modalVisible, setModalVisible, type } = props; +interface PrivacyModalProps { + modalVisible: boolean; + setModalVisible: (visible: boolean) => void; + type: 'ai' | 'terms' | 'privacy' | 'user' | 'membership'; +} + +const titleMap = { + ai: 'AI Policy', + terms: 'Terms of Service', + privacy: 'Privacy Policy', + user: 'User Agreement', + membership: 'Membership Agreement' +}; + +const PrivacyModal = ({ modalVisible, setModalVisible, type }: PrivacyModalProps) => { const [article, setArticle] = useState({} as Policy); + const insets = useSafeAreaInsets(); useEffect(() => { const loadArticle = async () => { // ai协议 @@ -41,6 +56,14 @@ const PrivacyModal = (props: { modalVisible: boolean, setModalVisible: (visible: console.log(error) }) } + // 会员协议 + if (type === 'membership') { + fetchApi(`/system-config/policy/membership_agreement`).then((res: any) => { + setArticle(res) + }).catch((error: any) => { + console.log(error) + }) + } }; if (type) { loadArticle(); @@ -63,11 +86,13 @@ const PrivacyModal = (props: { modalVisible: boolean, setModalVisible: (visible: onRequestClose={() => { setModalVisible(!modalVisible); }}> - + Settings - {type === 'ai' ? 'AI Policy' : type === 'terms' ? 'Terms of Service' : type === 'privacy' ? 'Privacy Policy' : 'User Agreement'} + + {titleMap[type] || 'User Agreement'} + setModalVisible(false)}> × diff --git a/components/owner/rights/payType.tsx b/components/owner/rights/payType.tsx new file mode 100644 index 0000000..3aa909f --- /dev/null +++ b/components/owner/rights/payType.tsx @@ -0,0 +1,156 @@ +import ChoicePaySvg from '@/assets/icons/svg/choicePay.svg'; +import YesSvg from '@/assets/icons/svg/yes.svg'; +import { ThemedText } from '@/components/ThemedText'; +import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +interface Props { + modalVisible: boolean; + setModalVisible: (visible: boolean) => void; + payChoice: 'ApplePay'; + setPayChoice: (choice: 'ApplePay') => void; + setConfirmPay: (confirm: boolean) => void; +} + +const PayTypeModal = (props: Props) => { + const { modalVisible, setModalVisible, payChoice, setPayChoice, setConfirmPay } = props; + const { t } = useTranslation(); + + useEffect(() => { + if (modalVisible) { + setConfirmPay(false) + } + }, [modalVisible]); + + return ( + { + setModalVisible(!modalVisible); + }}> + + + + {t('personal:rights.payType')} + {t('personal:rights.payType')} + setModalVisible(false)}> + × + + + + { setPayChoice('ApplePay') }} + > + + + + {t('personal:rights.apple')} + + + + {payChoice === 'ApplePay' ? : null} + + + + + { + setConfirmPay(true); + setModalVisible(false) + }} + > + + {t('personal:rights.confirm')} + + + { setModalVisible(false) }} + > + + {t('personal:rights.cancel')} + + + + + + + ); +}; + +const styles = StyleSheet.create({ + modalTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#4C320C', + }, + footer: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + alignItems: 'center', + gap: 16, + marginBottom: 32, + }, + button: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 12, + alignItems: 'center', + }, + payChoice: { + width: 20, + height: 20, + borderRadius: 15, + alignItems: 'center', + justifyContent: 'center', + }, + paymentMethod: { + marginHorizontal: 16, + marginVertical: 16, + borderRadius: 12, + backgroundColor: '#fff', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 5, + elevation: 5, + }, + centeredView: { + flex: 1, + justifyContent: 'flex-end', + backgroundColor: 'rgba(0,0,0,0.5)', + }, + modalView: { + width: '100%', + backgroundColor: 'white', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + 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, + } +}); +export default PayTypeModal; \ No newline at end of file diff --git a/components/owner/rights/premium.tsx b/components/owner/rights/premium.tsx index 634f993..2a64dcc 100644 --- a/components/owner/rights/premium.tsx +++ b/components/owner/rights/premium.tsx @@ -11,6 +11,7 @@ interface Props { premiumPay: any; loading: boolean; setShowTerms: (visible: boolean) => void; + restorePurchases: () => void; } export interface PayItem { @@ -30,7 +31,7 @@ export interface PayItem { } const Premium = (props: Props) => { - const { style, payType, setPayType, premiumPay, loading, setShowTerms } = props; + const { style, payType, setPayType, premiumPay, loading, setShowTerms, restorePurchases } = props; const bestValue = maxDiscountProduct(premiumPay)?.product_code const { t } = useTranslation(); @@ -53,6 +54,7 @@ const Premium = (props: Props) => { onPress={async () => { setPayType(item?.product_code); }} + key={item?.product_code} style={[styles.yearly, { borderColor: payType === item?.product_code ? '#FFB645' : '#E1E1E1', opacity: payType === item?.product_code ? 1 : 0.5 }]} activeOpacity={0.8} > @@ -67,24 +69,26 @@ const Premium = (props: Props) => { {item.product_code?.split('_')[item.product_code?.split('_')?.length - 1]} - $ {item.unit_price.amount} + $ {(Number(item.unit_price.amount) - Number(item.discount_amount.amount)).toFixed(2)} - $ {item.discount_amount.amount} + $ {item.unit_price.amount} }) } - + - {t('rights.cancelAnytimeBeforeRenewal', { ns: 'personal' })} - - setShowTerms(true)}> - {t('rights.terms', { ns: 'personal' })} + {t('personal:rights.restorePurchase')} + + + {t('personal:rights.restore')} + + ); diff --git a/components/owner/rights/utils.ts b/components/owner/rights/utils.ts index b7e16c8..475475b 100644 --- a/components/owner/rights/utils.ts +++ b/components/owner/rights/utils.ts @@ -1,12 +1,100 @@ +import { fetchApi } from "@/lib/server-api-util"; +import { CreateOrder, PayOrder } from "@/types/personal-info"; import { PayItem } from "./premium"; // 使用 reduce 方法获取 discount_amount 的 amount 值最大的对象 export const maxDiscountProduct = (products: PayItem[]) => { + if (!products || products.length === 0) { + return products?.[0]; + } return products?.reduce((max, current) => { - // 将 amount 转换为数字进行比较 - const maxAmount = parseFloat(max.discount_amount.amount); - const currentAmount = parseFloat(current.discount_amount.amount); + const maxAmount = parseFloat(max.discount_amount?.amount || '0'); + const currentAmount = parseFloat(current.discount_amount?.amount || '0'); return currentAmount > maxAmount ? current : max; }); -} \ No newline at end of file +} + + +// 查看产品项 +export const getPAy = async () => { + const payInfo = await fetchApi(`/order/product-items?product_type=Membership`) + let bestValue = maxDiscountProduct(payInfo) + return { bestValue, payInfo } +} + +// 创建订单 +export const createOrder = async (id: number, quantity: number) => { + const order = await fetchApi(`/order/create`, { + method: 'POST', + body: JSON.stringify({ + items: [{ + product_item_id: id, + quantity: quantity + }] + }) + }) + return order +} + +// 创建支付 +export const createPayment = async (order_id: string, payment_method: string) => { + const payment = await fetchApi(`/order/pay`, { + method: 'POST', + body: JSON.stringify({ + order_id, + payment_method + }) + }) + return payment +} + +// 支付中 +export const payProcessing = async (transaction_id: string, third_party_transaction_id: string) => { + const payment = await fetchApi(`/order/pay-processing`, { + method: 'POST', + body: JSON.stringify({ + transaction_id + }) + }) + return payment +} + +// 支付失败 +export const payFailure = async (transaction_id: string, reason: string) => { + const payment = await fetchApi(`/order/pay-failure`, { + method: 'POST', + body: JSON.stringify({ + transaction_id, + reason + }) + }) + return payment +} + +// 支付成功 +export const paySuccess = async (transaction_id: string, third_party_transaction_id: string) => { + const payment = await fetchApi(`/order/pay-success`, { + method: 'POST', + body: JSON.stringify({ + transaction_id, + third_party_transaction_id + }) + }) + return payment +} + +// 判断订单是否过期 +/** +* 判断指定时间戳是否已过期(即当前时间是否已超过该时间戳) +* @param expirationTimestamp - 过期时间戳(单位:毫秒) +* @returns boolean - true: 未过期,false: 已过期 +*/ +export const isOrderExpired = async (transactionDate: number) => { + // 如果没有提供过期时间,视为无效或未设置,认为“已过期”或状态未知 + if (!transactionDate || isNaN(transactionDate)) { + return false; + } + const now = Date.now(); // 当前时间戳(毫秒) + return now < transactionDate; +} diff --git a/i18n/locales/en/personal.json b/i18n/locales/en/personal.json index d1f4bd4..62cb0cc 100644 --- a/i18n/locales/en/personal.json +++ b/i18n/locales/en/personal.json @@ -109,6 +109,19 @@ "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." + "storageText": "Safely store your cherished photos, videos, and generated memories.", + "weChatPay": "WeChat", + "apple": "Apple Pay", + "confirm": "Confirm", + "cancel": "Cancel", + "confirmLoading": "Confirming...", + "againError": "You have already purchased this benefit, no need to purchase again", + "payType": "Pay Type", + "restoreSuccess": "Restore purchase successfully", + "restore": "restore purchase", + "restorePurchase": "Membership purchased but not active,please try to", + "agreement": "I have read and agree to", + "membership": "《Membership Agreement》", + "agreementError": "Please read and agree to the agreement" } } \ No newline at end of file diff --git a/i18n/locales/zh/personal.json b/i18n/locales/zh/personal.json index c2c740e..6ba9bab 100644 --- a/i18n/locales/zh/personal.json +++ b/i18n/locales/zh/personal.json @@ -109,6 +109,19 @@ "bonus": "每月享受100积分", "bonusText": "生成更多记忆照片和视频,探索你的过去。", "storage": "10GB的云存储", - "storageText": "安全存储你的珍贵照片、视频和生成的记忆。" + "storageText": "安全存储你的珍贵照片、视频和生成的记忆。", + "weChatPay": "微信支付", + "apple": "苹果支付", + "confirm": "确认", + "cancel": "取消", + "confirmLoading": "正在购买...", + "againError": "您已购买过该权益,无需重复购买", + "payType": "支付方式", + "restoreSuccess": "恢复购买成功", + "restore": "恢复购买", + "restorePurchase": "已购买会员,但未生效,请尝试", + "agreement": "我已阅读并同意", + "membership": "《会员协议》", + "agreementError": "请先阅读并同意协议" } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 21a7ef9..09db5d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "expo-audio": "~0.4.8", "expo-background-task": "^0.2.8", "expo-blur": "~14.1.5", + "expo-build-properties": "^0.14.8", "expo-clipboard": "~7.1.5", "expo-constants": "~17.1.6", "expo-dev-client": "~5.2.4", @@ -26,6 +27,7 @@ "expo-file-system": "~18.1.10", "expo-font": "~13.3.1", "expo-haptics": "~14.1.4", + "expo-iap": "^2.7.5", "expo-image-manipulator": "~13.1.7", "expo-image-picker": "~16.1.4", "expo-linear-gradient": "~14.1.5", @@ -8710,6 +8712,53 @@ "react-native": "*" } }, + "node_modules/expo-build-properties": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-0.14.8.tgz", + "integrity": "sha512-GTFNZc5HaCS9RmCi6HspCe2+isleuOWt2jh7UEKHTDQ9tdvzkIoWc7U6bQO9lH3Mefk4/BcCUZD/utl7b1wdqw==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "semver": "^7.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-build-properties/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/expo-build-properties/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/expo-build-properties/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/expo-clipboard": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-7.1.5.tgz", @@ -8879,6 +8928,17 @@ "expo": "*" } }, + "node_modules/expo-iap": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/expo-iap/-/expo-iap-2.7.5.tgz", + "integrity": "sha512-+UMLBXKtyoVsfJMQxqGLv4qXeMZzFoOoDMRVJa8OYngDCqfIADkpyNb28HKNZdYiaa0Yq5LHYu42zNkD2m2w0Q==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-image-loader": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.1.0.tgz", diff --git a/package.json b/package.json index f0c441a..788a6a7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "expo-audio": "~0.4.8", "expo-background-task": "^0.2.8", "expo-blur": "~14.1.5", + "expo-build-properties": "^0.14.8", "expo-clipboard": "~7.1.5", "expo-constants": "~17.1.6", "expo-dev-client": "~5.2.4", @@ -32,6 +33,7 @@ "expo-file-system": "~18.1.10", "expo-font": "~13.3.1", "expo-haptics": "~14.1.4", + "expo-iap": "^2.7.5", "expo-image-manipulator": "~13.1.7", "expo-image-picker": "~16.1.4", "expo-linear-gradient": "~14.1.5", diff --git a/types/personal-info.ts b/types/personal-info.ts index d1e77e0..317d5d2 100644 --- a/types/personal-info.ts +++ b/types/personal-info.ts @@ -51,4 +51,49 @@ export interface Policy { content: string, created_at: string, updated_at: string -} \ No newline at end of file +} + + +// 订单 +export interface Amount { + amount: string; + currency: string; +} + +export interface OrderItem { + discount_amount: Amount; + id: string; + product_code: string; + product_id: number; + product_name: string; + product_type: string; + quantity: number; + total_price: Amount; + unit_price: Amount; +} + +// 订单 +export interface CreateOrder { + created_at: string; + expired_at: string; + id: string; + items: OrderItem[]; + payment_info: any | null; // 使用 any 或更具体的类型 + status: string; + total_amount: Amount; + updated_at: string; + user_id: string; +} + +// 创建订单 +export interface PayOrder { + created_at: string; + id: string; + paid_at: string; + payment_amount: Amount; + payment_method: string; + payment_status: string; + third_party_transaction_id: string; + transaction_id: string; + updated_at: string; +}