Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { useRevenueCat } from '@/providers/RevenueCatProvider';
import { COLORS } from '@/utils/Colors';
import { useUser } from '@clerk/clerk-expo';
import * as Sentry from '@sentry/react-native';
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) {
Expand All @@ -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 (
<NativeTabs blurEffect="systemChromeMaterial" tintColor={COLORS.textDark}>
Expand Down
9 changes: 7 additions & 2 deletions app/(tabs)/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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');
Expand Down
23 changes: 13 additions & 10 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -109,16 +110,18 @@ const RootLayout = () => {
}}>
<ClerkProvider tokenCache={tokenCache}>
<ClerkLoaded>
<KeyboardProvider>
<Suspense fallback={<ActivityIndicator />}>
<SQLiteProvider
databaseName={DATABASE_NAME}
options={{ enableChangeListener: true }}
useSuspense>
<RootNav />
</SQLiteProvider>
</Suspense>
</KeyboardProvider>
<RevenueCatProvider>
<KeyboardProvider>
<Suspense fallback={<ActivityIndicator />}>
<SQLiteProvider
databaseName={DATABASE_NAME}
options={{ enableChangeListener: true }}
useSuspense>
<RootNav />
</SQLiteProvider>
</Suspense>
</KeyboardProvider>
</RevenueCatProvider>
</ClerkLoaded>
</ClerkProvider>
</ShareIntentProvider>
Expand Down
56 changes: 56 additions & 0 deletions providers/RevenueCatProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<RevenueCatContextType>({
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 (
<RevenueCatContext.Provider value={{ isInitialized }}>
{children}
</RevenueCatContext.Provider>
);
}
53 changes: 35 additions & 18 deletions utils/paywall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,47 @@ import Purchases from 'react-native-purchases';
import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui';

export async function presentPaywall(): Promise<boolean> {
// 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;
}
}