WellNuo/components/screens/purchase/PurchaseScreen.web.tsx
Sergei d453126c89 feat: Room location picker + robster credentials
- Backend: Update Legacy API credentials to robster/rob2
- Frontend: ROOM_LOCATIONS with icons and legacyCode mapping
- Device Settings: Modal picker for room selection
- api.ts: Bidirectional conversion (code ↔ name)
- Various UI/UX improvements across screens

PRD-DEPLOYMENT.md completed (Score: 9/10)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-24 15:22:40 -08:00

313 lines
10 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
ActivityIndicator,
Platform,
} 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'; // Removed for web
import { api } from '@/services/api';
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
import { useAuth } from '@/contexts/AuthContext';
import { useToast } from '@/components/ui/Toast';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { FullScreenError } from '@/components/ui/ErrorMessage';
import {
AppColors,
BorderRadius,
FontSizes,
FontWeights,
Spacing,
Shadows,
} from '@/constants/theme';
import type { Beneficiary } from '@/types';
import { hasBeneficiaryDevices } from '@/services/BeneficiaryDetailController';
// const STRIPE_API_URL = 'https://wellnuo.smartlaunchhub.com/api/stripe';
const STARTER_KIT = {
name: 'WellNuo Starter Kit',
price: '$399',
priceValue: 399,
description: '5 smart sensors that easily plug into any outlet and set up through the app in minutes',
};
export default function PurchaseScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { user } = useAuth();
const toast = useToast();
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
// const { initPaymentSheet, presentPaymentSheet } = usePaymentSheet(); // Removed for web
const loadBeneficiary = useCallback(async () => {
if (!id) return;
setIsLoading(true);
setError(null);
try {
const response = await api.getWellNuoBeneficiary(parseInt(id, 10));
if (response.ok && response.data) {
setBeneficiary(response.data);
// Redirect if user already has devices - shouldn't be on purchase page
if (hasBeneficiaryDevices(response.data)) {
router.replace(`/(tabs)/beneficiaries/${id}`);
return;
}
// Redirect if equipment is ordered/shipped/delivered
const status = response.data.equipmentStatus;
if (status && ['ordered', 'shipped', 'delivered'].includes(status)) {
router.replace(`/(tabs)/beneficiaries/${id}/equipment-status`);
return;
}
} else {
setError(response.error?.message || 'Failed to load beneficiary');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setIsLoading(false);
}
}, [id]);
useEffect(() => {
loadBeneficiary();
}, [loadBeneficiary]);
const handlePurchase = async () => {
// Web implementation: Placeholder or redirection to external Stripe checkout if needed.
// For now, we just inform the user.
alert('Purchases are currently only available in the mobile app.');
return;
};
const handleAlreadyHaveSensors = () => {
router.push({
pathname: '/(auth)/activate',
params: { beneficiaryId: id, lovedOneName: beneficiary?.name },
});
};
if (isLoading) {
return <LoadingSpinner fullScreen message="Loading..." />;
}
if (error || !beneficiary) {
return (
<FullScreenError
message={error || 'Beneficiary not found'}
onRetry={loadBeneficiary}
/>
);
}
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}>Get Started</Text>
<View style={styles.placeholder} />
</View>
<ScrollView
style={styles.content}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* Icon */}
<View style={styles.iconContainer}>
<Ionicons name="hardware-chip-outline" size={64} color={AppColors.primary} />
</View>
{/* Title */}
<Text style={styles.title}>Start Monitoring {beneficiary.displayName}</Text>
<Text style={styles.subtitle}>
To monitor wellness, you need WellNuo sensors installed in their home.
</Text>
{/* Kit Card */}
<View style={styles.kitCard}>
<Text style={styles.kitName}>{STARTER_KIT.name}</Text>
<Text style={styles.kitPrice}>{STARTER_KIT.price}</Text>
<Text style={styles.kitDescription}>{STARTER_KIT.description}</Text>
{/* 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}>
<TouchableOpacity
style={[styles.buyButton, isProcessing && styles.buyButtonDisabled]}
onPress={handlePurchase}
disabled={isProcessing}
>
{isProcessing ? (
<ActivityIndicator color={AppColors.white} />
) : (
<>
<Ionicons name="card" size={20} color={AppColors.white} />
<Text style={styles.buyButtonText}>Buy Now</Text>
</>
)}
</TouchableOpacity>
<TouchableOpacity style={styles.alreadyHaveButton} onPress={handleAlreadyHaveSensors}>
<Text style={styles.alreadyHaveText}>I already have sensors</Text>
</TouchableOpacity>
</View>
<View style={{ marginTop: 20, padding: 10, backgroundColor: '#f0f9ff', borderRadius: 8 }}>
<Text style={{ textAlign: 'center', color: '#0066cc', fontSize: 14 }}>
Web version: Payment flow is limited. Please use mobile app for purchases.
</Text>
</View>
</ScrollView>
</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,
},
content: {
flex: 1,
},
scrollContent: {
padding: Spacing.lg,
paddingBottom: Spacing.xxl,
},
iconContainer: {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: AppColors.primaryLighter,
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
marginBottom: Spacing.lg,
},
title: {
fontSize: FontSizes['2xl'],
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
textAlign: 'center',
marginBottom: Spacing.sm,
},
subtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
marginBottom: Spacing.xl,
lineHeight: 22,
},
kitCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
padding: Spacing.lg,
marginBottom: Spacing.lg,
...Shadows.md,
},
kitName: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
textAlign: 'center',
},
kitPrice: {
fontSize: FontSizes['3xl'],
fontWeight: FontWeights.bold,
color: AppColors.primary,
textAlign: 'center',
marginVertical: Spacing.md,
},
kitDescription: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
lineHeight: 22,
},
actionsSection: {
gap: Spacing.md,
},
buyButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.primary,
paddingVertical: Spacing.md,
borderRadius: BorderRadius.lg,
gap: Spacing.sm,
},
buyButtonDisabled: {
opacity: 0.7,
},
buyButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
securityBadge: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: Spacing.md,
gap: Spacing.xs,
},
securityText: {
fontSize: FontSizes.xs,
color: AppColors.success,
},
alreadyHaveButton: {
alignItems: 'center',
paddingVertical: Spacing.sm,
},
alreadyHaveText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textDecorationLine: 'underline',
},
});