781 lines
23 KiB
JavaScript
781 lines
23 KiB
JavaScript
import { useFocusEffect } from "@react-navigation/native";
|
||
import React, { useCallback, useMemo, useState } from "react";
|
||
import { Alert, Platform, StyleSheet, Text, View } from "react-native";
|
||
|
||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||
import CreditAmount from "../../components/CreditAmount";
|
||
import { background } from "../../assets";
|
||
import { getFunctionsClient } from "../../config/firebase";
|
||
import Page from "../../layouts/Page";
|
||
import { Routes } from "../../navigation/Routes";
|
||
import { useUserData } from "../../providers/UserDataProvider";
|
||
import { gutters, Palette } from "../../styles";
|
||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||
import { formatDate, toDate } from "../../utils/dateFormatting";
|
||
import { Image as ExpoImage } from "expo-image";
|
||
import { isWeb } from "../../hooks/useLayoutType";
|
||
|
||
const FUNCTIONS_REGION = "europe-west1";
|
||
|
||
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
|
||
"trialing",
|
||
"active",
|
||
"past_due",
|
||
"unpaid",
|
||
]);
|
||
|
||
const STATUS_LABELS = {
|
||
trialing: "Période d'essai",
|
||
active: "Actif",
|
||
past_due: "Paiement en attente",
|
||
unpaid: "Impaye",
|
||
canceled: "Annulé",
|
||
incomplete: "Incomplet",
|
||
incomplete_expired: "Expiré",
|
||
paused: "En pause",
|
||
};
|
||
|
||
const PLAN_LABELS = {
|
||
starter: "Starter",
|
||
pro: "Pro",
|
||
premium: "Premium",
|
||
};
|
||
|
||
const PERIOD_LABELS = {
|
||
monthly: "Mensuel",
|
||
annual: "Annuel",
|
||
};
|
||
|
||
const PAGE_BACKGROUND_COLOR = "#303438";
|
||
|
||
const getStatusColors = (status) => {
|
||
switch (status) {
|
||
case "trialing":
|
||
case "active":
|
||
return {
|
||
text: Palette.green,
|
||
background: Palette.transparentGreen,
|
||
};
|
||
case "past_due":
|
||
case "unpaid":
|
||
return {
|
||
text: Palette.orange,
|
||
background: Palette.transparentOrange,
|
||
};
|
||
case "canceled":
|
||
case "incomplete_expired":
|
||
return {
|
||
text: Palette.red,
|
||
background: Palette.transparentRed,
|
||
};
|
||
default:
|
||
return {
|
||
text: Palette.grayMid,
|
||
background: Palette.ultraLightWhite,
|
||
};
|
||
}
|
||
};
|
||
|
||
const capitalize = (value) => {
|
||
if (typeof value !== "string" || !value) {
|
||
return null;
|
||
}
|
||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||
};
|
||
|
||
const SCHEDULE_COMPARISON_HOUR = 2;
|
||
const SCHEDULE_COMPARISON_MINUTE = 30;
|
||
const SCHEDULE_DISPLAY_HOUR = 3;
|
||
const SCHEDULE_DISPLAY_MINUTE = 30;
|
||
|
||
const addMonthsSafe = (date, months = 1) => {
|
||
if (!(date instanceof Date) || !Number.isFinite(months)) {
|
||
return null;
|
||
}
|
||
const result = new Date(date.getTime());
|
||
const initialDay = result.getDate();
|
||
result.setMonth(result.getMonth() + months);
|
||
if (result.getDate() !== initialDay) {
|
||
result.setDate(0);
|
||
}
|
||
return result;
|
||
};
|
||
|
||
const addDaysSafe = (date, days = 1) => {
|
||
if (!(date instanceof Date) || !Number.isFinite(days)) {
|
||
return null;
|
||
}
|
||
const result = new Date(date.getTime());
|
||
result.setDate(result.getDate() + days);
|
||
return result;
|
||
};
|
||
|
||
const alignToScheduleTime = (date) => {
|
||
if (!(date instanceof Date)) {
|
||
return null;
|
||
}
|
||
const aligned = new Date(date.getTime());
|
||
aligned.setHours(SCHEDULE_DISPLAY_HOUR, SCHEDULE_DISPLAY_MINUTE, 0, 0);
|
||
return aligned;
|
||
};
|
||
|
||
const computeFirstAnnualGrantFromCreation = (creationDate) => {
|
||
if (!(creationDate instanceof Date)) {
|
||
return null;
|
||
}
|
||
const cutoff = new Date(creationDate.getTime());
|
||
cutoff.setHours(SCHEDULE_COMPARISON_HOUR, SCHEDULE_COMPARISON_MINUTE, 0, 0);
|
||
|
||
let base = null;
|
||
if (creationDate <= cutoff) {
|
||
base = addMonthsSafe(creationDate, 1);
|
||
} else {
|
||
base = addDaysSafe(creationDate, 1);
|
||
}
|
||
|
||
if (!base) {
|
||
return null;
|
||
}
|
||
return alignToScheduleTime(base);
|
||
};
|
||
|
||
const ManageSubscription = ({ navigation }) => {
|
||
const { currentUserData } = useUserData() || {};
|
||
const [isCancelling, setIsCancelling] = useState(false);
|
||
const [errorMessage, setErrorMessage] = useState(null);
|
||
const [successMessage, setSuccessMessage] = useState(null);
|
||
const [remoteSubscription, setRemoteSubscription] = useState(null);
|
||
const [remoteError, setRemoteError] = useState(null);
|
||
const [isFetchingRemote, setIsFetchingRemote] = useState(false);
|
||
const [refreshToken, setRefreshToken] = useState(0);
|
||
|
||
const stripeCustomerId = currentUserData?.stripeCustomerId || null;
|
||
const localSubscriptionId =
|
||
currentUserData?.stripeSubscription?.id ||
|
||
currentUserData?.stripeSubscription?.subscriptionId ||
|
||
null;
|
||
|
||
const triggerRefresh = useCallback(() => {
|
||
setRefreshToken((value) => value + 1);
|
||
setIsFetchingRemote(true);
|
||
}, []);
|
||
|
||
useFocusEffect(
|
||
useCallback(() => {
|
||
triggerRefresh();
|
||
}, [triggerRefresh]),
|
||
);
|
||
|
||
React.useEffect(() => {
|
||
let isMounted = true;
|
||
|
||
const run = async () => {
|
||
const hasLookupContext =
|
||
Boolean(stripeCustomerId) || Boolean(localSubscriptionId);
|
||
if (!hasLookupContext) {
|
||
if (isMounted) {
|
||
setRemoteSubscription(null);
|
||
setRemoteError(null);
|
||
setIsFetchingRemote(false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
setIsFetchingRemote(true);
|
||
setRemoteError(null);
|
||
|
||
try {
|
||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
|
||
"subscription-getActiveSubscription",
|
||
);
|
||
const { data } = await callable();
|
||
if (!isMounted) {
|
||
return;
|
||
}
|
||
setRemoteSubscription(data?.subscription || null);
|
||
} catch (error) {
|
||
console.warn(
|
||
"[ManageSubscription] fetch subscription error",
|
||
error?.message || error,
|
||
);
|
||
if (isMounted) {
|
||
const message =
|
||
error?.message ||
|
||
"Impossible de mettre à jour les informations d'abonnement.";
|
||
setRemoteError(message);
|
||
}
|
||
} finally {
|
||
if (isMounted) {
|
||
setIsFetchingRemote(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
run();
|
||
|
||
return () => {
|
||
isMounted = false;
|
||
};
|
||
}, [stripeCustomerId, localSubscriptionId, refreshToken]);
|
||
|
||
const subscriptionInfo = useMemo(() => {
|
||
const rawSubscription =
|
||
remoteSubscription || currentUserData?.stripeSubscription || null;
|
||
|
||
const pickString = (value) => {
|
||
if (typeof value !== "string") {
|
||
return null;
|
||
}
|
||
const trimmed = value.trim();
|
||
return trimmed ? trimmed : null;
|
||
};
|
||
|
||
const statusSource =
|
||
pickString(currentUserData?.stripeSubscriptionStatus) ||
|
||
pickString(remoteSubscription?.status) ||
|
||
pickString(currentUserData?.stripeSubscription?.status) ||
|
||
pickString(
|
||
currentUserData?.stripeSubscription?.stripeSubscriptionStatus,
|
||
) ||
|
||
pickString(rawSubscription?.status) ||
|
||
null;
|
||
|
||
const status = statusSource ? statusSource.toLowerCase() : null;
|
||
|
||
const cancelAtPeriodEnd =
|
||
remoteSubscription?.cancelAtPeriodEnd === true ||
|
||
rawSubscription?.cancelAtPeriodEnd === true ||
|
||
rawSubscription?.cancel_at_period_end === true ||
|
||
false;
|
||
|
||
const resolveLevelSource = () => {
|
||
const candidates = [
|
||
pickString(remoteSubscription?.level),
|
||
pickString(currentUserData?.premiumLevel),
|
||
pickString(rawSubscription?.metadata?.level),
|
||
pickString(rawSubscription?.metadata?.subscriptionLevel),
|
||
];
|
||
return candidates.find(Boolean) || null;
|
||
};
|
||
|
||
const resolvePeriodSource = () => {
|
||
const candidates = [
|
||
pickString(remoteSubscription?.billingPeriod),
|
||
pickString(currentUserData?.premiumBillingPeriod),
|
||
pickString(rawSubscription?.metadata?.billingPeriod),
|
||
pickString(rawSubscription?.metadata?.subscriptionBillingPeriod),
|
||
];
|
||
return candidates.find(Boolean) || null;
|
||
};
|
||
|
||
const levelSource = resolveLevelSource();
|
||
const periodSource = resolvePeriodSource();
|
||
|
||
const level = levelSource ? levelSource.toLowerCase() : null;
|
||
const billingPeriod = periodSource ? periodSource.toLowerCase() : null;
|
||
|
||
const isAnnual = billingPeriod === "annual";
|
||
|
||
const planLabelParts = [];
|
||
if (PLAN_LABELS[level]) {
|
||
planLabelParts.push(PLAN_LABELS[level]);
|
||
} else if (level) {
|
||
planLabelParts.push(capitalize(level));
|
||
}
|
||
if (PERIOD_LABELS[billingPeriod]) {
|
||
planLabelParts.push(PERIOD_LABELS[billingPeriod]);
|
||
} else if (billingPeriod) {
|
||
planLabelParts.push(capitalize(billingPeriod));
|
||
}
|
||
const planLabel =
|
||
planLabelParts.length > 0
|
||
? planLabelParts.join(" · ")
|
||
: "Abonnement Musicland";
|
||
|
||
const currentPeriodEndDate =
|
||
toDate(remoteSubscription?.currentPeriodEnd) ||
|
||
toDate(rawSubscription?.currentPeriodEnd) ||
|
||
toDate(rawSubscription?.current_period_end);
|
||
const createdAtDate =
|
||
toDate(remoteSubscription?.created) ||
|
||
toDate(rawSubscription?.createdAt) ||
|
||
toDate(rawSubscription?.created_at);
|
||
|
||
const statusLabelBase =
|
||
STATUS_LABELS[status] ||
|
||
(status ? status.replace(/_/g, " ").toLowerCase() : null);
|
||
const statusLabel = statusLabelBase ? capitalize(statusLabelBase) : null;
|
||
const statusColors = getStatusColors(status);
|
||
|
||
const hasAnySubscription = Boolean(rawSubscription?.id);
|
||
const hasActiveSubscription =
|
||
hasAnySubscription && ACTIVE_SUBSCRIPTION_STATUSES.has(status);
|
||
const canCancel = hasActiveSubscription && !cancelAtPeriodEnd;
|
||
|
||
const periodEndLabel = currentPeriodEndDate
|
||
? formatDate(currentPeriodEndDate)
|
||
: "—";
|
||
const createdAtLabel = createdAtDate ? formatDate(createdAtDate) : null;
|
||
|
||
let coinsPerMonth =
|
||
typeof remoteSubscription?.coinsPerMonth === "number"
|
||
? remoteSubscription.coinsPerMonth
|
||
: null;
|
||
|
||
if (
|
||
coinsPerMonth === null &&
|
||
typeof currentUserData?.subscriptionCoinsPerMonth === "number"
|
||
) {
|
||
const userCoins = currentUserData.subscriptionCoinsPerMonth;
|
||
coinsPerMonth = Number.isFinite(userCoins) ? userCoins : null;
|
||
}
|
||
|
||
const normalizedCoins =
|
||
typeof coinsPerMonth === "number" && Number.isFinite(coinsPerMonth)
|
||
? Math.round(coinsPerMonth)
|
||
: null;
|
||
const nextGrantTimestamp =
|
||
currentUserData?.subscriptionNextGrantAt ||
|
||
currentUserData?.subscriptionGrantNextAt ||
|
||
null;
|
||
const nextGrantRawDate = toDate(nextGrantTimestamp);
|
||
const now = new Date();
|
||
|
||
const alignedStoredNextGrant = alignToScheduleTime(nextGrantRawDate);
|
||
const futureStoredNextGrant =
|
||
alignedStoredNextGrant &&
|
||
alignedStoredNextGrant.getTime() >= now.getTime()
|
||
? alignedStoredNextGrant
|
||
: null;
|
||
|
||
const fallbackInitialGrant =
|
||
isAnnual && createdAtDate
|
||
? computeFirstAnnualGrantFromCreation(createdAtDate)
|
||
: null;
|
||
const futureFallbackGrant =
|
||
fallbackInitialGrant && fallbackInitialGrant.getTime() >= now.getTime()
|
||
? fallbackInitialGrant
|
||
: null;
|
||
|
||
let resolvedNextGrantDate =
|
||
futureStoredNextGrant || futureFallbackGrant || null;
|
||
|
||
if (futureStoredNextGrant && futureFallbackGrant) {
|
||
resolvedNextGrantDate =
|
||
futureFallbackGrant.getTime() < futureStoredNextGrant.getTime()
|
||
? futureFallbackGrant
|
||
: futureStoredNextGrant;
|
||
}
|
||
|
||
const nextGrantLabel =
|
||
resolvedNextGrantDate && isAnnual
|
||
? formatDate(resolvedNextGrantDate)
|
||
: null;
|
||
|
||
const helperMessages = [];
|
||
if (!hasActiveSubscription && hasAnySubscription) {
|
||
helperMessages.push(
|
||
"Ton abonnement n'est plus actif. Tu peux souscrire à nouveau à tout moment.",
|
||
);
|
||
}
|
||
const helperMessage = helperMessages.join("\n");
|
||
|
||
return {
|
||
hasAnySubscription,
|
||
hasActiveSubscription,
|
||
canCancel,
|
||
cancelAtPeriodEnd,
|
||
status,
|
||
statusLabel,
|
||
statusColors,
|
||
planLabel,
|
||
billingPeriodLabel:
|
||
PERIOD_LABELS[billingPeriod] || capitalize(billingPeriod),
|
||
periodEndLabel,
|
||
createdAtLabel,
|
||
helperMessage: helperMessage || null,
|
||
level: level || null,
|
||
subscriptionId:
|
||
remoteSubscription?.id ||
|
||
rawSubscription?.id ||
|
||
currentUserData?.stripeSubscription?.subscriptionId ||
|
||
null,
|
||
coinsPerMonth: normalizedCoins,
|
||
isAnnual,
|
||
nextGrantDate: resolvedNextGrantDate,
|
||
nextGrantLabel,
|
||
};
|
||
}, [currentUserData, remoteSubscription]);
|
||
|
||
const handleOpenPlans = useCallback(() => {
|
||
const params =
|
||
subscriptionInfo.level && typeof subscriptionInfo.level === "string"
|
||
? { pack: subscriptionInfo.level }
|
||
: undefined;
|
||
navigation.navigate(Routes.Payments, params);
|
||
}, [navigation, subscriptionInfo.level]);
|
||
|
||
const performCancellation = useCallback(async () => {
|
||
setIsCancelling(true);
|
||
setErrorMessage(null);
|
||
setSuccessMessage(null);
|
||
|
||
try {
|
||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
|
||
"subscription-cancelActiveSubscription",
|
||
);
|
||
const payload = subscriptionInfo.subscriptionId
|
||
? { subscriptionId: subscriptionInfo.subscriptionId }
|
||
: {};
|
||
const { data } = await callable(payload);
|
||
|
||
if (data?.alreadyCanceled) {
|
||
setSuccessMessage(
|
||
"Ton abonnement est déjà en cours d'annulation. L'accès premium restera actif jusqu'à la fin de la période en cours.",
|
||
);
|
||
} else if (data?.cancelAtPeriodEnd) {
|
||
setSuccessMessage(
|
||
"Ton abonnement sera résilié à la fin de la période en cours.",
|
||
);
|
||
} else {
|
||
setSuccessMessage(
|
||
"La demande d'annulation a été prise en compte. Vérifie ton abonnement dans quelques instants.",
|
||
);
|
||
}
|
||
triggerRefresh();
|
||
} catch (error) {
|
||
console.warn(
|
||
"[ManageSubscription] cancel subscription error",
|
||
error?.message || error,
|
||
);
|
||
const message =
|
||
error?.message || "Impossible d'annuler l'abonnement pour le moment.";
|
||
setErrorMessage(message);
|
||
} finally {
|
||
setIsCancelling(false);
|
||
}
|
||
}, [subscriptionInfo.subscriptionId, triggerRefresh]);
|
||
|
||
const handleCancel = useCallback(() => {
|
||
if (!subscriptionInfo.canCancel || isCancelling) {
|
||
return;
|
||
}
|
||
|
||
const confirm = () => {
|
||
performCancellation();
|
||
};
|
||
|
||
if (Platform.OS === "web" && typeof window !== "undefined") {
|
||
const confirmed = window.confirm(
|
||
"Confirmer l'annulation ? Ton accès premium restera actif jusqu'à la fin de la période en cours.",
|
||
);
|
||
if (confirmed) {
|
||
confirm();
|
||
}
|
||
return;
|
||
}
|
||
|
||
Alert.alert(
|
||
"Confirmer l'annulation",
|
||
"Ton accès premium restera actif jusqu'à la fin de la période en cours.",
|
||
[
|
||
{ text: "Conserver mon abonnement", style: "cancel" },
|
||
{
|
||
text: "Annuler l'abonnement",
|
||
style: "destructive",
|
||
onPress: confirm,
|
||
},
|
||
],
|
||
);
|
||
}, [isCancelling, performCancellation, subscriptionInfo.canCancel]);
|
||
|
||
const cancelButtonTitle = subscriptionInfo.cancelAtPeriodEnd
|
||
? "Annulation programmée"
|
||
: isCancelling
|
||
? "Annulation..."
|
||
: "Annuler l'abonnement";
|
||
|
||
const cancelTitleColor = subscriptionInfo.cancelAtPeriodEnd
|
||
? Palette.grayMid
|
||
: Palette.red;
|
||
|
||
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520;
|
||
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360;
|
||
const HOME_BACKGROUND_STYLE_WIDTH = HOME_BACKGROUND_WIDTH + 120;
|
||
const HOME_BACKGROUND_STYLE_HEIGHT = HOME_BACKGROUND_HEIGHT + 80;
|
||
|
||
return (
|
||
<Page
|
||
headerType="NAVIGATE"
|
||
title="Mon abonnement"
|
||
scrollEnabled
|
||
backgroundColor={PAGE_BACKGROUND_COLOR}
|
||
// backgroundImg={backgroundImage}
|
||
>
|
||
<ExpoImage
|
||
source={background.bgTrans}
|
||
contentFit="cover"
|
||
style={{
|
||
width: HOME_BACKGROUND_STYLE_WIDTH,
|
||
height: HOME_BACKGROUND_STYLE_HEIGHT,
|
||
borderRadius: 22,
|
||
overflow: "hidden",
|
||
position: "absolute",
|
||
top: isWeb ? "45%" : "50%",
|
||
left: "50%",
|
||
transform: [
|
||
{ translateX: -HOME_BACKGROUND_STYLE_WIDTH / 2 },
|
||
{ translateY: -HOME_BACKGROUND_STYLE_HEIGHT / 2 },
|
||
],
|
||
pointerEvents: "none",
|
||
zIndex: 0,
|
||
}}
|
||
/>
|
||
<View style={styles.container}>
|
||
{subscriptionInfo.hasAnySubscription ? (
|
||
<>
|
||
<View style={styles.card}>
|
||
<Text style={styles.cardTitle}>{subscriptionInfo.planLabel}</Text>
|
||
|
||
<View style={styles.detailRow}>
|
||
<Text style={styles.detailLabel}>Statut</Text>
|
||
{subscriptionInfo.statusLabel ? (
|
||
<View
|
||
style={[
|
||
styles.statusBadge,
|
||
{
|
||
backgroundColor:
|
||
subscriptionInfo.statusColors.background,
|
||
},
|
||
]}
|
||
>
|
||
<Text
|
||
style={[
|
||
styles.statusText,
|
||
{ color: subscriptionInfo.statusColors.text },
|
||
]}
|
||
>
|
||
{subscriptionInfo.statusLabel}
|
||
</Text>
|
||
</View>
|
||
) : (
|
||
<Text style={styles.detailValue}>—</Text>
|
||
)}
|
||
</View>
|
||
|
||
<View style={styles.detailRow}>
|
||
<Text style={styles.detailLabel}>Cycle de facturation</Text>
|
||
<Text style={styles.detailValue}>
|
||
{subscriptionInfo.billingPeriodLabel || "—"}
|
||
</Text>
|
||
</View>
|
||
|
||
<View style={styles.detailRow}>
|
||
<Text style={styles.detailLabel}>Crédits mensuels</Text>
|
||
{typeof subscriptionInfo.coinsPerMonth === "number" ? (
|
||
<CreditAmount
|
||
value={subscriptionInfo.coinsPerMonth}
|
||
textStyle={styles.detailValue}
|
||
iconSize={18}
|
||
/>
|
||
) : (
|
||
<Text style={styles.detailValue}>—</Text>
|
||
)}
|
||
</View>
|
||
|
||
{subscriptionInfo.isAnnual && subscriptionInfo.nextGrantLabel ? (
|
||
<View style={styles.detailRow}>
|
||
<Text style={styles.detailLabel}>Prochain versement</Text>
|
||
<Text style={styles.detailValue}>
|
||
{subscriptionInfo.nextGrantLabel}
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
|
||
<View style={styles.detailRow}>
|
||
<Text style={styles.detailLabel}>
|
||
{subscriptionInfo.cancelAtPeriodEnd
|
||
? "Fin d'accès"
|
||
: "Prochain renouvellement"}
|
||
</Text>
|
||
<Text style={styles.detailValue}>
|
||
{subscriptionInfo.periodEndLabel}
|
||
</Text>
|
||
</View>
|
||
|
||
{subscriptionInfo.createdAtLabel ? (
|
||
<View style={styles.detailRow}>
|
||
<Text style={styles.detailLabel}>Abonné depuis</Text>
|
||
<Text style={styles.detailValue}>
|
||
{subscriptionInfo.createdAtLabel}
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
|
||
{subscriptionInfo.cancelAtPeriodEnd ? (
|
||
<Text style={styles.helperText}>
|
||
Ton abonnement restera actif jusqu'à cette date.
|
||
</Text>
|
||
) : null}
|
||
|
||
{subscriptionInfo.helperMessage ? (
|
||
<Text style={styles.helperText}>
|
||
{subscriptionInfo.helperMessage}
|
||
</Text>
|
||
) : null}
|
||
{subscriptionInfo.isAnnual &&
|
||
typeof subscriptionInfo.coinsPerMonth === "number" ? (
|
||
<View style={styles.helperInlineRow}>
|
||
<Text style={styles.helperText}>
|
||
Tes pièces sont versées chaque mois (
|
||
</Text>
|
||
<CreditAmount
|
||
value={subscriptionInfo.coinsPerMonth}
|
||
textStyle={styles.helperText}
|
||
iconSize={14}
|
||
/>
|
||
<Text style={styles.helperText}>)</Text>
|
||
</View>
|
||
) : null}
|
||
</View>
|
||
|
||
{successMessage ? (
|
||
<Text style={styles.successMessage}>{successMessage}</Text>
|
||
) : null}
|
||
{errorMessage ? (
|
||
<Text style={styles.errorMessage}>{errorMessage}</Text>
|
||
) : null}
|
||
{remoteError ? (
|
||
<Text style={styles.errorMessage}>{remoteError}</Text>
|
||
) : null}
|
||
|
||
<BorderGradientButton
|
||
title="Changer d'abonnement"
|
||
onPress={handleOpenPlans}
|
||
containerStyle={styles.buttonSpacing}
|
||
/>
|
||
|
||
<BorderGradientButton
|
||
title={cancelButtonTitle}
|
||
onPress={handleCancel}
|
||
disabled={
|
||
!subscriptionInfo.canCancel ||
|
||
isCancelling ||
|
||
subscriptionInfo.cancelAtPeriodEnd ||
|
||
isFetchingRemote
|
||
}
|
||
tint="dark"
|
||
containerStyle={styles.cancelButton}
|
||
titleStyle={{ color: cancelTitleColor }}
|
||
/>
|
||
</>
|
||
) : (
|
||
<View style={styles.card}>
|
||
<Text style={styles.cardTitle}>Aucun abonnement actif</Text>
|
||
<Text style={styles.infoText}>
|
||
Souscris à l’une de nos offres pour profiter des fonctionnalités
|
||
premium de Musicland.
|
||
</Text>
|
||
<BorderGradientButton
|
||
title="Découvrir les offres"
|
||
onPress={handleOpenPlans}
|
||
containerStyle={styles.emptyButton}
|
||
/>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</Page>
|
||
);
|
||
};
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
paddingHorizontal: gutters * 1.5,
|
||
paddingBottom: gutters * 1.5,
|
||
paddingTop: gutters * 1.2,
|
||
gap: gutters,
|
||
},
|
||
card: {
|
||
padding: gutters * 1.5,
|
||
borderRadius: 24,
|
||
backgroundColor: "rgba(255, 255, 255, 0.04)",
|
||
borderWidth: 1,
|
||
borderColor: "rgba(255, 255, 255, 0.08)",
|
||
gap: gutters * 0.75,
|
||
},
|
||
cardTitle: {
|
||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||
fontSize: 20,
|
||
color: Palette.white,
|
||
},
|
||
detailRow: {
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
gap: gutters,
|
||
},
|
||
detailLabel: {
|
||
fontFamily: FONT_FAMILY.InterRegular,
|
||
fontSize: 14,
|
||
color: Palette.grayMid,
|
||
flexShrink: 1,
|
||
},
|
||
detailValue: {
|
||
fontFamily: FONT_FAMILY.InterMedium,
|
||
fontSize: 15,
|
||
color: Palette.white,
|
||
flexShrink: 0,
|
||
textAlign: "right",
|
||
},
|
||
statusBadge: {
|
||
borderRadius: 999,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 4,
|
||
},
|
||
statusText: {
|
||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||
fontSize: 12,
|
||
letterSpacing: 0.3,
|
||
},
|
||
helperText: {
|
||
fontFamily: FONT_FAMILY.InterRegular,
|
||
fontSize: 13,
|
||
lineHeight: 18,
|
||
color: Palette.grayMid,
|
||
},
|
||
helperInlineRow: {
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
flexWrap: "wrap",
|
||
gap: 4,
|
||
},
|
||
successMessage: {
|
||
fontFamily: FONT_FAMILY.InterMedium,
|
||
fontSize: 14,
|
||
color: Palette.green,
|
||
},
|
||
errorMessage: {
|
||
fontFamily: FONT_FAMILY.InterMedium,
|
||
fontSize: 14,
|
||
color: Palette.red,
|
||
},
|
||
infoText: {
|
||
fontFamily: FONT_FAMILY.InterRegular,
|
||
fontSize: 15,
|
||
lineHeight: 22,
|
||
color: Palette.white,
|
||
},
|
||
buttonSpacing: {
|
||
marginTop: gutters * 0.5,
|
||
},
|
||
cancelButton: {
|
||
marginTop: gutters * 0.5,
|
||
},
|
||
emptyButton: {
|
||
marginTop: gutters * 1.5,
|
||
},
|
||
});
|
||
|
||
export default ManageSubscription;
|