Files
Sauci/apps/mobile/src/features/profile/components/SubscriptionCard.tsx
T
jjbruntonandClaude Fable 5 6bc4e31719 chore(mobile): upgrade to Expo SDK 56
Bump expo, react, react-native, and all expo-* / react-native-* peers to
the SDK 56 line via expo install --fix, adding expo-splash-screen and
expo-system-ui for config plugins the SDK split out of core. Bump
@bacons/apple-targets, react-native-view-shot, and other native modules
to versions compatible with React Native 0.85 and the new architecture.

Remove @react-navigation/* direct dependencies and route their hooks
through expo-router, which is no longer compatible with react-navigation
as of SDK 56. Fix RN 0.85's removed StyleSheet.absoluteFillObject (use
absoluteFill) and react-native-view-shot 5.x's ViewShotRef type across
call sites. Downgrade eslint-plugin-react-hooks v7's new React Compiler
era rules to warnings pending a dedicated refactor, and repair Jest
fallout from the jest-expo/reanimated/worklets/expo-media-library bumps.

Regenerate the committed ios/ and android/ native projects with
`expo prebuild --clean` (required for @bacons/apple-targets 5.x) against
the production release channel, and document that requirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 11:11:37 +01:00

400 lines
13 KiB
TypeScript

import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { ShimmerEffect } from '../../../components/ui';
import { colors, gradients, spacing, typography, radius, shadows } from '../../../theme';
export interface SubscriptionCardProps {
/** Whether user has premium access (own or partner's) */
hasPremiumAccess: boolean;
/** Whether this is the user's own subscription (vs partner's) */
isOwnSubscription: boolean;
/** Formatted expiration/renewal date */
expirationDate: string;
/** Callback when upgrade button is pressed */
onUpgradePress: () => void;
/** Callback when manage subscription is pressed */
onManagePress: () => void;
/** Callback when restore purchases is pressed */
onRestorePress: () => void;
/** Whether restore is in progress */
isRestoring?: boolean;
/** Animation delay */
delay?: number;
}
const PREMIUM_FEATURES = [
{ icon: 'layers' as const, text: '50+ Premium Packs' },
{ icon: 'flame' as const, text: 'All Intensity Levels' },
{ icon: 'sparkles' as const, text: 'New Packs Weekly' },
];
export function SubscriptionCard({
hasPremiumAccess,
isOwnSubscription,
expirationDate,
onUpgradePress,
onManagePress,
onRestorePress,
isRestoring = false,
delay = 350,
}: SubscriptionCardProps) {
if (hasPremiumAccess) {
return (
<Animated.View
entering={FadeInDown.delay(delay).duration(500)}
style={styles.container}
>
<ProMemberCard
isOwnSubscription={isOwnSubscription}
expirationDate={expirationDate}
onManagePress={onManagePress}
/>
</Animated.View>
);
}
return (
<Animated.View
entering={FadeInDown.delay(delay).duration(500)}
style={styles.container}
>
<FreeUserCard
onUpgradePress={onUpgradePress}
onRestorePress={onRestorePress}
isRestoring={isRestoring}
/>
</Animated.View>
);
}
/** Premium member card - celebratory gold treatment */
function ProMemberCard({
isOwnSubscription,
expirationDate,
onManagePress,
}: {
isOwnSubscription: boolean;
expirationDate: string;
onManagePress: () => void;
}) {
return (
<View style={styles.proCard}>
{/* Gold gradient border effect */}
<LinearGradient
colors={[colors.premium.gold, colors.premium.goldDark]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.proBorderGradient}
/>
<View style={styles.proCardInner}>
{/* Header with badge */}
<View style={styles.proHeader}>
<LinearGradient
colors={gradients.premiumGold as [string, string]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.proBadge}
>
<Ionicons name="star" size={16} color={colors.background} />
</LinearGradient>
<View style={styles.proHeaderText}>
<Text style={styles.proTitle}>Pro Member</Text>
<Text style={styles.proSubtitle}>
{isOwnSubscription
? `Renews ${expirationDate}`
: "Via partner's subscription"
}
</Text>
</View>
{isOwnSubscription && (
<TouchableOpacity
style={styles.manageButton}
onPress={onManagePress}
activeOpacity={0.7}
>
<Text style={styles.manageButtonText}>Manage</Text>
</TouchableOpacity>
)}
</View>
{/* Status row */}
<View style={styles.proStatusRow}>
<View style={styles.proStatusItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.premium.gold} />
<Text style={styles.proStatusText}>All Packs Unlocked</Text>
</View>
<View style={styles.proStatusItem}>
<Ionicons name="infinite" size={16} color={colors.premium.gold} />
<Text style={styles.proStatusText}>Unlimited Access</Text>
</View>
</View>
</View>
</View>
);
}
/** Free user card - premium upsell with shimmer */
function FreeUserCard({
onUpgradePress,
onRestorePress,
isRestoring,
}: {
onUpgradePress: () => void;
onRestorePress: () => void;
isRestoring: boolean;
}) {
return (
<ShimmerEffect
shimmerColor={colors.premium.gold}
duration={5000}
style={styles.shimmerContainer}
>
<View style={styles.freeCard}>
{/* Gold gradient border */}
<LinearGradient
colors={[`${colors.premium.gold}60`, `${colors.premium.goldDark}40`]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.freeBorderGradient}
/>
<View style={styles.freeCardInner}>
{/* Header */}
<View style={styles.freeHeader}>
<LinearGradient
colors={gradients.premiumGold as [string, string]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.freeHeaderBadge}
>
<Ionicons name="sparkles" size={14} color={colors.background} />
<Text style={styles.freeHeaderBadgeText}>SAUCI PRO</Text>
</LinearGradient>
</View>
{/* Features list */}
<View style={styles.featuresList}>
{PREMIUM_FEATURES.map((feature, index) => (
<View key={index} style={styles.featureItem}>
<View style={styles.featureIconContainer}>
<Ionicons
name={feature.icon}
size={16}
color={colors.premium.gold}
/>
</View>
<Text style={styles.featureText}>{feature.text}</Text>
</View>
))}
</View>
{/* CTA Button */}
<TouchableOpacity
onPress={onUpgradePress}
activeOpacity={0.8}
style={styles.ctaButtonContainer}
>
<LinearGradient
colors={gradients.premiumGold as [string, string]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.ctaButton}
>
<Ionicons name="star" size={18} color={colors.background} />
<Text style={styles.ctaButtonText}>Upgrade to Pro</Text>
</LinearGradient>
</TouchableOpacity>
{/* Restore link */}
<TouchableOpacity
style={styles.restoreLink}
onPress={onRestorePress}
disabled={isRestoring}
>
<Text style={styles.restoreLinkText}>
{isRestoring ? "Restoring..." : "Restore Purchases"}
</Text>
</TouchableOpacity>
</View>
</View>
</ShimmerEffect>
);
}
const styles = StyleSheet.create({
container: {
marginBottom: spacing.lg,
paddingHorizontal: spacing.lg,
},
shimmerContainer: {
borderRadius: radius.lg,
},
// Pro Member Card Styles
proCard: {
borderRadius: radius.lg,
overflow: 'hidden',
},
proBorderGradient: {
...StyleSheet.absoluteFill,
opacity: 0.3,
},
proCardInner: {
backgroundColor: colors.backgroundLight, // Solid/Flat background
margin: 1,
borderRadius: radius.lg - 1,
padding: spacing.md,
},
// ...
proBadge: {
width: 36,
height: 36,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
// Removed glow
},
// ...
manageButton: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.md,
backgroundColor: colors.background,
borderWidth: 1,
borderColor: colors.border,
},
// ...
proStatusRow: {
flexDirection: 'row',
marginTop: spacing.md,
paddingTop: spacing.md,
borderTopWidth: 1,
borderTopColor: colors.border,
gap: spacing.lg,
},
// ...
freeCardInner: {
backgroundColor: colors.backgroundLight, // Solid/Flat background
margin: 1,
borderRadius: radius.lg - 1,
padding: spacing.lg,
},
// ...
ctaButtonContainer: {
borderRadius: radius.md,
overflow: 'hidden',
// Removed glow
},
proHeader: {
flexDirection: 'row',
alignItems: 'center',
},
proHeaderText: {
flex: 1,
marginLeft: spacing.md,
},
proTitle: {
...typography.headline,
color: colors.premium.champagne,
fontWeight: '700',
},
proSubtitle: {
...typography.caption1,
color: colors.textTertiary,
marginTop: 2,
},
manageButtonText: {
...typography.subhead,
color: colors.premium.gold,
fontWeight: '600',
},
proStatusItem: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
proStatusText: {
...typography.caption1,
color: colors.textSecondary,
},
// Free User Card Styles
freeCard: {
borderRadius: radius.lg,
overflow: 'hidden',
},
freeBorderGradient: {
...StyleSheet.absoluteFill,
},
freeHeader: {
alignItems: 'center',
marginBottom: spacing.lg,
},
freeHeaderBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.full,
gap: spacing.xs,
},
freeHeaderBadgeText: {
...typography.caption1,
fontWeight: '800',
letterSpacing: 2,
color: colors.background,
},
featuresList: {
marginBottom: spacing.lg,
},
featureItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.sm,
},
featureIconContainer: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.premium.goldLight,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
featureText: {
...typography.body,
color: colors.text,
},
ctaButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
gap: spacing.sm,
},
ctaButtonText: {
...typography.headline,
color: colors.background,
fontWeight: '700',
},
restoreLink: {
alignItems: 'center',
marginTop: spacing.md,
padding: spacing.sm,
},
restoreLinkText: {
...typography.caption1,
color: colors.textTertiary,
},
});
export default SubscriptionCard;