-
Notifications
You must be signed in to change notification settings - Fork 38
feat(purchases): paywall non-core features with RevenueCat #2597
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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 />; | ||
| } | ||
|
|
||
| return <>{children}</>; | ||
| } | ||
| 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'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check if the file exists and view its content
cat -n apps/expo/features/purchases/components/UpgradePrompt.tsxRepository: 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 2Repository: 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 catRepository: PackRat-AI/PackRat Length of output: 9432 Replace Per the apps/expo guidelines, local state must use Jotai instead of React's 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 AgentsSource: 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' }} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n apps/expo/features/purchases/components/UpgradePrompt.tsx | head -100Repository: 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 -20Repository: 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 2Repository: 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 -10Repository: PackRat-AI/PackRat Length of output: 269 🏁 Script executed: cat apps/expo/tailwind.config.jsRepository: PackRat-AI/PackRat Length of output: 2334 🏁 Script executed: # Check how useColorScheme is implemented
cat apps/expo/lib/hooks/useColorScheme.ts | head -50Repository: 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 -40Repository: PackRat-AI/PackRat Length of output: 90 🏁 Script executed: fd "useColorScheme" apps/expo --type fRepository: PackRat-AI/PackRat Length of output: 144 🏁 Script executed: # Search for useColorScheme implementation
rg "useColorScheme" apps/expo --type ts -lRepository: 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 -30Repository: PackRat-AI/PackRat Length of output: 2457 🏁 Script executed: head -60 apps/expo/lib/hooks/useColorScheme.tsxRepository: 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 -50Repository: PackRat-AI/PackRat Length of output: 4466 Replace inline color styles with NativeWind token classes. Inline 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 AgentsSource: 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> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: PackRat-AI/PackRat
Length of output: 233
🏁 Script executed:
Repository: PackRat-AI/PackRat
Length of output: 118
🏁 Script executed:
Repository: PackRat-AI/PackRat
Length of output: 153
🏁 Script executed:
Repository: PackRat-AI/PackRat
Length of output: 1203
🏁 Script executed:
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 exposeserrorandinvalidate—add an explicit error branch beforeif (!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
🤖 Prompt for AI Agents