Fehler webhook discod

This commit is contained in:
2026-07-02 23:28:30 +02:00
parent 3505bc149d
commit 41327f9557
21 changed files with 15637 additions and 47071 deletions

View File

@@ -10,16 +10,16 @@ import {
ColorPalette,
} from '../types';
import { ImageCacheService } from '../services/imageCacheService';
import { getTranslation } from '../utils/translations';
import { backendApiClient } from '../services/backend/backendApiClient';
import {
BillingSummary,
PurchaseProductId,
RevenueCatCustomerInfo,
RevenueCatEntitlementInfo,
RevenueCatSyncSource,
SimulatedWebhookEvent,
} from '../services/backend/contracts';
import { getTranslation } from '../utils/translations';
import { backendApiClient } from '../services/backend/backendApiClient';
import {
BillingSummary,
PurchaseProductId,
RevenueCatCustomerInfo,
RevenueCatEntitlementInfo,
RevenueCatSyncSource,
SimulatedWebhookEvent,
} from '../services/backend/contracts';
import { createIdempotencyKey } from '../utils/idempotency';
import { AuthService, AuthSession } from '../services/authService';
import { PlantsDb, SettingsDb, LexiconHistoryDb, AppMetaDb } from '../services/database';
@@ -32,9 +32,9 @@ interface AppState {
colorPalette: ColorPalette;
profileName: string;
profileImageUri: string | null;
billingSummary: BillingSummary | null;
isActivatingEntitlement: boolean;
resolvedScheme: AppColorScheme;
billingSummary: BillingSummary | null;
isActivatingEntitlement: boolean;
resolvedScheme: AppColorScheme;
isDarkMode: boolean;
isInitializing: boolean;
isLoadingPlants: boolean;
@@ -48,11 +48,11 @@ interface AppState {
changeLanguage: (lang: Language) => void;
savePlant: (result: IdentificationResult, imageUri: string, overrideSession?: AuthSession) => Promise<void>;
deletePlant: (id: string) => Promise<void>;
updatePlant: (plant: Plant) => void;
refreshPlants: () => void;
refreshBillingSummary: () => Promise<void>;
syncRevenueCatState: (customerInfo: RevenueCatCustomerInfo, source?: RevenueCatSyncSource) => Promise<BillingSummary | null>;
simulatePurchase: (productId: PurchaseProductId) => Promise<void>;
updatePlant: (plant: Plant) => void;
refreshPlants: () => void;
refreshBillingSummary: () => Promise<void>;
syncRevenueCatState: (customerInfo: RevenueCatCustomerInfo, source?: RevenueCatSyncSource) => Promise<BillingSummary | null>;
simulatePurchase: (productId: PurchaseProductId) => Promise<void>;
simulateWebhookEvent: (event: SimulatedWebhookEvent, payload?: { credits?: number }) => Promise<void>;
getLexiconSearchHistory: () => string[];
saveLexiconSearchQuery: (query: string) => void;
@@ -78,47 +78,49 @@ export const useApp = () => {
return ctx;
};
const isAppearanceMode = (v: string): v is AppearanceMode =>
v === 'system' || v === 'light' || v === 'dark';
const isColorPalette = (v: string): v is ColorPalette =>
v === 'forest' || v === 'ocean' || v === 'sunset' || v === 'mono';
const isLanguage = (v: string): v is Language => v === 'de' || v === 'en' || v === 'es';
const REVENUECAT_PRO_ENTITLEMENT_ID = (process.env.EXPO_PUBLIC_REVENUECAT_PRO_ENTITLEMENT_ID || 'pro').trim() || 'pro';
const SUPPORTED_REVENUECAT_SUBSCRIPTION_PRODUCTS = new Set<PurchaseProductId>(['monthly_pro', 'yearly_pro']);
const summarizeRevenueCatCustomerInfo = (customerInfo: RevenueCatCustomerInfo) => {
const activeEntitlements = customerInfo?.entitlements?.active || {};
return {
appUserId: customerInfo?.appUserId ?? null,
originalAppUserId: customerInfo?.originalAppUserId ?? null,
activeEntitlements: Object.entries(activeEntitlements).map(([id, entitlement]) => ({
id,
productIdentifier: entitlement?.productIdentifier ?? null,
expirationDate: entitlement?.expirationDate || entitlement?.expiresDate || null,
})),
allPurchasedProductIdentifiers: customerInfo?.allPurchasedProductIdentifiers ?? [],
nonSubscriptionTransactions: Object.values(customerInfo?.nonSubscriptions || {}).flatMap((entries) =>
(Array.isArray(entries) ? entries : []).map((transaction) => ({
productIdentifier: transaction?.productIdentifier ?? null,
transactionIdentifier: transaction?.transactionIdentifier || transaction?.transactionId || null,
}))),
};
};
const getValidProEntitlement = (customerInfo: RevenueCatCustomerInfo): RevenueCatEntitlementInfo | null => {
const activeEntitlements = customerInfo?.entitlements?.active || {};
const proEntitlement = activeEntitlements[REVENUECAT_PRO_ENTITLEMENT_ID];
if (!proEntitlement) {
return null;
}
if (proEntitlement.productIdentifier && SUPPORTED_REVENUECAT_SUBSCRIPTION_PRODUCTS.has(proEntitlement.productIdentifier as PurchaseProductId)) {
return proEntitlement;
}
console.warn('[Billing] Ignoring unsupported RevenueCat pro entitlement during local sync', summarizeRevenueCatCustomerInfo(customerInfo));
return null;
};
const isAppearanceMode = (v: string): v is AppearanceMode =>
v === 'system' || v === 'light' || v === 'dark';
const isColorPalette = (v: string): v is ColorPalette =>
v === 'forest' || v === 'ocean' || v === 'sunset' || v === 'mono';
const isLanguage = (v: string): v is Language => v === 'de' || v === 'en' || v === 'es';
const REVENUECAT_PRO_ENTITLEMENT_ID = (process.env.EXPO_PUBLIC_REVENUECAT_PRO_ENTITLEMENT_ID || 'pro').trim() || 'pro';
const SUPPORTED_REVENUECAT_SUBSCRIPTION_PRODUCTS = new Set<PurchaseProductId>(['monthly_pro', 'yearly_pro']);
const summarizeRevenueCatCustomerInfo = (customerInfo: RevenueCatCustomerInfo) => {
const activeEntitlements = customerInfo?.entitlements?.active || {};
return {
appUserId: customerInfo?.appUserId ?? null,
originalAppUserId: customerInfo?.originalAppUserId ?? null,
activeEntitlements: Object.entries(activeEntitlements).map(([id, entitlement]) => ({
id,
productIdentifier: entitlement?.productIdentifier ?? null,
expirationDate: entitlement?.expirationDate || entitlement?.expiresDate || null,
})),
allPurchasedProductIdentifiers: customerInfo?.allPurchasedProductIdentifiers ?? [],
nonSubscriptionTransactions: [
...(Array.isArray(customerInfo?.nonSubscriptionTransactions) ? customerInfo.nonSubscriptionTransactions : []),
...Object.values(customerInfo?.nonSubscriptions || {}).flatMap((entries) => (Array.isArray(entries) ? entries : [])),
].map((transaction) => ({
productIdentifier: transaction?.productIdentifier ?? null,
transactionIdentifier: transaction?.transactionIdentifier || transaction?.transactionId || null,
})),
};
};
const getValidProEntitlement = (customerInfo: RevenueCatCustomerInfo): RevenueCatEntitlementInfo | null => {
const activeEntitlements = customerInfo?.entitlements?.active || {};
const proEntitlement = activeEntitlements[REVENUECAT_PRO_ENTITLEMENT_ID];
if (!proEntitlement) {
return null;
}
if (proEntitlement.productIdentifier && SUPPORTED_REVENUECAT_SUBSCRIPTION_PRODUCTS.has(proEntitlement.productIdentifier as PurchaseProductId)) {
return proEntitlement;
}
console.warn('[Billing] Ignoring unsupported RevenueCat pro entitlement during local sync', summarizeRevenueCatCustomerInfo(customerInfo));
return null;
};
const getDeviceLanguage = (): Language => {
try {
@@ -153,9 +155,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 [isActivatingEntitlement, setIsActivatingEntitlement] = useState(false);
const [billingSummary, setBillingSummary] = useState<BillingSummary | null>(null);
const [isLoadingBilling, setIsLoadingBilling] = useState(true);
const [isActivatingEntitlement, setIsActivatingEntitlement] = useState(false);
const resolvedScheme: AppColorScheme =
appearanceMode === 'system'
@@ -386,93 +388,93 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const applyRevenueCatCustomerInfoLocally = useCallback((
customerInfo: RevenueCatCustomerInfo,
source: RevenueCatSyncSource = 'app_init',
) => {
if (source === 'topup_purchase') {
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 (!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: {
...prev.entitlement,
plan: isPro ? 'pro' : 'free',
provider: 'revenuecat',
status: isPro ? 'active' : 'inactive',
renewsAt: proEntitlement?.expirationDate || proEntitlement?.expiresDate || null,
},
};
});
return isPro;
}, []);
const syncRevenueCatState = useCallback(async (
customerInfo: RevenueCatCustomerInfo,
source: RevenueCatSyncSource = 'app_init',
) => {
console.log('[Billing] Syncing RevenueCat customer info', {
source,
customerInfo: summarizeRevenueCatCustomerInfo(customerInfo),
});
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);
return response.billing;
} catch (error) {
console.error('Failed to sync RevenueCat state with backend', error);
return null;
} finally {
if (isSubscriptionActivation) {
setIsActivatingEntitlement(false);
}
}
}, [applyRevenueCatCustomerInfoLocally]);
const simulatePurchase = useCallback(async (productId: PurchaseProductId) => {
const applyRevenueCatCustomerInfoLocally = useCallback((
customerInfo: RevenueCatCustomerInfo,
source: RevenueCatSyncSource = 'app_init',
) => {
if (source === 'topup_purchase') {
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 (!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: {
...prev.entitlement,
plan: isPro ? 'pro' : 'free',
provider: 'revenuecat',
status: isPro ? 'active' : 'inactive',
renewsAt: proEntitlement?.expirationDate || proEntitlement?.expiresDate || null,
},
};
});
return isPro;
}, []);
const syncRevenueCatState = useCallback(async (
customerInfo: RevenueCatCustomerInfo,
source: RevenueCatSyncSource = 'app_init',
) => {
console.log('[Billing] Syncing RevenueCat customer info', {
source,
customerInfo: summarizeRevenueCatCustomerInfo(customerInfo),
});
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);
return response.billing;
} catch (error) {
console.error('Failed to sync RevenueCat state with backend', error);
return null;
} finally {
if (isSubscriptionActivation) {
setIsActivatingEntitlement(false);
}
}
}, [applyRevenueCatCustomerInfoLocally]);
const simulatePurchase = useCallback(async (productId: PurchaseProductId) => {
const response = await backendApiClient.simulatePurchase({
idempotencyKey: createIdempotencyKey('purchase', productId),
productId,
@@ -574,9 +576,9 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
colorPalette,
profileName,
profileImageUri,
billingSummary,
isActivatingEntitlement,
resolvedScheme,
billingSummary,
isActivatingEntitlement,
resolvedScheme,
isDarkMode,
isInitializing,
isLoadingPlants,
@@ -589,11 +591,11 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
changeLanguage,
savePlant,
deletePlant,
updatePlant,
refreshPlants,
refreshBillingSummary,
syncRevenueCatState,
simulatePurchase,
updatePlant,
refreshPlants,
refreshBillingSummary,
syncRevenueCatState,
simulatePurchase,
simulateWebhookEvent,
getLexiconSearchHistory,
saveLexiconSearchQuery,