mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 19:59:40 +02:00
Complete migration of Context mobile app from direct Supabase access to NestJS backend API with mana-core-auth authentication. New files: - context/AuthProvider.tsx: mana-core-auth integration via @manacore/shared-auth - services/backendApi.ts: Backend API client for spaces, documents, AI, tokens Rewritten services (same exports, backend implementation): - supabaseService.ts: Now thin wrapper around backendApi - aiService.ts: Uses backendApi for auth token - tokenCountingService.ts: Model prices from backend API - tokenTransactionService.ts: All token ops via backend API - revenueCatService.ts: Token balance via backend API Updated 16 consumer files (auth forms, token components, AI toolbars) Deleted: - utils/supabase.ts, context/AuthContext.tsx - services/spaceService.ts, services/spaceServiceDirect.ts Dependencies: - Added: @manacore/shared-auth, expo-secure-store - Removed: @supabase/supabase-js, @google/generative-ai, openai, @azure/openai Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
386 lines
11 KiB
TypeScript
386 lines
11 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ActivityIndicator,
|
|
Alert,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
Platform,
|
|
} from 'react-native';
|
|
import { PurchasesPackage, PACKAGE_TYPE } from 'react-native-purchases';
|
|
import {
|
|
getOfferings,
|
|
purchasePackage,
|
|
getCurrentTokenBalance,
|
|
TOKEN_AMOUNTS,
|
|
ENTITLEMENTS,
|
|
} from '../../services/revenueCatService';
|
|
import { themeClasses, useColorModeValue } from '../../utils/theme/theme';
|
|
|
|
type TokenStoreProps = {
|
|
onClose?: () => void;
|
|
onPurchaseComplete?: () => void;
|
|
};
|
|
|
|
export const TokenStore: React.FC<TokenStoreProps> = ({ onClose, onPurchaseComplete }) => {
|
|
const [packages, setPackages] = useState<PurchasesPackage[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [purchasing, setPurchasing] = useState(false);
|
|
const [tokenBalance, setTokenBalance] = useState<number | null>(null);
|
|
const [activeTab, setActiveTab] = useState<'subscription' | 'onetime'>('subscription');
|
|
|
|
const bgColor = useColorModeValue('white', 'gray.800');
|
|
const textColor = useColorModeValue('gray.800', 'white');
|
|
const cardBgColor = useColorModeValue('gray.50', 'gray.700');
|
|
const accentColor = useColorModeValue('blue.500', 'blue.300');
|
|
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Angebote laden
|
|
const offerings = await getOfferings();
|
|
if (offerings) {
|
|
setPackages(offerings);
|
|
}
|
|
|
|
// Token-Guthaben laden
|
|
const balance = await getCurrentTokenBalance();
|
|
setTokenBalance(balance);
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Store-Daten:', error);
|
|
Alert.alert('Fehler', 'Beim Laden der Angebote ist ein Fehler aufgetreten.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadData();
|
|
}, []);
|
|
|
|
const handlePurchase = async (pkg: PurchasesPackage) => {
|
|
try {
|
|
setPurchasing(true);
|
|
|
|
// Kaufe das Paket
|
|
const success = await purchasePackage(pkg);
|
|
|
|
if (success) {
|
|
// Aktualisiere das Token-Guthaben
|
|
const newBalance = await getCurrentTokenBalance();
|
|
setTokenBalance(newBalance);
|
|
|
|
// Benachrichtige den Benutzer
|
|
Alert.alert('Kauf erfolgreich', 'Dein Credit-Guthaben wurde aktualisiert.', [
|
|
{ text: 'OK', onPress: () => onPurchaseComplete?.() },
|
|
]);
|
|
} else {
|
|
// Fehlerbehandlung
|
|
Alert.alert('Kauf fehlgeschlagen', 'Ein unbekannter Fehler ist aufgetreten.');
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Kauf:', error);
|
|
Alert.alert('Kauf fehlgeschlagen', 'Ein unerwarteter Fehler ist aufgetreten.');
|
|
} finally {
|
|
setPurchasing(false);
|
|
}
|
|
};
|
|
|
|
// Diese Funktion wird nicht mehr verwendet, da wir jetzt getCreditsForPackage verwenden
|
|
|
|
if (loading) {
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: bgColor }]}>
|
|
<ActivityIndicator size="large" color={accentColor} />
|
|
<Text style={[styles.loadingText, { color: textColor }]}>Angebote werden geladen...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// Filtere Pakete nach Typ (Abonnement oder Einmalkauf)
|
|
const subscriptionPackages = packages.filter(
|
|
(pkg) => pkg.packageType === PACKAGE_TYPE.MONTHLY || pkg.packageType === PACKAGE_TYPE.ANNUAL
|
|
);
|
|
|
|
const onetimePackages = packages.filter(
|
|
(pkg) => pkg.packageType === PACKAGE_TYPE.CUSTOM || pkg.packageType === PACKAGE_TYPE.LIFETIME
|
|
);
|
|
|
|
// Formatiere Credits in Millionen
|
|
const formatCredits = (credits: number) => {
|
|
const millions = credits / 1000000;
|
|
return millions.toFixed(1).replace(/\.0$/, '') + ' Mio';
|
|
};
|
|
|
|
// Bestimme den Pakettyp basierend auf der Produkt-ID
|
|
const getPackageType = (pkg: PurchasesPackage) => {
|
|
const productId = pkg.product.identifier.toLowerCase();
|
|
if (productId.includes('mini') || productId.includes('plus') || productId.includes('pro')) {
|
|
return 'subscription';
|
|
}
|
|
return 'onetime';
|
|
};
|
|
|
|
// Bestimme die Anzahl der Credits basierend auf der Produkt-ID
|
|
const getCreditsForPackage = (pkg: PurchasesPackage): number => {
|
|
const productId = pkg.product.identifier;
|
|
|
|
// Abonnements
|
|
if (productId.includes('Mini_5E')) return TOKEN_AMOUNTS[ENTITLEMENTS.MINI_SUB];
|
|
if (productId.includes('Plus_11E')) return TOKEN_AMOUNTS[ENTITLEMENTS.PLUS_SUB];
|
|
if (productId.includes('Pro_18E')) return TOKEN_AMOUNTS[ENTITLEMENTS.PRO_SUB];
|
|
|
|
// Einmalkäufe
|
|
if (productId.includes('Small_5E')) return TOKEN_AMOUNTS[ENTITLEMENTS.SMALL_TOKENS];
|
|
if (productId.includes('Medium_10E')) return TOKEN_AMOUNTS[ENTITLEMENTS.MEDIUM_TOKENS];
|
|
if (productId.includes('Large_20E')) return TOKEN_AMOUNTS[ENTITLEMENTS.LARGE_TOKENS];
|
|
|
|
return 0;
|
|
};
|
|
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: bgColor }]}>
|
|
<Text style={[styles.title, { color: textColor }]}>Credits kaufen</Text>
|
|
|
|
{tokenBalance !== null && (
|
|
<View style={styles.balanceContainer}>
|
|
<Text style={[styles.balanceText, { color: textColor }]}>
|
|
Aktuelles Guthaben:{' '}
|
|
<Text style={styles.balanceAmount}>{tokenBalance.toLocaleString()} Credits</Text>
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Tabs für Abonnements und Einmalkäufe */}
|
|
<View style={styles.tabContainer}>
|
|
<TouchableOpacity
|
|
style={[styles.tabButton, activeTab === 'subscription' && styles.activeTabButton]}
|
|
onPress={() => setActiveTab('subscription')}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.tabButtonText,
|
|
activeTab === 'subscription' && styles.activeTabButtonText,
|
|
]}
|
|
>
|
|
Abonnements
|
|
</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[styles.tabButton, activeTab === 'onetime' && styles.activeTabButton]}
|
|
onPress={() => setActiveTab('onetime')}
|
|
>
|
|
<Text
|
|
style={[styles.tabButtonText, activeTab === 'onetime' && styles.activeTabButtonText]}
|
|
>
|
|
Einmalkäufe
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<ScrollView style={styles.packagesContainer}>
|
|
{packages.length === 0 ? (
|
|
<Text style={[styles.noPackagesText, { color: textColor }]}>
|
|
Keine Angebote verfügbar. Bitte versuche es später erneut.
|
|
</Text>
|
|
) : activeTab === 'subscription' ? (
|
|
// Abonnements anzeigen
|
|
subscriptionPackages.length > 0 ? (
|
|
subscriptionPackages.map((pkg, index) => (
|
|
<TouchableOpacity
|
|
key={index}
|
|
style={[styles.packageCard, { backgroundColor: cardBgColor }]}
|
|
onPress={() => handlePurchase(pkg)}
|
|
disabled={purchasing}
|
|
>
|
|
<View style={styles.packageInfo}>
|
|
<Text style={[styles.packageTitle, { color: textColor }]}>
|
|
{pkg.product.title}
|
|
</Text>
|
|
<Text style={[styles.packageDescription, { color: textColor }]}>
|
|
{formatCredits(getCreditsForPackage(pkg))} Credits monatlich
|
|
</Text>
|
|
<Text style={[styles.packagePrice, { color: accentColor }]}>
|
|
{pkg.product.priceString} / Monat
|
|
</Text>
|
|
</View>
|
|
<View style={styles.buyButtonContainer}>
|
|
<TouchableOpacity
|
|
style={[styles.buyButton, { backgroundColor: accentColor }]}
|
|
onPress={() => handlePurchase(pkg)}
|
|
disabled={purchasing}
|
|
>
|
|
{purchasing ? (
|
|
<ActivityIndicator size="small" color="white" />
|
|
) : (
|
|
<Text style={styles.buyButtonText}>Abonnieren</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</TouchableOpacity>
|
|
))
|
|
) : (
|
|
<Text style={[styles.noPackagesText, { color: textColor }]}>
|
|
Keine Abonnements verfügbar. Bitte versuche es später erneut.
|
|
</Text>
|
|
)
|
|
) : // Einmalkäufe anzeigen
|
|
onetimePackages.length > 0 ? (
|
|
onetimePackages.map((pkg, index) => (
|
|
<TouchableOpacity
|
|
key={index}
|
|
style={[styles.packageCard, { backgroundColor: cardBgColor }]}
|
|
onPress={() => handlePurchase(pkg)}
|
|
disabled={purchasing}
|
|
>
|
|
<View style={styles.packageInfo}>
|
|
<Text style={[styles.packageTitle, { color: textColor }]}>{pkg.product.title}</Text>
|
|
<Text style={[styles.packageDescription, { color: textColor }]}>
|
|
{formatCredits(getCreditsForPackage(pkg))} Credits
|
|
</Text>
|
|
<Text style={[styles.packagePrice, { color: accentColor }]}>
|
|
{pkg.product.priceString}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.buyButtonContainer}>
|
|
<TouchableOpacity
|
|
style={[styles.buyButton, { backgroundColor: accentColor }]}
|
|
onPress={() => handlePurchase(pkg)}
|
|
disabled={purchasing}
|
|
>
|
|
{purchasing ? (
|
|
<ActivityIndicator size="small" color="white" />
|
|
) : (
|
|
<Text style={styles.buyButtonText}>Kaufen</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</TouchableOpacity>
|
|
))
|
|
) : (
|
|
<Text style={[styles.noPackagesText, { color: textColor }]}>
|
|
Keine Einmalkäufe verfügbar. Bitte versuche es später erneut.
|
|
</Text>
|
|
)}
|
|
</ScrollView>
|
|
|
|
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
|
<Text style={[styles.closeButtonText, { color: textColor }]}>Schließen</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
padding: 16,
|
|
},
|
|
title: {
|
|
fontSize: 24,
|
|
fontWeight: 'bold',
|
|
marginBottom: 16,
|
|
textAlign: 'center',
|
|
},
|
|
loadingText: {
|
|
marginTop: 16,
|
|
textAlign: 'center',
|
|
},
|
|
balanceContainer: {
|
|
marginBottom: 24,
|
|
padding: 12,
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: '#ddd',
|
|
},
|
|
balanceText: {
|
|
fontSize: 16,
|
|
textAlign: 'center',
|
|
},
|
|
balanceAmount: {
|
|
fontWeight: 'bold',
|
|
},
|
|
tabContainer: {
|
|
flexDirection: 'row',
|
|
marginBottom: 16,
|
|
borderRadius: 8,
|
|
overflow: 'hidden',
|
|
borderWidth: 1,
|
|
borderColor: '#ddd',
|
|
},
|
|
tabButton: {
|
|
flex: 1,
|
|
paddingVertical: 12,
|
|
alignItems: 'center',
|
|
backgroundColor: '#f5f5f5',
|
|
},
|
|
activeTabButton: {
|
|
backgroundColor: '#3b82f6',
|
|
},
|
|
tabButtonText: {
|
|
fontWeight: '600',
|
|
color: '#666',
|
|
},
|
|
activeTabButtonText: {
|
|
color: 'white',
|
|
},
|
|
packagesContainer: {
|
|
flex: 1,
|
|
},
|
|
noPackagesText: {
|
|
textAlign: 'center',
|
|
marginTop: 24,
|
|
},
|
|
packageCard: {
|
|
flexDirection: 'row',
|
|
borderRadius: 8,
|
|
marginBottom: 16,
|
|
padding: 16,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 4,
|
|
elevation: 2,
|
|
},
|
|
packageInfo: {
|
|
flex: 1,
|
|
},
|
|
packageTitle: {
|
|
fontSize: 18,
|
|
fontWeight: 'bold',
|
|
marginBottom: 4,
|
|
},
|
|
packageDescription: {
|
|
fontSize: 14,
|
|
marginBottom: 8,
|
|
},
|
|
packagePrice: {
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
},
|
|
buyButtonContainer: {
|
|
justifyContent: 'center',
|
|
},
|
|
buyButton: {
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 8,
|
|
borderRadius: 4,
|
|
},
|
|
buyButtonText: {
|
|
color: 'white',
|
|
fontWeight: 'bold',
|
|
},
|
|
closeButton: {
|
|
marginTop: 16,
|
|
padding: 12,
|
|
alignItems: 'center',
|
|
},
|
|
closeButtonText: {
|
|
fontSize: 16,
|
|
},
|
|
});
|
|
|
|
export default TokenStore;
|