Redirects should only happen on the main beneficiary page (index.tsx). Other pages (subscription, equipment, share) just show their content without redirecting - user navigated there intentionally via menu. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
806 lines
24 KiB
TypeScript
806 lines
24 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
Alert,
|
|
ActivityIndicator,
|
|
Modal,
|
|
ScrollView,
|
|
Linking,
|
|
} 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 { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu';
|
|
import type { Beneficiary } from '@/types';
|
|
|
|
const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe';
|
|
const SUBSCRIPTION_PRICE = 49;
|
|
|
|
type SubscriptionState = 'active' | 'canceling' | 'none';
|
|
|
|
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);
|
|
const [transactions, setTransactions] = useState<Array<{
|
|
id: string;
|
|
type: 'subscription' | 'one_time';
|
|
amount: number;
|
|
currency: string;
|
|
status: string;
|
|
date: string;
|
|
description: string;
|
|
invoicePdf?: string;
|
|
hostedUrl?: string;
|
|
receiptUrl?: string;
|
|
}>>([]);
|
|
const [isLoadingTransactions, setIsLoadingTransactions] = useState(false);
|
|
|
|
const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet();
|
|
const { user } = useAuth();
|
|
|
|
useEffect(() => {
|
|
loadBeneficiary();
|
|
loadTransactions();
|
|
}, [id]);
|
|
|
|
const loadTransactions = async () => {
|
|
if (!id) return;
|
|
setIsLoadingTransactions(true);
|
|
try {
|
|
const response = await api.getTransactionHistory(parseInt(id, 10));
|
|
if (response.ok && response.data) {
|
|
setTransactions(response.data.transactions);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load transactions:', error);
|
|
} finally {
|
|
setIsLoadingTransactions(false);
|
|
}
|
|
};
|
|
|
|
const loadBeneficiary = async () => {
|
|
if (!id) return;
|
|
try {
|
|
const response = await api.getWellNuoBeneficiary(parseInt(id, 10));
|
|
if (response.ok && response.data) {
|
|
setBeneficiary(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load beneficiary:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const subscription = beneficiary?.subscription;
|
|
|
|
// Determine subscription state
|
|
const getSubscriptionState = (): SubscriptionState => {
|
|
if (!subscription) return 'none';
|
|
|
|
const isStatusActive = subscription.status === 'active' || subscription.status === 'trialing';
|
|
const isNotExpired = !subscription.endDate || new Date(subscription.endDate) > new Date();
|
|
|
|
if (isStatusActive && isNotExpired) {
|
|
return subscription.cancelAtPeriodEnd ? 'canceling' : 'active';
|
|
}
|
|
return 'none';
|
|
};
|
|
|
|
const subscriptionState = getSubscriptionState();
|
|
|
|
|
|
const handleSubscribe = async () => {
|
|
if (!beneficiary) {
|
|
Alert.alert('Error', 'Beneficiary data not loaded.');
|
|
return;
|
|
}
|
|
|
|
setIsProcessing(true);
|
|
|
|
try {
|
|
const token = await api.getToken();
|
|
if (!token) {
|
|
Alert.alert('Error', 'Please log in again');
|
|
setIsProcessing(false);
|
|
return;
|
|
}
|
|
|
|
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 }),
|
|
});
|
|
|
|
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();
|
|
|
|
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 - no clientSecret');
|
|
}
|
|
|
|
const isSetupIntent = data.clientSecret.startsWith('seti_');
|
|
|
|
const paymentSheetParams: Parameters<typeof initPaymentSheet>[0] = {
|
|
merchantDisplayName: 'WellNuo',
|
|
customerId: data.customer,
|
|
...(data.customerSessionClientSecret
|
|
? { customerSessionClientSecret: data.customerSessionClientSecret }
|
|
: { customerEphemeralKeySecret: data.ephemeralKey }),
|
|
returnURL: 'wellnuo://stripe-redirect',
|
|
applePay: { merchantCountryCode: 'US' },
|
|
googlePay: { merchantCountryCode: 'US', testEnv: true },
|
|
};
|
|
|
|
if (isSetupIntent) {
|
|
paymentSheetParams.setupIntentClientSecret = data.clientSecret;
|
|
} else {
|
|
paymentSheetParams.paymentIntentClientSecret = data.clientSecret;
|
|
}
|
|
|
|
const { error: initError } = await initPaymentSheet(paymentSheetParams);
|
|
if (initError) {
|
|
throw new Error(initError.message);
|
|
}
|
|
|
|
const { error: presentError } = await presentPaymentSheet();
|
|
if (presentError) {
|
|
if (presentError.code === 'Canceled') {
|
|
setIsProcessing(false);
|
|
return;
|
|
}
|
|
throw new Error(presentError.message);
|
|
}
|
|
|
|
// For PaymentIntent (subscription with immediate payment), the subscription
|
|
// is automatically activated via Stripe webhook when payment succeeds.
|
|
// No need to call confirm endpoint for PaymentIntent.
|
|
|
|
if (isSetupIntent && data.subscriptionId) {
|
|
// Only for SetupIntent flow (future payment), we need to confirm
|
|
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,
|
|
beneficiaryId: beneficiary.id,
|
|
}),
|
|
});
|
|
const confirmData = await confirmResponse.json();
|
|
|
|
if (!confirmResponse.ok || confirmData.error) {
|
|
throw new Error(confirmData.error || 'Failed to confirm subscription');
|
|
}
|
|
}
|
|
|
|
// Wait a moment for webhook to process
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
setJustSubscribed(true);
|
|
await loadBeneficiary();
|
|
setShowSuccessModal(true);
|
|
} catch (error) {
|
|
const errorMsg = error instanceof Error ? error.message : 'Something went wrong';
|
|
Alert.alert('Payment Failed', errorMsg);
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
const handleCancelSubscription = () => {
|
|
if (!beneficiary) return;
|
|
|
|
Alert.alert(
|
|
'Cancel Subscription?',
|
|
`Your subscription will remain active until ${subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'the end of your billing period'}. You can reactivate anytime before then.`,
|
|
[
|
|
{ text: 'Keep Subscription', style: 'cancel' },
|
|
{ text: 'Cancel', 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');
|
|
await loadBeneficiary();
|
|
Alert.alert('Subscription Canceled', 'You can reactivate anytime before the period ends.');
|
|
} catch (error) {
|
|
Alert.alert('Error', error instanceof Error ? error.message : 'Something went wrong.');
|
|
} finally {
|
|
setIsCanceling(false);
|
|
}
|
|
};
|
|
|
|
const handleReactivateSubscription = async () => {
|
|
if (!beneficiary) return;
|
|
setIsProcessing(true);
|
|
|
|
try {
|
|
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');
|
|
}
|
|
|
|
await loadBeneficiary();
|
|
Alert.alert('Reactivated!', 'Your subscription will continue to renew automatically.');
|
|
} catch (error) {
|
|
Alert.alert('Error', error instanceof Error ? error.message : 'Something went wrong.');
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
const formatDate = (date: Date) => {
|
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
};
|
|
|
|
const openReceipt = (url?: string) => {
|
|
if (url) Linking.openURL(url);
|
|
};
|
|
|
|
const handleSuccessModalClose = () => {
|
|
setShowSuccessModal(false);
|
|
router.replace(`/(tabs)/beneficiaries/${id}`);
|
|
};
|
|
|
|
// Loading state
|
|
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.centerContainer}>
|
|
<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.centerContainer}>
|
|
<Text style={styles.errorText}>Unable to load beneficiary</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
// Render subscription status card based on state
|
|
const renderStatusCard = () => {
|
|
switch (subscriptionState) {
|
|
case 'active':
|
|
return (
|
|
<View style={styles.statusCard}>
|
|
<View style={styles.statusIconActive}>
|
|
<Ionicons name="checkmark-circle" size={32} color={AppColors.white} />
|
|
</View>
|
|
<Text style={styles.statusTitle}>Active Subscription</Text>
|
|
<Text style={styles.statusSubtitle}>
|
|
{subscription?.endDate
|
|
? `Renews ${formatDate(new Date(subscription.endDate))}`
|
|
: 'Renews monthly'}
|
|
</Text>
|
|
<View style={styles.priceRow}>
|
|
<Text style={styles.priceAmount}>${SUBSCRIPTION_PRICE}</Text>
|
|
<Text style={styles.pricePeriod}>/month</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
|
|
case 'canceling':
|
|
return (
|
|
<View style={[styles.statusCard, styles.statusCardCanceling]}>
|
|
<View style={styles.statusIconCanceling}>
|
|
<Ionicons name="time" size={32} color={AppColors.white} />
|
|
</View>
|
|
<Text style={styles.statusTitle}>Subscription Ending</Text>
|
|
<Text style={styles.statusSubtitleCanceling}>
|
|
Access until {subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'period end'}
|
|
</Text>
|
|
<Text style={styles.cancelingNote}>
|
|
After this date, monitoring and alerts for {beneficiary.name} will stop.
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
case 'none':
|
|
default:
|
|
return (
|
|
<View style={[styles.statusCard, styles.statusCardNone]}>
|
|
<View style={styles.statusIconNone}>
|
|
<Ionicons name="shield-outline" size={32} color={AppColors.textMuted} />
|
|
</View>
|
|
<Text style={styles.statusTitleNone}>No Active Subscription</Text>
|
|
<Text style={styles.statusSubtitleNone}>
|
|
Subscribe to unlock monitoring for {beneficiary.name}
|
|
</Text>
|
|
<View style={styles.priceRow}>
|
|
<Text style={styles.priceAmount}>${SUBSCRIPTION_PRICE}</Text>
|
|
<Text style={styles.pricePeriod}>/month</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
};
|
|
|
|
// Render action button based on state
|
|
const renderActionButton = () => {
|
|
switch (subscriptionState) {
|
|
case 'active':
|
|
return (
|
|
<TouchableOpacity
|
|
style={styles.cancelButton}
|
|
onPress={handleCancelSubscription}
|
|
disabled={isCanceling}
|
|
>
|
|
{isCanceling ? (
|
|
<ActivityIndicator size="small" color={AppColors.textMuted} />
|
|
) : (
|
|
<Text style={styles.cancelButtonText}>Cancel Subscription</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
case 'canceling':
|
|
return (
|
|
<TouchableOpacity
|
|
style={[styles.primaryButton, 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.primaryButtonText}>Reactivate Subscription</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
case 'none':
|
|
default:
|
|
return (
|
|
<TouchableOpacity
|
|
style={[styles.primaryButton, isProcessing && styles.buttonDisabled]}
|
|
onPress={handleSubscribe}
|
|
disabled={isProcessing}
|
|
>
|
|
{isProcessing ? (
|
|
<ActivityIndicator color={AppColors.white} />
|
|
) : (
|
|
<>
|
|
<Ionicons name="card" size={20} color={AppColors.white} />
|
|
<Text style={styles.primaryButtonText}>Subscribe</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
}
|
|
};
|
|
|
|
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>
|
|
<BeneficiaryMenu
|
|
beneficiaryId={id || ''}
|
|
currentPage="subscription"
|
|
/>
|
|
</View>
|
|
|
|
<ScrollView style={styles.scrollContent} contentContainerStyle={styles.content}>
|
|
{/* Status Card */}
|
|
{renderStatusCard()}
|
|
|
|
{/* Action Button */}
|
|
<View style={styles.actionSection}>
|
|
{renderActionButton()}
|
|
|
|
{/* Security note */}
|
|
<View style={styles.securityRow}>
|
|
<Ionicons name="shield-checkmark" size={14} color={AppColors.success} />
|
|
<Text style={styles.securityText}>Secure payment by Stripe</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Transaction History */}
|
|
{transactions.length > 0 && (
|
|
<View style={styles.transactionSection}>
|
|
<Text style={styles.sectionTitle}>Payment History</Text>
|
|
{transactions.map((tx) => (
|
|
<TouchableOpacity
|
|
key={tx.id}
|
|
style={styles.transactionItem}
|
|
onPress={() => openReceipt(tx.invoicePdf || tx.hostedUrl || tx.receiptUrl)}
|
|
disabled={!tx.invoicePdf && !tx.hostedUrl && !tx.receiptUrl}
|
|
>
|
|
<View style={styles.transactionLeft}>
|
|
<Text style={styles.transactionDesc}>{tx.description}</Text>
|
|
<Text style={styles.transactionDate}>{formatDate(new Date(tx.date))}</Text>
|
|
</View>
|
|
<View style={styles.transactionRight}>
|
|
<Text style={styles.transactionAmount}>${tx.amount.toFixed(2)}</Text>
|
|
{(tx.invoicePdf || tx.hostedUrl || tx.receiptUrl) && (
|
|
<Ionicons name="chevron-forward" size={16} color={AppColors.textMuted} />
|
|
)}
|
|
</View>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
|
|
{/* Success Modal */}
|
|
<Modal
|
|
visible={showSuccessModal}
|
|
transparent={true}
|
|
animationType="fade"
|
|
onRequestClose={handleSuccessModalClose}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<View style={styles.modalContent}>
|
|
<View style={styles.modalIcon}>
|
|
<Ionicons name="checkmark-circle" size={56} color={AppColors.success} />
|
|
</View>
|
|
<Text style={styles.modalTitle}>Subscription Active!</Text>
|
|
<Text style={styles.modalMessage}>
|
|
Monitoring for {beneficiary?.name} is now enabled.
|
|
</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,
|
|
},
|
|
centerContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
errorText: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textMuted,
|
|
},
|
|
scrollContent: {
|
|
flex: 1,
|
|
},
|
|
content: {
|
|
padding: Spacing.lg,
|
|
paddingBottom: Spacing.xxl,
|
|
},
|
|
|
|
// Status Card Styles
|
|
statusCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
padding: Spacing.xl,
|
|
alignItems: 'center',
|
|
borderWidth: 2,
|
|
borderColor: AppColors.success,
|
|
...Shadows.sm,
|
|
},
|
|
statusCardCanceling: {
|
|
borderColor: '#F59E0B',
|
|
backgroundColor: '#FFFBEB',
|
|
},
|
|
statusCardNone: {
|
|
borderColor: AppColors.border,
|
|
backgroundColor: AppColors.surface,
|
|
},
|
|
statusIconActive: {
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: 28,
|
|
backgroundColor: AppColors.success,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
statusIconCanceling: {
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: 28,
|
|
backgroundColor: '#F59E0B',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
statusIconNone: {
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: 28,
|
|
backgroundColor: AppColors.surfaceSecondary,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
statusTitle: {
|
|
fontSize: FontSizes.xl,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
statusTitleNone: {
|
|
fontSize: FontSizes.xl,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textSecondary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
statusSubtitle: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.success,
|
|
marginBottom: Spacing.md,
|
|
},
|
|
statusSubtitleCanceling: {
|
|
fontSize: FontSizes.sm,
|
|
color: '#B45309',
|
|
fontWeight: FontWeights.medium,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
statusSubtitleNone: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textMuted,
|
|
textAlign: 'center',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
cancelingNote: {
|
|
fontSize: FontSizes.sm,
|
|
color: '#92400E',
|
|
textAlign: 'center',
|
|
lineHeight: 20,
|
|
},
|
|
priceRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'baseline',
|
|
},
|
|
priceAmount: {
|
|
fontSize: FontSizes['2xl'],
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.primary,
|
|
},
|
|
pricePeriod: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textMuted,
|
|
marginLeft: 2,
|
|
},
|
|
|
|
// Action Section
|
|
actionSection: {
|
|
marginTop: Spacing.xl,
|
|
gap: Spacing.md,
|
|
},
|
|
primaryButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: Spacing.sm,
|
|
backgroundColor: AppColors.primary,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
...Shadows.sm,
|
|
},
|
|
reactivateButton: {
|
|
backgroundColor: AppColors.success,
|
|
},
|
|
buttonDisabled: {
|
|
opacity: 0.7,
|
|
},
|
|
primaryButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.white,
|
|
},
|
|
cancelButton: {
|
|
paddingVertical: Spacing.sm,
|
|
alignItems: 'center',
|
|
},
|
|
cancelButtonText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textMuted,
|
|
textDecorationLine: 'underline',
|
|
},
|
|
securityRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: Spacing.xs,
|
|
},
|
|
securityText: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
},
|
|
|
|
// Transaction Section
|
|
transactionSection: {
|
|
marginTop: Spacing.xl,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: Spacing.md,
|
|
},
|
|
transactionItem: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.surface,
|
|
padding: Spacing.md,
|
|
borderRadius: BorderRadius.md,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
transactionLeft: {
|
|
flex: 1,
|
|
},
|
|
transactionDesc: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
transactionDate: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
marginTop: 2,
|
|
},
|
|
transactionRight: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.xs,
|
|
},
|
|
transactionAmount: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
|
|
// Modal
|
|
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: 320,
|
|
alignItems: 'center',
|
|
...Shadows.lg,
|
|
},
|
|
modalIcon: {
|
|
marginBottom: Spacing.md,
|
|
},
|
|
modalTitle: {
|
|
fontSize: FontSizes.xl,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
modalMessage: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
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',
|
|
},
|
|
});
|