feat: push

This commit is contained in:
jinyaqiu 2025-07-25 17:37:30 +08:00
parent 9c5d7a53a0
commit 1d9f80f975
3 changed files with 302 additions and 10 deletions

View File

@ -20,6 +20,18 @@ export default function Rights() {
const insets = useSafeAreaInsets();
const router = useRouter();
const { t } = useTranslation();
const {
connected,
products,
subscriptions,
currentPurchase,
currentPurchaseError,
requestProducts,
requestPurchase,
finishTransaction,
validateReceipt,
} = useIAP();
// 选择购买方式
const [payChoice, setPayChoice] = useState<'weChatPay' | 'apple'>('weChatPay');
// 获取路由参数
@ -49,16 +61,44 @@ export default function Rights() {
useEffect(() => {
getPAy();
}, []);
// 处理购买
const handlePurchase = async (productId: string) => {
console.log(productId);
function TestComponent() {
const { connected } = useIAP();
try {
// Platform-specific purchase requests (v2.7.0+)
await requestPurchase({
request: {
ios: {
sku: "MEMBERSHIP_PRO_QUARTERLY",
andDangerouslyFinishTransactionAutomaticallyIOS: false,
},
},
});
} catch (error) {
alert(productId)
console.error('Purchase failed:', error);
}
};
useEffect(() => {
console.log('connected', connected);
console.log('IAP Connection status:', connected);
if (!connected) return;
const initializeStore = async () => {
try {
await requestProducts({ skus: ["MEMBERSHIP_PRO_QUARTERLY"], type: 'subs' });
// await getSubscriptions(["MEMBERSHIP_PRO_QUARTERLY"])
console.log("products", products);
} catch (error) {
console.error('Failed to initialize store:', error);
}
};
initializeStore();
}, [connected, premiumPay]);
return null;
}
TestComponent()
return (
<View style={{ flex: 1 }}>
<ScrollView style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom + 80 }]}>
@ -149,8 +189,7 @@ export default function Rights() {
<View style={{ flex: 1, marginBottom: 80 }}>
<ProRights style={{ display: userType === 'normal' ? "none" : "flex" }} />
</View>
{/* <ApplePay /> */}
</ScrollView>
{/* 付费按钮 */}
<View style={{
@ -168,6 +207,7 @@ export default function Rights() {
style={styles.goPay}
onPress={async () => {
setUserType('premium');
handlePurchase(payType);
}}
activeOpacity={0.8}
>

View File

@ -0,0 +1,252 @@
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

@ -67,11 +67,11 @@ const Premium = (props: Props) => {
{item.product_code?.split('_')[item.product_code?.split('_')?.length - 1]}
</ThemedText>
<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>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<ThemedText style={[styles.titleText, { fontSize: 12, color: "#AC7E35", textDecorationLine: 'line-through' }]}>
$ {item.discount_amount.amount}
$ {item.unit_price.amount}
</ThemedText>
</View>
</TouchableOpacity>