clear more ticket, sub and design
This commit is contained in:
+258
-56
@@ -69,7 +69,7 @@ const getIntervalLabel = (recurring) => {
|
||||
return `tous les ${count} ${count > 1 ? terms.plural : terms.singular}`;
|
||||
};
|
||||
|
||||
function SubscriptionCard({ plan, selected, onSelect }) {
|
||||
function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
||||
const planKey = getPlanKeyForBadge(plan);
|
||||
const planName =
|
||||
(planKey && PLAN_DISPLAY_NAME_BY_KEY[planKey]) ||
|
||||
@@ -87,7 +87,6 @@ function SubscriptionCard({ plan, selected, onSelect }) {
|
||||
const planBadgeKey = planKey;
|
||||
const planBadgeSource =
|
||||
planBadgeKey && subBadges[planBadgeKey] ? subBadges[planBadgeKey] : null;
|
||||
const planFeatures = SUBSCRIPTION_FEATURES;
|
||||
const handleSelect = React.useCallback(() => {
|
||||
if (typeof onSelect === "function" && plan?.priceId) {
|
||||
onSelect(plan.priceId);
|
||||
@@ -156,22 +155,23 @@ function SubscriptionCard({ plan, selected, onSelect }) {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{planFeatures?.length ? (
|
||||
<View style={styles.features}>
|
||||
{planFeatures.map((feature) => (
|
||||
<Text key={feature} style={styles.featureText}>
|
||||
{feature}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</BlurView>
|
||||
{isAnnual ? (
|
||||
<View style={styles.annualBadge}>
|
||||
<Text style={styles.annualBadgeText}>2 mois offert</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const PRICE_PRIORITY_BY_PERIOD = {
|
||||
monthly: [
|
||||
"price_1SPgitCzf2o5bDRdbnhLFx6f", // Starter
|
||||
"price_1SPgjCCzf2o5bDRdr08Xzp8u", // Pro
|
||||
"price_1SPgjaCzf2o5bDRdd9Xo2u26", // Premium
|
||||
],
|
||||
annual: [
|
||||
"price_1SPgkDCzf2o5bDRdNGLVNeQ3", // Starter
|
||||
"price_1SPgkXCzf2o5bDRdejBVxEBY", // Pro
|
||||
@@ -180,6 +180,11 @@ const PRICE_PRIORITY_BY_PERIOD = {
|
||||
};
|
||||
|
||||
const PACK_PRICE_ID_BY_PERIOD = {
|
||||
monthly: {
|
||||
starter: "price_1SPgitCzf2o5bDRdbnhLFx6f",
|
||||
pro: "price_1SPgjCCzf2o5bDRdr08Xzp8u",
|
||||
premium: "price_1SPgjaCzf2o5bDRdd9Xo2u26",
|
||||
},
|
||||
annual: {
|
||||
starter: "price_1SPgkDCzf2o5bDRdNGLVNeQ3",
|
||||
pro: "price_1SPgkXCzf2o5bDRdejBVxEBY",
|
||||
@@ -212,12 +217,6 @@ const PLAN_DISPLAY_NAME_BY_KEY = {
|
||||
premium: "Play Backer Gold",
|
||||
};
|
||||
|
||||
const SUBSCRIPTION_FEATURES = [
|
||||
"*Eligible au Hit parade Vidéo",
|
||||
"Privilège Membre : Récompense doublée",
|
||||
"Eligible au hit parade Artiste trimestriel et annuel",
|
||||
];
|
||||
|
||||
const getPlanKeyForBadge = (plan) => {
|
||||
if (!plan || typeof plan !== "object") {
|
||||
return null;
|
||||
@@ -293,7 +292,7 @@ const HERO_IMAGE_HEIGHT = isWeb ? 760 : 360;
|
||||
const PAGE_BACKGROUND_COLOR = "#303438";
|
||||
const SUBSCRIPTION_DISCLAIMER =
|
||||
"Résiliable en un clic à tout moment. Les prix sont indiqués TTC. Contacte-nous pour des besoins spécifiques (facturation annuelle, volume, offres éducation).";
|
||||
const CARD_MIN_HEIGHT = isWeb ? 320 : 240;
|
||||
const CARD_MIN_HEIGHT = isWeb ? 260 : 180;
|
||||
|
||||
export default function Subscriptions() {
|
||||
const route = useRoute();
|
||||
@@ -313,16 +312,29 @@ export default function Subscriptions() {
|
||||
catalogError,
|
||||
createSubscriptionCheckout,
|
||||
} = useStripe();
|
||||
const [selectedPeriodKey, setSelectedPeriodKey] = React.useState("annual");
|
||||
const [selectedPriceId, setSelectedPriceId] = React.useState(null);
|
||||
const [processingPriceId, setProcessingPriceId] = React.useState(null);
|
||||
const [errorMessage, setErrorMessage] = React.useState(null);
|
||||
const initialPackHandledRef = React.useRef(false);
|
||||
|
||||
const normalizedPlans = React.useMemo(
|
||||
() => normalizePlans(subscriptions?.annual, "annual"),
|
||||
[subscriptions?.annual]
|
||||
const normalizedPlansByPeriod = React.useMemo(
|
||||
() => ({
|
||||
monthly: normalizePlans(subscriptions?.monthly, "monthly"),
|
||||
annual: normalizePlans(subscriptions?.annual, "annual"),
|
||||
}),
|
||||
[subscriptions?.annual, subscriptions?.monthly]
|
||||
);
|
||||
const hasAnyPlan = (normalizedPlans?.length || 0) > 0;
|
||||
|
||||
const availablePeriods = React.useMemo(
|
||||
() =>
|
||||
["monthly", "annual"].filter(
|
||||
(period) => (normalizedPlansByPeriod?.[period]?.length || 0) > 0
|
||||
),
|
||||
[normalizedPlansByPeriod]
|
||||
);
|
||||
|
||||
const hasAnyPlan = availablePeriods.length > 0;
|
||||
const isLoadingPlans = isCatalogLoading && !hasAnyPlan;
|
||||
const combinedErrorMessage = errorMessage || catalogError;
|
||||
|
||||
@@ -331,25 +343,39 @@ export default function Subscriptions() {
|
||||
}, [initialSubscriptionPack]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const periodEntries = Object.entries(normalizedPlansByPeriod).filter(
|
||||
([, plans]) => (plans?.length || 0) > 0
|
||||
);
|
||||
|
||||
if (!periodEntries.length) {
|
||||
setSelectedPriceId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldApplyPack =
|
||||
Boolean(initialSubscriptionPack) && !initialPackHandledRef.current;
|
||||
|
||||
let matchedPriceId = null;
|
||||
let matchedPeriodKey = selectedPeriodKey;
|
||||
let nextPeriodKey =
|
||||
periodEntries.find(([periodKey]) => periodKey === selectedPeriodKey)?.[0] ||
|
||||
periodEntries[0]?.[0] ||
|
||||
"annual";
|
||||
|
||||
if (shouldApplyPack && normalizedPlans?.length) {
|
||||
if (shouldApplyPack) {
|
||||
const desired = initialSubscriptionPack.trim().toLowerCase();
|
||||
const candidateId = PACK_PRICE_ID_BY_PERIOD?.annual?.[desired];
|
||||
if (candidateId) {
|
||||
const exists = normalizedPlans.some(
|
||||
(plan) => plan?.priceId === candidateId
|
||||
);
|
||||
if (exists) {
|
||||
matchedPriceId = candidateId;
|
||||
for (const [periodKey, plans] of periodEntries) {
|
||||
const candidateId = PACK_PRICE_ID_BY_PERIOD?.[periodKey]?.[desired];
|
||||
if (candidateId) {
|
||||
const exists = plans.some((plan) => plan?.priceId === candidateId);
|
||||
if (exists) {
|
||||
matchedPriceId = candidateId;
|
||||
matchedPeriodKey = periodKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedPriceId) {
|
||||
const matched = normalizedPlans.find((plan) => {
|
||||
const matched = plans.find((plan) => {
|
||||
const label = (plan?.product?.name || plan?.nickname || "")
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
@@ -358,25 +384,38 @@ export default function Subscriptions() {
|
||||
|
||||
if (matched?.priceId) {
|
||||
matchedPriceId = matched.priceId;
|
||||
matchedPeriodKey = periodKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedPriceId((current) => {
|
||||
if (matchedPriceId) {
|
||||
nextPeriodKey = matchedPeriodKey;
|
||||
}
|
||||
|
||||
const plansForPeriod = normalizedPlansByPeriod?.[nextPeriodKey] || [];
|
||||
|
||||
const nextPriceId = (() => {
|
||||
if (matchedPriceId) {
|
||||
return matchedPriceId;
|
||||
}
|
||||
|
||||
const hasCurrent = normalizedPlans?.some(
|
||||
(plan) => plan.priceId === current
|
||||
const hasCurrent = plansForPeriod.some(
|
||||
(plan) => plan.priceId === selectedPriceId
|
||||
);
|
||||
|
||||
if (hasCurrent) {
|
||||
return current;
|
||||
return selectedPriceId;
|
||||
}
|
||||
return plansForPeriod?.[0]?.priceId || null;
|
||||
})();
|
||||
|
||||
return normalizedPlans?.[0]?.priceId || null;
|
||||
});
|
||||
if (nextPeriodKey !== selectedPeriodKey) {
|
||||
setSelectedPeriodKey(nextPeriodKey);
|
||||
}
|
||||
|
||||
if (nextPriceId !== selectedPriceId) {
|
||||
setSelectedPriceId(nextPriceId);
|
||||
}
|
||||
|
||||
if (matchedPriceId || (shouldApplyPack && !isCatalogLoading)) {
|
||||
initialPackHandledRef.current = true;
|
||||
@@ -384,10 +423,12 @@ export default function Subscriptions() {
|
||||
}, [
|
||||
initialSubscriptionPack,
|
||||
isCatalogLoading,
|
||||
normalizedPlans,
|
||||
normalizedPlansByPeriod,
|
||||
selectedPeriodKey,
|
||||
selectedPriceId,
|
||||
]);
|
||||
|
||||
const currentPlans = normalizedPlans || [];
|
||||
const currentPlans = normalizedPlansByPeriod?.[selectedPeriodKey] || [];
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(priceId) => {
|
||||
@@ -438,13 +479,36 @@ export default function Subscriptions() {
|
||||
goBack();
|
||||
}, []);
|
||||
|
||||
const handlePeriodChange = React.useCallback(
|
||||
(periodKey) => {
|
||||
if (!periodKey || periodKey === selectedPeriodKey) {
|
||||
return;
|
||||
}
|
||||
setSelectedPeriodKey(periodKey);
|
||||
setSelectedPriceId(null);
|
||||
setProcessingPriceId(null);
|
||||
},
|
||||
[selectedPeriodKey]
|
||||
);
|
||||
|
||||
const renderHeaderSection = React.useCallback(() => {
|
||||
return (
|
||||
<>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>
|
||||
Choisissez l’abonnement qui vous correspond
|
||||
Rejoignez le club MusicLand
|
||||
</Text>
|
||||
<View style={styles.benefitsBox}>
|
||||
<Text style={styles.benefitsTitle}>Privilège Membre :</Text>
|
||||
<View style={styles.benefitsList}>
|
||||
<Text style={styles.benefitsItem}>
|
||||
- Eligible au concours mensuel chanson/Vidéo
|
||||
</Text>
|
||||
<Text style={styles.benefitsItem}>
|
||||
- Crédits gratuits tous les mois
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{combinedErrorMessage ? (
|
||||
<Text style={styles.errorText}>{combinedErrorMessage}</Text>
|
||||
@@ -453,6 +517,66 @@ export default function Subscriptions() {
|
||||
);
|
||||
}, [combinedErrorMessage]);
|
||||
|
||||
const renderSegmentedControl = React.useCallback(() => {
|
||||
const segments = [
|
||||
{ key: "monthly", label: "Mensuel", plans: normalizedPlansByPeriod?.monthly },
|
||||
{ key: "annual", label: "Annuel", plans: normalizedPlansByPeriod?.annual },
|
||||
];
|
||||
|
||||
const visibleSegments = segments.filter(
|
||||
(segment) => (segment.plans?.length || 0) > 0
|
||||
);
|
||||
|
||||
if (visibleSegments.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.segmentedControl,
|
||||
isMobile && styles.segmentedControlMobile,
|
||||
]}
|
||||
>
|
||||
{visibleSegments.map((segment) => {
|
||||
const isActive = selectedPeriodKey === segment.key;
|
||||
const showAnnualPromo = segment.key === "annual";
|
||||
return (
|
||||
<Pressable
|
||||
key={segment.key}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected: isActive }}
|
||||
onPress={() => handlePeriodChange(segment.key)}
|
||||
style={[
|
||||
styles.segmentButton,
|
||||
isActive && styles.segmentButtonActive,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.segmentLabel,
|
||||
isActive && styles.segmentLabelActive,
|
||||
]}
|
||||
>
|
||||
{segment.label}
|
||||
</Text>
|
||||
{showAnnualPromo ? (
|
||||
<View style={styles.segmentBadge}>
|
||||
<Text style={styles.segmentBadgeText}>-16%</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}, [
|
||||
handlePeriodChange,
|
||||
isMobile,
|
||||
normalizedPlansByPeriod,
|
||||
selectedPeriodKey,
|
||||
]);
|
||||
|
||||
const renderMobilePlanItem = React.useCallback(
|
||||
({ item }) => (
|
||||
<View style={styles.mobileCard}>
|
||||
@@ -460,10 +584,11 @@ export default function Subscriptions() {
|
||||
plan={item}
|
||||
selected={selectedPriceId === item.priceId}
|
||||
onSelect={handleSelect}
|
||||
isAnnual={selectedPeriodKey === "annual"}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
[handleSelect, selectedPriceId]
|
||||
[handleSelect, selectedPeriodKey, selectedPriceId]
|
||||
);
|
||||
|
||||
const renderMobileEmptyComponent = React.useCallback(() => {
|
||||
@@ -497,9 +622,10 @@ export default function Subscriptions() {
|
||||
return (
|
||||
<View style={styles.mobileStickyHeader}>
|
||||
{renderHeaderSection()}
|
||||
{renderSegmentedControl()}
|
||||
</View>
|
||||
);
|
||||
}, [renderHeaderSection]);
|
||||
}, [renderHeaderSection, renderSegmentedControl]);
|
||||
|
||||
const renderMobileBottomActions = React.useCallback(() => {
|
||||
return (
|
||||
@@ -588,6 +714,8 @@ export default function Subscriptions() {
|
||||
<>
|
||||
{renderHeaderSection()}
|
||||
|
||||
{renderSegmentedControl()}
|
||||
|
||||
{isLoadingPlans && currentPlanCount === 0 ? (
|
||||
<View style={styles.loaderContainer}>
|
||||
<ActivityIndicator color={Palette.white} />
|
||||
@@ -607,6 +735,7 @@ export default function Subscriptions() {
|
||||
plan={plan}
|
||||
selected={selectedPriceId === plan.priceId}
|
||||
onSelect={handleSelect}
|
||||
isAnnual={selectedPeriodKey === "annual"}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
@@ -700,6 +829,29 @@ const styles = StyleSheet.create({
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
},
|
||||
benefitsBox: {
|
||||
width: "100%",
|
||||
gap: 6,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: 16,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||
},
|
||||
benefitsTitle: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
textAlign: "center",
|
||||
},
|
||||
benefitsList: {
|
||||
gap: 2,
|
||||
},
|
||||
benefitsItem: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 13,
|
||||
color: "rgba(255, 255, 255, 0.85)",
|
||||
textAlign: "center",
|
||||
},
|
||||
webBackContainer: {
|
||||
alignSelf: "flex-start",
|
||||
marginBottom: 12,
|
||||
@@ -760,6 +912,61 @@ const styles = StyleSheet.create({
|
||||
alignItems: "stretch",
|
||||
justifyContent: "center",
|
||||
},
|
||||
segmentedControl: {
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
padding: 4,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||
marginTop: isWeb ? 12 : 8,
|
||||
marginBottom: isWeb ? 8 : 4,
|
||||
},
|
||||
segmentButton: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 18,
|
||||
borderRadius: 999,
|
||||
position: "relative",
|
||||
},
|
||||
segmentButtonActive: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.18)",
|
||||
},
|
||||
segmentLabel: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 14,
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
},
|
||||
segmentLabelActive: {
|
||||
color: Palette.white,
|
||||
},
|
||||
segmentBadge: {
|
||||
position: "absolute",
|
||||
top: -6,
|
||||
right: -8,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 999,
|
||||
backgroundColor: Palette.red,
|
||||
},
|
||||
segmentBadgeText: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 11,
|
||||
color: Palette.white,
|
||||
},
|
||||
annualBadge: {
|
||||
position: "absolute",
|
||||
top: isWeb ? 12 : 10,
|
||||
right: isWeb ? 12 : 10,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 999,
|
||||
backgroundColor: Palette.red,
|
||||
},
|
||||
annualBadgeText: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
},
|
||||
actions: {
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
@@ -772,6 +979,7 @@ const styles = StyleSheet.create({
|
||||
width: "100%",
|
||||
minWidth: 0,
|
||||
minHeight: CARD_MIN_HEIGHT,
|
||||
position: "relative",
|
||||
borderRadius: 24,
|
||||
overflow: "hidden",
|
||||
borderWidth: 1,
|
||||
@@ -854,16 +1062,6 @@ const styles = StyleSheet.create({
|
||||
fontSize: 13,
|
||||
color: Palette.primary,
|
||||
},
|
||||
features: {
|
||||
marginTop: isWeb ? 12 : 8,
|
||||
gap: 6,
|
||||
},
|
||||
featureText: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
color: "rgba(255, 255, 255, 0.85)",
|
||||
},
|
||||
priceValue: {
|
||||
fontFamily: FONT_FAMILY.InterBold,
|
||||
fontSize: isWeb ? 22 : 20,
|
||||
@@ -904,6 +1102,10 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: PAGE_BACKGROUND_COLOR,
|
||||
alignItems: "center",
|
||||
},
|
||||
segmentedControlMobile: {
|
||||
alignSelf: "center",
|
||||
marginBottom: 0,
|
||||
},
|
||||
mobileList: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
|
||||
Reference in New Issue
Block a user