Skip to content
Open
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
11 changes: 10 additions & 1 deletion apps/expo/app/(app)/ai-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -665,3 +666,11 @@ function Composer({
</BlurView>
);
}

export default function AIChatRoute() {
return (
<PaywallGate>
<AIChat />
</PaywallGate>
);
}
9 changes: 7 additions & 2 deletions apps/expo/app/(app)/pack-templates/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <PackTemplateListScreen />;
export default function PackTemplatesRoute() {
return (
<PaywallGate>
<PackTemplateListScreen />
</PaywallGate>
);
}
11 changes: 10 additions & 1 deletion apps/expo/app/(app)/season-suggestions-results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 }>();
Expand Down Expand Up @@ -520,3 +521,11 @@ export default function SeasonSuggestionsResultsScreen() {
</>
);
}

export default function SeasonSuggestionsResultsRoute() {
return (
<PaywallGate>
<SeasonSuggestionsResultsScreen />
</PaywallGate>
);
}
11 changes: 10 additions & 1 deletion apps/expo/app/(app)/season-suggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ 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';
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);
Expand Down Expand Up @@ -161,3 +162,11 @@ export default function SeasonSuggestionsScreen() {
</>
);
}

export default function SeasonSuggestionsRoute() {
return (
<PaywallGate>
<SeasonSuggestionsScreen />
</PaywallGate>
);
}
11 changes: 10 additions & 1 deletion apps/expo/app/(app)/shared-packs.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -173,7 +174,7 @@ function SharedPackCard({ pack }: { pack: (typeof SHARED_PACKS)[0] }) {
);
}

export default function SharedPacksScreen() {
function SharedPacksScreen() {
const { t } = useTranslation();
return (
<SafeAreaView className="flex-1" edges={['bottom']}>
Expand Down Expand Up @@ -210,3 +211,11 @@ export default function SharedPacksScreen() {
</SafeAreaView>
);
}

export default function SharedPacksRoute() {
return (
<PaywallGate>
<SharedPacksScreen />
</PaywallGate>
);
}
11 changes: 10 additions & 1 deletion apps/expo/app/(app)/shopping-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -257,3 +258,11 @@ export default function ShoppingListScreen() {
</SafeAreaView>
);
}

export default function ShoppingListRoute() {
return (
<PaywallGate>
<ShoppingListScreen />
</PaywallGate>
);
}
11 changes: 10 additions & 1 deletion apps/expo/app/(app)/trail-conditions.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -37,7 +38,7 @@ function getSurfaceBadgeColor(surface: TrailSurface): string {
}
}

export default function TrailConditionsScreen() {
function TrailConditionsScreen() {
const [showSubmitForm, setShowSubmitForm] = useState(false);
const [selectedSurface, setSelectedSurface] = useState<SurfaceFilter>('all');
const { data: reports, isLoading, error } = useTrailConditionReports();
Expand Down Expand Up @@ -219,3 +220,11 @@ export default function TrailConditionsScreen() {
</SafeAreaView>
);
}

export default function TrailConditionsRoute() {
return (
<PaywallGate>
<TrailConditionsScreen />
</PaywallGate>
);
}
11 changes: 10 additions & 1 deletion apps/expo/app/(app)/weather-alerts.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -185,3 +186,11 @@ export default function WeatherAlertsScreen() {
</SafeAreaView>
);
}

export default function WeatherAlertsRoute() {
return (
<PaywallGate>
<WeatherAlertsScreen />
</PaywallGate>
);
}
11 changes: 10 additions & 1 deletion apps/expo/app/(app)/weight-analysis/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -38,7 +39,7 @@ function WeightCard({
);
}

export default function WeightAnalysisScreen() {
function WeightAnalysisScreen() {
const params = useLocalSearchParams();
const packId = params.id;
const { t } = useTranslation();
Expand Down Expand Up @@ -138,3 +139,11 @@ export default function WeightAnalysisScreen() {
</SafeAreaView>
);
}

export default function WeightAnalysisRoute() {
return (
<PaywallGate>
<WeightAnalysisScreen />
</PaywallGate>
);
}
7 changes: 6 additions & 1 deletion apps/expo/app/(app)/wildlife/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
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';

export default function WildlifeRoute() {
if (!featureFlags.enableWildlifeIdentification) {
return <Redirect href="/" />;
}
return <WildlifeScreen />;
return (
<PaywallGate>
<WildlifeScreen />
</PaywallGate>
);
}
26 changes: 26 additions & 0 deletions apps/expo/features/purchases/components/PaywallGate.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View className="flex-1 items-center justify-center bg-background">
<ActivityIndicator />
</View>
);
}

if (!isPro) {
return <UpgradePrompt />;
}
Comment on lines +10 to +23

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the useEntitlement hook implementation
fd -type f -name "*.ts*" | xargs grep -l "useEntitlement" | head -20

Repository: PackRat-AI/PackRat

Length of output: 233


🏁 Script executed:

# Read the PaywallGate.tsx file to see the complete current implementation
find apps/expo -name "PaywallGate.tsx" -type f

Repository: PackRat-AI/PackRat

Length of output: 118


🏁 Script executed:

# Look for the hook definition in the purchases feature
find apps/expo/features/purchases -type f -name "*.ts*" | xargs grep -l "export.*useEntitlement" 2>/dev/null

Repository: PackRat-AI/PackRat

Length of output: 153


🏁 Script executed:

cat -n apps/expo/features/purchases/hooks/useEntitlement.ts

Repository: PackRat-AI/PackRat

Length of output: 1203


🏁 Script executed:

cat -n apps/expo/features/purchases/components/PaywallGate.tsx

Repository: PackRat-AI/PackRat

Length of output: 865


Handle entitlement fetch errors separately from the "not Pro" branch.

If useEntitlement() fails, the component falls through to <UpgradePrompt />, which can incorrectly block already-entitled users during transient RevenueCat failures. The hook already exposes error and invalidate—add an explicit error branch before if (!isPro) to render a retry UI instead of an upsell.

Proposed fix
 export function PaywallGate({ children }: PaywallGateProps) {
-  const { isPro, isLoading } = useEntitlement();
+  const { isPro, isLoading, error, invalidate } = useEntitlement();

   if (isLoading) {
     return (
       <View className="flex-1 items-center justify-center bg-background">
         <ActivityIndicator />
       </View>
     );
   }

+  if (error) {
+    return (
+      <View className="flex-1 items-center justify-center bg-background">
+        <Text>Failed to load. Tap to retry.</Text>
+        <Pressable onPress={invalidate}>
+          <Text>Retry</Text>
+        </Pressable>
+      </View>
+    );
+  }
+
   if (!isPro) {
     return <UpgradePrompt />;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function PaywallGate({ children }: PaywallGateProps) {
const { isPro, isLoading } = useEntitlement();
if (isLoading) {
return (
<View className="flex-1 items-center justify-center bg-background">
<ActivityIndicator />
</View>
);
}
if (!isPro) {
return <UpgradePrompt />;
}
export function PaywallGate({ children }: PaywallGateProps) {
const { isPro, isLoading, error, invalidate } = useEntitlement();
if (isLoading) {
return (
<View className="flex-1 items-center justify-center bg-background">
<ActivityIndicator />
</View>
);
}
if (error) {
return (
<View className="flex-1 items-center justify-center bg-background">
<Text>Failed to load. Tap to retry.</Text>
<Pressable onPress={invalidate}>
<Text>Retry</Text>
</Pressable>
</View>
);
}
if (!isPro) {
return <UpgradePrompt />;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/features/purchases/components/PaywallGate.tsx` around lines 10 -
23, The PaywallGate component does not handle errors from the useEntitlement
hook, causing it to incorrectly show the UpgradePrompt when the entitlement
fetch fails transiently. Destructure the error property from useEntitlement()
alongside isPro and isLoading, then add an explicit error handling branch after
the isLoading check but before the isPro check. When error exists, render a
retry UI component instead of falling through to UpgradePrompt, ensuring
transient RevenueCat failures do not block already-entitled users.


return <>{children}</>;
}
110 changes: 110 additions & 0 deletions apps/expo/features/purchases/components/UpgradePrompt.tsx
Original file line number Diff line number Diff line change
@@ -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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and view its content
cat -n apps/expo/features/purchases/components/UpgradePrompt.tsx

Repository: PackRat-AI/PackRat

Length of output: 4540


🏁 Script executed:

# Search for other uses of this component or atom-related patterns in the purchases feature
rg "UpgradePrompt|isPresentingPaywall" apps/expo/features/purchases/ -A 2 -B 2

Repository: PackRat-AI/PackRat

Length of output: 3399


🏁 Script executed:

# Check how other features in apps/expo structure their state (look at an existing feature using Jotai)
find apps/expo/features -name "*.tsx" -type f | head -5 | xargs grep -l "useAtom" | head -2 | xargs cat

Repository: PackRat-AI/PackRat

Length of output: 9432


Replace useState with Jotai for local state management.

Per the apps/expo guidelines, local state must use Jotai instead of React's useState. Move the isPresentingPaywall state to a module-scoped atom and use useAtom to access it (see pattern in similar features like AI mode selector).

Suggested refactor
-import { useState } from 'react';
+import { atom, useAtom } from 'jotai';
@@
+const isPresentingPaywallAtom = atom(false);
+
 export function UpgradePrompt() {
@@
-  const [isPresentingPaywall, setIsPresentingPaywall] = useState(false);
+  const [isPresentingPaywall, setIsPresentingPaywall] = useAtom(isPresentingPaywallAtom);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/features/purchases/components/UpgradePrompt.tsx` at line 5, Replace
the React useState hook with Jotai for state management in the UpgradePrompt
component. Remove the useState import from react and add an import for useAtom
from jotai. Create a module-scoped atom at the top of the file (or in a separate
atoms file following the project pattern) for the isPresentingPaywall state,
then replace any useState(isPresentingPaywall) call with
useAtom(isPresentingPaywallAtom) throughout the component to follow the
apps/expo guidelines for local state management.

Source: Coding guidelines

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 (
<ScrollView
contentContainerClassName="flex-grow items-center justify-center p-6"
className="flex-1 bg-background"
>
<View className="w-full max-w-sm">
<View className="mb-6 items-center">
<View
className="mb-4 h-20 w-20 items-center justify-center rounded-full"
style={{ backgroundColor: colors.primary + '20' }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/expo/features/purchases/components/UpgradePrompt.tsx | head -100

Repository: PackRat-AI/PackRat

Length of output: 4194


🏁 Script executed:

# Check NativeWind configuration and color token definitions
fd -t f "(tailwind|nativewind)" apps/expo --type f | head -20

Repository: PackRat-AI/PackRat

Length of output: 121


🏁 Script executed:

# Search for color token definitions in the project
rg "colors\.primary" apps/expo --type tsx --type ts -B 2 -A 2

Repository: PackRat-AI/PackRat

Length of output: 90


🏁 Script executed:

# Check for NativeWind config or theme definitions
find apps/expo -name "*.config.*" -o -name "tailwind.config.*" -o -name "nativewind.config.*" | head -10

Repository: PackRat-AI/PackRat

Length of output: 269


🏁 Script executed:

cat apps/expo/tailwind.config.js

Repository: PackRat-AI/PackRat

Length of output: 2334


🏁 Script executed:

# Check how useColorScheme is implemented
cat apps/expo/lib/hooks/useColorScheme.ts | head -50

Repository: PackRat-AI/PackRat

Length of output: 132


🏁 Script executed:

# Search for color definitions and how colors are used in the project
rg "colors\.primary|colors\.background" apps/expo --type ts --type tsx -B 1 -A 1 | head -40

Repository: PackRat-AI/PackRat

Length of output: 90


🏁 Script executed:

fd "useColorScheme" apps/expo --type f

Repository: PackRat-AI/PackRat

Length of output: 144


🏁 Script executed:

# Search for useColorScheme implementation
rg "useColorScheme" apps/expo --type ts -l

Repository: PackRat-AI/PackRat

Length of output: 6969


🏁 Script executed:

# Let's verify if bg-primary/20 syntax is valid by checking how opacity is used in the codebase
rg "bg-.*/" apps/expo --type ts -A 1 -B 1 | head -30

Repository: PackRat-AI/PackRat

Length of output: 2457


🏁 Script executed:

head -60 apps/expo/lib/hooks/useColorScheme.tsx

Repository: PackRat-AI/PackRat

Length of output: 1622


🏁 Script executed:

# Check if there are examples of dynamic color values being replaced with static classes
rg "style.*backgroundColor.*colors\." apps/expo -B 2 -A 2 | head -50

Repository: PackRat-AI/PackRat

Length of output: 4466


Replace inline color styles with NativeWind token classes.

Inline backgroundColor styles with colors.primary bypass the NativeWind CSS variable system and break dark mode consistency. Use bg-primary and bg-primary/20 classes instead, which properly reference the theme's CSS variables via Tailwind's opacity syntax.

Proposed refactor
-          <View
-            className="mb-4 h-20 w-20 items-center justify-center rounded-full"
-            style={{ backgroundColor: colors.primary + '20' }}
-          >
+          <View className="mb-4 h-20 w-20 items-center justify-center rounded-full bg-primary/20">
@@
-              <View
-                className="h-5 w-5 items-center justify-center rounded-full"
-                style={{ backgroundColor: colors.primary }}
-              >
+              <View className="h-5 w-5 items-center justify-center rounded-full bg-primary">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/features/purchases/components/UpgradePrompt.tsx` at line 61, The
inline backgroundColor style in the UpgradePrompt component using colors.primary
with concatenated opacity bypasses the NativeWind CSS variable system and breaks
dark mode support. Replace the style prop containing backgroundColor set to
colors.primary + '20' with the NativeWind utility class bg-primary/20, which
properly references the theme's CSS variables through Tailwind's opacity syntax
to ensure consistent dark mode styling.

Source: Coding guidelines

>
<Text style={{ fontSize: 40 }}>🎒</Text>
</View>

<Text variant="largeTitle" className="text-center font-bold">
{t('purchases.upgradeTitle')}
</Text>

<Text variant="body" className="mt-2 text-center text-muted-foreground">
{t('purchases.upgradeSubtitle')}
</Text>
</View>

<View className="mb-8 gap-3 rounded-2xl bg-card p-5">
{PRO_FEATURES.map((feature) => (
<View key={feature.id} className="flex-row items-center gap-3">
<View
className="h-5 w-5 items-center justify-center rounded-full"
style={{ backgroundColor: colors.primary }}
>
<Text className="text-xs font-bold text-white">✓</Text>
</View>
<Text variant="body">{feature.label}</Text>
</View>
))}
</View>

<Button
onPress={handleViewPlans}
disabled={isPresentingPaywall}
className="mb-3 w-full"
size="lg"
>
{isPresentingPaywall ? (
<ActivityIndicator size="small" color="white" />
) : (
<Text className="font-semibold text-primary-foreground">
{t('purchases.viewPlans')}
</Text>
)}
</Button>

<Button variant="plain" onPress={handleRestore} disabled={isPresentingPaywall}>
<Text className="text-sm text-muted-foreground">{t('purchases.restorePurchases')}</Text>
</Button>
</View>
</ScrollView>
);
}
Loading
Loading