Slefhostet und postgres
This commit is contained in:
@@ -4,12 +4,68 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import Constants from 'expo-constants';
|
||||
import Purchases, { PRODUCT_CATEGORY } from 'react-native-purchases';
|
||||
import Purchases, {
|
||||
PACKAGE_TYPE,
|
||||
PRODUCT_CATEGORY,
|
||||
PurchasesOffering,
|
||||
PurchasesPackage,
|
||||
PurchasesStoreProduct,
|
||||
} from 'react-native-purchases';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { Language } from '../../types';
|
||||
import { PurchaseProductId } from '../../services/backend/contracts';
|
||||
import { Language } from '../../types';
|
||||
import { PurchaseProductId } from '../../services/backend/contracts';
|
||||
|
||||
type SubscriptionProductId = 'monthly_pro' | 'yearly_pro';
|
||||
type TopupProductId = Extract<PurchaseProductId, 'topup_small' | 'topup_medium' | 'topup_large'>;
|
||||
type SubscriptionPackages = Partial<Record<SubscriptionProductId, PurchasesPackage>>;
|
||||
type TopupProducts = Partial<Record<TopupProductId, PurchasesStoreProduct>>;
|
||||
|
||||
const isMatchingPackage = (
|
||||
pkg: PurchasesPackage,
|
||||
productId: SubscriptionProductId,
|
||||
expectedPackageType: PACKAGE_TYPE,
|
||||
) => {
|
||||
return (
|
||||
pkg.product.identifier === productId
|
||||
|| pkg.identifier === productId
|
||||
|| pkg.packageType === expectedPackageType
|
||||
);
|
||||
};
|
||||
|
||||
const resolveSubscriptionPackages = (offering: PurchasesOffering | null): SubscriptionPackages => {
|
||||
if (!offering) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const availablePackages = [
|
||||
offering.monthly,
|
||||
offering.annual,
|
||||
...offering.availablePackages,
|
||||
].filter((value): value is PurchasesPackage => Boolean(value));
|
||||
|
||||
return {
|
||||
monthly_pro: availablePackages.find((pkg) => isMatchingPackage(pkg, 'monthly_pro', PACKAGE_TYPE.MONTHLY)),
|
||||
yearly_pro: availablePackages.find((pkg) => isMatchingPackage(pkg, 'yearly_pro', PACKAGE_TYPE.ANNUAL)),
|
||||
};
|
||||
};
|
||||
|
||||
const summarizeOfferingPackages = (offering: PurchasesOffering | null) => {
|
||||
if (!offering) {
|
||||
return { identifier: null, packages: [] as Array<Record<string, string | null>> };
|
||||
}
|
||||
|
||||
return {
|
||||
identifier: offering.identifier,
|
||||
packages: offering.availablePackages.map((pkg) => ({
|
||||
identifier: pkg.identifier,
|
||||
packageType: pkg.packageType,
|
||||
productIdentifier: pkg.product.identifier,
|
||||
priceString: pkg.product.priceString,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const getBillingCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
@@ -161,8 +217,8 @@ export default function BillingScreen() {
|
||||
const [subModalVisible, setSubModalVisible] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [storeReady, setStoreReady] = useState(isExpoGo);
|
||||
const [subscriptionPackages, setSubscriptionPackages] = useState<Partial<Record<'monthly_pro' | 'yearly_pro', any>>>({});
|
||||
const [topupProducts, setTopupProducts] = useState<Partial<Record<'topup_small' | 'topup_medium' | 'topup_large', any>>>({});
|
||||
const [subscriptionPackages, setSubscriptionPackages] = useState<SubscriptionPackages>({});
|
||||
const [topupProducts, setTopupProducts] = useState<TopupProducts>({});
|
||||
|
||||
// Cancel Flow State
|
||||
const [cancelStep, setCancelStep] = useState<'none' | 'survey' | 'offer'>('none');
|
||||
@@ -187,11 +243,13 @@ export default function BillingScreen() {
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
const currentOffering = offerings.current;
|
||||
setSubscriptionPackages({
|
||||
monthly_pro: currentOffering?.monthly ?? undefined,
|
||||
yearly_pro: currentOffering?.annual ?? undefined,
|
||||
});
|
||||
const currentOffering = offerings.current;
|
||||
const resolvedPackages = resolveSubscriptionPackages(currentOffering);
|
||||
if (!resolvedPackages.monthly_pro || !resolvedPackages.yearly_pro) {
|
||||
console.warn('[Billing] RevenueCat offering missing expected subscription packages', summarizeOfferingPackages(currentOffering));
|
||||
}
|
||||
|
||||
setSubscriptionPackages(resolvedPackages);
|
||||
|
||||
setTopupProducts({
|
||||
topup_small: topups.find((product) => product.identifier === 'topup_small'),
|
||||
@@ -244,22 +302,29 @@ export default function BillingScreen() {
|
||||
return;
|
||||
}
|
||||
const selectedPackage = productId === 'monthly_pro' ? monthlyPackage : yearlyPackage;
|
||||
if (!selectedPackage) {
|
||||
const latestOffering = !selectedPackage
|
||||
? await Purchases.getOfferings().then((offerings) => offerings.current)
|
||||
: null;
|
||||
if (!selectedPackage) {
|
||||
console.warn('[Billing] Purchase blocked because subscription package was not resolved', {
|
||||
productId,
|
||||
offering: summarizeOfferingPackages(latestOffering),
|
||||
});
|
||||
throw new Error('Abo-Paket konnte nicht geladen werden. Bitte RevenueCat Offering prüfen.');
|
||||
}
|
||||
await Purchases.purchasePackage(selectedPackage);
|
||||
// Derive plan locally from RevenueCat — backend sync via webhook comes later (Step 3)
|
||||
const customerInfo = await Purchases.getCustomerInfo();
|
||||
await syncRevenueCatState(customerInfo as any);
|
||||
} else {
|
||||
await syncRevenueCatState(customerInfo as any, 'subscription_purchase');
|
||||
} else {
|
||||
const selectedProduct = topupProducts[productId];
|
||||
if (!selectedProduct) {
|
||||
throw new Error('Top-up Produkt konnte nicht geladen werden. Bitte Store-Produkt IDs prüfen.');
|
||||
}
|
||||
await Purchases.purchaseStoreProduct(selectedProduct);
|
||||
const customerInfo = await Purchases.getCustomerInfo();
|
||||
await syncRevenueCatState(customerInfo as any);
|
||||
}
|
||||
await syncRevenueCatState(customerInfo as any, 'topup_purchase');
|
||||
}
|
||||
}
|
||||
setSubModalVisible(false);
|
||||
} catch (e) {
|
||||
@@ -282,7 +347,7 @@ export default function BillingScreen() {
|
||||
try {
|
||||
if (!isExpoGo) {
|
||||
const customerInfo = await Purchases.restorePurchases();
|
||||
await syncRevenueCatState(customerInfo as any);
|
||||
await syncRevenueCatState(customerInfo as any, 'restore');
|
||||
}
|
||||
Alert.alert(copy.restorePurchases, '✓');
|
||||
} catch (e) {
|
||||
@@ -413,11 +478,11 @@ export default function BillingScreen() {
|
||||
</View>
|
||||
|
||||
<View style={[styles.legalLinksRow, { marginTop: 16 }]}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlns-landing.vercel.app/privacy')}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/privacy')}>
|
||||
<Text style={[styles.legalLink, { color: colors.primary }]}>Privacy Policy</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.legalSep, { color: colors.textMuted }]}> · </Text>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlns-landing.vercel.app/terms')}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/terms')}>
|
||||
<Text style={[styles.legalLink, { color: colors.primary }]}>Terms of Use</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -467,11 +532,11 @@ export default function BillingScreen() {
|
||||
))}
|
||||
</View>
|
||||
<View style={[styles.legalLinksRow, { marginTop: 12 }]}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlns-landing.vercel.app/privacy')}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/privacy')}>
|
||||
<Text style={[styles.legalLink, { color: colors.primary }]}>Privacy Policy</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.legalSep, { color: colors.textMuted }]}> · </Text>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlns-landing.vercel.app/terms')}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/terms')}>
|
||||
<Text style={[styles.legalLink, { color: colors.primary }]}>Terms of Use</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -524,10 +589,10 @@ export default function BillingScreen() {
|
||||
styles.planOption,
|
||||
{ borderColor: colors.border },
|
||||
planId === 'pro' && { borderColor: colors.primary, backgroundColor: colors.primary + '10' }
|
||||
]}
|
||||
onPress={() => handlePurchase('monthly_pro')}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
]}
|
||||
onPress={() => handlePurchase('monthly_pro')}
|
||||
disabled={isUpdating || !storeReady}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.planHeaderRow}>
|
||||
<Text style={[styles.planName, { color: colors.text }]}>{copy.proPlanName}</Text>
|
||||
@@ -582,11 +647,11 @@ export default function BillingScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.legalLinksRow}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlns-landing.vercel.app/privacy')}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/privacy')}>
|
||||
<Text style={[styles.legalLink, { color: colors.primary }]}>Privacy Policy</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.legalSep, { color: colors.textMuted }]}> · </Text>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlns-landing.vercel.app/terms')}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/terms')}>
|
||||
<Text style={[styles.legalLink, { color: colors.primary }]}>Terms of Use</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user