diff --git a/app/(tabs)/rights.tsx b/app/(tabs)/rights.tsx index b40a201..52e9423 100644 --- a/app/(tabs)/rights.tsx +++ b/app/(tabs)/rights.tsx @@ -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(false); + // 用户选择购买的loading + const [confirmLoading, setConfirmLoading] = useState(false); // 选择购买方式 const [payChoice, setPayChoice] = useState<'ApplePay'>('ApplePay'); // 获取路由参数 @@ -51,14 +50,12 @@ export default function Rights() { const [premiumPay, setPremiumPay] = useState(); const [loading, setLoading] = useState(false); - // 确认支付 - const [confirmPay, setConfirmPay] = useState(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 ( + {/* 整个页面的中间添加一个loading */} + {confirmLoading && ( + + + + + {t('personal:rights.confirmLoading')} + + + + )} {/* 导航栏 */} - { router.push('/owner') }} style={{ padding: 16 }}> + { router.push('/owner'); setConfirmLoading(false) }} style={{ padding: 16 }}> @@ -219,12 +238,11 @@ export default function Rights() { {/* 支付方式 */} - + {/* */} {/* 会员权益信息 */} - {/* */} {/* 付费按钮 */} { - 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.'); } - diff --git a/components/owner/rights/apple.tsx b/components/owner/rights/apple.tsx deleted file mode 100644 index 2da2305..0000000 --- a/components/owner/rights/apple.tsx +++ /dev/null @@ -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 ( - - {/* Your purchase UI components */} - Connection Status: {connected ? 'Connected' : 'Disconnected'} - Products Ready: {isReady ? 'Yes' : 'No'} - {/* Add your purchase buttons and UI here */} - - ); -} \ No newline at end of file diff --git a/components/owner/rights/utils.ts b/components/owner/rights/utils.ts index a82aac7..db9c48f 100644 --- a/components/owner/rights/utils.ts +++ b/components/owner/rights/utils.ts @@ -83,4 +83,19 @@ export const paySuccess = async (transaction_id: string, third_party_transaction }) }) return payment -} \ No newline at end of file +} + +// 判断订单是否过期 +/** +* 判断指定时间戳是否已过期(即当前时间是否已超过该时间戳) +* @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 a31d2c5..95d6e12 100644 --- a/i18n/locales/en/personal.json +++ b/i18n/locales/en/personal.json @@ -113,6 +113,7 @@ "weChatPay": "WeChat", "apple": "Apple Pay", "confirm": "Confirm", - "cancel": "Cancel" + "cancel": "Cancel", + "confirmLoading": "Confirming..." } } \ No newline at end of file diff --git a/i18n/locales/zh/personal.json b/i18n/locales/zh/personal.json index cafcf38..0ad8e49 100644 --- a/i18n/locales/zh/personal.json +++ b/i18n/locales/zh/personal.json @@ -113,6 +113,7 @@ "weChatPay": "微信支付", "apple": "苹果支付", "confirm": "确认", - "cancel": "取消" + "cancel": "取消", + "confirmLoading": "正在购买..." } } \ No newline at end of file