-
Notifications
You must be signed in to change notification settings - Fork 2
paywall
Complete guide to GitNotēs Pro — RevenueCat integration, StoreKit 2, entitlements, feature gates, and analytics. See Architecture for context.
GitNotēs has two tiers: Free and Pro.
| Feature | Free | Pro |
|---|---|---|
| Notes, todos, canvases | ✅ | ✅ |
| GitHub sync | ✅ | ✅ |
| Neumorphic "Fancy UI" | ❌ | ✅ |
| Advanced AI (Claude 3.5, GPT-4o) | ❌ | ✅ |
| Multi-host (GitLab, Gitea) | ❌ | ✅ |
| Unlimited repos | 3 | Unlimited |
| Canvas AI vision | Limited | ✅ |
| Priority support | ❌ | ✅ |
Package: react-native-purchases (v10.7.1) wrapping RevenueCat SDK
Entitlement ID: GitNotēs Pro
File: src/services/RevenueCatService.ts
// Called once on app start via proStore.initialize()
configureRevenueCat()
.then(({ configured }) => {
if (configured) {
// RevenueCat is active — entitlements will be checked
} else {
// Placeholder API key — Pro features hidden
}
});API keys are configured via environment variables:
-
EXPO_PUBLIC_REVENUECAT_API_KEY_IOS— iOS RevenueCat key -
EXPO_PUBLIC_REVENUECAT_API_KEY_ANDROID— Android RevenueCat key
On iOS, RevenueCat uses StoreKit 2 automatically:
// RevenueCatService.ts:46
await Purchases.configure({
apiKey,
storeKitVersion: STOREKIT_VERSION.STOREKIT_2, // Force StoreKit 2
});StoreKit 2 provides:
- Faster purchase confirmation
- Improved subscription status tracking
- Better offline purchase handling
Three subscription packages are offered:
interface Packages {
monthly: PurchasesPackage; // $3.99/month
yearly?: PurchasesPackage; // $29.99/year (optional)
lifetime?: PurchasesPackage; // $39.99 one-time (optional)
offerings: PurchasesOfferings;
}Packages are loaded via getPackages() which queries RevenueCat's offerings endpoint and matches package identifiers.
| Function | Purpose |
|---|---|
configureRevenueCat() |
Initialize RevenueCat SDK |
getPackages() |
Load available subscription packages |
purchasePackage(pkg) |
Initiate purchase flow |
restorePurchases() |
Restore purchases from App Store |
getCustomerInfo() |
Fetch current entitlement status |
logInAppUser(id) |
Bind RevenueCat identity to app user ID (cross-device sync) |
logOutAppUser() |
Reset to anonymous identity |
trackPaywallImpression(offering) |
Track paywall view event |
The proStore is the central state manager for Pro tier.
interface ProState {
status: 'loading' | 'pro' | 'free';
entitlementActive: boolean; // True if user has active Pro entitlement
trialActive: boolean; // True if in trial period
trialEndsAt: number | null; // Trial end timestamp
entitlementExpiresAt: number | null;
offeringsReady: boolean;
monthlyPackage: PurchasesPackage | null;
yearlyPackage: PurchasesPackage | null;
lifetimePackage: PurchasesPackage | null;
currentOffering: PurchasesOffering | null;
isPurchasing: boolean;
isRestoring: boolean;
error: string | null;
interstitialEligible: boolean; // Show interstitial after trial ends
configured: boolean; // RevenueCat SDK initialized
}| Action | Purpose |
|---|---|
initialize() |
Boot RevenueCat, resolve entitlement |
refresh() |
Re-fetch customer info after purchase/restore |
purchaseMonthly() |
Purchase monthly subscription |
purchaseYearly() |
Purchase annual subscription |
purchaseLifetime() |
Purchase one-time lifetime access |
restore() |
Restore purchases — checks App Store for existing entitlement |
loadOfferingsIfNeeded() |
Load subscription packages if not yet loaded |
markInterstitialShown() |
Mark that the paywall interstitial was shown |
bindAccount(appUserID) |
Bind RevenueCat account to GitNotēs account for cross-device Pro |
unbindAccount() |
Remove account binding |
In development (__DEV__) on iOS Simulator only, Pro is forced open for QA testing without real IAP:
export const DEV_FORCE_PRO =
__DEV__ &&
Platform.OS === 'ios' &&
isSimulator() &&
process.env.EXPO_PUBLIC_FORCE_ENABLE_PRO_ON_SIMULATOR !== 'false';The gate quad: __DEV__ && iOS && simulator && env !== 'false'
Note: This does NOT bypass RevenueCat calls. The SDK still initializes and makes API calls — only the derived
isProgate is forced totrue.
File: src/hooks/useProGate.ts
// Safe to call anywhere (does NOT use navigation)
export function useProStatus() {
const isPro = useProStore(selectIsPro);
const status = useProStore(s => s.status);
return { isPro, status, loading: status === 'loading' };
}
// Opens paywall if non-Pro user tries to access Pro feature
export function useProGate() {
const { isPro, status, loading } = useProStatus();
const openPaywall = useCallback(() => navigation.navigate('Paywall'), []);
return { isPro, status, loading, openPaywall };
}File: src/hooks/useProScreenGuard.ts
Redirects away from a Pro-only screen if the user is not Pro.
File: src/services/TierLimits.ts
Enforces feature limits per tier — repo count on free, canvas AI usage limits, etc.
File: src/services/PaywallAnalytics.ts
Tracks RevenueCat events:
| Event | Trigger |
|---|---|
paywall_impression |
Paywall screen mounts |
purchase_attempt |
User taps buy button |
purchase_success |
Purchase completes |
purchase_cancelled |
User cancels purchase |
purchase_error |
Purchase fails |
restore_tap |
User taps Restore |
restore_success |
Restore finds entitlement |
restore_nothing |
Restore finds no purchases |
File: src/screens/PaywallScreen.tsx
Full-screen purchase UI:
- Feature comparison table (Free vs Pro)
-
PaywallPlanGrid— plan cards (monthly/yearly/lifetime) -
PaywallFeatureGrid— feature checklist - Restore purchases button
- Close button (if presented as modal)
When a free user who previously had a trial reaches the end of their trial:
-
proStore.initialize()detects that a trial was previously active but has now ended, and 3+ days have passed since expiration - Sets
interstitialEligible = trueinProState -
AppNavigatordetects this flag and navigates toPaywallScreenautomatically (one-shot)
markInterstitialShown() is called after the interstitial is displayed — it sets interstitialEligible = false via AsyncStorage to prevent repeat showing.