feat: 苹果支付 (#15)
Reviewed-on: #15 Co-authored-by: jinyaqiu <jinyaqiu@fairclip.cn> Co-committed-by: jinyaqiu <jinyaqiu@fairclip.cn>
This commit is contained in:
parent
1498e9c6a8
commit
714001f6ac
2
app.json
2
app.json
@ -46,7 +46,7 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-router",
|
"expo-router",
|
||||||
"expo-secure-store",
|
"expo-secure-store",
|
||||||
[
|
[
|
||||||
"expo-background-task",
|
"expo-background-task",
|
||||||
{
|
{
|
||||||
"minimumInterval": 15
|
"minimumInterval": 15
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export default function OwnerPage() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.push({
|
onPress={() => router.push({
|
||||||
pathname: '/rights',
|
pathname: '/rights',
|
||||||
params: { credit: userInfoDetails?.remain_points }
|
params: { credit: userInfoDetails?.remain_points, pro: userInfoDetails?.membership_level }
|
||||||
})}
|
})}
|
||||||
style={styles.resourceContainer}
|
style={styles.resourceContainer}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,28 +1,49 @@
|
|||||||
import ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg';
|
import ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg';
|
||||||
import StarSvg from '@/assets/icons/svg/whiteStart.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 PrivacyModal from '@/components/owner/qualification/privacy';
|
||||||
import Normal from '@/components/owner/rights/normal';
|
import Normal from '@/components/owner/rights/normal';
|
||||||
import Premium, { PayItem } from '@/components/owner/rights/premium';
|
import Premium, { PayItem } from '@/components/owner/rights/premium';
|
||||||
import ProRights from '@/components/owner/rights/proRights';
|
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 { 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 { useLocalSearchParams, useRouter } from "expo-router";
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
export default function Rights() {
|
export default function Rights() {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
connected,
|
||||||
|
products,
|
||||||
|
subscriptions,
|
||||||
|
currentPurchase,
|
||||||
|
currentPurchaseError,
|
||||||
|
requestProducts,
|
||||||
|
requestPurchase,
|
||||||
|
finishTransaction,
|
||||||
|
validateReceipt,
|
||||||
|
} = useIAP();
|
||||||
|
// 用户勾选协议
|
||||||
|
const [agree, setAgree] = useState<boolean>(false);
|
||||||
|
// 用户选择购买的loading
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
|
||||||
|
// 选择购买方式
|
||||||
|
const [payChoice, setPayChoice] = useState<'ApplePay'>('ApplePay');
|
||||||
// 获取路由参数
|
// 获取路由参数
|
||||||
const { credit } = useLocalSearchParams<{
|
const { credit, pro } = useLocalSearchParams<{
|
||||||
credit: string;
|
credit: string;
|
||||||
|
pro: string;
|
||||||
}>();
|
}>();
|
||||||
// 普通用户,会员
|
// 普通用户,会员
|
||||||
const [userType, setUserType] = useState<'normal' | 'premium'>('normal');
|
const [userType, setUserType] = useState<'normal' | 'premium'>('normal');
|
||||||
|
|
||||||
// 选择权益方式
|
// 选择权益方式
|
||||||
const [payType, setPayType] = useState<string>('');
|
const [payType, setPayType] = useState<string>('');
|
||||||
|
|
||||||
@ -32,26 +53,154 @@ export default function Rights() {
|
|||||||
// 调接口获取支付信息
|
// 调接口获取支付信息
|
||||||
const [premiumPay, setPremiumPay] = useState<PayItem[]>();
|
const [premiumPay, setPremiumPay] = useState<PayItem[]>();
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const getPAy = async () => {
|
|
||||||
setLoading(true);
|
// 查看历史订单
|
||||||
const payInfo = await fetchApi<PayItem[]>(`/order/product-items?product_type=Membership`)
|
const fetchPurchaseHistory = async () => {
|
||||||
let bestValue = maxDiscountProduct(payInfo)
|
try {
|
||||||
setPayType(bestValue?.product_code)
|
const purchaseHistories = await getPurchaseHistories();
|
||||||
setPremiumPay([bestValue, ...payInfo?.filter((item) => item.product_code !== bestValue?.product_code)]);
|
console.log('Purchase history fetched:', purchaseHistories);
|
||||||
setLoading(false);
|
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(() => {
|
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 (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
|
{/* 整个页面的中间添加一个loading */}
|
||||||
|
{confirmLoading && (
|
||||||
|
<View style={[styles.loadingContainer, { top: insets.top + 60 }]}>
|
||||||
|
<View style={styles.loadingContent}>
|
||||||
|
<ActivityIndicator size="large" color="#AC7E35" />
|
||||||
|
<ThemedText style={{ color: '#AC7E35', fontSize: 14, fontWeight: '700' }}>
|
||||||
|
{t('personal:rights.confirmLoading')}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
<ScrollView style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom + 80 }]}>
|
<ScrollView style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom + 80 }]}>
|
||||||
{/* 导航栏 */}
|
{/* 导航栏 */}
|
||||||
<View
|
<View
|
||||||
style={styles.header}
|
style={styles.header}
|
||||||
>
|
>
|
||||||
<TouchableOpacity onPress={() => { router.push('/owner') }} style={{ padding: 16 }}>
|
<TouchableOpacity onPress={() => { router.push('/owner'); setConfirmLoading(false) }} style={{ padding: 16 }}>
|
||||||
<ReturnArrowSvg />
|
<ReturnArrowSvg />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<ThemedText style={styles.headerTitle}>
|
<ThemedText style={styles.headerTitle}>
|
||||||
@ -99,8 +248,10 @@ export default function Rights() {
|
|||||||
{/* 普通权益 */}
|
{/* 普通权益 */}
|
||||||
<Normal setUserType={setUserType} style={{ display: userType === 'normal' ? "flex" : "none" }} />
|
<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" }} />
|
<Premium restorePurchases={restorePurchases} setPayType={setPayType} setShowTerms={setShowTerms} payType={payType} premiumPay={premiumPay} loading={loading} style={{ display: userType === 'normal' ? "none" : "flex" }} />
|
||||||
</View>
|
</View>
|
||||||
|
{/* 支付方式 */}
|
||||||
|
{/* <PayTypeModal setConfirmPay={setConfirmPay} modalVisible={showPayType} setModalVisible={setShowPayType} payChoice={payChoice} setPayChoice={setPayChoice} /> */}
|
||||||
{/* 会员权益信息 */}
|
{/* 会员权益信息 */}
|
||||||
<View style={{ flex: 1, marginBottom: 80 }}>
|
<View style={{ flex: 1, marginBottom: 80 }}>
|
||||||
<ProRights style={{ display: userType === 'normal' ? "none" : "flex" }} />
|
<ProRights style={{ display: userType === 'normal' ? "none" : "flex" }} />
|
||||||
@ -109,6 +260,7 @@ export default function Rights() {
|
|||||||
{/* 付费按钮 */}
|
{/* 付费按钮 */}
|
||||||
<View style={{
|
<View style={{
|
||||||
padding: 16,
|
padding: 16,
|
||||||
|
paddingBottom: 32,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#eee',
|
borderTopColor: '#eee',
|
||||||
@ -118,10 +270,32 @@ export default function Rights() {
|
|||||||
right: 0,
|
right: 0,
|
||||||
display: userType === 'normal' ? "none" : "flex"
|
display: userType === 'normal' ? "none" : "flex"
|
||||||
}}>
|
}}>
|
||||||
|
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center', marginLeft: 8, marginBottom: 8 }}>
|
||||||
|
<TouchableOpacity onPress={() => { setAgree(!agree) }} activeOpacity={0.8}>
|
||||||
|
<View style={[styles.agree, { backgroundColor: agree ? '#FFB645' : '#fff', borderColor: agree ? '#FFB645' : '#AC7E35' }]}>
|
||||||
|
{agree && <CheckSvg />}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={{ flexDirection: 'row', gap: 4 }}>
|
||||||
|
<ThemedText style={{ fontWeight: '400', fontSize: 11 }}>
|
||||||
|
{t('personal:rights.agreement')}
|
||||||
|
</ThemedText>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={async () => {
|
||||||
|
setShowTerms(true);
|
||||||
|
}}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<ThemedText style={{ color: '#AC7E35', fontWeight: '400', fontSize: 11, textAlign: 'center' }}>
|
||||||
|
{t('personal:rights.membership')}
|
||||||
|
</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.goPay}
|
style={styles.goPay}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setUserType('premium');
|
confirmPurchase()
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
@ -129,24 +303,62 @@ export default function Rights() {
|
|||||||
{t('rights.subscribe', { ns: 'personal' })}
|
{t('rights.subscribe', { ns: 'personal' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
|
||||||
onPress={async () => {
|
|
||||||
setShowTerms(true);
|
|
||||||
}}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
|
||||||
<ThemedText style={{ color: '#AC7E35', fontWeight: '400', fontSize: 11, textDecorationLine: 'underline', textAlign: 'center' }}>
|
|
||||||
{t('rights.terms', { ns: 'personal' })}
|
|
||||||
</ThemedText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
{/* 协议弹窗 */}
|
{/* 协议弹窗 */}
|
||||||
<PrivacyModal modalVisible={showTerms} setModalVisible={setShowTerms} type={"user"} />
|
<PrivacyModal modalVisible={showTerms} setModalVisible={setShowTerms} type={"member"} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
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: {
|
goPay: {
|
||||||
backgroundColor: '#E2793F',
|
backgroundColor: '#E2793F',
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
@ -243,3 +455,6 @@ const styles = StyleSheet.create({
|
|||||||
lineHeight: 32
|
lineHeight: 32
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
function validateAndGrantPurchase(purchase: ProductPurchase) {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
|
|||||||
3
assets/icons/svg/choicePay.svg
Normal file
3
assets/icons/svg/choicePay.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="20" height="20" rx="6" fill="#FFB645"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 156 B |
@ -3,10 +3,25 @@ import { Policy } from '@/types/personal-info';
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import RenderHtml from 'react-native-render-html';
|
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 }) => {
|
interface PrivacyModalProps {
|
||||||
const { modalVisible, setModalVisible, type } = props;
|
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<Policy>({} as Policy);
|
const [article, setArticle] = useState<Policy>({} as Policy);
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadArticle = async () => {
|
const loadArticle = async () => {
|
||||||
// ai协议
|
// ai协议
|
||||||
@ -41,6 +56,14 @@ const PrivacyModal = (props: { modalVisible: boolean, setModalVisible: (visible:
|
|||||||
console.log(error)
|
console.log(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 会员协议
|
||||||
|
if (type === 'membership') {
|
||||||
|
fetchApi<Policy>(`/system-config/policy/membership_agreement`).then((res: any) => {
|
||||||
|
setArticle(res)
|
||||||
|
}).catch((error: any) => {
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if (type) {
|
if (type) {
|
||||||
loadArticle();
|
loadArticle();
|
||||||
@ -63,11 +86,13 @@ const PrivacyModal = (props: { modalVisible: boolean, setModalVisible: (visible:
|
|||||||
onRequestClose={() => {
|
onRequestClose={() => {
|
||||||
setModalVisible(!modalVisible);
|
setModalVisible(!modalVisible);
|
||||||
}}>
|
}}>
|
||||||
<View style={styles.centeredView}>
|
<View style={[styles.centeredView, { bottom: insets.bottom }]}>
|
||||||
<View style={styles.modalView}>
|
<View style={styles.modalView}>
|
||||||
<View style={styles.modalHeader}>
|
<View style={styles.modalHeader}>
|
||||||
<Text style={{ opacity: 0 }}>Settings</Text>
|
<Text style={{ opacity: 0 }}>Settings</Text>
|
||||||
<Text style={styles.modalTitle}>{type === 'ai' ? 'AI Policy' : type === 'terms' ? 'Terms of Service' : type === 'privacy' ? 'Privacy Policy' : 'User Agreement'}</Text>
|
<Text style={styles.modalTitle}>
|
||||||
|
{titleMap[type] || 'User Agreement'}
|
||||||
|
</Text>
|
||||||
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||||
<Text style={styles.closeButton}>×</Text>
|
<Text style={styles.closeButton}>×</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
156
components/owner/rights/payType.tsx
Normal file
156
components/owner/rights/payType.tsx
Normal file
@ -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 (
|
||||||
|
<Modal
|
||||||
|
animationType="slide"
|
||||||
|
transparent={true}
|
||||||
|
visible={modalVisible}
|
||||||
|
onRequestClose={() => {
|
||||||
|
setModalVisible(!modalVisible);
|
||||||
|
}}>
|
||||||
|
<View style={styles.centeredView}>
|
||||||
|
<View style={styles.modalView}>
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<ThemedText style={{ opacity: 0 }}>{t('personal:rights.payType')}</ThemedText>
|
||||||
|
<ThemedText style={styles.modalTitle}>{t('personal:rights.payType')}</ThemedText>
|
||||||
|
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||||
|
<Text style={styles.closeButton}>×</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.paymentMethod]}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ padding: 16, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}
|
||||||
|
onPress={() => { setPayChoice('ApplePay') }}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
|
||||||
|
<ChoicePaySvg />
|
||||||
|
<ThemedText style={{ fontWeight: '700', fontSize: 16, color: "#4C320C" }}>
|
||||||
|
{t('personal:rights.apple')}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.payChoice, { backgroundColor: payChoice === 'ApplePay' ? '#FFB645' : '#D9D9D9' }]}>
|
||||||
|
{payChoice === 'ApplePay' ? <YesSvg /> : null}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, { backgroundColor: '#FFB645' }]}
|
||||||
|
onPress={() => {
|
||||||
|
setConfirmPay(true);
|
||||||
|
setModalVisible(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemedText style={{ fontWeight: '700', fontSize: 16, color: "#4C320C" }}>
|
||||||
|
{t('personal:rights.confirm')}
|
||||||
|
</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, { backgroundColor: '#D9D9D9' }]}
|
||||||
|
onPress={() => { setModalVisible(false) }}
|
||||||
|
>
|
||||||
|
<ThemedText style={{ fontWeight: '700', fontSize: 16, color: "#4C320C" }}>
|
||||||
|
{t('personal:rights.cancel')}
|
||||||
|
</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
@ -11,6 +11,7 @@ interface Props {
|
|||||||
premiumPay: any;
|
premiumPay: any;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
setShowTerms: (visible: boolean) => void;
|
setShowTerms: (visible: boolean) => void;
|
||||||
|
restorePurchases: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PayItem {
|
export interface PayItem {
|
||||||
@ -30,7 +31,7 @@ export interface PayItem {
|
|||||||
|
|
||||||
}
|
}
|
||||||
const Premium = (props: Props) => {
|
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 bestValue = maxDiscountProduct(premiumPay)?.product_code
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -53,6 +54,7 @@ const Premium = (props: Props) => {
|
|||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setPayType(item?.product_code);
|
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 }]}
|
style={[styles.yearly, { borderColor: payType === item?.product_code ? '#FFB645' : '#E1E1E1', opacity: payType === item?.product_code ? 1 : 0.5 }]}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
@ -67,24 +69,26 @@ const Premium = (props: Props) => {
|
|||||||
{item.product_code?.split('_')[item.product_code?.split('_')?.length - 1]}
|
{item.product_code?.split('_')[item.product_code?.split('_')?.length - 1]}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText style={[styles.titleText, { fontSize: 32, lineHeight: 32 }]}>
|
<ThemedText style={[styles.titleText, { fontSize: 32, lineHeight: 32 }]}>
|
||||||
$ {item.unit_price.amount}
|
$ {(Number(item.unit_price.amount) - Number(item.discount_amount.amount)).toFixed(2)}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<ThemedText style={[styles.titleText, { fontSize: 12, color: "#AC7E35", textDecorationLine: 'line-through' }]}>
|
<ThemedText style={[styles.titleText, { fontSize: 12, color: "#AC7E35", textDecorationLine: 'line-through' }]}>
|
||||||
$ {item.discount_amount.amount}
|
$ {item.unit_price.amount}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={{ flexDirection: 'row', gap: 8, marginLeft: 4 }}>
|
<View style={{ flexDirection: 'row', gap: 8, marginLeft: 4, marginTop: 8 }}>
|
||||||
<ThemedText style={{ color: '#AC7E35', fontSize: 10 }}>
|
<ThemedText style={{ color: '#AC7E35', fontSize: 10 }}>
|
||||||
{t('rights.cancelAnytimeBeforeRenewal', { ns: 'personal' })}
|
{t('personal:rights.restorePurchase')}
|
||||||
</ThemedText>
|
|
||||||
<ThemedText style={{ color: '#E2793F', fontSize: 10, textDecorationLine: 'underline' }} onPress={() => setShowTerms(true)}>
|
|
||||||
{t('rights.terms', { ns: 'personal' })}
|
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
|
<TouchableOpacity onPress={restorePurchases}>
|
||||||
|
<ThemedText style={{ color: '#E2793F', fontSize: 10, textDecorationLine: 'underline' }}>
|
||||||
|
{t('personal:rights.restore')}
|
||||||
|
</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,12 +1,100 @@
|
|||||||
|
import { fetchApi } from "@/lib/server-api-util";
|
||||||
|
import { CreateOrder, PayOrder } from "@/types/personal-info";
|
||||||
import { PayItem } from "./premium";
|
import { PayItem } from "./premium";
|
||||||
|
|
||||||
// 使用 reduce 方法获取 discount_amount 的 amount 值最大的对象
|
// 使用 reduce 方法获取 discount_amount 的 amount 值最大的对象
|
||||||
export const maxDiscountProduct = (products: PayItem[]) => {
|
export const maxDiscountProduct = (products: PayItem[]) => {
|
||||||
|
if (!products || products.length === 0) {
|
||||||
|
return products?.[0];
|
||||||
|
}
|
||||||
return products?.reduce((max, current) => {
|
return products?.reduce((max, current) => {
|
||||||
// 将 amount 转换为数字进行比较
|
const maxAmount = parseFloat(max.discount_amount?.amount || '0');
|
||||||
const maxAmount = parseFloat(max.discount_amount.amount);
|
const currentAmount = parseFloat(current.discount_amount?.amount || '0');
|
||||||
const currentAmount = parseFloat(current.discount_amount.amount);
|
|
||||||
|
|
||||||
return currentAmount > maxAmount ? current : max;
|
return currentAmount > maxAmount ? current : max;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 查看产品项
|
||||||
|
export const getPAy = async () => {
|
||||||
|
const payInfo = await fetchApi<PayItem[]>(`/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<CreateOrder>(`/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<PayOrder>(`/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;
|
||||||
|
}
|
||||||
|
|||||||
@ -109,6 +109,19 @@
|
|||||||
"bonus": "Enjoy 100 Bonus Credits Every Month",
|
"bonus": "Enjoy 100 Bonus Credits Every Month",
|
||||||
"bonusText": "Generate more memory pictures & videos and explore your past.",
|
"bonusText": "Generate more memory pictures & videos and explore your past.",
|
||||||
"storage": "10GB of Cloud Storage",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,6 +109,19 @@
|
|||||||
"bonus": "每月享受100积分",
|
"bonus": "每月享受100积分",
|
||||||
"bonusText": "生成更多记忆照片和视频,探索你的过去。",
|
"bonusText": "生成更多记忆照片和视频,探索你的过去。",
|
||||||
"storage": "10GB的云存储",
|
"storage": "10GB的云存储",
|
||||||
"storageText": "安全存储你的珍贵照片、视频和生成的记忆。"
|
"storageText": "安全存储你的珍贵照片、视频和生成的记忆。",
|
||||||
|
"weChatPay": "微信支付",
|
||||||
|
"apple": "苹果支付",
|
||||||
|
"confirm": "确认",
|
||||||
|
"cancel": "取消",
|
||||||
|
"confirmLoading": "正在购买...",
|
||||||
|
"againError": "您已购买过该权益,无需重复购买",
|
||||||
|
"payType": "支付方式",
|
||||||
|
"restoreSuccess": "恢复购买成功",
|
||||||
|
"restore": "恢复购买",
|
||||||
|
"restorePurchase": "已购买会员,但未生效,请尝试",
|
||||||
|
"agreement": "我已阅读并同意",
|
||||||
|
"membership": "《会员协议》",
|
||||||
|
"agreementError": "请先阅读并同意协议"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
60
package-lock.json
generated
60
package-lock.json
generated
@ -19,6 +19,7 @@
|
|||||||
"expo-audio": "~0.4.8",
|
"expo-audio": "~0.4.8",
|
||||||
"expo-background-task": "^0.2.8",
|
"expo-background-task": "^0.2.8",
|
||||||
"expo-blur": "~14.1.5",
|
"expo-blur": "~14.1.5",
|
||||||
|
"expo-build-properties": "^0.14.8",
|
||||||
"expo-clipboard": "~7.1.5",
|
"expo-clipboard": "~7.1.5",
|
||||||
"expo-constants": "~17.1.6",
|
"expo-constants": "~17.1.6",
|
||||||
"expo-dev-client": "~5.2.4",
|
"expo-dev-client": "~5.2.4",
|
||||||
@ -26,6 +27,7 @@
|
|||||||
"expo-file-system": "~18.1.10",
|
"expo-file-system": "~18.1.10",
|
||||||
"expo-font": "~13.3.1",
|
"expo-font": "~13.3.1",
|
||||||
"expo-haptics": "~14.1.4",
|
"expo-haptics": "~14.1.4",
|
||||||
|
"expo-iap": "^2.7.5",
|
||||||
"expo-image-manipulator": "~13.1.7",
|
"expo-image-manipulator": "~13.1.7",
|
||||||
"expo-image-picker": "~16.1.4",
|
"expo-image-picker": "~16.1.4",
|
||||||
"expo-linear-gradient": "~14.1.5",
|
"expo-linear-gradient": "~14.1.5",
|
||||||
@ -8710,6 +8712,53 @@
|
|||||||
"react-native": "*"
|
"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": {
|
"node_modules/expo-clipboard": {
|
||||||
"version": "7.1.5",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-7.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-7.1.5.tgz",
|
||||||
@ -8879,6 +8928,17 @@
|
|||||||
"expo": "*"
|
"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": {
|
"node_modules/expo-image-loader": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.1.0.tgz",
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"expo-audio": "~0.4.8",
|
"expo-audio": "~0.4.8",
|
||||||
"expo-background-task": "^0.2.8",
|
"expo-background-task": "^0.2.8",
|
||||||
"expo-blur": "~14.1.5",
|
"expo-blur": "~14.1.5",
|
||||||
|
"expo-build-properties": "^0.14.8",
|
||||||
"expo-clipboard": "~7.1.5",
|
"expo-clipboard": "~7.1.5",
|
||||||
"expo-constants": "~17.1.6",
|
"expo-constants": "~17.1.6",
|
||||||
"expo-dev-client": "~5.2.4",
|
"expo-dev-client": "~5.2.4",
|
||||||
@ -32,6 +33,7 @@
|
|||||||
"expo-file-system": "~18.1.10",
|
"expo-file-system": "~18.1.10",
|
||||||
"expo-font": "~13.3.1",
|
"expo-font": "~13.3.1",
|
||||||
"expo-haptics": "~14.1.4",
|
"expo-haptics": "~14.1.4",
|
||||||
|
"expo-iap": "^2.7.5",
|
||||||
"expo-image-manipulator": "~13.1.7",
|
"expo-image-manipulator": "~13.1.7",
|
||||||
"expo-image-picker": "~16.1.4",
|
"expo-image-picker": "~16.1.4",
|
||||||
"expo-linear-gradient": "~14.1.5",
|
"expo-linear-gradient": "~14.1.5",
|
||||||
|
|||||||
@ -51,4 +51,49 @@ export interface Policy {
|
|||||||
content: string,
|
content: string,
|
||||||
created_at: string,
|
created_at: string,
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 订单
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user