feat: 苹果支付

This commit is contained in:
jinyaqiu 2025-07-28 17:24:10 +08:00
parent 1d9f80f975
commit c3151e80d1
6 changed files with 383 additions and 64 deletions

View File

@ -1,15 +1,14 @@
import ChoicePaySvg from '@/assets/icons/svg/choicePay.svg';
import ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg';
import StarSvg from '@/assets/icons/svg/whiteStart.svg';
import YesSvg from '@/assets/icons/svg/yes.svg';
import PrivacyModal from '@/components/owner/qualification/privacy';
import Normal from '@/components/owner/rights/normal';
import PayTypeModal from '@/components/owner/rights/payType';
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, payFailure, paySuccess } from '@/components/owner/rights/utils';
import { ThemedText } from '@/components/ThemedText';
import { fetchApi } from '@/lib/server-api-util';
import { useIAP } from 'expo-iap';
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';
@ -31,9 +30,10 @@ export default function Rights() {
finishTransaction,
validateReceipt,
} = useIAP();
// 购买方式弹窗
const [showPayType, setShowPayType] = useState<boolean>(false);
// 选择购买方式
const [payChoice, setPayChoice] = useState<'weChatPay' | 'apple'>('weChatPay');
const [payChoice, setPayChoice] = useState<'ApplePay'>('ApplePay');
// 获取路由参数
const { credit } = useLocalSearchParams<{
credit: string;
@ -50,25 +50,39 @@ export default function Rights() {
// 调接口获取支付信息
const [premiumPay, setPremiumPay] = useState<PayItem[]>();
const [loading, setLoading] = useState<boolean>(false);
const getPAy = async () => {
setLoading(true);
const payInfo = await fetchApi<PayItem[]>(`/order/product-items?product_type=Membership`)
let bestValue = maxDiscountProduct(payInfo)
setPayType(bestValue?.product_code)
setPremiumPay([bestValue, ...payInfo?.filter((item) => item.product_code !== bestValue?.product_code)]);
setLoading(false);
}
useEffect(() => {
getPAy();
}, []);
// 处理购买
const handlePurchase = async (productId: string) => {
console.log(productId);
// 确认支付
const [confirmPay, setConfirmPay] = useState<boolean>(false);
// 查看历史订单
const fetchPurchaseHistory = async () => {
try {
// Platform-specific purchase requests (v2.7.0+)
await requestPurchase({
const purchaseHistories = await getPurchaseHistories();
console.log('Purchase history fetched:', 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);
}
} catch (error) {
console.error('Restore failed:', error);
}
};
// 处理购买
const handlePurchase = async (sku: string, transaction_id: string) => {
console.log('handlePurchase', sku);
try {
const res = await requestPurchase({
request: {
ios: {
sku: "MEMBERSHIP_PRO_QUARTERLY",
@ -76,11 +90,16 @@ export default function Rights() {
},
},
});
} catch (error) {
alert(productId)
console.error('Purchase failed:', error);
// 支付成功
paySuccess(transaction_id, res?.transaction_id || "")
} catch (error: any) {
console.log('Purchase failed:', error);
// 支付失败
payFailure(transaction_id, ErrorCode[error?.code as keyof typeof ErrorCode || "E_UNKNOWN"])
}
};
// 获取苹果订单信息
useEffect(() => {
console.log('connected', connected);
@ -89,16 +108,59 @@ export default function Rights() {
const initializeStore = async () => {
try {
await requestProducts({ skus: ["MEMBERSHIP_PRO_QUARTERLY"], type: 'subs' });
// await getSubscriptions(["MEMBERSHIP_PRO_QUARTERLY"])
console.log("products", products);
console.log("subscriptions", subscriptions);
} catch (error) {
console.error('Failed to initialize store:', error);
}
};
initializeStore();
}, [connected, premiumPay]);
}, [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);
})
}, []);
// 用户确认购买时,进行 创建订单,创建支付 接口调用
useEffect(() => {
console.log('confirmPay', confirmPay);
if (confirmPay) {
// 创建订单
createOrder(premiumPay?.filter((item) => item.product_code === payType)?.[0]?.id || 1, 1).then((res: CreateOrder) => {
// 创建支付
createPayment(res?.id || "", payChoice).then((res) => {
console.log("createPayment", res);
console.log("payType", payType);
// 苹果支付
handlePurchase(payType, res?.transaction_id || "")
}).catch((err) => {
console.log("createPayment", err);
})
}).catch((err) => {
console.log("createOrder", err);
})
}
}, [confirmPay]);
useEffect(() => {
if (currentPurchase) {
console.log('currentPurchase', currentPurchase);
}
}, [currentPurchase]);
useEffect(() => {
console.log('currentPurchaseError', currentPurchaseError);
}, [currentPurchaseError]);
return (
<View style={{ flex: 1 }}>
<ScrollView style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom + 80 }]}>
@ -157,34 +219,7 @@ export default function Rights() {
<Premium setPayType={setPayType} setShowTerms={setShowTerms} payType={payType} premiumPay={premiumPay} loading={loading} style={{ display: userType === 'normal' ? "none" : "flex" }} />
</View>
{/* 支付方式 */}
<View style={[styles.paymentMethod, { display: userType === 'normal' ? "none" : "flex" }]}>
<TouchableOpacity
onPress={() => { setPayChoice('weChatPay') }}
style={{ padding: 16, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderBottomWidth: 1, borderBottomColor: '#FFD38D' }}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
<ChoicePaySvg />
<ThemedText style={{ fontWeight: '700', fontSize: 16, color: "#4C320C" }}>
{t('personal:rights.weChatPay')}
</ThemedText>
</View>
<View style={[styles.payChoice, { backgroundColor: payChoice === 'weChatPay' ? '#FFB645' : '#D9D9D9' }]}>{payChoice === 'weChatPay' ? <YesSvg /> : null}</View>
</TouchableOpacity>
<TouchableOpacity
style={{ padding: 16, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}
onPress={() => { setPayChoice('apple') }}
>
<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 === 'apple' ? '#FFB645' : '#D9D9D9' }]}>
{payChoice === 'apple' ? <YesSvg /> : null}
</View>
</TouchableOpacity>
</View>
<PayTypeModal setConfirmPay={setConfirmPay} modalVisible={showPayType} setModalVisible={setShowPayType} payChoice={payChoice} setPayChoice={setPayChoice} />
{/* 会员权益信息 */}
<View style={{ flex: 1, marginBottom: 80 }}>
<ProRights style={{ display: userType === 'normal' ? "none" : "flex" }} />
@ -207,7 +242,8 @@ export default function Rights() {
style={styles.goPay}
onPress={async () => {
setUserType('premium');
handlePurchase(payType);
// handlePurchase(payType);
setShowPayType(true)
}}
activeOpacity={0.8}
>
@ -350,3 +386,7 @@ const styles = StyleSheet.create({
lineHeight: 32
}
});
function validateAndGrantPurchase(purchase: ProductPurchase) {
throw new Error('Function not implemented.');
}

View 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 }}></ThemedText>
<ThemedText style={styles.modalTitle}></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;

View File

@ -1,12 +1,86 @@
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;
});
}
// 查看产品项
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,
third_party_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
}

View File

@ -111,6 +111,8 @@
"storage": "10GB of Cloud Storage",
"storageText": "Safely store your cherished photos, videos, and generated memories.",
"weChatPay": "WeChat",
"apple": "Apple Pay"
"apple": "Apple Pay",
"confirm": "Confirm",
"cancel": "Cancel"
}
}

View File

@ -111,6 +111,8 @@
"storage": "10GB的云存储",
"storageText": "安全存储你的珍贵照片、视频和生成的记忆。",
"weChatPay": "微信支付",
"apple": "苹果支付"
"apple": "苹果支付",
"confirm": "确认",
"cancel": "取消"
}
}

View File

@ -51,4 +51,49 @@ export interface Policy {
content: string,
created_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;
}