feat: 苹果支付
This commit is contained in:
parent
108b96d0bf
commit
9e41d2e6a3
@ -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.');
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -113,6 +113,7 @@
|
||||
"weChatPay": "WeChat",
|
||||
"apple": "Apple Pay",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel"
|
||||
"cancel": "Cancel",
|
||||
"confirmLoading": "Confirming..."
|
||||
}
|
||||
}
|
||||
@ -113,6 +113,7 @@
|
||||
"weChatPay": "微信支付",
|
||||
"apple": "苹果支付",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消"
|
||||
"cancel": "取消",
|
||||
"confirmLoading": "正在购买..."
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user