import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
} 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
interface PlanFeatureProps {
text: string;
included: boolean;
}
function PlanFeature({ text, included }: PlanFeatureProps) {
return (
{text}
);
}
export default function SubscriptionScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const [isProcessing, setIsProcessing] = useState(false);
const [isCanceling, setIsCanceling] = useState(false);
const [beneficiary, setBeneficiary] = useState(null);
const [isLoading, setIsLoading] = useState(true);
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;
const daysRemaining = subscription?.endDate
? Math.max(0, Math.ceil((new Date(subscription.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)))
: 0;
// Self-guard: redirect if user shouldn't be on this page
useEffect(() => {
if (isLoading || !beneficiary || !id) 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]);
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,
}),
});
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
const { error: initError } = await initPaymentSheet({
merchantDisplayName: 'WellNuo',
paymentIntentClientSecret: data.clientSecret,
customerId: data.customer,
customerEphemeralKeySecret: data.ephemeralKey,
returnURL: 'wellnuo://stripe-redirect',
applePay: {
merchantCountryCode: 'US',
},
googlePay: {
merchantCountryCode: 'US',
testEnv: true,
},
});
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! 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();
// Reload beneficiary to get updated subscription
await loadBeneficiary();
Alert.alert(
'Subscription Activated!',
`Subscription for ${beneficiary.name} is now active.`,
[{ text: 'Great!' }]
);
} 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 handleRestorePurchases = () => {
Alert.alert(
'Restoring Purchases',
'Looking for previous purchases...',
[{ text: 'OK' }]
);
setTimeout(() => {
Alert.alert('No Purchases Found', 'We couldn\'t find any previous purchases associated with your account.');
}, 1500);
};
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
if (isLoading) {
return (
router.back()}>
Subscription
);
}
if (!beneficiary) {
return (
router.back()}>
Subscription
Beneficiary Not Found
Unable to load beneficiary information.
);
}
return (
{/* Header */}
router.back()}>
Subscription
{/* Beneficiary Info */}
{beneficiary.name.charAt(0).toUpperCase()}
Subscription for
{beneficiary.name}
{/* Current Status */}
{isActive ? (
isCanceledButActive ? (
// Subscription canceled but still active until period end
<>
ENDING SOON
Active until {subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'N/A'}
{daysRemaining}
days left
>
) : (
// Active subscription
<>
ACTIVE
Subscription is active
Valid until {subscription?.endDate ? formatDate(new Date(subscription.endDate)) : 'N/A'}
{daysRemaining}
days remaining
>
)
) : (
<>
{subscription?.status === 'expired' ? 'EXPIRED' : 'NO SUBSCRIPTION'}
{subscription?.status === 'expired'
? 'Subscription has expired'
: `Subscribe for ${beneficiary.name}`}
Get full access to monitoring features
>
)}
{/* Subscription Card */}
WellNuo
${SUBSCRIPTION_PRICE}
/month
{/* Show Subscribe button when not active */}
{!isActive && (
{isProcessing ? (
) : (
<>
Subscribe — ${SUBSCRIPTION_PRICE}/month
>
)}
)}
{/* Show Reactivate button when subscription is canceled but still active */}
{isCanceledButActive && (
{isProcessing ? (
) : (
<>
Reactivate Subscription
>
)}
)}
{/* Secure Payment Badge */}
Secure payment powered by Stripe
{/* Links section */}
Restore Purchases
{isActive && !isCanceledButActive && (
<>
•
{isCanceling ? (
) : (
Cancel
)}
>
)}
{/* Terms */}
Payment will be charged to your account at the confirmation of purchase.
Subscription can be cancelled at any time from your account settings.
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
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',
},
// No beneficiary state
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,
},
// Beneficiary banner
beneficiaryBanner: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.primaryLighter,
marginHorizontal: Spacing.lg,
marginTop: Spacing.md,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
},
beneficiaryAvatar: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
},
beneficiaryAvatarText: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.white,
},
beneficiaryInfo: {
marginLeft: Spacing.md,
},
beneficiaryLabel: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
beneficiaryName: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
},
// Status banner
statusBanner: {
backgroundColor: AppColors.background,
padding: Spacing.xl,
alignItems: 'center',
},
activeBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#D1FAE5',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full,
gap: Spacing.xs,
},
activeBadgeText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.bold,
color: AppColors.success,
},
inactiveBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FEE2E2',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full,
gap: Spacing.xs,
},
inactiveBadgeText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.bold,
color: AppColors.error,
},
cancelingBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FEF3C7',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full,
gap: Spacing.xs,
},
cancelingBadgeText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.bold,
color: AppColors.warning,
},
statusTitle: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
marginTop: Spacing.md,
textAlign: 'center',
},
statusDescription: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
marginTop: Spacing.xs,
textAlign: 'center',
},
daysRemaining: {
marginTop: Spacing.lg,
alignItems: 'center',
},
daysRemainingNumber: {
fontSize: 48,
fontWeight: FontWeights.bold,
color: AppColors.primary,
},
daysRemainingLabel: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
section: {
marginTop: Spacing.md,
},
subscriptionCard: {
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
borderRadius: BorderRadius.lg,
overflow: 'hidden',
borderWidth: 2,
borderColor: AppColors.primary,
},
cardHeader: {
backgroundColor: `${AppColors.primary}10`,
padding: Spacing.lg,
alignItems: 'center',
},
proBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.xs,
},
proBadgeText: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.bold,
color: AppColors.primary,
},
priceContainer: {
flexDirection: 'row',
alignItems: 'baseline',
marginTop: Spacing.md,
},
priceAmount: {
fontSize: 48,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
},
pricePeriod: {
fontSize: FontSizes.lg,
color: AppColors.textSecondary,
marginLeft: Spacing.xs,
},
featuresContainer: {
padding: Spacing.lg,
},
featureRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.xs,
},
featureText: {
fontSize: FontSizes.sm,
color: AppColors.textPrimary,
marginLeft: Spacing.sm,
},
featureTextDisabled: {
color: AppColors.textMuted,
},
subscribeButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.sm,
backgroundColor: AppColors.primary,
marginHorizontal: Spacing.lg,
marginBottom: Spacing.lg,
paddingVertical: Spacing.lg,
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,
marginHorizontal: Spacing.lg,
marginBottom: Spacing.lg,
paddingVertical: Spacing.lg,
borderRadius: BorderRadius.lg,
},
securityBadge: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.xs,
marginTop: Spacing.lg,
paddingVertical: Spacing.md,
},
securityText: {
fontSize: FontSizes.sm,
color: AppColors.success,
},
linksSection: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: Spacing.md,
gap: Spacing.sm,
},
linkButton: {
paddingVertical: Spacing.xs,
paddingHorizontal: Spacing.xs,
},
linkButtonText: {
fontSize: FontSizes.sm,
color: AppColors.primary,
},
linkButtonTextMuted: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
},
linkDivider: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
},
termsText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
textAlign: 'center',
paddingHorizontal: Spacing.xl,
paddingBottom: Spacing.xl,
lineHeight: 16,
},
});