- Refactor subscription page with simplified UI flow - Update Stripe routes and config for price handling - Improve AuthContext with better profile management - Fix equipment status and beneficiary screens - Update voice screen and profile pages - Simplify purchase flow
761 lines
23 KiB
TypeScript
761 lines
23 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
Alert,
|
|
ActivityIndicator,
|
|
Modal,
|
|
} from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { router, useLocalSearchParams } from 'expo-router';
|
|
import { usePaymentSheet } from '@stripe/stripe-react-native';
|
|
import { AppColors, BorderRadius, FontSizes, FontWeights, Spacing, Shadows } from '@/constants/theme';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { api } from '@/services/api';
|
|
import { hasBeneficiaryDevices } from '@/services/BeneficiaryDetailController';
|
|
import type { Beneficiary } from '@/types';
|
|
|
|
const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe';
|
|
const SUBSCRIPTION_PRICE = 49; // $49/month
|
|
|
|
|
|
export default function SubscriptionScreen() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [isCanceling, setIsCanceling] = useState(false);
|
|
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
|
const [justSubscribed, setJustSubscribed] = useState(false); // Prevent self-guard redirect after payment
|
|
|
|
const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet();
|
|
const { user } = useAuth();
|
|
|
|
useEffect(() => {
|
|
loadBeneficiary();
|
|
}, [id]);
|
|
|
|
const loadBeneficiary = async () => {
|
|
if (!id) return;
|
|
|
|
try {
|
|
const response = await api.getWellNuoBeneficiary(parseInt(id, 10));
|
|
if (response.ok && response.data) {
|
|
setBeneficiary(response.data);
|
|
} else {
|
|
console.error('Failed to load beneficiary:', response.error);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load beneficiary:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const subscription = beneficiary?.subscription;
|
|
const isActive = (subscription?.status === 'active' || subscription?.status === 'trialing') &&
|
|
subscription?.endDate &&
|
|
new Date(subscription.endDate) > new Date();
|
|
|
|
// Check if subscription is canceled but still active until period end
|
|
const isCanceledButActive = isActive && subscription?.cancelAtPeriodEnd === true;
|
|
|
|
// Self-guard: redirect if user shouldn't be on this page
|
|
useEffect(() => {
|
|
if (isLoading || !beneficiary || !id) return;
|
|
|
|
// Don't redirect if we just subscribed and modal is showing
|
|
if (justSubscribed || showSuccessModal) return;
|
|
|
|
// If no devices - redirect to purchase (waterfall priority)
|
|
if (!hasBeneficiaryDevices(beneficiary)) {
|
|
const status = beneficiary.equipmentStatus;
|
|
if (status && ['ordered', 'shipped', 'delivered'].includes(status)) {
|
|
router.replace(`/(tabs)/beneficiaries/${id}/equipment-status`);
|
|
} else {
|
|
router.replace(`/(tabs)/beneficiaries/${id}/purchase`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If already has active subscription - redirect to dashboard
|
|
if (isActive) {
|
|
router.replace(`/(tabs)/beneficiaries/${id}`);
|
|
}
|
|
}, [beneficiary, isLoading, id, isActive, justSubscribed, showSuccessModal]);
|
|
|
|
const handleSubscribe = async () => {
|
|
if (!beneficiary) {
|
|
Alert.alert('Error', 'Beneficiary data not loaded.');
|
|
return;
|
|
}
|
|
|
|
setIsProcessing(true);
|
|
|
|
try {
|
|
// Get auth token
|
|
const token = await api.getToken();
|
|
if (!token) {
|
|
Alert.alert('Error', 'Please log in again');
|
|
setIsProcessing(false);
|
|
return;
|
|
}
|
|
|
|
// 1. Create subscription payment sheet via Stripe Subscriptions API
|
|
const response = await fetch(`${STRIPE_API_URL}/create-subscription-payment-sheet`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
beneficiaryId: beneficiary.id,
|
|
}),
|
|
});
|
|
|
|
// Check if response is OK before parsing JSON
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
let errorMessage = 'Failed to create payment';
|
|
try {
|
|
const errorJson = JSON.parse(errorText);
|
|
errorMessage = errorJson.error || errorJson.message || errorMessage;
|
|
} catch {
|
|
errorMessage = `Server error (${response.status})`;
|
|
}
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Check if already subscribed
|
|
if (data.alreadySubscribed) {
|
|
Alert.alert(
|
|
'Already Subscribed!',
|
|
`${beneficiary.name} already has an active subscription.`
|
|
);
|
|
await loadBeneficiary();
|
|
return;
|
|
}
|
|
|
|
if (!data.clientSecret) {
|
|
throw new Error(data.error || 'Failed to create subscription');
|
|
}
|
|
|
|
// 2. Initialize the Payment Sheet
|
|
// Determine if clientSecret is for PaymentIntent (pi_) or SetupIntent (seti_)
|
|
const isSetupIntent = data.clientSecret.startsWith('seti_');
|
|
|
|
const paymentSheetParams: Parameters<typeof initPaymentSheet>[0] = {
|
|
merchantDisplayName: 'WellNuo',
|
|
customerId: data.customer,
|
|
// Use Customer Session for showing saved payment methods (new API)
|
|
// Falls back to Ephemeral Key for backwards compatibility
|
|
...(data.customerSessionClientSecret
|
|
? { customerSessionClientSecret: data.customerSessionClientSecret }
|
|
: { customerEphemeralKeySecret: data.ephemeralKey }),
|
|
returnURL: 'wellnuo://stripe-redirect',
|
|
applePay: {
|
|
merchantCountryCode: 'US',
|
|
},
|
|
googlePay: {
|
|
merchantCountryCode: 'US',
|
|
testEnv: true,
|
|
},
|
|
};
|
|
|
|
// Use correct parameter based on secret type
|
|
if (isSetupIntent) {
|
|
paymentSheetParams.setupIntentClientSecret = data.clientSecret;
|
|
} else {
|
|
paymentSheetParams.paymentIntentClientSecret = data.clientSecret;
|
|
}
|
|
|
|
const { error: initError } = await initPaymentSheet(paymentSheetParams);
|
|
|
|
if (initError) {
|
|
throw new Error(initError.message);
|
|
}
|
|
|
|
// 3. Present the Payment Sheet
|
|
const { error: presentError } = await presentPaymentSheet();
|
|
|
|
if (presentError) {
|
|
if (presentError.code === 'Canceled') {
|
|
setIsProcessing(false);
|
|
return;
|
|
}
|
|
throw new Error(presentError.message);
|
|
}
|
|
|
|
// 4. Payment successful! Confirm the subscription payment
|
|
// This ensures the subscription invoice gets paid if using manual PaymentIntent
|
|
console.log('[Subscription] Payment Sheet completed, confirming payment...');
|
|
const confirmResponse = await fetch(
|
|
`${STRIPE_API_URL}/confirm-subscription-payment`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
subscriptionId: data.subscriptionId,
|
|
}),
|
|
}
|
|
);
|
|
const confirmData = await confirmResponse.json();
|
|
console.log('[Subscription] Confirm response:', confirmData);
|
|
|
|
// 5. Fetch subscription status from Stripe
|
|
const statusResponse = await fetch(
|
|
`${STRIPE_API_URL}/subscription-status/${beneficiary.id}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
}
|
|
);
|
|
const statusData = await statusResponse.json();
|
|
|
|
// Mark as just subscribed to prevent self-guard redirect during modal
|
|
setJustSubscribed(true);
|
|
|
|
// Reload beneficiary to get updated subscription
|
|
await loadBeneficiary();
|
|
|
|
// Show success modal instead of Alert
|
|
setShowSuccessModal(true);
|
|
} catch (error) {
|
|
console.error('Payment error:', error);
|
|
Alert.alert(
|
|
'Payment Failed',
|
|
error instanceof Error ? error.message : 'Something went wrong. Please try again.'
|
|
);
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
const handleCancelSubscription = () => {
|
|
if (!beneficiary) return;
|
|
|
|
Alert.alert(
|
|
'Cancel Subscription?',
|
|
`Are you sure you want to cancel the subscription for ${beneficiary.name}?\n\n• You'll keep access until ${subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'the end of the billing period'}\n• No refunds for remaining time\n• You can reactivate anytime before the period ends`,
|
|
[
|
|
{ text: 'Keep Subscription', style: 'cancel' },
|
|
{
|
|
text: 'Cancel Subscription',
|
|
style: 'destructive',
|
|
onPress: confirmCancelSubscription,
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
const confirmCancelSubscription = async () => {
|
|
if (!beneficiary) return;
|
|
|
|
setIsCanceling(true);
|
|
|
|
try {
|
|
const response = await api.cancelSubscription(beneficiary.id);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(response.error || 'Failed to cancel subscription');
|
|
}
|
|
|
|
// Reload beneficiary to get updated subscription status
|
|
await loadBeneficiary();
|
|
|
|
Alert.alert(
|
|
'Subscription Canceled',
|
|
`Your subscription will remain active until ${response.data?.cancelAt ? formatDate(new Date(response.data.cancelAt)) : 'the end of the billing period'}. You can reactivate anytime before then.`,
|
|
[{ text: 'OK' }]
|
|
);
|
|
} catch (error) {
|
|
console.error('Cancel error:', error);
|
|
Alert.alert(
|
|
'Cancellation Failed',
|
|
error instanceof Error ? error.message : 'Something went wrong. Please try again.'
|
|
);
|
|
} finally {
|
|
setIsCanceling(false);
|
|
}
|
|
};
|
|
|
|
const handleReactivateSubscription = async () => {
|
|
if (!beneficiary) return;
|
|
|
|
setIsProcessing(true);
|
|
|
|
try {
|
|
// Get auth token
|
|
const token = await api.getToken();
|
|
if (!token) {
|
|
Alert.alert('Error', 'Please log in again');
|
|
setIsProcessing(false);
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`${STRIPE_API_URL}/reactivate-subscription`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
beneficiaryId: beneficiary.id,
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok || data.error) {
|
|
throw new Error(data.error || 'Failed to reactivate subscription');
|
|
}
|
|
|
|
// Reload beneficiary to get updated subscription status
|
|
await loadBeneficiary();
|
|
|
|
Alert.alert(
|
|
'Subscription Reactivated!',
|
|
'Your subscription has been reactivated and will continue to renew automatically.',
|
|
[{ text: 'Great!' }]
|
|
);
|
|
} catch (error) {
|
|
console.error('Reactivate error:', error);
|
|
Alert.alert(
|
|
'Reactivation Failed',
|
|
error instanceof Error ? error.message : 'Something went wrong. Please try again.'
|
|
);
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
const formatDate = (date: Date) => {
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
};
|
|
|
|
const handleSuccessModalClose = () => {
|
|
setShowSuccessModal(false);
|
|
// Navigate to beneficiary dashboard after closing modal
|
|
router.replace(`/(tabs)/beneficiaries/${id}`);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>Subscription</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (!beneficiary) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>Subscription</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
<View style={styles.noBeneficiaryContainer}>
|
|
<View style={styles.noBeneficiaryIcon}>
|
|
<Ionicons name="person-outline" size={48} color={AppColors.textMuted} />
|
|
</View>
|
|
<Text style={styles.noBeneficiaryTitle}>Beneficiary Not Found</Text>
|
|
<Text style={styles.noBeneficiaryText}>
|
|
Unable to load beneficiary information.
|
|
</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>Subscription</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
<View style={styles.content}>
|
|
{/* Subscription Card */}
|
|
<View style={styles.subscriptionCard}>
|
|
<View style={styles.cardHeader}>
|
|
<Text style={styles.proBadgeText}>WellNuo Subscription</Text>
|
|
<View style={styles.priceContainer}>
|
|
<Text style={styles.priceAmount}>${SUBSCRIPTION_PRICE}</Text>
|
|
<Text style={styles.pricePeriod}>/month</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<Text style={styles.planDescription}>
|
|
Full access to real-time monitoring, AI insights, alerts, voice companion, and family sharing for {beneficiary.name}
|
|
</Text>
|
|
|
|
{/* Status indicator */}
|
|
{isActive && (
|
|
<View style={isCanceledButActive ? styles.cancelingBadge : styles.activeBadge}>
|
|
<Ionicons
|
|
name={isCanceledButActive ? 'time-outline' : 'checkmark-circle'}
|
|
size={16}
|
|
color={isCanceledButActive ? AppColors.warning : AppColors.success}
|
|
/>
|
|
<Text style={isCanceledButActive ? styles.cancelingBadgeText : styles.activeBadgeText}>
|
|
{isCanceledButActive
|
|
? `Ends ${subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'soon'}`
|
|
: `Active until ${subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'N/A'}`}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Security Badge */}
|
|
<View style={styles.securityBadge}>
|
|
<Ionicons name="shield-checkmark" size={16} color={AppColors.success} />
|
|
<Text style={styles.securityText}>Secure payment by Stripe</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Actions */}
|
|
<View style={styles.actionsSection}>
|
|
{/* Subscribe button when not active */}
|
|
{!isActive && (
|
|
<TouchableOpacity
|
|
style={[styles.subscribeButton, isProcessing && styles.buttonDisabled]}
|
|
onPress={handleSubscribe}
|
|
disabled={isProcessing}
|
|
>
|
|
{isProcessing ? (
|
|
<ActivityIndicator color={AppColors.white} />
|
|
) : (
|
|
<>
|
|
<Ionicons name="card" size={20} color={AppColors.white} />
|
|
<Text style={styles.subscribeButtonText}>
|
|
Subscribe
|
|
</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
)}
|
|
|
|
{/* Reactivate button when canceled but still active */}
|
|
{isCanceledButActive && (
|
|
<TouchableOpacity
|
|
style={[styles.reactivateButton, isProcessing && styles.buttonDisabled]}
|
|
onPress={handleReactivateSubscription}
|
|
disabled={isProcessing}
|
|
>
|
|
{isProcessing ? (
|
|
<ActivityIndicator color={AppColors.white} />
|
|
) : (
|
|
<>
|
|
<Ionicons name="refresh" size={20} color={AppColors.white} />
|
|
<Text style={styles.subscribeButtonText}>Reactivate Subscription</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
)}
|
|
|
|
{/* Cancel link - only show when active subscription */}
|
|
{isActive && !isCanceledButActive && (
|
|
<TouchableOpacity
|
|
style={styles.linkButton}
|
|
onPress={handleCancelSubscription}
|
|
disabled={isCanceling}
|
|
>
|
|
{isCanceling ? (
|
|
<ActivityIndicator size="small" color={AppColors.textMuted} />
|
|
) : (
|
|
<Text style={styles.linkButtonTextMuted}>Cancel Subscription</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Success Modal */}
|
|
<Modal
|
|
visible={showSuccessModal}
|
|
transparent={true}
|
|
animationType="fade"
|
|
onRequestClose={handleSuccessModalClose}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<View style={styles.modalContent}>
|
|
<View style={styles.modalIconContainer}>
|
|
<Ionicons name="checkmark-circle" size={64} color={AppColors.success} />
|
|
</View>
|
|
<Text style={styles.modalTitle}>Subscription Activated!</Text>
|
|
<Text style={styles.modalMessage}>
|
|
Subscription for {beneficiary?.name} is now active.
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={styles.modalButton}
|
|
onPress={handleSuccessModalClose}
|
|
>
|
|
<Text style={styles.modalButtonText}>Continue</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: Spacing.md,
|
|
paddingVertical: Spacing.sm,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: AppColors.border,
|
|
},
|
|
backButton: {
|
|
padding: Spacing.xs,
|
|
},
|
|
headerTitle: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
placeholder: {
|
|
width: 32,
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
noBeneficiaryContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
paddingHorizontal: Spacing.xl,
|
|
},
|
|
noBeneficiaryIcon: {
|
|
width: 96,
|
|
height: 96,
|
|
borderRadius: 48,
|
|
backgroundColor: AppColors.surfaceSecondary,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
noBeneficiaryTitle: {
|
|
fontSize: FontSizes.xl,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
noBeneficiaryText: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
lineHeight: 24,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
padding: Spacing.lg,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
subscriptionCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
padding: Spacing.lg,
|
|
borderWidth: 2,
|
|
borderColor: AppColors.primary,
|
|
width: '100%',
|
|
...Shadows.md,
|
|
},
|
|
cardHeader: {
|
|
alignItems: 'center',
|
|
},
|
|
proBadgeText: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
priceContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'baseline',
|
|
marginTop: Spacing.sm,
|
|
},
|
|
priceAmount: {
|
|
fontSize: FontSizes['3xl'],
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.primary,
|
|
},
|
|
pricePeriod: {
|
|
fontSize: FontSizes.lg,
|
|
color: AppColors.textSecondary,
|
|
marginLeft: Spacing.xs,
|
|
},
|
|
planDescription: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
lineHeight: 22,
|
|
marginTop: Spacing.md,
|
|
},
|
|
activeBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: '#D1FAE5',
|
|
paddingHorizontal: Spacing.md,
|
|
paddingVertical: Spacing.sm,
|
|
borderRadius: BorderRadius.lg,
|
|
gap: Spacing.xs,
|
|
marginTop: Spacing.md,
|
|
},
|
|
activeBadgeText: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.success,
|
|
},
|
|
cancelingBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: '#FEF3C7',
|
|
paddingHorizontal: Spacing.md,
|
|
paddingVertical: Spacing.sm,
|
|
borderRadius: BorderRadius.lg,
|
|
gap: Spacing.xs,
|
|
marginTop: Spacing.md,
|
|
},
|
|
cancelingBadgeText: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.warning,
|
|
},
|
|
securityBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginTop: Spacing.md,
|
|
gap: Spacing.xs,
|
|
},
|
|
securityText: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.success,
|
|
},
|
|
actionsSection: {
|
|
width: '100%',
|
|
gap: Spacing.md,
|
|
marginTop: Spacing.xl,
|
|
},
|
|
subscribeButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: Spacing.sm,
|
|
backgroundColor: AppColors.primary,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
},
|
|
buttonDisabled: {
|
|
opacity: 0.7,
|
|
},
|
|
subscribeButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.white,
|
|
},
|
|
reactivateButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: Spacing.sm,
|
|
backgroundColor: AppColors.success,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
},
|
|
linkButton: {
|
|
paddingVertical: Spacing.xs,
|
|
alignItems: 'center',
|
|
},
|
|
linkButtonTextMuted: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textMuted,
|
|
textDecorationLine: 'underline',
|
|
},
|
|
// Modal styles
|
|
modalOverlay: {
|
|
flex: 1,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: Spacing.lg,
|
|
},
|
|
modalContent: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
padding: Spacing.xl,
|
|
width: '100%',
|
|
maxWidth: 340,
|
|
alignItems: 'center',
|
|
...Shadows.lg,
|
|
},
|
|
modalIconContainer: {
|
|
marginBottom: Spacing.md,
|
|
},
|
|
modalTitle: {
|
|
fontSize: FontSizes.xl,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: Spacing.sm,
|
|
textAlign: 'center',
|
|
},
|
|
modalMessage: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
marginBottom: Spacing.lg,
|
|
lineHeight: 22,
|
|
},
|
|
modalButton: {
|
|
backgroundColor: AppColors.primary,
|
|
paddingVertical: Spacing.md,
|
|
paddingHorizontal: Spacing.xl,
|
|
borderRadius: BorderRadius.lg,
|
|
width: '100%',
|
|
},
|
|
modalButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.white,
|
|
textAlign: 'center',
|
|
},
|
|
});
|