diff --git a/docs/docs/api/methods/listeners.md b/docs/docs/api/methods/listeners.md index b8205b80e..5dc440149 100644 --- a/docs/docs/api/methods/listeners.md +++ b/docs/docs/api/methods/listeners.md @@ -379,19 +379,14 @@ For simpler usage, consider using the `useIAP` hook which automatically manages import {useIAP} from 'react-native-iap'; export default function StoreComponent() { - const {currentPurchase, currentPurchaseError} = useIAP(); - - useEffect(() => { - if (currentPurchase) { - handlePurchaseUpdate(currentPurchase); - } - }, [currentPurchase]); - - useEffect(() => { - if (currentPurchaseError) { - handlePurchaseError(currentPurchaseError); - } - }, [currentPurchaseError]); + const {/* other props */} = useIAP({ + onPurchaseSuccess: async (purchase) => { + await handlePurchaseUpdate(purchase); + }, + onPurchaseError: (error) => { + handlePurchaseError(error); + }, + }); // Rest of component } diff --git a/docs/docs/api/use-iap.md b/docs/docs/api/use-iap.md index b67e8b948..9ea031033 100644 --- a/docs/docs/api/use-iap.md +++ b/docs/docs/api/use-iap.md @@ -23,9 +23,8 @@ import {useIAP} from 'react-native-iap'; The `useIAP` hook follows React Hooks conventions and differs from calling functions directly from `react-native-iap` (index exports): - **Automatic connection**: Automatically calls `initConnection` on mount and `endConnection` on unmount. -- **Void-returning methods**: Methods like `fetchProducts`, `requestPurchase`, `getAvailablePurchases`, etc. return `Promise` in the hook. They do not resolve to data. Instead, they update internal state exposed by the hook: `products`, `subscriptions`, `availablePurchases`, `currentPurchase`, etc. -- **Don’t await for data**: When using the hook, do not write `const x = await fetchProducts(...)`. Call the method, then read the corresponding state from the hook. -- **Prefer callbacks over `currentPurchase`**: `currentPurchase` was historically useful for debugging and migration, but for new code you should rely on `onPurchaseSuccess` and `onPurchaseError` options passed to `useIAP`. +- **Void-returning methods**: Methods like `fetchProducts`, `requestPurchase`, `getAvailablePurchases`, etc. return `Promise` in the hook. They do not resolve to data. Instead, they update internal state exposed by the hook: `products`, `subscriptions`, `availablePurchases`, etc. +- **Don't await for data**: When using the hook, do not write `const x = await fetchProducts(...)`. Call the method, then read the corresponding state from the hook. ## Basic Usage @@ -35,8 +34,6 @@ const { products, subscriptions, availablePurchases, - currentPurchase, // Debugging/migration friendly; prefer callbacks - currentPurchaseError, // Debugging/migration friendly; prefer callbacks fetchProducts, requestPurchase, validateReceipt, @@ -144,19 +141,6 @@ interface UseIAPOptions { )); ``` -#### currentPurchase - -- **Type**: `Purchase | null` -- **Description**: Last purchase event captured by the hook. This value is primarily helpful for debugging and migration. For production flows, prefer handling purchase results via `onPurchaseSuccess` and errors via `onPurchaseError` passed to `useIAP`. -- **Example (debug logging only)**: - - ```tsx - useEffect(() => { - if (currentPurchase) { - console.log('Debug purchase event:', currentPurchase.id); - } - }, [currentPurchase]); - ``` #### currentPurchaseError @@ -235,7 +219,7 @@ interface UseIAPOptions { ```tsx const buyProduct = async (productId: string) => { try { - // In hook: returns void. Listen via callbacks or `currentPurchase`. + // In hook: returns void. Listen via callbacks. await requestPurchase({ request: { ios: {sku: productId}, diff --git a/docs/docs/examples/available-purchases.md b/docs/docs/examples/available-purchases.md index f35f04ad7..a32afc37f 100644 --- a/docs/docs/examples/available-purchases.md +++ b/docs/docs/examples/available-purchases.md @@ -10,11 +10,11 @@ import AdFitTopFixed from "@site/src/uis/AdFitTopFixed"; -This example shows how to list and restore previously purchased items (non‑consumables and active subscriptions) using `getAvailablePurchases()` and `getActiveSubscriptions()`. +This guide demonstrates how to list and restore previously purchased items (non‑consumables and active subscriptions) using `getAvailablePurchases()` and `getActiveSubscriptions()`. -View the full example source: - -- GitHub: https://github.com/hyochan/react-native-iap/blob/main/example/app/available-purchases.tsx +:::note +The complete working example can be found at [example/screens/AvailablePurchases.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/screens/AvailablePurchases.tsx). Note that the example code was heavily vibe-coded with Claude and is quite verbose/messy for demonstration purposes - use it as a reference only. +::: ## Restore Flow diff --git a/docs/docs/examples/offer-code.md b/docs/docs/examples/offer-code.md index 561ec6709..d3f040c6e 100644 --- a/docs/docs/examples/offer-code.md +++ b/docs/docs/examples/offer-code.md @@ -10,11 +10,11 @@ import AdFitTopFixed from "@site/src/uis/AdFitTopFixed"; -Redeem App Store offer/promo codes using the native iOS sheet. This is useful for subscription promotional codes and requires a real iOS device. +This guide demonstrates how to redeem App Store offer/promo codes using the native iOS sheet. This is useful for subscription promotional codes and requires a real iOS device. -View the full example source: - -- GitHub: https://github.com/hyochan/react-native-iap/blob/main/example/app/offer-code.tsx +:::note +The complete working example can be found at [example/screens/OfferCode.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/screens/OfferCode.tsx). Note that the example code was heavily vibe-coded with Claude and is quite verbose/messy for demonstration purposes - use it as a reference only. +::: ## Usage diff --git a/docs/docs/examples/purchase-flow.md b/docs/docs/examples/purchase-flow.md index 5ee8831da..ed6e13ceb 100644 --- a/docs/docs/examples/purchase-flow.md +++ b/docs/docs/examples/purchase-flow.md @@ -10,11 +10,11 @@ import AdFitTopFixed from "@site/src/uis/AdFitTopFixed"; -This example walks through a clean purchase flow using react-native-iap with the `useIAP` hook and the new platform‑specific request shape. It mirrors the working sample in `example/app/purchase-flow.tsx`. +This guide demonstrates a clean purchase flow using react-native-iap with the `useIAP` hook and the new platform‑specific request shape. -View the full example source: - -- GitHub: [example/app/purchase-flow.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/app/purchase-flow.tsx) +:::note +The complete working example can be found at [example/screens/PurchaseFlow.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/screens/PurchaseFlow.tsx). Note that the example code was heavily vibe-coded with Claude and is quite verbose/messy for demonstration purposes - use it as a reference only. +::: ## Flow Overview diff --git a/docs/docs/examples/subscription-flow.md b/docs/docs/examples/subscription-flow.md index 32f84c6d3..6ad02001d 100644 --- a/docs/docs/examples/subscription-flow.md +++ b/docs/docs/examples/subscription-flow.md @@ -12,806 +12,618 @@ import AdFitTopFixed from "@site/src/uis/AdFitTopFixed"; -This example walks through a practical subscriptions flow with react-native-iap. It mirrors the working sample in `example/app/subscription-flow.tsx`, including status checks, renewal handling, and subscription management UI. +This guide demonstrates practical subscription scenarios with react-native-iap. -View the full example source: +:::note +The complete working example can be found at [example/screens/SubscriptionFlow.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/screens/SubscriptionFlow.tsx). Note that the example code was heavily vibe-coded with Claude and is quite verbose/messy for demonstration purposes - use it as a reference only. +::: -- GitHub: [example/app/subscription-flow.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/app/subscription-flow.tsx) +## 1. Purchasing a Subscription with `requestPurchase` -## Important: Platform-Specific Subscription Properties - -When checking subscription status, different platforms provide different properties: - -### iOS Subscription Properties - -- **`expirationDateIos`**: Unix timestamp (milliseconds) indicating when the subscription expires -- **`originalTransactionDateIos`**: Original purchase date -- **`environmentIos`**: Can be 'Production' or 'Sandbox' (useful for testing) - -### Android Subscription Properties - -- **`autoRenewingAndroid`**: Boolean indicating if the subscription will auto-renew -- **`purchaseStateAndroid`**: Purchase state (0 = purchased, 1 = canceled) -- **`obfuscatedAccountIdAndroid`**: Account identifier if provided during purchase - -### Key Differences - -- **iOS**: You must check `expirationDateIos` against current time to determine if active -- **Android**: You can check `autoRenewingAndroid` - if false, the user has canceled - -⚠️ **Note**: Always validate subscription status on your server for production apps. Client-side checks are useful for UI updates but should not be the sole source of truth. - -## Complete Subscription Flow - -View the full example source: - -- GitHub: [example/app/subscription-flow.tsx](https://github.com/hyochan/react-native-iap/blob/main/example/app/subscription-flow.tsx) +### Basic Subscription Purchase ```tsx -import React, {useEffect, useState} from 'react'; -import { - View, - Text, - TouchableOpacity, - Alert, - StyleSheet, - ScrollView, - ActivityIndicator, - Platform, -} from 'react-native'; import {useIAP} from 'react-native-iap'; +import {Platform} from 'react-native'; -// Subscription product IDs -const SUBSCRIPTION_SKUS = [ - 'com.yourapp.premium_monthly', - 'com.yourapp.premium_yearly', -]; - -interface SubscriptionStatus { - isActive: boolean; - productId?: string; - expirationDate?: Date; - autoRenewing?: boolean; - inGracePeriod?: boolean; -} - -export default function SubscriptionManager() { +function SubscriptionPurchase() { const { connected, subscriptions, - currentPurchase, - currentPurchaseError, - fetchProducts, - getAvailablePurchases, requestPurchase, - finishTransaction, + fetchProducts, } = useIAP(); - const [loading, setLoading] = useState(false); - const [subscriptionStatus, setSubscriptionStatus] = - useState({ - isActive: false, - }); - - // Initialize and load subscriptions useEffect(() => { + // Load subscription products if (connected) { - loadSubscriptions(); - checkSubscriptionStatus(); + fetchProducts({ + skus: ['com.app.premium_monthly', 'com.app.premium_yearly'], + type: 'subs', + }); } }, [connected]); - // Handle subscription purchases - useEffect(() => { - if (currentPurchase) { - handleSubscriptionPurchase(currentPurchase); - } - }, [currentPurchase]); - - // Handle purchase errors - useEffect(() => { - if (currentPurchaseError) { - handlePurchaseError(currentPurchaseError); + const purchaseSubscription = async (productId: string) => { + if (!connected) { + Alert.alert('Error', 'Store not connected'); + return; } - }, [currentPurchaseError]); - const loadSubscriptions = async () => { try { - setLoading(true); - await fetchProducts({skus: SUBSCRIPTION_SKUS, type: 'subs'}); - console.log('Subscriptions loaded'); - } catch (error) { - console.error('Failed to load subscriptions:', error); - Alert.alert('Error', 'Failed to load subscription options'); - } finally { - setLoading(false); - } - }; + // Find the subscription product + const subscription = subscriptions.find(sub => sub.id === productId); + if (!subscription) { + throw new Error('Subscription not found'); + } - const checkSubscriptionStatus = async () => { - try { - // In hook: updates state, does not return purchases - await getAvailablePurchases(); - const activeSubscription = findActiveSubscription(availablePurchases); + // Platform-specific purchase request + await requestPurchase({ + request: { + ios: { + sku: productId, + andDangerouslyFinishTransactionAutomatically: false, + }, + android: { + skus: [productId], + // Android requires subscriptionOffers for subscriptions + subscriptionOffers: subscription.subscriptionOfferDetailsAndroid + ?.map(offer => ({ + sku: subscription.id, + offerToken: offer.offerToken, + })) || [], + }, + }, + type: 'subs', + }); + + // Success handling is done in onPurchaseSuccess callback - if (activeSubscription) { - const status = await validateSubscriptionStatus(activeSubscription); - setSubscriptionStatus(status); - } else { - setSubscriptionStatus({isActive: false}); - } } catch (error) { - console.error('Failed to check subscription status:', error); + console.error('Purchase failed:', error); + Alert.alert('Error', 'Failed to purchase subscription'); } }; - const findActiveSubscription = (purchases) => { - // Find subscriptions and check if they're still active - return purchases.find((purchase) => { - if (!SUBSCRIPTION_SKUS.includes(purchase.productId)) { - return false; - } - // Check if the subscription is still active - return isSubscriptionActive(purchase); - }); - }; + return ( + + {subscriptions.map(sub => ( + purchaseSubscription(sub.id)} + > + {sub.title} - {sub.localizedPrice} + + ))} + + ); +} +``` - /** - * Platform-specific subscription status checking - * iOS: Uses expirationDateIos to check if subscription is expired - * Android: Uses autoRenewingAndroid to check renewal status - */ - const isSubscriptionActive = (purchase) => { - const currentTime = Date.now(); - - // Check platform-specific subscription properties - if (Platform.OS === 'ios') { - // iOS: Check expiration date - if (purchase.expirationDateIos) { - console.log( - 'iOS Subscription expiration:', - new Date(purchase.expirationDateIos).toISOString(), - ); - return purchase.expirationDateIos > currentTime; - } +### Handling Purchase Success with Hook Callbacks - // For sandbox/development environment - if (purchase.environmentIOS === 'Sandbox') { - console.log('iOS Sandbox environment detected'); - // In sandbox, also check if it's a recent purchase (within 24 hours) - const dayInMs = 24 * 60 * 60 * 1000; - if ( - purchase.transactionDate && - currentTime - purchase.transactionDate < dayInMs - ) { - return true; - } - } - } else if (Platform.OS === 'android') { - // Android: Check auto-renewing status - if (purchase.autoRenewingAndroid !== undefined) { - console.log( - 'Android auto-renewing status:', - purchase.autoRenewingAndroid, - ); - return purchase.autoRenewingAndroid; - } +```tsx +function SubscriptionManager() { + const [activeSubscription, setActiveSubscription] = useState(null); - // Fallback: Check if purchase is recent (within 30 days for monthly subscriptions) - const monthInMs = 30 * 24 * 60 * 60 * 1000; - if ( - purchase.transactionDate && - currentTime - purchase.transactionDate < monthInMs - ) { - return true; - } - } + const { + connected, + subscriptions, + requestPurchase, + finishTransaction, + } = useIAP({ + onPurchaseSuccess: async (purchase) => { + console.log('Purchase successful:', purchase.productId); - // If we can't determine status, assume inactive - return false; - }; + // Validate with your server + const isValid = await validatePurchaseOnServer(purchase); - const validateSubscriptionStatus = async (purchase) => { - try { - // Validate subscription on your server - const response = await fetch( - 'https://your-server.com/validate-subscription', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - productId: purchase.productId, - purchaseToken: purchase.purchaseToken, // Unified field (iOS: JWS, Android: purchaseToken) - packageName: (purchase as PurchaseAndroid)?.packageNameAndroid, // Will be needed in android - }), - }, - ); + if (isValid) { + // Update local state + setActiveSubscription(purchase.productId); - const result = await response.json(); + // Finish the transaction + await finishTransaction({purchase}); - return { - isActive: result.isActive, - productId: purchase.productId, - expirationDate: new Date(result.expirationDate), - autoRenewing: result.autoRenewing, - inGracePeriod: result.inGracePeriod, - }; - } catch (error) { - console.error('Subscription validation error:', error); - return {isActive: false}; - } + Alert.alert('Success', 'Subscription activated!'); + } + }, + onPurchaseError: (error) => { + if (error.code !== 'E_USER_CANCELLED') { + Alert.alert('Error', error.message); + } + }, + }); + + // Purchase function remains simple + const subscribe = async (productId: string) => { + const subscription = subscriptions.find(s => s.id === productId); + if (!subscription) return; + + await requestPurchase({ + request: { + ios: { + sku: productId, + andDangerouslyFinishTransactionAutomatically: false, + }, + android: { + skus: [productId], + subscriptionOffers: subscription.subscriptionOfferDetailsAndroid + ?.map(offer => ({ + sku: subscription.id, + offerToken: offer.offerToken, + })) || [], + }, + }, + type: 'subs', + }); + // Don't handle success here - use onPurchaseSuccess callback }; +} +``` - const handleSubscriptionPurchase = async (purchase) => { - try { - console.log('Processing subscription purchase:', purchase.productId); - - // Validate the subscription purchase - const subscriptionInfo = await validateSubscriptionStatus(purchase); +## 2. Checking Subscription Status with `getActiveSubscriptions` - if (subscriptionInfo.isActive) { - // Grant subscription benefits - await grantSubscriptionBenefits(purchase); +### Basic Status Check After Purchase - // Update local status - setSubscriptionStatus(subscriptionInfo); +```tsx +import {useIAP} from 'react-native-iap'; +import {Platform} from 'react-native'; - // Finish the transaction - await finishTransaction({purchase}); +function useSubscriptionStatus() { + const {getActiveSubscriptions} = useIAP(); + const [isSubscribed, setIsSubscribed] = useState(false); + const [subscriptionDetails, setSubscriptionDetails] = useState(null); - Alert.alert( - 'Subscription Activated', - `Welcome to Premium! Your subscription is now active.`, - ); + const checkSubscriptionStatus = async () => { + try { + // Get active subscriptions - returns array of active subscriptions + const activeSubscriptions = await getActiveSubscriptions(); + + if (activeSubscriptions.length > 0) { + // User has at least one active subscription + setIsSubscribed(true); + + // Check specific subscription details + const subscription = activeSubscriptions[0]; + + // Platform-specific status checks + if (Platform.OS === 'ios') { + // iOS provides expirationDateIos + const isExpired = subscription.expirationDateIos < Date.now(); + setSubscriptionDetails({ + productId: subscription.productId, + isActive: !isExpired, + expiresAt: new Date(subscription.expirationDateIos), + environment: subscription.environmentIOS, // 'Production' or 'Sandbox' + }); + } else { + // Android provides autoRenewingAndroid + setSubscriptionDetails({ + productId: subscription.productId, + isActive: subscription.autoRenewingAndroid, + willAutoRenew: subscription.autoRenewingAndroid, + purchaseState: subscription.purchaseStateAndroid, // 0 = purchased, 1 = canceled + }); + } } else { - Alert.alert('Error', 'Subscription validation failed'); + setIsSubscribed(false); + setSubscriptionDetails(null); } } catch (error) { - console.error('Error processing subscription:', error); - Alert.alert('Error', 'Failed to activate subscription'); + console.error('Failed to check subscription status:', error); } }; - const handlePurchaseError = (error) => { - console.error('Subscription purchase error:', error); + return {isSubscribed, subscriptionDetails, checkSubscriptionStatus}; +} +``` - switch (error.code) { - case 'E_USER_CANCELLED': - // User cancelled - no action needed - break; - case 'E_ALREADY_OWNED': - Alert.alert( - 'Already Subscribed', - 'You already have an active subscription. Check your subscription status.', - ); - checkSubscriptionStatus(); // Refresh status - break; - default: - Alert.alert( - 'Subscription Failed', - error.message || 'Unknown error occurred', - ); - break; - } - }; +### Checking Multiple Subscription Tiers - const grantSubscriptionBenefits = async (purchase) => { - try { - // Grant subscription benefits on your server - await fetch('https://your-server.com/grant-subscription', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userId: 'current-user-id', - productId: purchase.productId, - transactionId: purchase.transactionId, - }), - }); +```tsx +const SUBSCRIPTION_SKUS = { + BASIC: 'com.app.basic_monthly', + PREMIUM: 'com.app.premium_monthly', + PREMIUM_YEARLY: 'com.app.premium_yearly', +}; - console.log('Subscription benefits granted'); - } catch (error) { - console.error('Failed to grant subscription benefits:', error); - throw error; - } - }; +async function getUserSubscriptionTier() { + const {getActiveSubscriptions} = useIAP(); - /** - * Platform-Specific Subscription Purchase Options - * - * Android: Requires subscriptionOffers array with offer tokens from fetchProducts() - * subscriptionOffers: Array of AndroidSubscriptionOfferInput objects containing sku and offerToken - * Each offer token corresponds to a specific pricing plan (base plan, introductory offer, etc.) - * Without subscriptionOffers, Android purchases will fail with "number of skus (1) must match: the number of offerTokens (0)" - * - * iOS: Optional withOffer for promotional discounts - * withOffer: DiscountOfferInputIOS object for applying promotional offers - * Includes offerIdentifier, keyIdentifier, nonce, signature, and timestamp - * Only needed when applying specific promotional offers to the purchase - */ - const purchaseSubscription = async (productId) => { - if (!connected) { - Alert.alert('Error', 'Store is not connected'); - return; - } + try { + const activeSubscriptions = await getActiveSubscriptions(); - try { - console.log('Requesting subscription:', productId); + // Check for premium yearly first (highest tier) + const yearlyPremium = activeSubscriptions.find( + sub => sub.productId === SUBSCRIPTION_SKUS.PREMIUM_YEARLY + ); + if (yearlyPremium) return 'PREMIUM_YEARLY'; - // Find the subscription product to get offer details - const subscription = subscriptions.find((sub) => sub.id === productId); - if (!subscription) { - Alert.alert('Error', 'Subscription product not found'); - return; - } + // Then check monthly premium + const monthlyPremium = activeSubscriptions.find( + sub => sub.productId === SUBSCRIPTION_SKUS.PREMIUM + ); + if (monthlyPremium) return 'PREMIUM'; - // 2) Build subscriptionOffers from the fetched product details - const subscriptionOffers = ( - subscription.subscriptionOfferDetailsAndroid ?? [] - ).map((offer) => ({ - sku: subscription.id, - offerToken: offer.offerToken, - })); - - // Platform-specific subscription purchase requests - // For Android: subscriptionOffers are required for subscriptions to specify pricing plans and offer tokens - // For iOS: withOffer can be used for promotional offers or discounts - await requestPurchase({ - request: { - ios: { - sku: productId, - andDangerouslyFinishTransactionAutomatically: false, - // withOffer: { /* DiscountOfferInputIOS for promotional offers */ } - }, - android: { - skus: [productId], - subscriptionOffers, - }, - }, - type: 'subs', - }); - } catch (error) { - console.error('Subscription request failed:', error); - Alert.alert('Error', 'Failed to start subscription purchase'); - } - }; + // Finally check basic + const basic = activeSubscriptions.find( + sub => sub.productId === SUBSCRIPTION_SKUS.BASIC + ); + if (basic) return 'BASIC'; - const openSubscriptionManagement = () => { - import('react-native-iap').then(({deepLinkToSubscriptions}) => { - deepLinkToSubscriptions({skuAndroid: 'your_subscription_sku'}); - }); - }; + return 'FREE'; + } catch (error) { + console.error('Failed to get subscription tier:', error); + return 'FREE'; + } +} +``` - const restoreSubscriptions = async () => { - try { - setLoading(true); - await checkSubscriptionStatus(); - Alert.alert('Restore Complete', 'Subscription status has been updated'); - } catch (error) { - Alert.alert('Error', 'Failed to restore subscriptions'); - } finally { - setLoading(false); - } - }; +## 3. Subscription Plan Changes (Upgrade/Downgrade) - const formatDate = (date: Date) => { - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); - }; +### iOS: Automatic Subscription Group Management - const renderSubscriptionStatus = () => { - if (subscriptionStatus.isActive) { - return ( - - Premium Active - - Your premium subscription is active - - - {subscriptionStatus.expirationDate && ( - - {subscriptionStatus.autoRenewing ? 'Renews' : 'Expires'} on{' '} - {formatDate(subscriptionStatus.expirationDate)} - - )} - - {subscriptionStatus.inGracePeriod && ( - - Your subscription is in grace period. Please update your payment - method. - - )} - - - Manage Subscription - - - ); - } +On iOS, subscriptions in the same subscription group automatically replace each other when purchased. The App Store handles the proration and timing automatically. - return ( - - No Active Subscription - - Subscribe to unlock premium features - - - ); - }; +```tsx +// iOS Subscription Configuration in App Store Connect: +// Subscription Group: "Premium Access" +// - com.app.premium_monthly (Rank 1) +// - com.app.premium_yearly (Rank 2 - higher rank = better value) - const renderSubscriptionOption = (subscription) => { - const isYearly = subscription.productId.includes('yearly'); - const savings = isYearly ? '2 months free!' : null; - - return ( - - - - {isYearly ? 'Yearly Premium' : 'Monthly Premium'} - - - {subscription.localizedPrice} - - {subscription.subscriptionPeriod && ( - - per {subscription.subscriptionPeriod} - - )} - {savings && {savings}} - +async function handleIOSSubscriptionChange(newProductId: string) { + const {requestPurchase, getActiveSubscriptions} = useIAP(); - purchaseSubscription(subscription.productId)} - disabled={loading || subscriptionStatus.isActive} - > - - {subscriptionStatus.isActive ? 'Active' : 'Subscribe'} - - - + try { + // Check current subscription + const currentSubs = await getActiveSubscriptions(); + const currentSub = currentSubs.find(sub => + sub.productId === 'com.app.premium_monthly' || + sub.productId === 'com.app.premium_yearly' ); - }; - if (!connected) { - return ( - - - Connecting to store... - - ); - } + if (currentSub) { + console.log(`Changing from ${currentSub.productId} to ${newProductId}`); + // iOS automatically handles the switch when both products are in the same group + } - return ( - - Subscription Management + // Simply purchase the new subscription + // iOS will automatically: + // 1. Cancel the old subscription at the end of the current period + // 2. Start the new subscription + // 3. Handle any necessary proration + await requestPurchase({ + request: { + ios: { + sku: newProductId, + andDangerouslyFinishTransactionAutomatically: false, + }, + android: { + skus: [newProductId], + }, + }, + type: 'subs', + }); - {renderSubscriptionStatus()} + Alert.alert( + 'Subscription Updated', + 'Your subscription will change at the end of the current billing period.' + ); - Subscription Options + } catch (error) { + console.error('Subscription change failed:', error); + } +} - {loading ? ( - - - Loading subscriptions... - - ) : ( - {subscriptions.map(renderSubscriptionOption)} - )} +// Usage example +function IOSSubscriptionManager() { + const handleUpgradeToYearly = () => { + handleIOSSubscriptionChange('com.app.premium_yearly'); + }; - - - Restore Purchases - - - - - - Subscriptions auto-renew unless cancelled. You can manage your - subscriptions in your device settings. - - - + const handleDowngradeToMonthly = () => { + handleIOSSubscriptionChange('com.app.premium_monthly'); + }; + + return ( + + iOS subscriptions in the same group auto-replace each other +