continue subscriptions and coins
This commit is contained in:
@@ -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_";
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 || {},
|
||||||
|
|||||||
@@ -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 },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
+1128
-44
File diff suppressed because it is too large
Load Diff
@@ -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 }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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={{}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 à 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,
|
||||||
|
},
|
||||||
|
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;
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user