388 lines
9.9 KiB
TypeScript
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,
|
|
},
|
|
});
|