bw-expo-app/app/exchange.tsx

388 lines
9.9 KiB
TypeScript

import Ionicons from '@expo/vector-icons/Ionicons';
import { useRouter } from 'expo-router';
import React, { useCallback, useMemo, useState } from 'react';
import {
Alert,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
type PurchaseTab = 'subscription' | 'pack';
type PointsBundle = {
id: string;
points: number;
price: number;
};
const screenPalette = {
background: '#050505',
surface: '#101010',
surfaceRaised: '#1A1B1E',
primaryText: '#F5F5F5',
secondaryText: '#7F7F7F',
mutedText: '#4C4C4C',
accent: '#FEB840',
energyHalo: 'rgba(254, 184, 64, 0.22)',
divider: '#1C1C1C',
button: '#D1FE17',
buttonText: '#101010',
};
const CURRENT_POINTS = 60;
const SUBSCRIPTION_TAGLINE = 'No active subscription plans';
const TABS: { key: PurchaseTab; label: string }[] = [
{ key: 'subscription', label: 'Subscription' },
{ key: 'pack', label: 'points pack' },
];
const POINT_BUNDLES: PointsBundle[] = [
{ id: 'bundle-500', points: 500, price: 35 },
{ id: 'bundle-2000', points: 2000, price: 140 },
{ id: 'bundle-5000', points: 5000, price: 350 },
{ id: 'bundle-10000', points: 10000, price: 700 },
];
export default function PointsExchangeScreen() {
const router = useRouter();
const insets = useSafeAreaInsets();
const [activeTab, setActiveTab] = useState<PurchaseTab>('pack');
const [selectedBundleId, setSelectedBundleId] = useState<string | null>(null);
const visibleBundles = useMemo(
() => (activeTab === 'pack' ? POINT_BUNDLES : []),
[activeTab],
);
const changeTab = useCallback((nextTab: PurchaseTab) => {
setActiveTab(nextTab);
if (nextTab !== 'pack') {
setSelectedBundleId(null);
}
}, []);
const handleClose = useCallback(() => {
router.back();
}, [router]);
const openPointsDetails = useCallback(() => {
router.push('/points');
}, [router]);
const handleBundleSelect = useCallback((bundleId: string) => {
setSelectedBundleId(bundleId);
}, []);
const handlePurchase = useCallback(() => {
const bundle = POINT_BUNDLES.find(item => item.id === selectedBundleId);
if (!bundle) {
Alert.alert('Select a bundle', 'Please pick the bundle you wish to purchase.');
return;
}
Alert.alert(
'Confirm purchase',
`You are purchasing ${bundle.points} points for ¥ ${bundle.price}.`,
);
}, [selectedBundleId]);
return (
<View style={[styles.screen, { paddingTop: insets.top + 12 }]}>
<View style={styles.headerRow}>
<TouchableOpacity
style={styles.iconButton}
accessibilityRole="button"
onPress={handleClose}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Ionicons name="close" size={22} color={screenPalette.primaryText} />
</TouchableOpacity>
<TouchableOpacity
style={styles.secondaryPill}
accessibilityRole="button"
onPress={openPointsDetails}
activeOpacity={0.85}
>
<Text style={styles.secondaryPillLabel}>Points Details</Text>
</TouchableOpacity>
</View>
<ScrollView
style={styles.content}
contentContainerStyle={[
styles.contentContainer,
{ paddingBottom: Math.max(insets.bottom + 96, 160) },
]}
showsVerticalScrollIndicator={false}
>
<View style={styles.balanceCluster}>
<View style={styles.energyOrb}>
<Ionicons name="flash" size={22} color={screenPalette.accent} />
</View>
<Text style={styles.balanceValue}>{CURRENT_POINTS}</Text>
<Text style={styles.balanceSubtitle}>{SUBSCRIPTION_TAGLINE}</Text>
</View>
<View style={styles.tabBar}>
{TABS.map(tab => {
const isActive = tab.key === activeTab;
return (
<TouchableOpacity
key={tab.key}
style={styles.tabButton}
onPress={() => changeTab(tab.key)}
accessibilityRole="tab"
accessibilityState={{ selected: isActive }}
activeOpacity={0.8}
>
<Text style={[styles.tabLabel, isActive && styles.tabLabelActive]}>
{tab.label}
</Text>
<View
style={[styles.tabIndicator, isActive && styles.tabIndicatorActive]}
/>
</TouchableOpacity>
);
})}
</View>
<View style={styles.tabDivider} />
{activeTab === 'pack' ? (
<View style={styles.packGrid}>
{visibleBundles.map(bundle => {
const isSelected = bundle.id === selectedBundleId;
return (
<TouchableOpacity
key={bundle.id}
style={[
styles.packCard,
isSelected && styles.packCardSelected,
]}
onPress={() => handleBundleSelect(bundle.id)}
accessibilityRole="button"
accessibilityState={{ selected: isSelected }}
activeOpacity={0.88}
>
<View style={styles.packHeader}>
<Ionicons name="flash" size={18} color={screenPalette.accent} />
<Text style={styles.packPoints}>{bundle.points}</Text>
</View>
<Text style={styles.packPrice}>¥ {bundle.price}</Text>
</TouchableOpacity>
);
})}
</View>
) : (
<View style={styles.subscriptionEmpty}>
<Text style={styles.emptyTitle}>Subscription</Text>
<Text style={styles.emptySubtitle}>
No subscription tiers are available at the moment.
</Text>
</View>
)}
</ScrollView>
<View style={[styles.bottomBar, { paddingBottom: Math.max(insets.bottom, 16) }]}>
<TouchableOpacity
style={styles.primaryButton}
onPress={handlePurchase}
activeOpacity={0.85}
>
<Text style={styles.primaryButtonLabel}>Purchase points</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: screenPalette.background,
},
headerRow: {
paddingHorizontal: 24,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
iconButton: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
},
secondaryPill: {
paddingHorizontal: 16,
height: 34,
borderRadius: 17,
backgroundColor: screenPalette.surface,
borderWidth: StyleSheet.hairlineWidth,
borderColor: screenPalette.divider,
justifyContent: 'center',
alignItems: 'center',
},
secondaryPillLabel: {
fontSize: 14,
fontWeight: '600',
color: screenPalette.primaryText,
letterSpacing: 0.2,
},
content: {
flex: 1,
},
contentContainer: {
paddingHorizontal: 24,
},
balanceCluster: {
alignItems: 'center',
marginTop: 32,
marginBottom: 36,
gap: 10,
},
energyOrb: {
width: 58,
height: 58,
borderRadius: 29,
backgroundColor: screenPalette.energyHalo,
alignItems: 'center',
justifyContent: 'center',
shadowColor: screenPalette.accent,
shadowOpacity: 0.28,
shadowRadius: 14,
shadowOffset: { width: 0, height: 8 },
elevation: 6,
},
balanceValue: {
fontSize: 44,
fontWeight: '700',
color: screenPalette.primaryText,
letterSpacing: 0.5,
fontVariant: ['tabular-nums'],
},
balanceSubtitle: {
fontSize: 13,
color: screenPalette.secondaryText,
letterSpacing: 0.2,
},
tabBar: {
flexDirection: 'row',
gap: 32,
paddingHorizontal: 4,
},
tabButton: {
flex: 1,
alignItems: 'center',
paddingBottom: 12,
},
tabLabel: {
fontSize: 14,
color: screenPalette.mutedText,
fontWeight: '500',
letterSpacing: 0.2,
textTransform: 'capitalize',
},
tabLabelActive: {
color: screenPalette.primaryText,
},
tabIndicator: {
marginTop: 10,
height: 2,
width: '100%',
backgroundColor: 'transparent',
borderRadius: 1,
},
tabIndicatorActive: {
backgroundColor: screenPalette.accent,
},
tabDivider: {
marginTop: 12,
height: StyleSheet.hairlineWidth,
backgroundColor: screenPalette.divider,
},
packGrid: {
marginTop: 28,
flexDirection: 'row',
flexWrap: 'wrap',
gap: 18,
},
packCard: {
width: '47%',
backgroundColor: screenPalette.surfaceRaised,
borderRadius: 24,
paddingHorizontal: 18,
paddingVertical: 24,
justifyContent: 'space-between',
borderWidth: 1,
borderColor: screenPalette.surfaceRaised,
},
packCardSelected: {
borderColor: screenPalette.accent,
},
packHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
packPoints: {
fontSize: 18,
fontWeight: '700',
color: screenPalette.primaryText,
fontVariant: ['tabular-nums'],
},
packPrice: {
marginTop: 18,
fontSize: 14,
color: screenPalette.secondaryText,
},
subscriptionEmpty: {
marginTop: 64,
backgroundColor: screenPalette.surface,
borderRadius: 22,
paddingVertical: 40,
paddingHorizontal: 32,
alignItems: 'center',
gap: 12,
},
emptyTitle: {
fontSize: 16,
fontWeight: '600',
color: screenPalette.primaryText,
},
emptySubtitle: {
fontSize: 13,
color: screenPalette.secondaryText,
textAlign: 'center',
lineHeight: 20,
},
bottomBar: {
paddingHorizontal: 24,
},
primaryButton: {
height: 54,
borderRadius: 27,
backgroundColor: screenPalette.button,
alignItems: 'center',
justifyContent: 'center',
},
primaryButtonLabel: {
fontSize: 16,
fontWeight: '700',
color: screenPalette.buttonText,
letterSpacing: 0.3,
},
});