Fehler webhook discod
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user