diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 825209a..f03da77 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,3 +1,4 @@ +import { useRevenueCat } from '@/providers/RevenueCatProvider'; import { COLORS } from '@/utils/Colors'; import { useUser } from '@clerk/clerk-expo'; import * as Sentry from '@sentry/react-native'; @@ -5,13 +6,13 @@ import { useRouter } from 'expo-router'; import { Icon, Label, NativeTabs } from 'expo-router/unstable-native-tabs'; import { useShareIntentContext } from 'expo-share-intent'; import { useEffect } from 'react'; -import { Platform } from 'react-native'; -import Purchases, { LOG_LEVEL } from 'react-native-purchases'; +import Purchases from 'react-native-purchases'; export default function TabLayout() { const { hasShareIntent, shareIntent, resetShareIntent, error } = useShareIntentContext(); const router = useRouter(); const user = useUser(); + const { isInitialized } = useRevenueCat(); useEffect(() => { if (hasShareIntent && shareIntent.type === 'weburl' && shareIntent.webUrl) { @@ -20,24 +21,18 @@ export default function TabLayout() { } }, [hasShareIntent]); - useEffect(() => { - Purchases.setLogLevel(LOG_LEVEL.VERBOSE); - - if (Platform.OS === 'ios') { - Purchases.configure({ apiKey: process.env.EXPO_PUBLIC_RC_APPLE_KEY! }); - } else if (Platform.OS === 'android') { - Purchases.configure({ apiKey: process.env.EXPO_PUBLIC_REVENUECAT_PROJECT_GOOGLE_API_KEY! }); - } - }, []); - useEffect(() => { if (user && user.user) { Sentry.setUser({ email: user.user.emailAddresses[0].emailAddress, id: user.user.id }); - Purchases.logIn(user.user.id); + + // Only try to log in to RevenueCat after it's initialized + if (isInitialized) { + Purchases.logIn(user.user.id); + } } else { Sentry.setUser(null); } - }, [user]); + }, [user, isInitialized]); return ( diff --git a/app/(tabs)/settings/index.tsx b/app/(tabs)/settings/index.tsx index 072c55e..ff06f5a 100644 --- a/app/(tabs)/settings/index.tsx +++ b/app/(tabs)/settings/index.tsx @@ -1,3 +1,4 @@ +import { useRevenueCat } from '@/providers/RevenueCatProvider'; import { COLORS } from '@/utils/Colors'; import { isUsePremium, presentPaywall } from '@/utils/paywall'; import { useAuth, useUser } from '@clerk/clerk-expo'; @@ -13,10 +14,14 @@ export default function SettingsScreen() { const { isSignedIn, signOut } = useAuth(); const [isPro, setIsPro] = useState(false); const { user } = useUser(); + const { isInitialized } = useRevenueCat(); useEffect(() => { - isUsePremium().then(setIsPro); - }, []); + // Only check premium status after RevenueCat is initialized + if (isInitialized) { + isUsePremium().then(setIsPro); + } + }, [isInitialized]); const openLink = () => { WebBrowser.openBrowserAsync('https://galaxies.dev'); diff --git a/app/_layout.tsx b/app/_layout.tsx index 75397b7..bc5d181 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,4 +1,5 @@ import migrations from '@/drizzle/migrations'; +import { RevenueCatProvider } from '@/providers/RevenueCatProvider'; import { COLORS } from '@/utils/Colors'; import { ClerkLoaded, ClerkProvider, useAuth } from '@clerk/clerk-expo'; import { tokenCache } from '@clerk/clerk-expo/token-cache'; @@ -109,16 +110,18 @@ const RootLayout = () => { }}> - - }> - - - - - + + + }> + + + + + + diff --git a/providers/RevenueCatProvider.tsx b/providers/RevenueCatProvider.tsx new file mode 100644 index 0000000..6f01951 --- /dev/null +++ b/providers/RevenueCatProvider.tsx @@ -0,0 +1,56 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { Platform } from 'react-native'; +import Purchases, { LOG_LEVEL } from 'react-native-purchases'; + +interface RevenueCatContextType { + isInitialized: boolean; +} + +const RevenueCatContext = createContext({ + isInitialized: false, +}); + +export const useRevenueCat = () => { + const context = useContext(RevenueCatContext); + if (!context) { + throw new Error('useRevenueCat must be used within a RevenueCatProvider'); + } + return context; +}; + +interface RevenueCatProviderProps { + children: React.ReactNode; +} + +export function RevenueCatProvider({ children }: RevenueCatProviderProps) { + const [isInitialized, setIsInitialized] = useState(false); + + useEffect(() => { + const initializeRevenueCat = async () => { + try { + Purchases.setLogLevel(LOG_LEVEL.VERBOSE); + + if (Platform.OS === 'ios') { + await Purchases.configure({ apiKey: process.env.EXPO_PUBLIC_RC_APPLE_KEY! }); + } else if (Platform.OS === 'android') { + await Purchases.configure({ apiKey: process.env.EXPO_PUBLIC_REVENUECAT_PROJECT_GOOGLE_API_KEY! }); + } + + setIsInitialized(true); + } catch (error) { + console.error('Failed to initialize RevenueCat:', error); + // Still set initialized to true to prevent infinite loading + // The error will be handled when actually using RevenueCat + setIsInitialized(true); + } + }; + + initializeRevenueCat(); + }, []); + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/utils/paywall.ts b/utils/paywall.ts index af0e2f3..0283864 100644 --- a/utils/paywall.ts +++ b/utils/paywall.ts @@ -2,30 +2,47 @@ import Purchases from 'react-native-purchases'; import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui'; export async function presentPaywall(): Promise { - // Present paywall for current offering: - const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywall(); + try { + // Present paywall for current offering: + const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywall(); - switch (paywallResult) { - case PAYWALL_RESULT.NOT_PRESENTED: - case PAYWALL_RESULT.ERROR: - case PAYWALL_RESULT.CANCELLED: - return false; - case PAYWALL_RESULT.PURCHASED: - case PAYWALL_RESULT.RESTORED: - return true; - default: - return false; + switch (paywallResult) { + case PAYWALL_RESULT.NOT_PRESENTED: + case PAYWALL_RESULT.ERROR: + case PAYWALL_RESULT.CANCELLED: + return false; + case PAYWALL_RESULT.PURCHASED: + case PAYWALL_RESULT.RESTORED: + return true; + default: + return false; + } + } catch (error) { + console.error('Error presenting paywall:', error); + return false; } } export async function presentPaywallIfNeeded() { - // Present paywall for current offering: - const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywallIfNeeded({ - requiredEntitlementIdentifier: 'premium', - }); + try { + // Present paywall for current offering: + const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywallIfNeeded({ + requiredEntitlementIdentifier: 'premium', + }); + return paywallResult; + } catch (error) { + console.error('Error presenting paywall if needed:', error); + return PAYWALL_RESULT.ERROR; + } } export async function isUsePremium() { - const customerInfo = await Purchases.getCustomerInfo(); - return typeof customerInfo.entitlements.active['premium'] !== 'undefined'; + try { + const customerInfo = await Purchases.getCustomerInfo(); + return typeof customerInfo.entitlements.active['premium'] !== 'undefined'; + } catch (error) { + console.error('Error checking premium status:', error); + // Return false if RevenueCat is not initialized or any other error occurs + return false; + } }