feat: 苹果支付

This commit is contained in:
jinyaqiu 2025-07-28 19:50:34 +08:00
parent 108b96d0bf
commit 9e41d2e6a3
5 changed files with 81 additions and 284 deletions

View File

@ -2,17 +2,16 @@ 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 PayTypeModal from '@/components/owner/rights/payType';
import Premium, { PayItem } from '@/components/owner/rights/premium';
import ProRights from '@/components/owner/rights/proRights';
import { createOrder, createPayment, getPAy, payFailure, paySuccess } from '@/components/owner/rights/utils';
import { createOrder, createPayment, getPAy, isOrderExpired, payFailure, paySuccess } from '@/components/owner/rights/utils';
import { ThemedText } from '@/components/ThemedText';
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() {
@ -30,8 +29,8 @@ export default function Rights() {
finishTransaction,
validateReceipt,
} = useIAP();
// 购买方式弹窗
const [showPayType, setShowPayType] = useState<boolean>(false);
// 用户选择购买的loading
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
// 选择购买方式
const [payChoice, setPayChoice] = useState<'ApplePay'>('ApplePay');
// 获取路由参数
@ -51,14 +50,12 @@ export default function Rights() {
const [premiumPay, setPremiumPay] = useState<PayItem[]>();
const [loading, setLoading] = useState<boolean>(false);
// 确认支付
const [confirmPay, setConfirmPay] = useState<boolean>(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);
}
@ -85,13 +82,14 @@ export default function Rights() {
const res = await requestPurchase({
request: {
ios: {
sku: "MEMBERSHIP_PRO_QUARTERLY",
sku: payType,
andDangerouslyFinishTransactionAutomaticallyIOS: false,
},
},
});
// 支付成功
paySuccess(transaction_id, res?.transaction_id || "")
console.log('Purchase success:', res);
// 支付成功
paySuccess(transaction_id, res?.transactionId || "")
} catch (error: any) {
console.log('Purchase failed:', error);
// 支付失败
@ -107,8 +105,7 @@ export default function Rights() {
const initializeStore = async () => {
try {
await requestProducts({ skus: ["MEMBERSHIP_PRO_QUARTERLY"], type: 'subs' });
console.log("subscriptions", subscriptions);
await requestProducts({ skus: ["MEMBERSHIP_PRO_QUARTERLY", "MEMBERSHIP_PRO_YEARLY", "MEMBERSHIP_PRO_MONTH"], type: 'subs' });
} catch (error) {
console.error('Failed to initialize store:', error);
}
@ -130,15 +127,23 @@ export default function Rights() {
}, []);
// 用户确认购买时,进行 创建订单,创建支付 接口调用
useEffect(() => {
console.log('confirmPay', confirmPay);
if (confirmPay) {
const confirmPurchase = async () => {
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("您已购买过该权益,无需重复购买");
}, 0);
return
}
try {
// 创建订单
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) => {
@ -147,8 +152,12 @@ export default function Rights() {
}).catch((err) => {
console.log("createOrder", err);
})
} catch (error) {
console.log("confirmPurchase", error);
} finally {
setConfirmLoading(false);
}
}, [confirmPay]);
};
useEffect(() => {
if (currentPurchase) {
@ -157,18 +166,28 @@ export default function Rights() {
}, [currentPurchase]);
useEffect(() => {
fetchPurchaseHistory()
}, [])
console.log('currentPurchaseError', currentPurchaseError);
}, [currentPurchaseError]);
return (
<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 }]}>
{/* 导航栏 */}
<View
style={styles.header}
>
<TouchableOpacity onPress={() => { router.push('/owner') }} style={{ padding: 16 }}>
<TouchableOpacity onPress={() => { router.push('/owner'); setConfirmLoading(false) }} style={{ padding: 16 }}>
<ReturnArrowSvg />
</TouchableOpacity>
<ThemedText style={styles.headerTitle}>
@ -219,12 +238,11 @@ export default function Rights() {
<Premium setPayType={setPayType} setShowTerms={setShowTerms} payType={payType} premiumPay={premiumPay} loading={loading} style={{ display: userType === 'normal' ? "none" : "flex" }} />
</View>
{/* 支付方式 */}
<PayTypeModal setConfirmPay={setConfirmPay} modalVisible={showPayType} setModalVisible={setShowPayType} payChoice={payChoice} setPayChoice={setPayChoice} />
{/* <PayTypeModal setConfirmPay={setConfirmPay} modalVisible={showPayType} setModalVisible={setShowPayType} payChoice={payChoice} setPayChoice={setPayChoice} /> */}
{/* 会员权益信息 */}
<View style={{ flex: 1, marginBottom: 80 }}>
<ProRights style={{ display: userType === 'normal' ? "none" : "flex" }} />
</View>
{/* <ApplePay /> */}
</ScrollView>
{/* 付费按钮 */}
<View style={{
@ -241,9 +259,7 @@ export default function Rights() {
<TouchableOpacity
style={styles.goPay}
onPress={async () => {
setUserType('premium');
// handlePurchase(payType);
setShowPayType(true)
confirmPurchase()
}}
activeOpacity={0.8}
>
@ -269,6 +285,23 @@ export default function Rights() {
}
const styles = StyleSheet.create({
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,
@ -389,4 +422,3 @@ const styles = StyleSheet.create({
function validateAndGrantPurchase(purchase: ProductPurchase) {
throw new Error('Function not implemented.');
}

View File

@ -1,252 +0,0 @@
import { ThemedText } from '@/components/ThemedText';
import { useIAP } from 'expo-iap';
import React, { useCallback, useEffect, useState } from 'react';
import { Alert, InteractionManager, Platform, View } from 'react-native';
// Define your product SKUs
const bulbPackSkus = ['dev.hyo.martie.10bulbs', 'dev.hyo.martie.30bulbs'];
const subscriptionSkus = ['dev.hyo.martie.premium'];
export default function PurchaseScreen() {
const {
connected,
products,
subscriptions,
currentPurchase,
currentPurchaseError,
requestProducts,
requestPurchase,
finishTransaction,
validateReceipt,
} = useIAP();
const [isLoading, setIsLoading] = useState(false);
const [isReady, setIsReady] = useState(false);
// Initialize products when IAP connection is established
useEffect(() => {
if (!connected) return;
const initializeIAP = async () => {
try {
// Get both products and subscriptions
await requestProducts({ skus: bulbPackSkus, type: 'inapp' });
await requestProducts({ skus: subscriptionSkus, type: 'subs' });
setIsReady(true);
} catch (error) {
console.error('Error initializing IAP:', error);
}
};
initializeIAP();
}, [connected, requestProducts]);
// Validate receipt helper
const handleValidateReceipt = useCallback(
async (sku: string, purchase: any) => {
try {
if (Platform.OS === 'ios') {
return await validateReceipt(sku);
} else if (Platform.OS === 'android') {
const purchaseToken = purchase.purchaseTokenAndroid;
const packageName =
purchase.packageNameAndroid || 'your.package.name';
const isSub = subscriptionSkus.includes(sku);
return await validateReceipt(sku, {
packageName,
productToken: purchaseToken,
isSub,
});
}
return { isValid: true }; // Default for unsupported platforms
} catch (error) {
console.error('Receipt validation failed:', error);
return { isValid: false };
}
},
[validateReceipt],
);
// Handle successful purchases
useEffect(() => {
if (currentPurchase) {
handlePurchaseUpdate(currentPurchase);
}
}, [currentPurchase]);
// Handle purchase errors
useEffect(() => {
if (currentPurchaseError) {
setIsLoading(false);
// Don't show error for user cancellation
if (currentPurchaseError.code === 'E_USER_CANCELLED') {
return;
}
Alert.alert(
'Purchase Error',
'Failed to complete purchase. Please try again.',
);
console.error('Purchase error:', currentPurchaseError);
}
}, [currentPurchaseError]);
const handlePurchaseUpdate = async (purchase: any) => {
try {
setIsLoading(true);
console.log('Processing purchase:', purchase);
const productId = purchase.id;
// Validate receipt on your server
const validationResult = await handleValidateReceipt(productId, purchase);
if (validationResult.isValid) {
// Determine if this is a consumable product
const isConsumable = bulbPackSkus.includes(productId);
// Finish the transaction
await finishTransaction({
purchase,
isConsumable, // Set to true for consumable products
});
// Record purchase in your database
await recordPurchaseInDatabase(purchase, productId);
// Update local state (e.g., add bulbs, enable premium features)
await updateLocalState(productId);
// Show success message
showSuccessMessage(productId);
} else {
Alert.alert(
'Validation Error',
'Purchase could not be validated. Please contact support.',
);
}
} catch (error) {
console.error('Error handling purchase:', error);
Alert.alert('Error', 'Failed to process purchase.');
} finally {
setIsLoading(false);
}
};
// Request purchase for products
const handlePurchaseBulbs = async (productId: string) => {
if (!connected) {
Alert.alert(
'Not Connected',
'Store connection unavailable. Please try again later.',
);
return;
}
try {
setIsLoading(true);
// Platform-specific purchase request (v2.7.0+)
await requestPurchase({
request: {
ios: {
sku: productId,
andDangerouslyFinishTransactionAutomaticallyIOS: false,
},
android: {
skus: [productId],
},
},
});
} catch (error) {
setIsLoading(false);
console.error('Purchase request failed:', error);
}
};
// Request purchase for subscriptions
const handlePurchaseSubscription = async (subscriptionId: string) => {
if (!connected) {
Alert.alert(
'Not Connected',
'Store connection unavailable. Please try again later.',
);
return;
}
try {
setIsLoading(true);
// Find subscription to get offer details
const subscription = subscriptions.find((s) => s.id === subscriptionId);
const subscriptionOffers = subscription?.subscriptionOfferDetails?.map(
(offer) => ({
sku: subscriptionId,
offerToken: offer.offerToken,
}),
) || [{ sku: subscriptionId, offerToken: '' }];
// Platform-specific subscription request (v2.7.0+)
await requestPurchase({
request: {
ios: {
sku: subscriptionId,
},
android: {
skus: [subscriptionId],
subscriptionOffers,
},
},
type: 'subs',
});
} catch (error) {
setIsLoading(false);
console.error('Subscription request failed:', error);
}
};
const recordPurchaseInDatabase = async (purchase: any, productId: string) => {
// Implement your database recording logic here
console.log('Recording purchase in database:', { purchase, productId });
};
const updateLocalState = async (productId: string) => {
// Update your local app state based on the purchase
if (bulbPackSkus.includes(productId)) {
// Add bulbs to user's account
const bulbCount = productId.includes('10bulbs') ? 10 : 30;
console.log(`Adding ${bulbCount} bulbs to user account`);
} else if (subscriptionSkus.includes(productId)) {
// Enable premium features
console.log('Enabling premium features');
}
};
const showSuccessMessage = (productId: string) => {
InteractionManager.runAfterInteractions(() => {
if (bulbPackSkus.includes(productId)) {
const bulbCount = productId.includes('10bulbs') ? 10 : 30;
Alert.alert(
'Thank You!',
`${bulbCount} bulbs have been added to your account.`,
);
} else if (subscriptionSkus.includes(productId)) {
Alert.alert(
'Thank You!',
'Premium subscription activated successfully.',
);
}
});
};
return (
<View>
{/* Your purchase UI components */}
<ThemedText>Connection Status: {connected ? 'Connected' : 'Disconnected'}</ThemedText>
<ThemedText>Products Ready: {isReady ? 'Yes' : 'No'}</ThemedText>
{/* Add your purchase buttons and UI here */}
</View>
);
}

View File

@ -83,4 +83,19 @@ export const paySuccess = async (transaction_id: string, third_party_transaction
})
})
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;
}

View File

@ -113,6 +113,7 @@
"weChatPay": "WeChat",
"apple": "Apple Pay",
"confirm": "Confirm",
"cancel": "Cancel"
"cancel": "Cancel",
"confirmLoading": "Confirming..."
}
}

View File

@ -113,6 +113,7 @@
"weChatPay": "微信支付",
"apple": "苹果支付",
"confirm": "确认",
"cancel": "取消"
"cancel": "取消",
"confirmLoading": "正在购买..."
}
}