diff --git a/apps/expo/app/(app)/ai-chat.tsx b/apps/expo/app/(app)/ai-chat.tsx index 491cb89472..570ef25b76 100644 --- a/apps/expo/app/(app)/ai-chat.tsx +++ b/apps/expo/app/(app)/ai-chat.tsx @@ -34,6 +34,7 @@ import { useTemperatureUnit } from 'expo-app/features/auth/hooks/useTemperatureU import { useWeightUnit } from 'expo-app/features/auth/hooks/useWeightUnit'; import { getPackItems, packItemsStore } from 'expo-app/features/packs/store/packItems'; import { packsStore } from 'expo-app/features/packs/store/packs'; +import { PaywallGate } from 'expo-app/features/purchases'; import { useActiveLocation } from 'expo-app/features/weather/hooks'; import type { WeatherLocation } from 'expo-app/features/weather/types'; import { authClient, getStoredSessionToken } from 'expo-app/lib/auth-client'; @@ -78,7 +79,7 @@ const ROOT_STYLE: ViewStyle = { minHeight: 2, }; -export default function AIChat() { +function AIChat() { const { colors, isDarkColorScheme } = useColorScheme(); const insets = useSafeAreaInsets(); const { progress } = useReanimatedKeyboardAnimation(); @@ -665,3 +666,11 @@ function Composer({ ); } + +export default function AIChatRoute() { + return ( + + + + ); +} diff --git a/apps/expo/app/(app)/pack-templates/index.tsx b/apps/expo/app/(app)/pack-templates/index.tsx index 002a312723..91fe05de18 100644 --- a/apps/expo/app/(app)/pack-templates/index.tsx +++ b/apps/expo/app/(app)/pack-templates/index.tsx @@ -1,5 +1,10 @@ import { PackTemplateListScreen } from 'expo-app/features/pack-templates/screens/PackTemplateListScreen'; +import { PaywallGate } from 'expo-app/features/purchases'; -export default function () { - return ; +export default function PackTemplatesRoute() { + return ( + + + + ); } diff --git a/apps/expo/app/(app)/season-suggestions-results.tsx b/apps/expo/app/(app)/season-suggestions-results.tsx index ed3c123d01..02b5f5286c 100644 --- a/apps/expo/app/(app)/season-suggestions-results.tsx +++ b/apps/expo/app/(app)/season-suggestions-results.tsx @@ -9,6 +9,7 @@ import { SeasonSuggestionsError, useSeasonSuggestions, } from 'expo-app/features/packs/hooks/useSeasonSuggestions'; +import { PaywallGate } from 'expo-app/features/purchases'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { LinearGradient } from 'expo-linear-gradient'; @@ -330,7 +331,7 @@ function ErrorCard({ error, onRetry, onGoBack, onGoToInventory, onSignIn }: Erro ); } -export default function SeasonSuggestionsResultsScreen() { +function SeasonSuggestionsResultsScreen() { const router = useRouter(); const { t } = useTranslation(); const { location, date } = useLocalSearchParams<{ location: string; date: string }>(); @@ -520,3 +521,11 @@ export default function SeasonSuggestionsResultsScreen() { ); } + +export default function SeasonSuggestionsResultsRoute() { + return ( + + + + ); +} diff --git a/apps/expo/app/(app)/season-suggestions.tsx b/apps/expo/app/(app)/season-suggestions.tsx index b2cc57ce44..f20683adf4 100644 --- a/apps/expo/app/(app)/season-suggestions.tsx +++ b/apps/expo/app/(app)/season-suggestions.tsx @@ -6,6 +6,7 @@ import * as Sentry from '@sentry/react-native'; import { Icon } from 'expo-app/components/Icon'; import { LocationSearchSheet } from 'expo-app/features/packs/components/LocationSearchSheet'; import { LocationSourceSheet } from 'expo-app/features/packs/components/LocationSourceSheet'; +import { PaywallGate } from 'expo-app/features/purchases'; import { useBottomSheetAction } from 'expo-app/lib/hooks/useBottomSheetAction'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import * as Location from 'expo-location'; @@ -13,7 +14,7 @@ import { Stack, useRouter } from 'expo-router'; import { useRef, useState } from 'react'; import { ActivityIndicator, Alert, Linking, Platform, ScrollView, View } from 'react-native'; -export default function SeasonSuggestionsScreen() { +function SeasonSuggestionsScreen() { const router = useRouter(); const { t } = useTranslation(); const [isGettingLocation, setIsGettingLocation] = useState(false); @@ -161,3 +162,11 @@ export default function SeasonSuggestionsScreen() { ); } + +export default function SeasonSuggestionsRoute() { + return ( + + + + ); +} diff --git a/apps/expo/app/(app)/shared-packs.tsx b/apps/expo/app/(app)/shared-packs.tsx index f90e653b1d..3075ae5165 100644 --- a/apps/expo/app/(app)/shared-packs.tsx +++ b/apps/expo/app/(app)/shared-packs.tsx @@ -1,5 +1,6 @@ import { Avatar, AvatarFallback, AvatarImage, Text } from '@packrat/ui/nativewindui'; import { getAppBarOptions } from '@packrat/ui/src/app-bar'; +import { PaywallGate } from 'expo-app/features/purchases'; import { cn } from 'expo-app/lib/cn'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { Stack } from 'expo-router'; @@ -173,7 +174,7 @@ function SharedPackCard({ pack }: { pack: (typeof SHARED_PACKS)[0] }) { ); } -export default function SharedPacksScreen() { +function SharedPacksScreen() { const { t } = useTranslation(); return ( @@ -210,3 +211,11 @@ export default function SharedPacksScreen() { ); } + +export default function SharedPacksRoute() { + return ( + + + + ); +} diff --git a/apps/expo/app/(app)/shopping-list.tsx b/apps/expo/app/(app)/shopping-list.tsx index 8b2fb77988..c073965768 100644 --- a/apps/expo/app/(app)/shopping-list.tsx +++ b/apps/expo/app/(app)/shopping-list.tsx @@ -3,6 +3,7 @@ import { Text } from '@packrat/ui/nativewindui'; import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { Icon } from 'expo-app/components/Icon'; +import { PaywallGate } from 'expo-app/features/purchases'; import { cn } from 'expo-app/lib/cn'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; @@ -159,7 +160,7 @@ function ShoppingItemCard({ item }: { item: (typeof SHOPPING_LIST)[0] }) { ); } -export default function ShoppingListScreen() { +function ShoppingListScreen() { const { t } = useTranslation(); const [filter, setFilter] = useState<'all' | 'pending' | 'purchased'>('pending'); @@ -257,3 +258,11 @@ export default function ShoppingListScreen() { ); } + +export default function ShoppingListRoute() { + return ( + + + + ); +} diff --git a/apps/expo/app/(app)/trail-conditions.tsx b/apps/expo/app/(app)/trail-conditions.tsx index 441185aca7..a26c7ab776 100644 --- a/apps/expo/app/(app)/trail-conditions.tsx +++ b/apps/expo/app/(app)/trail-conditions.tsx @@ -1,6 +1,7 @@ import { ActivityIndicator, Text } from '@packrat/ui/nativewindui'; import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { featureFlags } from 'expo-app/config'; +import { PaywallGate } from 'expo-app/features/purchases'; import { SubmitConditionReportForm } from 'expo-app/features/trail-conditions/components/SubmitConditionReportForm'; import { TrailConditionReportCard } from 'expo-app/features/trail-conditions/components/TrailConditionReportCard'; import { useTrailConditionReports } from 'expo-app/features/trail-conditions/hooks/useTrailConditionReports'; @@ -37,7 +38,7 @@ function getSurfaceBadgeColor(surface: TrailSurface): string { } } -export default function TrailConditionsScreen() { +function TrailConditionsScreen() { const [showSubmitForm, setShowSubmitForm] = useState(false); const [selectedSurface, setSelectedSurface] = useState('all'); const { data: reports, isLoading, error } = useTrailConditionReports(); @@ -219,3 +220,11 @@ export default function TrailConditionsScreen() { ); } + +export default function TrailConditionsRoute() { + return ( + + + + ); +} diff --git a/apps/expo/app/(app)/weather-alerts.tsx b/apps/expo/app/(app)/weather-alerts.tsx index 96ef2c07e7..26320aceb8 100644 --- a/apps/expo/app/(app)/weather-alerts.tsx +++ b/apps/expo/app/(app)/weather-alerts.tsx @@ -1,6 +1,7 @@ import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Text } from '@packrat/ui/nativewindui'; import { getAppBarOptions } from '@packrat/ui/src/app-bar'; +import { PaywallGate } from 'expo-app/features/purchases'; import { useWeatherAlerts } from 'expo-app/features/weather/hooks/useWeatherAlert'; import { cn } from 'expo-app/lib/cn'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; @@ -128,7 +129,7 @@ function WeatherAlertCard({ alert }: { alert: WeatherAlert }) { ); } -export default function WeatherAlertsScreen() { +function WeatherAlertsScreen() { const { t } = useTranslation(); const router = useRouter(); const { alerts, loading, error, activeLocation } = useWeatherAlerts(); @@ -185,3 +186,11 @@ export default function WeatherAlertsScreen() { ); } + +export default function WeatherAlertsRoute() { + return ( + + + + ); +} diff --git a/apps/expo/app/(app)/weight-analysis/[id].tsx b/apps/expo/app/(app)/weight-analysis/[id].tsx index d0cf934fbd..38e9cd0e88 100644 --- a/apps/expo/app/(app)/weight-analysis/[id].tsx +++ b/apps/expo/app/(app)/weight-analysis/[id].tsx @@ -4,6 +4,7 @@ import { Text } from '@packrat/ui/nativewindui'; import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { useWeightUnit } from 'expo-app/features/auth/hooks/useWeightUnit'; import { usePackWeightAnalysis } from 'expo-app/features/packs/hooks/usePackWeightAnalysis'; +import { PaywallGate } from 'expo-app/features/purchases'; import { cn } from 'expo-app/lib/cn'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { Stack, useLocalSearchParams } from 'expo-router'; @@ -38,7 +39,7 @@ function WeightCard({ ); } -export default function WeightAnalysisScreen() { +function WeightAnalysisScreen() { const params = useLocalSearchParams(); const packId = params.id; const { t } = useTranslation(); @@ -138,3 +139,11 @@ export default function WeightAnalysisScreen() { ); } + +export default function WeightAnalysisRoute() { + return ( + + + + ); +} diff --git a/apps/expo/app/(app)/wildlife/index.tsx b/apps/expo/app/(app)/wildlife/index.tsx index 47b5afb522..b88f107e42 100644 --- a/apps/expo/app/(app)/wildlife/index.tsx +++ b/apps/expo/app/(app)/wildlife/index.tsx @@ -1,4 +1,5 @@ import { featureFlags } from 'expo-app/config'; +import { PaywallGate } from 'expo-app/features/purchases'; import { WildlifeScreen } from 'expo-app/features/wildlife/screens/WildlifeScreen'; import { Redirect } from 'expo-router'; @@ -6,5 +7,9 @@ export default function WildlifeRoute() { if (!featureFlags.enableWildlifeIdentification) { return ; } - return ; + return ( + + + + ); } diff --git a/apps/expo/features/purchases/components/PaywallGate.tsx b/apps/expo/features/purchases/components/PaywallGate.tsx new file mode 100644 index 0000000000..003c6a8193 --- /dev/null +++ b/apps/expo/features/purchases/components/PaywallGate.tsx @@ -0,0 +1,26 @@ +import { ActivityIndicator } from '@packrat/ui/nativewindui'; +import { View } from 'react-native'; +import { useEntitlement } from '../hooks/useEntitlement'; +import { UpgradePrompt } from './UpgradePrompt'; + +interface PaywallGateProps { + children: React.ReactNode; +} + +export function PaywallGate({ children }: PaywallGateProps) { + const { isPro, isLoading } = useEntitlement(); + + if (isLoading) { + return ( + + + + ); + } + + if (!isPro) { + return ; + } + + return <>{children}; +} diff --git a/apps/expo/features/purchases/components/UpgradePrompt.tsx b/apps/expo/features/purchases/components/UpgradePrompt.tsx new file mode 100644 index 0000000000..78b2e64391 --- /dev/null +++ b/apps/expo/features/purchases/components/UpgradePrompt.tsx @@ -0,0 +1,110 @@ +import { ActivityIndicator, Button, Text } from '@packrat/ui/nativewindui'; +import * as Sentry from '@sentry/react-native'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; +import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; +import { useState } from 'react'; +import { ScrollView, View } from 'react-native'; +import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui'; +import { useEntitlement } from '../hooks/useEntitlement'; +import { ENTITLEMENT_PRO, PRO_FEATURES } from '../lib/config'; + +export function UpgradePrompt() { + const { t } = useTranslation(); + const { colors } = useColorScheme(); + const { invalidate } = useEntitlement(); + const [isPresentingPaywall, setIsPresentingPaywall] = useState(false); + + async function handleViewPlans() { + setIsPresentingPaywall(true); + try { + const result = await RevenueCatUI.presentPaywallIfNeeded({ + requiredEntitlementIdentifier: ENTITLEMENT_PRO, + }); + + if (result === PAYWALL_RESULT.PURCHASED || result === PAYWALL_RESULT.RESTORED) { + invalidate(); + } + } catch (error) { + Sentry.captureException(error, { + tags: { feature: 'purchases', action: 'presentPaywall' }, + }); + } finally { + setIsPresentingPaywall(false); + } + } + + async function handleRestore() { + setIsPresentingPaywall(true); + try { + await RevenueCatUI.presentPaywallIfNeeded({ + requiredEntitlementIdentifier: ENTITLEMENT_PRO, + }); + invalidate(); + } catch (error) { + Sentry.captureException(error, { + tags: { feature: 'purchases', action: 'restore' }, + }); + } finally { + setIsPresentingPaywall(false); + } + } + + return ( + + + + + 🎒 + + + + {t('purchases.upgradeTitle')} + + + + {t('purchases.upgradeSubtitle')} + + + + + {PRO_FEATURES.map((feature) => ( + + + ✓ + + {feature.label} + + ))} + + + + + + + + ); +} diff --git a/apps/expo/features/purchases/hooks/useEntitlement.ts b/apps/expo/features/purchases/hooks/useEntitlement.ts new file mode 100644 index 0000000000..7e661daabd --- /dev/null +++ b/apps/expo/features/purchases/hooks/useEntitlement.ts @@ -0,0 +1,32 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import Purchases from 'react-native-purchases'; +import { ENTITLEMENT_PRO } from '../lib/config'; +import { isRevenueCatInitialized } from '../lib/init'; + +export const CUSTOMER_INFO_QUERY_KEY = ['customerInfo'] as const; + +export function useEntitlement() { + const queryClient = useQueryClient(); + + const { + data: customerInfo, + isLoading, + error, + } = useQuery({ + queryKey: CUSTOMER_INFO_QUERY_KEY, + queryFn: () => Purchases.getCustomerInfo(), + enabled: isRevenueCatInitialized(), + staleTime: 1000 * 60 * 5, + retry: 1, + }); + + const isPro = + isRevenueCatInitialized() && + typeof customerInfo?.entitlements.active[ENTITLEMENT_PRO] !== 'undefined'; + + function invalidate() { + queryClient.invalidateQueries({ queryKey: CUSTOMER_INFO_QUERY_KEY }); + } + + return { isPro, customerInfo, isLoading, error, invalidate }; +} diff --git a/apps/expo/features/purchases/hooks/useIdentifyUser.ts b/apps/expo/features/purchases/hooks/useIdentifyUser.ts new file mode 100644 index 0000000000..502e18ee72 --- /dev/null +++ b/apps/expo/features/purchases/hooks/useIdentifyUser.ts @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/react-native'; +import { useQueryClient } from '@tanstack/react-query'; +import { authClient } from 'expo-app/lib/auth-client'; +import { useEffect } from 'react'; +import Purchases from 'react-native-purchases'; +import { isRevenueCatInitialized } from '../lib/init'; +import { CUSTOMER_INFO_QUERY_KEY } from './useEntitlement'; + +export function useIdentifyUser() { + const queryClient = useQueryClient(); + const { data: session } = authClient.useSession(); + const userId = session?.user?.id; + + useEffect(() => { + if (!isRevenueCatInitialized()) return; + + if (userId) { + Purchases.logIn(userId) + .then(() => queryClient.invalidateQueries({ queryKey: CUSTOMER_INFO_QUERY_KEY })) + .catch((error: unknown) => { + Sentry.captureException(error, { + tags: { feature: 'purchases', action: 'logIn' }, + extra: { userId }, + }); + }); + } else { + Purchases.logOut() + .then(() => queryClient.invalidateQueries({ queryKey: CUSTOMER_INFO_QUERY_KEY })) + .catch((error: unknown) => { + Sentry.captureException(error, { + tags: { feature: 'purchases', action: 'logOut' }, + }); + }); + } + }, [userId, queryClient]); +} diff --git a/apps/expo/features/purchases/index.ts b/apps/expo/features/purchases/index.ts new file mode 100644 index 0000000000..8a173e2d7e --- /dev/null +++ b/apps/expo/features/purchases/index.ts @@ -0,0 +1,6 @@ +export { PaywallGate } from './components/PaywallGate'; +export { UpgradePrompt } from './components/UpgradePrompt'; +export { useEntitlement } from './hooks/useEntitlement'; +export { useIdentifyUser } from './hooks/useIdentifyUser'; +export { ENTITLEMENT_PRO, PRO_FEATURES } from './lib/config'; +export { initRevenueCat, isRevenueCatInitialized } from './lib/init'; diff --git a/apps/expo/features/purchases/lib/config.ts b/apps/expo/features/purchases/lib/config.ts new file mode 100644 index 0000000000..6ab1a71368 --- /dev/null +++ b/apps/expo/features/purchases/lib/config.ts @@ -0,0 +1,12 @@ +export const ENTITLEMENT_PRO = 'pro'; + +export const PRO_FEATURES = [ + { id: 'ai', label: 'AI Chat & Smart Suggestions' }, + { id: 'weather', label: 'Weather Alerts & Forecasts' }, + { id: 'trail', label: 'Trail Conditions' }, + { id: 'weight', label: 'Pack Weight Analysis' }, + { id: 'wildlife', label: 'Wildlife Identification' }, + { id: 'shared', label: 'Shared Packs' }, + { id: 'templates', label: 'Pack Templates' }, + { id: 'shopping', label: 'Shopping List' }, +] as const; diff --git a/apps/expo/features/purchases/lib/init.ts b/apps/expo/features/purchases/lib/init.ts new file mode 100644 index 0000000000..91c7a64023 --- /dev/null +++ b/apps/expo/features/purchases/lib/init.ts @@ -0,0 +1,33 @@ +import { clientEnvs } from '@packrat/env/expo-client'; +import { Platform } from 'react-native'; +import Purchases, { LOG_LEVEL } from 'react-native-purchases'; + +let initialized = false; + +export function initRevenueCat(): void { + const apiKey = Platform.select({ + ios: clientEnvs.EXPO_PUBLIC_REVENUECAT_APPLE_API_KEY, + android: clientEnvs.EXPO_PUBLIC_REVENUECAT_GOOGLE_API_KEY, + default: clientEnvs.EXPO_PUBLIC_REVENUECAT_APPLE_API_KEY, + }); + + if (!apiKey) { + if (__DEV__) { + console.warn('[RevenueCat] No API key configured — purchases disabled.'); + } + return; + } + + if (initialized) return; + + if (__DEV__) { + Purchases.setLogLevel(LOG_LEVEL.DEBUG); + } + + Purchases.configure({ apiKey }); + initialized = true; +} + +export function isRevenueCatInitialized(): boolean { + return initialized; +} diff --git a/apps/expo/lib/i18n/locales/en.json b/apps/expo/lib/i18n/locales/en.json index 18c9aaab6f..6f7963fa26 100644 --- a/apps/expo/lib/i18n/locales/en.json +++ b/apps/expo/lib/i18n/locales/en.json @@ -1218,5 +1218,11 @@ "fish": "fish", "other": "other" } + }, + "purchases": { + "upgradeTitle": "PackRat Pro", + "upgradeSubtitle": "Unlock the full outdoor adventure experience", + "viewPlans": "View Plans", + "restorePurchases": "Restore Purchases" } } diff --git a/apps/expo/package.json b/apps/expo/package.json index cd2c886971..e383d1a2a2 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -148,6 +148,8 @@ "react-native-keyboard-controller": "1.21.6", "react-native-maps": "1.27.2", "react-native-pager-view": "8.0.1", + "react-native-purchases": "^9.0.0", + "react-native-purchases-ui": "^9.0.0", "react-native-reanimated": "4.3.1", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2", diff --git a/apps/expo/providers/PurchasesProvider.tsx b/apps/expo/providers/PurchasesProvider.tsx new file mode 100644 index 0000000000..7c8a63b047 --- /dev/null +++ b/apps/expo/providers/PurchasesProvider.tsx @@ -0,0 +1,20 @@ +import { initRevenueCat, useIdentifyUser } from 'expo-app/features/purchases'; +import { useEffect } from 'react'; + +function PurchasesEffects() { + useIdentifyUser(); + return null; +} + +export function PurchasesProvider({ children }: { children: React.ReactNode }) { + useEffect(() => { + initRevenueCat(); + }, []); + + return ( + <> + + {children} + + ); +} diff --git a/apps/expo/providers/index.tsx b/apps/expo/providers/index.tsx index 8bb092b9a4..8e029c071e 100644 --- a/apps/expo/providers/index.tsx +++ b/apps/expo/providers/index.tsx @@ -7,6 +7,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { KeyboardProvider } from 'react-native-keyboard-controller'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { JotaiProvider } from './JotaiProvider'; +import { PurchasesProvider } from './PurchasesProvider'; import { TanstackProvider } from './TanstackProvider'; export function Providers({ children }: { children: React.ReactNode }) { @@ -14,16 +15,18 @@ export function Providers({ children }: { children: React.ReactNode }) { - - - - - {children} - - - - - + + + + + + {children} + + + + + + diff --git a/apps/expo/types/react-native-purchases-ui.d.ts b/apps/expo/types/react-native-purchases-ui.d.ts new file mode 100644 index 0000000000..1efff27704 --- /dev/null +++ b/apps/expo/types/react-native-purchases-ui.d.ts @@ -0,0 +1,35 @@ +// Minimal type shim for react-native-purchases-ui. +// Full types are provided by the installed package after `bun install`. +declare module 'react-native-purchases-ui' { + import type { ReactNode } from 'react'; + + export enum PAYWALL_RESULT { + NOT_PRESENTED = 'NOT_PRESENTED', + ERROR = 'ERROR', + CANCELLED = 'CANCELLED', + PURCHASED = 'PURCHASED', + RESTORED = 'RESTORED', + } + + export interface PaywallProps { + onPurchaseCompleted?: (event: { + customerInfo: import('react-native-purchases').CustomerInfo; + }) => void; + onRestoreCompleted?: (event: { + customerInfo: import('react-native-purchases').CustomerInfo; + }) => void; + onDismiss?: () => void; + children?: ReactNode; + } + + export interface PresentPaywallIfNeededOptions { + requiredEntitlementIdentifier: string; + } + + const RevenueCatUI: { + Paywall: (props: PaywallProps) => ReactNode; + presentPaywallIfNeeded(options: PresentPaywallIfNeededOptions): Promise; + }; + + export default RevenueCatUI; +} diff --git a/apps/expo/types/react-native-purchases.d.ts b/apps/expo/types/react-native-purchases.d.ts new file mode 100644 index 0000000000..044717557a --- /dev/null +++ b/apps/expo/types/react-native-purchases.d.ts @@ -0,0 +1,37 @@ +// Minimal type shim for react-native-purchases. +// Full types are provided by the installed package after `bun install`. +declare module 'react-native-purchases' { + export enum LOG_LEVEL { + VERBOSE = 'VERBOSE', + DEBUG = 'DEBUG', + INFO = 'INFO', + WARN = 'WARN', + ERROR = 'ERROR', + } + + export interface CustomerInfo { + entitlements: { + active: Record; + }; + } + + export interface EntitlementInfo { + identifier: string; + isActive: boolean; + } + + export interface LogInResult { + customerInfo: CustomerInfo; + created: boolean; + } + + const Purchases: { + setLogLevel(level: LOG_LEVEL): void; + configure(options: { apiKey: string; useAmazon?: boolean }): void; + getCustomerInfo(): Promise; + logIn(appUserID: string): Promise; + logOut(): Promise; + }; + + export default Purchases; +} diff --git a/packages/env/src/expo-client.ts b/packages/env/src/expo-client.ts index b56c2fc4cf..67bce48e1d 100644 --- a/packages/env/src/expo-client.ts +++ b/packages/env/src/expo-client.ts @@ -22,6 +22,8 @@ export const clientEnvSchema = z.object({ EXPO_PUBLIC_SENTRY_DSN: z.string().optional(), EXPO_PUBLIC_GOOGLE_MAPS_API_KEY: z.string().optional(), EXPO_PUBLIC_DISABLE_LOGBOX: z.enum(['true', 'false']).optional().default('false'), + EXPO_PUBLIC_REVENUECAT_APPLE_API_KEY: z.string().optional(), + EXPO_PUBLIC_REVENUECAT_GOOGLE_API_KEY: z.string().optional(), }); export type ClientEnv = z.infer; @@ -36,6 +38,8 @@ const processEnv = { EXPO_PUBLIC_SENTRY_DSN: process.env.EXPO_PUBLIC_SENTRY_DSN, EXPO_PUBLIC_GOOGLE_MAPS_API_KEY: process.env.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY, EXPO_PUBLIC_DISABLE_LOGBOX: process.env.EXPO_PUBLIC_DISABLE_LOGBOX, + EXPO_PUBLIC_REVENUECAT_APPLE_API_KEY: process.env.EXPO_PUBLIC_REVENUECAT_APPLE_API_KEY, + EXPO_PUBLIC_REVENUECAT_GOOGLE_API_KEY: process.env.EXPO_PUBLIC_REVENUECAT_GOOGLE_API_KEY, }; /**