feat: update mobile app screens
- Enhance browse tab with improved navigation - Update home tab with new features - Refactor profile tab - Update paywall screen styling
This commit is contained in:
393
app/paywall.tsx
393
app/paywall.tsx
@@ -3,7 +3,7 @@
|
||||
* Premium subscription purchase flow
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
@@ -18,7 +18,8 @@ import Ionicons from '@expo/vector-icons/Ionicons'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHaptics, usePurchases } from '@/src/shared/hooks'
|
||||
import { BRAND, darkColors } from '@/src/shared/theme'
|
||||
import { useThemeColors, BRAND, GRADIENTS } from '@/src/shared/theme'
|
||||
import type { ThemeColors } from '@/src/shared/theme/types'
|
||||
import { SPACING } from '@/src/shared/constants/spacing'
|
||||
import { RADIUS } from '@/src/shared/constants/borderRadius'
|
||||
|
||||
@@ -39,6 +40,18 @@ const PREMIUM_FEATURES = [
|
||||
// COMPONENTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
interface PlanCardStyles {
|
||||
planCard: object
|
||||
planCardPressed: object
|
||||
savingsBadge: object
|
||||
savingsText: object
|
||||
planInfo: object
|
||||
planTitle: object
|
||||
planPeriod: object
|
||||
planPrice: object
|
||||
checkmark: object
|
||||
}
|
||||
|
||||
function PlanCard({
|
||||
title,
|
||||
price,
|
||||
@@ -46,6 +59,8 @@ function PlanCard({
|
||||
savings,
|
||||
isSelected,
|
||||
onPress,
|
||||
colors,
|
||||
styles,
|
||||
}: {
|
||||
title: string
|
||||
price: string
|
||||
@@ -53,6 +68,8 @@ function PlanCard({
|
||||
savings?: string
|
||||
isSelected: boolean
|
||||
onPress: () => void
|
||||
colors: ThemeColors
|
||||
styles: PlanCardStyles
|
||||
}) {
|
||||
const haptics = useHaptics()
|
||||
|
||||
@@ -66,8 +83,12 @@ function PlanCard({
|
||||
onPress={handlePress}
|
||||
style={({ pressed }) => [
|
||||
styles.planCard,
|
||||
isSelected && styles.planCardSelected,
|
||||
isSelected && { borderColor: BRAND.PRIMARY },
|
||||
pressed && styles.planCardPressed,
|
||||
{
|
||||
backgroundColor: colors.bg.surface,
|
||||
borderColor: isSelected ? BRAND.PRIMARY : colors.border.glass,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{savings && (
|
||||
@@ -76,10 +97,16 @@ function PlanCard({
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.planInfo}>
|
||||
<Text style={styles.planTitle}>{title}</Text>
|
||||
<Text style={styles.planPeriod}>{period}</Text>
|
||||
<Text style={[styles.planTitle, { color: colors.text.primary }]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={[styles.planPeriod, { color: colors.text.tertiary }]}>
|
||||
{period}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.planPrice}>{price}</Text>
|
||||
<Text style={[styles.planPrice, { color: BRAND.PRIMARY }]}>
|
||||
{price}
|
||||
</Text>
|
||||
{isSelected && (
|
||||
<View style={styles.checkmark}>
|
||||
<Ionicons name="checkmark-circle" size={24} color={BRAND.PRIMARY} />
|
||||
@@ -98,6 +125,25 @@ export default function PaywallScreen() {
|
||||
const router = useRouter()
|
||||
const insets = useSafeAreaInsets()
|
||||
const haptics = useHaptics()
|
||||
const colors = useThemeColors()
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
|
||||
// Extract plan card styles for the child component
|
||||
const planCardStyles = useMemo(
|
||||
() => ({
|
||||
planCard: styles.planCard,
|
||||
planCardPressed: styles.planCardPressed,
|
||||
savingsBadge: styles.savingsBadge,
|
||||
savingsText: styles.savingsText,
|
||||
planInfo: styles.planInfo,
|
||||
planTitle: styles.planTitle,
|
||||
planPeriod: styles.planPeriod,
|
||||
planPrice: styles.planPrice,
|
||||
checkmark: styles.checkmark,
|
||||
}),
|
||||
[styles],
|
||||
)
|
||||
|
||||
const {
|
||||
monthlyPackage,
|
||||
annualPackage,
|
||||
@@ -148,15 +194,9 @@ export default function PaywallScreen() {
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
{/* Background Gradient */}
|
||||
<LinearGradient
|
||||
colors={['#1a1a2e', '#16213e', '#0f0f1a']}
|
||||
style={styles.gradient}
|
||||
/>
|
||||
|
||||
{/* Close Button */}
|
||||
<Pressable style={styles.closeButton} onPress={handleClose}>
|
||||
<Ionicons name="close" size={28} color={darkColors.text.secondary} />
|
||||
<Pressable style={[styles.closeButton, { top: insets.top + SPACING[2] }]} onPress={handleClose}>
|
||||
<Ionicons name="close" size={28} color={colors.text.secondary} />
|
||||
</Pressable>
|
||||
|
||||
<ScrollView
|
||||
@@ -177,10 +217,10 @@ export default function PaywallScreen() {
|
||||
<View style={styles.featuresGrid}>
|
||||
{PREMIUM_FEATURES.map((feature) => (
|
||||
<View key={feature.key} style={styles.featureItem}>
|
||||
<View style={styles.featureIcon}>
|
||||
<View style={[styles.featureIcon, { backgroundColor: colors.glass.tinted.backgroundColor }]}>
|
||||
<Ionicons name={feature.icon as any} size={22} color={BRAND.PRIMARY} />
|
||||
</View>
|
||||
<Text style={styles.featureText}>
|
||||
<Text style={[styles.featureText, { color: colors.text.secondary }]}>
|
||||
{t(`paywall.features.${feature.key}`)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -196,6 +236,8 @@ export default function PaywallScreen() {
|
||||
savings={t('paywall.save50')}
|
||||
isSelected={selectedPlan === 'annual'}
|
||||
onPress={() => setSelectedPlan('annual')}
|
||||
colors={colors}
|
||||
styles={planCardStyles}
|
||||
/>
|
||||
<PlanCard
|
||||
title={t('paywall.monthly')}
|
||||
@@ -203,12 +245,14 @@ export default function PaywallScreen() {
|
||||
period={t('paywall.perMonth')}
|
||||
isSelected={selectedPlan === 'monthly'}
|
||||
onPress={() => setSelectedPlan('monthly')}
|
||||
colors={colors}
|
||||
styles={planCardStyles}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Price Note */}
|
||||
{selectedPlan === 'annual' && (
|
||||
<Text style={styles.priceNote}>
|
||||
<Text style={[styles.priceNote, { color: colors.text.tertiary }]}>
|
||||
{t('paywall.equivalent', { price: annualMonthlyEquivalent })}
|
||||
</Text>
|
||||
)}
|
||||
@@ -220,12 +264,12 @@ export default function PaywallScreen() {
|
||||
disabled={isLoading}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[BRAND.PRIMARY, '#FF8A5B']}
|
||||
colors={GRADIENTS.CTA}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.ctaGradient}
|
||||
>
|
||||
<Text style={styles.ctaText}>
|
||||
<Text style={[styles.ctaText, { color: colors.text.primary }]}>
|
||||
{isLoading ? t('paywall.processing') : t('paywall.subscribe')}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
@@ -234,10 +278,14 @@ export default function PaywallScreen() {
|
||||
{/* Restore & Terms */}
|
||||
<View style={styles.footer}>
|
||||
<Pressable onPress={handleRestore}>
|
||||
<Text style={styles.restoreText}>{t('paywall.restore')}</Text>
|
||||
<Text style={[styles.restoreText, { color: colors.text.tertiary }]}>
|
||||
{t('paywall.restore')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Text style={styles.termsText}>{t('paywall.terms')}</Text>
|
||||
<Text style={[styles.termsText, { color: colors.text.tertiary }]}>
|
||||
{t('paywall.terms')}
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
@@ -248,162 +296,147 @@ export default function PaywallScreen() {
|
||||
// STYLES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradient: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
top: SPACING[4],
|
||||
right: SPACING[4],
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: SPACING[5],
|
||||
paddingTop: SPACING[8],
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: '700',
|
||||
color: '#FFF',
|
||||
textAlign: 'center',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: darkColors.text.secondary,
|
||||
textAlign: 'center',
|
||||
marginTop: SPACING[2],
|
||||
},
|
||||
featuresGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: SPACING[6],
|
||||
marginHorizontal: -SPACING[2],
|
||||
},
|
||||
featureItem: {
|
||||
width: '33%',
|
||||
alignItems: 'center',
|
||||
paddingVertical: SPACING[3],
|
||||
},
|
||||
featureIcon: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: 'rgba(255, 107, 53, 0.15)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: SPACING[2],
|
||||
},
|
||||
featureText: {
|
||||
fontSize: 13,
|
||||
color: darkColors.text.secondary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
plansContainer: {
|
||||
marginTop: SPACING[6],
|
||||
gap: SPACING[3],
|
||||
},
|
||||
planCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: RADIUS.LG,
|
||||
padding: SPACING[4],
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
planCardSelected: {
|
||||
borderColor: BRAND.PRIMARY,
|
||||
backgroundColor: 'rgba(255, 107, 53, 0.1)',
|
||||
},
|
||||
planCardPressed: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
savingsBadge: {
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: SPACING[3],
|
||||
backgroundColor: BRAND.PRIMARY,
|
||||
paddingHorizontal: SPACING[2],
|
||||
paddingVertical: 2,
|
||||
borderRadius: RADIUS.SM,
|
||||
},
|
||||
savingsText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
color: '#FFF',
|
||||
},
|
||||
planInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
planTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: darkColors.text.primary,
|
||||
},
|
||||
planPeriod: {
|
||||
fontSize: 13,
|
||||
color: darkColors.text.tertiary,
|
||||
marginTop: 2,
|
||||
},
|
||||
planPrice: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: BRAND.PRIMARY,
|
||||
},
|
||||
checkmark: {
|
||||
marginLeft: SPACING[2],
|
||||
},
|
||||
priceNote: {
|
||||
fontSize: 13,
|
||||
color: darkColors.text.tertiary,
|
||||
textAlign: 'center',
|
||||
marginTop: SPACING[3],
|
||||
},
|
||||
ctaButton: {
|
||||
borderRadius: RADIUS.LG,
|
||||
overflow: 'hidden',
|
||||
marginTop: SPACING[6],
|
||||
},
|
||||
ctaButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
ctaGradient: {
|
||||
paddingVertical: SPACING[4],
|
||||
alignItems: 'center',
|
||||
},
|
||||
ctaText: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
color: '#FFF',
|
||||
},
|
||||
footer: {
|
||||
marginTop: SPACING[5],
|
||||
alignItems: 'center',
|
||||
gap: SPACING[4],
|
||||
},
|
||||
restoreText: {
|
||||
fontSize: 14,
|
||||
color: darkColors.text.tertiary,
|
||||
},
|
||||
termsText: {
|
||||
fontSize: 11,
|
||||
color: darkColors.text.tertiary,
|
||||
textAlign: 'center',
|
||||
lineHeight: 18,
|
||||
paddingHorizontal: SPACING[4],
|
||||
},
|
||||
})
|
||||
function createStyles(colors: ThemeColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.bg.base,
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
top: SPACING[4],
|
||||
right: SPACING[4],
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: SPACING[5],
|
||||
paddingTop: SPACING[8],
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: '700',
|
||||
color: colors.text.primary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: colors.text.secondary,
|
||||
textAlign: 'center',
|
||||
marginTop: SPACING[2],
|
||||
},
|
||||
featuresGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: SPACING[6],
|
||||
marginHorizontal: -SPACING[2],
|
||||
},
|
||||
featureItem: {
|
||||
width: '33%',
|
||||
alignItems: 'center',
|
||||
paddingVertical: SPACING[3],
|
||||
},
|
||||
featureIcon: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: SPACING[2],
|
||||
},
|
||||
featureText: {
|
||||
fontSize: 13,
|
||||
textAlign: 'center',
|
||||
},
|
||||
plansContainer: {
|
||||
marginTop: SPACING[6],
|
||||
gap: SPACING[3],
|
||||
},
|
||||
planCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: RADIUS.LG,
|
||||
padding: SPACING[4],
|
||||
borderWidth: 2,
|
||||
},
|
||||
planCardPressed: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
savingsBadge: {
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: SPACING[3],
|
||||
backgroundColor: BRAND.PRIMARY,
|
||||
paddingHorizontal: SPACING[2],
|
||||
paddingVertical: 2,
|
||||
borderRadius: RADIUS.SM,
|
||||
},
|
||||
savingsText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
planInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
planTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
planPeriod: {
|
||||
fontSize: 13,
|
||||
marginTop: 2,
|
||||
},
|
||||
planPrice: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
},
|
||||
checkmark: {
|
||||
marginLeft: SPACING[2],
|
||||
},
|
||||
priceNote: {
|
||||
fontSize: 13,
|
||||
textAlign: 'center',
|
||||
marginTop: SPACING[3],
|
||||
},
|
||||
ctaButton: {
|
||||
borderRadius: RADIUS.LG,
|
||||
overflow: 'hidden',
|
||||
marginTop: SPACING[6],
|
||||
},
|
||||
ctaButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
ctaGradient: {
|
||||
paddingVertical: SPACING[4],
|
||||
alignItems: 'center',
|
||||
},
|
||||
ctaText: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
},
|
||||
footer: {
|
||||
marginTop: SPACING[5],
|
||||
alignItems: 'center',
|
||||
gap: SPACING[4],
|
||||
},
|
||||
restoreText: {
|
||||
fontSize: 14,
|
||||
},
|
||||
termsText: {
|
||||
fontSize: 11,
|
||||
textAlign: 'center',
|
||||
lineHeight: 18,
|
||||
paddingHorizontal: SPACING[4],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user