continue subscriptions and coins

This commit is contained in:
Thomas Demirdjian
2025-11-05 15:39:15 +01:00
parent a58eeac079
commit 5f777f3b6d
25 changed files with 1994 additions and 222 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
exports.GEMINI_API_KEY = "AIzaSyBoPQC5ZaMKP73TlKGpZQp1mAw8ArHiH9Y"; exports.GEMINI_API_KEY = "AIzaSyBoPQC5ZaMKP73TlKGpZQp1mAw8ArHiH9Y";
exports.SUNO_API_KEY = "5a689606d3c59f58268b4963d5107361"; // api key exports.SUNO_API_KEY = "c1636e04f606811511e19ec6e1545aa6"; // api key
exports.RESEND_API_KEY = "re_"; exports.RESEND_API_KEY = "re_";
+7 -6
View File
@@ -1,5 +1,6 @@
const { onDocumentCreated } = require("firebase-functions/v2/firestore"); const { onDocumentCreated } = require("firebase-functions/v2/firestore");
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const logger = require("firebase-functions/logger"); const logger = require("firebase-functions/logger");
const { generateImageV2 } = require("../helpers/gemini"); const { generateImageV2 } = require("../helpers/gemini");
const { generatePicturePrompt } = require("../helpers/prompts"); const { generatePicturePrompt } = require("../helpers/prompts");
@@ -129,7 +130,7 @@ async function performCoverGeneration(project) {
options, options,
}, },
coverStatus: "GENERATED", coverStatus: "GENERATED",
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}, },
{ merge: true } { merge: true }
); );
@@ -178,14 +179,14 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
status: "CANCELLED", status: "CANCELLED",
error: error:
"La personnalisation de la pochette avec une photo n'est plus disponible.", "La personnalisation de la pochette avec une photo n'est plus disponible.",
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}); });
return; return;
} }
await refList.projects.doc(projectId).update({ await refList.projects.doc(projectId).update({
coverStatus: "GENERATING", coverStatus: "GENERATING",
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}); });
project = (await refList.projects.doc(projectId).get())?.data() || null; project = (await refList.projects.doc(projectId).get())?.data() || null;
@@ -212,12 +213,12 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
}); });
await refList.projects.doc(projectId).update({ await refList.projects.doc(projectId).update({
coverStatus: "GENERATED", coverStatus: "GENERATED",
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}); });
await event.data.ref.update({ await event.data.ref.update({
status: "CANCELLED", status: "CANCELLED",
error: "Cover already generated", error: "Cover already generated",
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}); });
return; return;
} }
@@ -231,7 +232,7 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
await event.data.ref.update({ await event.data.ref.update({
status: "DONE", status: "DONE",
coverUrl, coverUrl,
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}); });
logger.info("📦 [Task] Marked DONE", { logger.info("📦 [Task] Marked DONE", {
taskId: event?.params?.taskId, taskId: event?.params?.taskId,
+3 -1
View File
@@ -1,9 +1,11 @@
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const ORDER_TYPES = { const ORDER_TYPES = {
GIFT: "GIFT", GIFT: "GIFT",
SONG: "SONG", SONG: "SONG",
COINS: "COINS", COINS: "COINS",
SUBSCRIPTION: "SUBSCRIPTION",
}; };
const ORDER_STATUS = { const ORDER_STATUS = {
@@ -60,7 +62,7 @@ const createOrderDocument = async ({
type, type,
amount: normalizedAmount, amount: normalizedAmount,
songId: type === ORDER_TYPES.SONG ? songId || null : null, songId: type === ORDER_TYPES.SONG ? songId || null : null,
createdAt: admin.firestore.FieldValue.serverTimestamp(), createdAt: FieldValue.serverTimestamp(),
createdBy: createdBy || "system", createdBy: createdBy || "system",
status: ORDER_STATUS.PENDING, status: ORDER_STATUS.PENDING,
metadata: metadata || {}, metadata: metadata || {},
+2 -1
View File
@@ -8,6 +8,7 @@ const {
const { SUNO_API_KEY } = require("../config/keys"); const { SUNO_API_KEY } = require("../config/keys");
const axios = require("axios"); const axios = require("axios");
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const { refList } = require("../index"); const { refList } = require("../index");
const STRUCTURE_PROMPT_LABELS = { const STRUCTURE_PROMPT_LABELS = {
@@ -453,7 +454,7 @@ async function getSunoTimestamps(projectId) {
musicTimestamps: { musicTimestamps: {
[songIndex]: dataToReturn, [songIndex]: dataToReturn,
}, },
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}, },
{ merge: true }, { merge: true },
); );
+6 -5
View File
@@ -5,6 +5,7 @@ const {
} = require("firebase-functions/v2/https"); } = require("firebase-functions/v2/https");
const axios = require("axios"); const axios = require("axios");
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const { logger } = require("firebase-functions/logger"); const { logger } = require("firebase-functions/logger");
const { ALERT_TYPE, refList } = require("../index"); const { ALERT_TYPE, refList } = require("../index");
const { sendNotification } = require("./notifications"); const { sendNotification } = require("./notifications");
@@ -44,10 +45,10 @@ async function markProjectMusicFailure(projectId, error) {
await docRef.set( await docRef.set(
{ {
musicStatus: "FAILED", musicStatus: "FAILED",
sunoTaskId: admin.firestore.FieldValue.delete(), sunoTaskId: FieldValue.delete(),
generationStartAt: admin.firestore.FieldValue.delete(), generationStartAt: FieldValue.delete(),
musicError: errorPayload, musicError: errorPayload,
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}, },
{ merge: true }, { merge: true },
); );
@@ -677,8 +678,8 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
{ {
musicStatus: "GENERATED", musicStatus: "GENERATED",
musicUrls, musicUrls,
musicError: admin.firestore.FieldValue.delete(), musicError: FieldValue.delete(),
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}, },
{ merge: true }, { merge: true },
); );
+4 -3
View File
@@ -3,6 +3,7 @@ const {
onDocumentWritten, onDocumentWritten,
} = require("firebase-functions/v2/firestore"); } = require("firebase-functions/v2/firestore");
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const { refList, ALERT_TYPE } = require("../index"); const { refList, ALERT_TYPE } = require("../index");
const { Expo } = require("expo-server-sdk"); const { Expo } = require("expo-server-sdk");
const { Resend } = require("resend"); const { Resend } = require("resend");
@@ -209,11 +210,11 @@ async function removeInvalidTokens({
const userData = userSnap?.data() || {}; const userData = userSnap?.data() || {};
const updates = { const updates = {
pushTokens: admin.firestore.FieldValue.arrayRemove(...uniqueTokens), pushTokens: FieldValue.arrayRemove(...uniqueTokens),
}; };
if (uniqueTokens.includes(userData?.pushToken)) { if (uniqueTokens.includes(userData?.pushToken)) {
updates.pushToken = admin.firestore.FieldValue.delete(); updates.pushToken = FieldValue.delete();
} }
await docRef.set(updates, { merge: true }); await docRef.set(updates, { merge: true });
@@ -246,7 +247,7 @@ const sendNotification = async ({
receiverCollection, receiverCollection,
title, title,
message, message,
time: admin.firestore.FieldValue.serverTimestamp(), time: FieldValue.serverTimestamp(),
read: false, read: false,
readAt: null, readAt: null,
mailOnly, mailOnly,
+21 -13
View File
@@ -1,4 +1,5 @@
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const { onDocumentCreated } = require("firebase-functions/firestore"); const { onDocumentCreated } = require("firebase-functions/firestore");
const { HttpsError, onCall } = require("firebase-functions/https"); const { HttpsError, onCall } = require("firebase-functions/https");
@@ -35,7 +36,7 @@ const onOrderCreated = onDocumentCreated(
await orderRef.set( await orderRef.set(
{ {
status: ORDER_STATUS.REJECTED, status: ORDER_STATUS.REJECTED,
processedAt: admin.firestore.FieldValue.serverTimestamp(), processedAt: FieldValue.serverTimestamp(),
failureReason: "USER_NOT_FOUND", failureReason: "USER_NOT_FOUND",
}, },
{ merge: true }, { merge: true },
@@ -47,7 +48,7 @@ const onOrderCreated = onDocumentCreated(
await orderRef.set( await orderRef.set(
{ {
status: ORDER_STATUS.REJECTED, status: ORDER_STATUS.REJECTED,
processedAt: admin.firestore.FieldValue.serverTimestamp(), processedAt: FieldValue.serverTimestamp(),
failureReason: "INVALID_AMOUNT", failureReason: "INVALID_AMOUNT",
}, },
{ merge: true }, { merge: true },
@@ -76,7 +77,7 @@ const onOrderCreated = onDocumentCreated(
orderRef, orderRef,
{ {
status: ORDER_STATUS.REJECTED, status: ORDER_STATUS.REJECTED,
processedAt: admin.firestore.FieldValue.serverTimestamp(), processedAt: FieldValue.serverTimestamp(),
failureReason: "INSUFFICIENT_FUNDS", failureReason: "INSUFFICIENT_FUNDS",
balanceBefore: currentBalance, balanceBefore: currentBalance,
balanceAfter: currentBalance, balanceAfter: currentBalance,
@@ -89,15 +90,15 @@ const onOrderCreated = onDocumentCreated(
if (userSnapshot?.exists) { if (userSnapshot?.exists) {
transaction.update(userRef, { transaction.update(userRef, {
coins: nextBalance, coins: nextBalance,
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}); });
} else { } else {
transaction.set( transaction.set(
userRef, userRef,
{ {
coins: nextBalance, coins: nextBalance,
createdAt: admin.firestore.FieldValue.serverTimestamp(), createdAt: FieldValue.serverTimestamp(),
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}, },
{ merge: true }, { merge: true },
); );
@@ -107,7 +108,7 @@ const onOrderCreated = onDocumentCreated(
orderRef, orderRef,
{ {
status: ORDER_STATUS.APPLIED, status: ORDER_STATUS.APPLIED,
processedAt: admin.firestore.FieldValue.serverTimestamp(), processedAt: FieldValue.serverTimestamp(),
balanceBefore: currentBalance, balanceBefore: currentBalance,
balanceAfter: nextBalance, balanceAfter: nextBalance,
}, },
@@ -124,7 +125,7 @@ const onOrderCreated = onDocumentCreated(
await orderRef.set( await orderRef.set(
{ {
status: ORDER_STATUS.REJECTED, status: ORDER_STATUS.REJECTED,
processedAt: admin.firestore.FieldValue.serverTimestamp(), processedAt: FieldValue.serverTimestamp(),
failureReason: "PROCESSING_ERROR", failureReason: "PROCESSING_ERROR",
errorMessage: error?.message || String(error), errorMessage: error?.message || String(error),
}, },
@@ -168,17 +169,24 @@ const createSongOrder = onCall({ region: REGION }, async (request) => {
); );
} }
const metadata = {
source:
typeof data?.source === "string" && data.source.trim()
? data.source.trim()
: "music_generation",
};
if (typeof data?.requestId === "string" && data.requestId.trim()) {
metadata.requestId = data.requestId.trim();
}
const { orderId } = await createOrderDocument({ const { orderId } = await createOrderDocument({
userId: auth.uid, userId: auth.uid,
type: ORDER_TYPES.SONG, type: ORDER_TYPES.SONG,
amount, amount,
songId, songId,
createdBy: auth.uid, createdBy: auth.uid,
metadata: { metadata,
source: data?.source || "music_generation",
requestId:
typeof data?.requestId === "string" ? data.requestId : undefined,
},
}); });
return { orderId }; return { orderId };
+3 -2
View File
@@ -1,4 +1,5 @@
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const { onSchedule } = require("firebase-functions/v2/scheduler"); const { onSchedule } = require("firebase-functions/v2/scheduler");
const _ = require("lodash"); const _ = require("lodash");
const { refList, db, ALERT_TYPE } = require("../index"); const { refList, db, ALERT_TYPE } = require("../index");
@@ -123,7 +124,7 @@ exports.distributeMonthlyPayouts = onSchedule(
totalStreams: userData.totalStreams, totalStreams: userData.totalStreams,
projects: userData.projects, projects: userData.projects,
computedAt: now, computedAt: now,
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}, },
}; };
}); });
@@ -195,7 +196,7 @@ exports.distributeMonthlyPayouts = onSchedule(
totalAllocated: _.round(_.sumBy(payouts, "amount"), 2), totalAllocated: _.round(_.sumBy(payouts, "amount"), 2),
payouts, payouts,
status: payouts.length ? "computed" : "no-data", status: payouts.length ? "computed" : "no-data",
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
computedAt: now, computedAt: now,
}; };
+3 -2
View File
@@ -1,4 +1,5 @@
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const { onDocumentWritten } = require("firebase-functions/firestore"); const { onDocumentWritten } = require("firebase-functions/firestore");
const _ = require("lodash"); const _ = require("lodash");
const { refList, db } = require("../index"); const { refList, db } = require("../index");
@@ -76,7 +77,7 @@ exports.onProjectWritten = onDocumentWritten(
updatedAt: now, updatedAt: now,
lastStreamAt: now, lastStreamAt: now,
lastDelta: delta, lastDelta: delta,
streams: admin.firestore.FieldValue.increment(delta), streams: FieldValue.increment(delta),
}; };
const hasFirstStream = const hasFirstStream =
@@ -92,7 +93,7 @@ exports.onProjectWritten = onDocumentWritten(
updatedAt: now, updatedAt: now,
lastStreamAt: now, lastStreamAt: now,
lastDelta: delta, lastDelta: delta,
totalStreams: admin.firestore.FieldValue.increment(delta), totalStreams: FieldValue.increment(delta),
}; };
const totalsHasFirstStream = const totalsHasFirstStream =
totalsSnapshot.exists && totalsSnapshot.data()?.firstStreamAt; totalsSnapshot.exists && totalsSnapshot.data()?.firstStreamAt;
+3 -2
View File
@@ -1,4 +1,5 @@
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const { onSchedule } = require("firebase-functions/v2/scheduler"); const { onSchedule } = require("firebase-functions/v2/scheduler");
const { refList } = require("../index"); const { refList } = require("../index");
const firestore = refList.projects.firestore; const firestore = refList.projects.firestore;
@@ -71,11 +72,11 @@ exports.snapshotMonthlyTopSongs = onSchedule(
}, },
topProjects, topProjects,
totalProjects: topProjects.length, totalProjects: topProjects.length,
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}; };
if (!existingSnapshot.exists) { if (!existingSnapshot.exists) {
payload.createdAt = admin.firestore.FieldValue.serverTimestamp(); payload.createdAt = FieldValue.serverTimestamp();
} }
await docRef.set(payload, { merge: true }); await docRef.set(payload, { merge: true });
+3 -2
View File
@@ -27,8 +27,9 @@ const getServerTimestamp = () => {
if (FieldValue?.serverTimestamp) { if (FieldValue?.serverTimestamp) {
return FieldValue.serverTimestamp(); return FieldValue.serverTimestamp();
} }
if (admin.firestore?.FieldValue?.serverTimestamp) { const fallback = admin.firestore?.FieldValue;
return admin.firestore.FieldValue.serverTimestamp(); if (fallback?.serverTimestamp) {
return fallback.serverTimestamp();
} }
throw new HttpsError( throw new HttpsError(
"failed-precondition", "failed-precondition",
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,6 +1,7 @@
const { onObjectFinalized } = require("firebase-functions/v2/storage"); const { onObjectFinalized } = require("firebase-functions/v2/storage");
const logger = require("firebase-functions/logger"); const logger = require("firebase-functions/logger");
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const ffmpeg = require("fluent-ffmpeg"); const ffmpeg = require("fluent-ffmpeg");
const ffmpegInstaller = require("@ffmpeg-installer/ffmpeg"); const ffmpegInstaller = require("@ffmpeg-installer/ffmpeg");
const fs = require("node:fs/promises"); const fs = require("node:fs/promises");
@@ -150,7 +151,7 @@ exports.generateVideoThumbnail = onObjectFinalized(
await refList.projects.doc(projectId).set( await refList.projects.doc(projectId).set(
{ {
thumbnailUrl, thumbnailUrl,
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}, },
{ merge: true } { merge: true }
); );
+2 -1
View File
@@ -1,4 +1,5 @@
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const { const {
onDocumentDeleted, onDocumentDeleted,
onDocumentCreated, onDocumentCreated,
@@ -126,7 +127,7 @@ async function clearAllUserData(userID) {
const snapshot = await ref.where(arrayName, "array-contains", userID).get(); const snapshot = await ref.where(arrayName, "array-contains", userID).get();
snapshot.forEach((item) => snapshot.forEach((item) =>
item.ref.update({ item.ref.update({
[arrayName]: admin.firestore.FieldValue.arrayRemove(userID), [arrayName]: FieldValue.arrayRemove(userID),
}), }),
); );
}; };
+5 -4
View File
@@ -3,6 +3,7 @@ const { defineSecret } = require("firebase-functions/params");
const logger = require("firebase-functions/logger"); const logger = require("firebase-functions/logger");
const functions = require("firebase-functions"); const functions = require("firebase-functions");
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const axios = require("axios"); const axios = require("axios");
const fs = require("node:fs"); const fs = require("node:fs");
const fsp = require("node:fs/promises"); const fsp = require("node:fs/promises");
@@ -240,7 +241,7 @@ exports.publishPlaybackToYoutube = onCall(
youtubeStatus: "PUBLISHING", youtubeStatus: "PUBLISHING",
youtubePublished: false, youtubePublished: false,
youtubeError: null, youtubeError: null,
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}, },
{ merge: true } { merge: true }
); );
@@ -270,9 +271,9 @@ exports.publishPlaybackToYoutube = onCall(
youtubePublished: true, youtubePublished: true,
youtubeUrl: youtubeLink, youtubeUrl: youtubeLink,
youtubeVideoId: videoId, youtubeVideoId: videoId,
youtubePublishedAt: admin.firestore.FieldValue.serverTimestamp(), youtubePublishedAt: FieldValue.serverTimestamp(),
youtubeError: null, youtubeError: null,
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}, },
{ merge: true } { merge: true }
); );
@@ -302,7 +303,7 @@ exports.publishPlaybackToYoutube = onCall(
youtubeStatus: "FAILED", youtubeStatus: "FAILED",
youtubePublished: false, youtubePublished: false,
youtubeError: errorMessage, youtubeError: errorMessage,
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(),
}, },
{ merge: true } { merge: true }
); );
+5
View File
@@ -38,6 +38,7 @@ import Follows from "../screens/Profile/Follows";
import Language from "../screens/Profile/Language"; import Language from "../screens/Profile/Language";
import Notifications from "../screens/Profile/Notifications"; import Notifications from "../screens/Profile/Notifications";
import OrderHistory from "../screens/Profile/OrderHistory"; import OrderHistory from "../screens/Profile/OrderHistory";
import ManageSubscription from "../screens/Profile/ManageSubscription";
import Profile from "../screens/Profile/Profile"; import Profile from "../screens/Profile/Profile";
import Reels from "../screens/Profile/Reels"; import Reels from "../screens/Profile/Reels";
import Settings from "../screens/Profile/Settings"; import Settings from "../screens/Profile/Settings";
@@ -272,6 +273,10 @@ const baseScreens = [
name: Routes.Settings, name: Routes.Settings,
component: Settings, component: Settings,
}, },
{
name: Routes.ManageSubscription,
component: ManageSubscription,
},
{ {
name: Routes.OrderHistory, name: Routes.OrderHistory,
component: OrderHistory, component: OrderHistory,
+1
View File
@@ -77,6 +77,7 @@ export const Routes = {
EditProfile: "EditProfile", EditProfile: "EditProfile",
Settings: "Settings", Settings: "Settings",
OrderHistory: "OrderHistory", OrderHistory: "OrderHistory",
ManageSubscription: "ManageSubscription",
ChangeEmailAddress: "ChangeEmailAddress", ChangeEmailAddress: "ChangeEmailAddress",
ChangePassword: "ChangePassword", ChangePassword: "ChangePassword",
Notifications: "Notifications", Notifications: "Notifications",
+1 -1
View File
@@ -293,7 +293,7 @@ export default ({ navigation }) => {
<ItemRowList <ItemRowList
title={"Gérer mon abonnement"} title={"Gérer mon abonnement"}
action={() => {}} action={() => navigation.navigate(Routes.ManageSubscription)}
containerStyle={{}} containerStyle={{}}
/> />
+14
View File
@@ -78,6 +78,10 @@ function SubscriptionCard({ plan, selected, onSelect }) {
const planDescription = plan?.product?.description || ""; const planDescription = plan?.product?.description || "";
const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency); const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency);
const intervalLabel = getIntervalLabel(plan?.recurring); const intervalLabel = getIntervalLabel(plan?.recurring);
const coinsPerMonth =
typeof plan?.coinsPerMonth === "number" && Number.isFinite(plan.coinsPerMonth)
? Math.round(plan.coinsPerMonth)
: null;
const handleSelect = React.useCallback(() => { const handleSelect = React.useCallback(() => {
if (typeof onSelect === "function" && plan?.priceId) { if (typeof onSelect === "function" && plan?.priceId) {
onSelect(plan.priceId); onSelect(plan.priceId);
@@ -120,6 +124,11 @@ function SubscriptionCard({ plan, selected, onSelect }) {
<Text style={styles.period}>{intervalLabel}</Text> <Text style={styles.period}>{intervalLabel}</Text>
) : null} ) : null}
</View> </View>
{coinsPerMonth !== null ? (
<Text style={styles.coinsPerMonth}>
{`+${coinsPerMonth} crédits / mois`}
</Text>
) : null}
</View> </View>
) : null} ) : null}
@@ -707,6 +716,11 @@ const styles = StyleSheet.create({
alignItems: "baseline", alignItems: "baseline",
gap: 8, gap: 8,
}, },
coinsPerMonth: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 13,
color: Palette.primary,
},
priceValue: { priceValue: {
fontFamily: FONT_FAMILY.InterBold, fontFamily: FONT_FAMILY.InterBold,
fontSize: 22, fontSize: 22,
+699
View File
@@ -0,0 +1,699 @@
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 { 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";
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 formatCoinsAmount = (value) => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return null;
}
try {
return new Intl.NumberFormat("fr-FR", {
maximumFractionDigits: 0,
}).format(value);
} catch (_error) {
return `${Math.round(value)}`;
}
};
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 toDate = (value) => {
if (!value) {
return null;
}
if (typeof value.toDate === "function") {
try {
return value.toDate();
} catch (_error) {
return null;
}
}
if (value instanceof Date) {
return value;
}
if (typeof value === "number" && Number.isFinite(value)) {
if (value > 1e12) {
return new Date(value);
}
return new Date(value * 1000);
}
if (typeof value === "object" && Number.isFinite(value.seconds)) {
return new Date(value.seconds * 1000);
}
return null;
};
const formatDate = (date) => {
if (!date) {
return "À déterminer";
}
try {
return new Intl.DateTimeFormat("fr-FR", {
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
} catch (_error) {
return date.toString();
}
};
const capitalize = (value) => {
if (typeof value !== "string" || !value) {
return null;
}
return value.charAt(0).toUpperCase() + value.slice(1);
};
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 coinsPerMonthLabel =
normalizedCoins !== null
? `${formatCoinsAmount(normalizedCoins)} coins`
: null;
const nextGrantTimestamp =
currentUserData?.subscriptionNextGrantAt ||
currentUserData?.subscriptionGrantNextAt ||
null;
const nextGrantDate = toDate(nextGrantTimestamp);
const nextGrantLabel =
nextGrantDate && isAnnual ? formatDate(nextGrantDate) : null;
const helperMessages = [];
if (!hasActiveSubscription && hasAnySubscription) {
helperMessages.push(
"Ton abonnement n'est plus actif. Tu peux souscrire à nouveau à tout moment.",
);
}
if (isAnnual && coinsPerMonthLabel) {
helperMessages.push(
`Tes crédits sont versés chaque mois (${coinsPerMonthLabel}).`,
);
}
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,
coinsPerMonthLabel,
isAnnual,
nextGrantDate,
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 ||
error?.codeMessage ||
"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;
return (
<Page headerType="NAVIGATE" title="Mon abonnement" scrollEnabled>
<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>
<Text style={styles.detailValue}>
{subscriptionInfo.coinsPerMonthLabel || "—"}
</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}
</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 à lune 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,
},
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;
+74 -110
View File
@@ -18,12 +18,7 @@ const ORDER_TYPE_LABELS = {
GIFT: "Crédit offert", GIFT: "Crédit offert",
COINS: "Achat de coins", COINS: "Achat de coins",
SONG: "Génération de musique", SONG: "Génération de musique",
}; SUBSCRIPTION: "Abonnement",
const STATUS_LABELS = {
PENDING: "En cours",
APPLIED: "Confirmée",
REJECTED: "Refusée",
}; };
const formatCoins = (amount) => { const formatCoins = (amount) => {
@@ -39,26 +34,39 @@ const formatCoins = (amount) => {
} }
}; };
const formatDate = (date) => { const formatDateParts = (date) => {
if (!date) { if (!date) {
return "En attente de confirmation"; return {
dateLabel: "En attente de confirmation",
timeLabel: "",
};
} }
try { try {
return date.toLocaleString("fr-FR", { const dateLabel = date.toLocaleDateString("fr-FR", {
day: "2-digit", day: "2-digit",
month: "short", month: "short",
year: "numeric", year: "numeric",
});
const timeLabel = date.toLocaleTimeString("fr-FR", {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
}); });
return { dateLabel, timeLabel };
} catch (_error) { } catch (_error) {
return date.toString(); return {
dateLabel: date.toString(),
timeLabel: "",
};
} }
}; };
const mapOrderType = (type) => ORDER_TYPE_LABELS[type] || "Opération"; const mapOrderType = (type) => ORDER_TYPE_LABELS[type] || "Opération";
const mapStatus = (status) => STATUS_LABELS[status] || "Inconnue"; const shortenIdentifier = (value, visible = 6) => {
if (typeof value !== "string") return null;
if (value.length <= visible + 2) return value;
return `${value.slice(0, visible)}`;
};
const OrderHistory = () => { const OrderHistory = () => {
const { currentUID } = useUser() || {}; const { currentUID } = useUser() || {};
@@ -126,64 +134,64 @@ const OrderHistory = () => {
: 0; : 0;
const isPositive = amountValue > 0; const isPositive = amountValue > 0;
const amountLabel = `${isPositive ? "+" : ""}${formatCoins(amountValue)} ${ const amountLabel = `${isPositive ? "+" : ""}${formatCoins(amountValue)} ${
amountValue === 1 ? "coin" : "coins" Math.abs(amountValue) === 1 ? "coin" : "coins"
}`; }`;
const status = item.status || "PENDING"; const { dateLabel } = formatDateParts(item.createdAt);
const details = (() => { let reasonLabel = mapOrderType(item.type);
if (item.type === "GIFT" && item.metadata?.reason) {
switch (item.metadata.reason) { if (item.type === "GIFT") {
case "WELCOME_BONUS": const reason = item.metadata?.reason;
return "Crédit de bienvenue"; if (reason === "WELCOME_BONUS") {
default: reasonLabel = "Crédit de bienvenue";
return item.metadata.reason; } else if (typeof reason === "string" && reason.trim()) {
} reasonLabel = reason.trim();
} }
if (item.type === "SONG" && item.songId) { } else if (item.type === "SONG") {
return `Musique ${item.songId}`; reasonLabel = "Génération de musique";
} else if (item.type === "COINS") {
if (item.metadata?.coinPackKey) {
const shortenedPack = shortenIdentifier(
item.metadata.coinPackKey,
10,
);
reasonLabel = `Pack ${shortenedPack || item.metadata.coinPackKey}`;
} else if (
typeof item.metadata?.source === "string" &&
item.metadata.source.trim()
) {
const source = item.metadata.source.trim();
reasonLabel =
source === "STRIPE_CHECKOUT" ? "Recharge Stripe" : source;
} else {
reasonLabel = "Rechargement de coins";
} }
if (item.type === "COINS" && item.metadata?.paymentId) { } else if (item.type === "SUBSCRIPTION") {
return `Paiement ${item.metadata.paymentId}`; const rawPeriod =
} typeof item.metadata?.billingPeriod === "string"
return null; ? item.metadata.billingPeriod.toLowerCase()
})(); : null;
reasonLabel =
rawPeriod === "annual"
? "Abonnement annuel"
: rawPeriod === "monthly"
? "Abonnement mensuel"
: "Abonnement";
}
return ( return (
<View style={styles.orderCard}> <View style={styles.orderCard}>
<View style={styles.orderHeader}> <Text
<Text style={styles.orderTitle}>{mapOrderType(item.type)}</Text> style={[
<Text styles.orderAmount,
style={[ isPositive ? styles.amountPositive : styles.amountNegative,
styles.orderAmount, ]}
isPositive ? styles.amountPositive : styles.amountNegative, >
]} {amountLabel}
> </Text>
{amountLabel} <Text style={styles.orderReason}>{reasonLabel}</Text>
</Text> <Text style={styles.orderTimestamp}>{dateLabel}</Text>
</View>
<View style={styles.orderMetaRow}>
<Text style={styles.orderDate}>{formatDate(item.createdAt)}</Text>
<View
style={[
styles.statusBadge,
status === "APPLIED"
? styles.statusApplied
: status === "REJECTED"
? styles.statusRejected
: styles.statusPending,
]}
>
<Text style={styles.statusText}>{mapStatus(status)}</Text>
</View>
</View>
{details ? <Text style={styles.orderDetails}>{details}</Text> : null}
{typeof item.balanceAfter === "number" &&
Number.isFinite(item.balanceAfter) ? (
<Text style={styles.orderBalance}>
Solde après opération: {formatCoins(item.balanceAfter)} coins
</Text>
) : null}
</View> </View>
); );
}, []); }, []);
@@ -268,19 +276,7 @@ const styles = StyleSheet.create({
backgroundColor: Palette.ultraLightWhite, backgroundColor: Palette.ultraLightWhite,
borderWidth: 1, borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.08)", borderColor: "rgba(255, 255, 255, 0.08)",
}, gap: 6,
orderHeader: {
flexDirection: "row",
alignItems: "flex-start",
justifyContent: "space-between",
marginBottom: 8,
gap: 12,
},
orderTitle: {
flex: 1,
fontSize: 16,
fontFamily: FONT_FAMILY.InterSemiBold,
color: Palette.white,
}, },
orderAmount: { orderAmount: {
fontSize: 16, fontSize: 16,
@@ -292,47 +288,15 @@ const styles = StyleSheet.create({
amountNegative: { amountNegative: {
color: Palette.red, color: Palette.red,
}, },
orderMetaRow: { orderReason: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 6,
gap: 12,
},
orderDate: {
fontSize: 13,
color: Palette.grayMid,
fontFamily: FONT_FAMILY.InterRegular,
},
statusBadge: {
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 999,
},
statusApplied: {
backgroundColor: Palette.transparentGreen,
},
statusPending: {
backgroundColor: Palette.transparentOrange,
},
statusRejected: {
backgroundColor: Palette.transparentRed,
},
statusText: {
fontSize: 12,
fontFamily: FONT_FAMILY.InterMedium,
color: Palette.white,
},
orderDetails: {
marginBottom: 6,
fontSize: 14, fontSize: 14,
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
color: Palette.white, color: Palette.white,
}, },
orderBalance: { orderTimestamp: {
fontSize: 13, fontSize: 12,
fontFamily: FONT_FAMILY.InterRegular,
color: Palette.grayMid, color: Palette.grayMid,
fontFamily: FONT_FAMILY.InterMedium,
}, },
emptyText: { emptyText: {
textAlign: "center", textAlign: "center",
-14
View File
@@ -18,7 +18,6 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn"; import { useGlobal } from "reactn";
import { background, icons } from "../../assets"; import { background, icons } from "../../assets";
import BorderGradient from "../../components/BorderGradient/BorderGradient"; import BorderGradient from "../../components/BorderGradient/BorderGradient";
import GradientButton from "../../components/GradientButton";
import MoreMenu from "../../components/MoreMenu"; import MoreMenu from "../../components/MoreMenu";
import PressableScale from "../../components/PressableScale"; import PressableScale from "../../components/PressableScale";
import ProfilePicture from "../../components/ProfilePicture"; import ProfilePicture from "../../components/ProfilePicture";
@@ -473,19 +472,6 @@ const Profile = () => {
<Text style={styles.label}>abonnements</Text> <Text style={styles.label}>abonnements</Text>
</Pressable> </Pressable>
</View> </View>
{isSelf ? (
<GradientButton
title="Découvrir les abonnements"
size="large"
onPress={() => push(Routes.Payments)}
containerStyle={{
marginTop: 18,
width: "80%",
alignSelf: "center",
}}
gradientStyle={{ width: "100%" }}
/>
) : null}
{!isSelf && ( {!isSelf && (
<Pressable onPress={handleFollowUser}> <Pressable onPress={handleFollowUser}>
<View <View
+3 -7
View File
@@ -43,15 +43,11 @@ const SETTINGS = [
navigate(Routes.OrderHistory); navigate(Routes.OrderHistory);
}, },
}, },
{
title: "Découvrir les packs",
action: () => {
navigate(Routes.Payments, { pack: "premium" });
},
},
{ {
title: "Gérer mon abonnement", title: "Gérer mon abonnement",
action: () => {}, action: () => {
navigate(Routes.ManageSubscription);
},
}, },
]; ];
+2 -1
View File
@@ -24,6 +24,7 @@ import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
const { width } = Dimensions.get("window"); const { width } = Dimensions.get("window");
const MUSIC_GENERATION_COIN_COST = 8; const MUSIC_GENERATION_COIN_COST = 8;
const CONFIRM_MODAL_MAX_WIDTH = 540; const CONFIRM_MODAL_MAX_WIDTH = 540;
const FUNCTIONS_REGION = "europe-west1";
const ComposeSong = () => { const ComposeSong = () => {
const scrollRef = useRef(null); const scrollRef = useRef(null);
@@ -144,7 +145,7 @@ const ComposeSong = () => {
} }
try { try {
const functionsClient = getFunctionsClient(); const functionsClient = getFunctionsClient(FUNCTIONS_REGION);
const createSongOrder = const createSongOrder =
functionsClient.httpsCallable("orders-createSongOrder"); functionsClient.httpsCallable("orders-createSongOrder");
+2 -1
View File
@@ -29,6 +29,7 @@ import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
const { width: windowWidth } = Dimensions.get("window"); const { width: windowWidth } = Dimensions.get("window");
const MUSIC_GENERATION_COIN_COST = 8; const MUSIC_GENERATION_COIN_COST = 8;
const CONFIRM_MODAL_MAX_WIDTH = 540; const CONFIRM_MODAL_MAX_WIDTH = 540;
const FUNCTIONS_REGION = "europe-west1";
const ComposeSong = () => { const ComposeSong = () => {
const scrollRef = useRef(null); const scrollRef = useRef(null);
@@ -199,7 +200,7 @@ const ComposeSong = () => {
} }
try { try {
const functionsClient = getFunctionsClient(); const functionsClient = getFunctionsClient(FUNCTIONS_REGION);
const createSongOrder = const createSongOrder =
functionsClient.httpsCallable("orders-createSongOrder"); functionsClient.httpsCallable("orders-createSongOrder");