Harte Paywall

This commit is contained in:
2026-04-29 21:16:16 +02:00
committed by Timo Knuth
parent 0f933da3c9
commit d37b49f1f6
10 changed files with 305 additions and 129 deletions

View File

@@ -32,8 +32,9 @@ interface AppState {
colorPalette: ColorPalette;
profileName: string;
profileImageUri: string | null;
billingSummary: BillingSummary | null;
resolvedScheme: AppColorScheme;
billingSummary: BillingSummary | null;
isActivatingEntitlement: boolean;
resolvedScheme: AppColorScheme;
isDarkMode: boolean;
isInitializing: boolean;
isLoadingPlants: boolean;
@@ -152,8 +153,9 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
const [guestScanCount, setGuestScanCount] = useState(0);
const [isInitializing, setIsInitializing] = useState(true);
const [isLoadingPlants, setIsLoadingPlants] = useState(true);
const [billingSummary, setBillingSummary] = useState<BillingSummary | null>(null);
const [isLoadingBilling, setIsLoadingBilling] = useState(true);
const [billingSummary, setBillingSummary] = useState<BillingSummary | null>(null);
const [isLoadingBilling, setIsLoadingBilling] = useState(true);
const [isActivatingEntitlement, setIsActivatingEntitlement] = useState(false);
const resolvedScheme: AppColorScheme =
appearanceMode === 'system'
@@ -389,20 +391,45 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
source: RevenueCatSyncSource = 'app_init',
) => {
if (source === 'topup_purchase') {
return;
return false;
}
const activeEntitlements = customerInfo?.entitlements?.active || {};
const rawProEntitlement = activeEntitlements[REVENUECAT_PRO_ENTITLEMENT_ID];
const proEntitlement = getValidProEntitlement(customerInfo);
const isPro = Boolean(proEntitlement);
const now = new Date();
const renewsAt = proEntitlement?.expirationDate || proEntitlement?.expiresDate || null;
const isTrial = (proEntitlement?.periodType || proEntitlement?.period_type || '').toUpperCase() === 'TRIAL';
const monthlyAllowance = isTrial ? 30 : 100;
setBillingSummary((prev) => {
if (!prev) return prev;
if (!proEntitlement && rawProEntitlement) {
return prev;
}
if (!prev && isPro) {
return {
entitlement: {
plan: 'pro',
provider: 'revenuecat',
status: 'active',
renewsAt,
},
credits: {
monthlyAllowance,
usedThisCycle: 0,
topupBalance: 0,
available: monthlyAllowance,
cycleStartedAt: now.toISOString(),
cycleEndsAt: renewsAt || new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(),
},
availableProducts: ['monthly_pro', 'yearly_pro', 'topup_small', 'topup_medium', 'topup_large'],
};
}
if (!prev) return prev;
return {
...prev,
entitlement: {
@@ -414,6 +441,8 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
},
};
});
return isPro;
}, []);
const syncRevenueCatState = useCallback(async (
@@ -424,7 +453,11 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
source,
customerInfo: summarizeRevenueCatCustomerInfo(customerInfo),
});
applyRevenueCatCustomerInfoLocally(customerInfo, source);
const didActivatePro = applyRevenueCatCustomerInfoLocally(customerInfo, source);
const isSubscriptionActivation = source === 'subscription_purchase' && didActivatePro;
if (isSubscriptionActivation) {
setIsActivatingEntitlement(true);
}
try {
const response = await backendApiClient.syncRevenueCatState({ customerInfo, source });
setBillingSummary(response.billing);
@@ -432,6 +465,10 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
} catch (error) {
console.error('Failed to sync RevenueCat state with backend', error);
return null;
} finally {
if (isSubscriptionActivation) {
setIsActivatingEntitlement(false);
}
}
}, [applyRevenueCatCustomerInfoLocally]);
@@ -537,8 +574,9 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
colorPalette,
profileName,
profileImageUri,
billingSummary,
resolvedScheme,
billingSummary,
isActivatingEntitlement,
resolvedScheme,
isDarkMode,
isInitializing,
isLoadingPlants,