add orders collecrtion for coins and subscriptions page
This commit is contained in:
@@ -50,3 +50,4 @@ exports.rankings = require("./src/rankings");
|
|||||||
exports.payouts = require("./src/payouts");
|
exports.payouts = require("./src/payouts");
|
||||||
exports.subscription = require("./src/subscription");
|
exports.subscription = require("./src/subscription");
|
||||||
exports.youtube = require("./src/youtube");
|
exports.youtube = require("./src/youtube");
|
||||||
|
exports.orders = require("./src/orders");
|
||||||
|
|||||||
Generated
+416
-12
@@ -68,6 +68,16 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
||||||
@@ -137,6 +147,34 @@
|
|||||||
"integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
|
"integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@ffmpeg-installer/darwin-arm64": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "https://git.ffmpeg.org/gitweb/ffmpeg.git/blob_plain/HEAD:/LICENSE.md",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@ffmpeg-installer/darwin-x64": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "LGPL-2.1",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@ffmpeg-installer/ffmpeg": {
|
"node_modules/@ffmpeg-installer/ffmpeg": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz",
|
||||||
@@ -153,6 +191,48 @@
|
|||||||
"@ffmpeg-installer/win32-x64": "4.1.0"
|
"@ffmpeg-installer/win32-x64": "4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ffmpeg-installer/linux-arm": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "GPLv3",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@ffmpeg-installer/linux-arm64": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "GPLv3",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@ffmpeg-installer/linux-ia32": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "GPLv3",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@ffmpeg-installer/linux-x64": {
|
"node_modules/@ffmpeg-installer/linux-x64": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz",
|
||||||
@@ -167,6 +247,32 @@
|
|||||||
"linux"
|
"linux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@ffmpeg-installer/win32-ia32": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "GPLv3",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@ffmpeg-installer/win32-x64": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "GPLv3",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@firebase/app-check-interop-types": {
|
"node_modules/@firebase/app-check-interop-types": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz",
|
||||||
@@ -433,6 +539,8 @@
|
|||||||
"version": "7.11.6",
|
"version": "7.11.6",
|
||||||
"resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz",
|
"resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz",
|
||||||
"integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==",
|
"integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/api": "^1.3.0",
|
"@opentelemetry/api": "^1.3.0",
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
@@ -622,9 +730,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@google-cloud/storage": {
|
"node_modules/@google-cloud/storage": {
|
||||||
"version": "7.17.2",
|
"version": "7.17.3",
|
||||||
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.17.2.tgz",
|
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.17.3.tgz",
|
||||||
"integrity": "sha512-6xN0KNO8L/LIA5zu3CJwHkJiB6n65eykBLOb0E+RooiHYgX8CSao6lvQiKT9TBk2gL5g33LL3fmhDodZnt56rw==",
|
"integrity": "sha512-gOnCAbFgAYKRozywLsxagdevTF7Gm+2Ncz5u5CQAuOv/2VCa0rdGJWvJFDOftPx1tc+q8TXiC2pEJfFKu+yeMQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/paginator": "^5.0.0",
|
"@google-cloud/paginator": "^5.0.0",
|
||||||
@@ -743,6 +851,130 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
||||||
@@ -759,6 +991,22 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||||
@@ -775,6 +1023,72 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-linux-x64": {
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.33.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
||||||
@@ -797,6 +1111,28 @@
|
|||||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.33.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||||
@@ -819,6 +1155,63 @@
|
|||||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-wasm32": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/runtime": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-ia32": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@js-sdsl/ordered-map": {
|
"node_modules/@js-sdsl/ordered-map": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
||||||
@@ -872,6 +1265,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
@@ -967,6 +1361,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
|
||||||
"integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
|
"integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/semantic-conventions": "1.25.1"
|
"@opentelemetry/semantic-conventions": "1.25.1"
|
||||||
},
|
},
|
||||||
@@ -2154,6 +2549,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz",
|
||||||
"integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==",
|
"integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "1.25.1",
|
"@opentelemetry/core": "1.25.1",
|
||||||
"@opentelemetry/semantic-conventions": "1.25.1"
|
"@opentelemetry/semantic-conventions": "1.25.1"
|
||||||
@@ -2196,6 +2592,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz",
|
||||||
"integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==",
|
"integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "1.25.1",
|
"@opentelemetry/core": "1.25.1",
|
||||||
"@opentelemetry/resources": "1.25.1",
|
"@opentelemetry/resources": "1.25.1",
|
||||||
@@ -2249,6 +2646,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz",
|
||||||
"integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==",
|
"integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "1.25.1",
|
"@opentelemetry/core": "1.25.1",
|
||||||
"@opentelemetry/resources": "1.25.1",
|
"@opentelemetry/resources": "1.25.1",
|
||||||
@@ -2759,6 +3157,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2937,9 +3336,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.1",
|
"version": "1.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||||
"integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
@@ -3586,6 +3985,7 @@
|
|||||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@@ -4080,6 +4480,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz",
|
||||||
"integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==",
|
"integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/busboy": "^3.0.0",
|
"@fastify/busboy": "^3.0.0",
|
||||||
"@firebase/database-compat": "1.0.8",
|
"@firebase/database-compat": "1.0.8",
|
||||||
@@ -4367,6 +4768,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/genkit/-/genkit-1.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/genkit/-/genkit-1.22.0.tgz",
|
||||||
"integrity": "sha512-GoVVO3EnNHrjkMkUPRvgx1MjBHKvOlZAu/ffMIJgLFxrH7rrUbvfHXE6Nk7uh5BNvET7+DApyhbhqz9G8sy+mQ==",
|
"integrity": "sha512-GoVVO3EnNHrjkMkUPRvgx1MjBHKvOlZAu/ffMIJgLFxrH7rrUbvfHXE6Nk7uh5BNvET7+DApyhbhqz9G8sy+mQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@genkit-ai/ai": "1.22.0",
|
"@genkit-ai/ai": "1.22.0",
|
||||||
"@genkit-ai/core": "1.22.0",
|
"@genkit-ai/core": "1.22.0",
|
||||||
@@ -6047,9 +6449,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/resend": {
|
"node_modules/resend": {
|
||||||
"version": "6.4.0",
|
"version": "6.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/resend/-/resend-6.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/resend/-/resend-6.4.1.tgz",
|
||||||
"integrity": "sha512-CTr4ix4RI5M/ucL58Wqr+LE8eI4JHtJEFaBAx6yUVNOI3eaPVtJjpNL0G/BdRSWMbwv6CtpprVOY8Xvpp6UJlA==",
|
"integrity": "sha512-+P1gF6bYT0lKd0vsYyKKTHd6PfMH37yDgNRA6wAARQgQbYlc2BZYuo9sB/uZKkVg1oGSh0cgpnxNKGsZJutXMg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"svix": "1.76.1"
|
"svix": "1.76.1"
|
||||||
@@ -6546,9 +6948,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stripe": {
|
"node_modules/stripe": {
|
||||||
"version": "19.2.0",
|
"version": "19.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.1.tgz",
|
||||||
"integrity": "sha512-strzN8luMGMC1LEleGKg7pJGXFx0kSS4y/uSjK8yPQV9SUBMtJVAp/v8XMQLRnMbXaSaWLrIaHcMlKcsizdRDQ==",
|
"integrity": "sha512-eRc2T413316D7fAjSSEgPbJJ+4a8KY9rOTOb27aXd7bkw9ADO/3OxmIk7YWDhWvHgvxnEZ/29YjcmBBOu4mhrw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"qs": "^6.11.0"
|
"qs": "^6.11.0"
|
||||||
@@ -6962,6 +7364,7 @@
|
|||||||
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
|
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@colors/colors": "^1.6.0",
|
"@colors/colors": "^1.6.0",
|
||||||
"@dabh/diagnostics": "^2.0.8",
|
"@dabh/diagnostics": "^2.0.8",
|
||||||
@@ -7126,6 +7529,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
const admin = require("firebase-admin");
|
||||||
|
|
||||||
|
const ORDER_TYPES = {
|
||||||
|
GIFT: "GIFT",
|
||||||
|
SONG: "SONG",
|
||||||
|
COINS: "COINS",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ORDER_STATUS = {
|
||||||
|
PENDING: "PENDING",
|
||||||
|
APPLIED: "APPLIED",
|
||||||
|
REJECTED: "REJECTED",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ORDERS_COLLECTION = "orders";
|
||||||
|
|
||||||
|
const isFiniteNumber = (value) => {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAmount = (amount) => {
|
||||||
|
if (!isFiniteNumber(amount)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Number(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createOrderDocument = async ({
|
||||||
|
userId,
|
||||||
|
type,
|
||||||
|
amount,
|
||||||
|
songId = null,
|
||||||
|
createdBy = "system",
|
||||||
|
metadata = {},
|
||||||
|
orderId = null,
|
||||||
|
}) => {
|
||||||
|
if (!userId || typeof userId !== "string") {
|
||||||
|
throw new Error("[orders] Missing userId when creating order");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.values(ORDER_TYPES).includes(type)) {
|
||||||
|
throw new Error(`[orders] Invalid order type "${type}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedAmount = normalizeAmount(amount);
|
||||||
|
|
||||||
|
if (normalizedAmount === null || normalizedAmount === 0) {
|
||||||
|
throw new Error("[orders] Invalid order amount");
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
userId,
|
||||||
|
type,
|
||||||
|
amount: normalizedAmount,
|
||||||
|
songId: type === ORDER_TYPES.SONG ? songId || null : null,
|
||||||
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
createdBy: createdBy || "system",
|
||||||
|
status: ORDER_STATUS.PENDING,
|
||||||
|
metadata: metadata || {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectionRef = admin.firestore().collection(ORDERS_COLLECTION);
|
||||||
|
const orderRef = orderId ? collectionRef.doc(orderId) : collectionRef.doc();
|
||||||
|
|
||||||
|
if (orderId) {
|
||||||
|
const existingSnapshot = await orderRef.get();
|
||||||
|
if (existingSnapshot.exists) {
|
||||||
|
return { orderRef, orderId: orderRef.id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await orderRef.set(payload);
|
||||||
|
|
||||||
|
return { orderRef, orderId: orderRef.id };
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ORDER_TYPES,
|
||||||
|
ORDER_STATUS,
|
||||||
|
ORDERS_COLLECTION,
|
||||||
|
createOrderDocument,
|
||||||
|
normalizeAmount,
|
||||||
|
};
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
const admin = require("firebase-admin");
|
||||||
|
const { onDocumentCreated } = require("firebase-functions/firestore");
|
||||||
|
const { HttpsError, onCall } = require("firebase-functions/https");
|
||||||
|
|
||||||
|
const { REGION } = require("../index");
|
||||||
|
const {
|
||||||
|
ORDER_TYPES,
|
||||||
|
ORDER_STATUS,
|
||||||
|
ORDERS_COLLECTION,
|
||||||
|
createOrderDocument,
|
||||||
|
normalizeAmount,
|
||||||
|
} = require("./helpers/orders");
|
||||||
|
|
||||||
|
const USERS_COLLECTION = "users";
|
||||||
|
|
||||||
|
const onOrderCreated = onDocumentCreated(
|
||||||
|
`${ORDERS_COLLECTION}/{orderId}`,
|
||||||
|
async (event) => {
|
||||||
|
const orderRef = event?.data?.ref;
|
||||||
|
const orderData = event?.data?.data();
|
||||||
|
|
||||||
|
if (!orderRef || !orderData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderData?.processedAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId =
|
||||||
|
typeof orderData.userId === "string" ? orderData.userId.trim() : "";
|
||||||
|
const amount = normalizeAmount(orderData.amount);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
await orderRef.set(
|
||||||
|
{
|
||||||
|
status: ORDER_STATUS.REJECTED,
|
||||||
|
processedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
failureReason: "USER_NOT_FOUND",
|
||||||
|
},
|
||||||
|
{ merge: true },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount === null || amount === 0) {
|
||||||
|
await orderRef.set(
|
||||||
|
{
|
||||||
|
status: ORDER_STATUS.REJECTED,
|
||||||
|
processedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
failureReason: "INVALID_AMOUNT",
|
||||||
|
},
|
||||||
|
{ merge: true },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRef = admin.firestore().collection(USERS_COLLECTION).doc(userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await admin.firestore().runTransaction(async (transaction) => {
|
||||||
|
const userSnapshot = await transaction.get(userRef);
|
||||||
|
const userData = userSnapshot?.data() || {};
|
||||||
|
const currentBalanceValue = normalizeAmount(userData?.coins);
|
||||||
|
const currentBalance =
|
||||||
|
currentBalanceValue !== null ? currentBalanceValue : 0;
|
||||||
|
|
||||||
|
const nextBalance = currentBalance + amount;
|
||||||
|
|
||||||
|
if (
|
||||||
|
amount < 0 &&
|
||||||
|
nextBalance < 0 &&
|
||||||
|
orderData?.type === ORDER_TYPES.SONG
|
||||||
|
) {
|
||||||
|
transaction.set(
|
||||||
|
orderRef,
|
||||||
|
{
|
||||||
|
status: ORDER_STATUS.REJECTED,
|
||||||
|
processedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
failureReason: "INSUFFICIENT_FUNDS",
|
||||||
|
balanceBefore: currentBalance,
|
||||||
|
balanceAfter: currentBalance,
|
||||||
|
},
|
||||||
|
{ merge: true },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userSnapshot?.exists) {
|
||||||
|
transaction.update(userRef, {
|
||||||
|
coins: nextBalance,
|
||||||
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
transaction.set(
|
||||||
|
userRef,
|
||||||
|
{
|
||||||
|
coins: nextBalance,
|
||||||
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
},
|
||||||
|
{ merge: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.set(
|
||||||
|
orderRef,
|
||||||
|
{
|
||||||
|
status: ORDER_STATUS.APPLIED,
|
||||||
|
processedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
balanceBefore: currentBalance,
|
||||||
|
balanceAfter: nextBalance,
|
||||||
|
},
|
||||||
|
{ merge: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[orders-onOrderCreated] Failed to process order",
|
||||||
|
orderRef.id,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
|
await orderRef.set(
|
||||||
|
{
|
||||||
|
status: ORDER_STATUS.REJECTED,
|
||||||
|
processedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
failureReason: "PROCESSING_ERROR",
|
||||||
|
errorMessage: error?.message || String(error),
|
||||||
|
},
|
||||||
|
{ merge: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const createSongOrder = onCall({ region: REGION }, async (request) => {
|
||||||
|
const { auth, data } = request || {};
|
||||||
|
|
||||||
|
if (!auth?.uid) {
|
||||||
|
throw new HttpsError("unauthenticated", "Authentification requise.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = normalizeAmount(data?.amount);
|
||||||
|
|
||||||
|
if (amount === null || amount >= 0) {
|
||||||
|
throw new HttpsError(
|
||||||
|
"invalid-argument",
|
||||||
|
"Le montant doit être négatif pour un achat de musique.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const songId =
|
||||||
|
typeof data?.songId === "string" && data.songId.trim()
|
||||||
|
? data.songId.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const userRef = admin.firestore().collection(USERS_COLLECTION).doc(auth.uid);
|
||||||
|
const userSnapshot = await userRef.get();
|
||||||
|
const currentCoinsValue = normalizeAmount(userSnapshot?.data()?.coins);
|
||||||
|
const currentCoins =
|
||||||
|
currentCoinsValue !== null ? currentCoinsValue : 0;
|
||||||
|
|
||||||
|
if (currentCoins + amount < 0) {
|
||||||
|
throw new HttpsError(
|
||||||
|
"failed-precondition",
|
||||||
|
"Crédits insuffisants pour finaliser l'opération.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orderId } = await createOrderDocument({
|
||||||
|
userId: auth.uid,
|
||||||
|
type: ORDER_TYPES.SONG,
|
||||||
|
amount,
|
||||||
|
songId,
|
||||||
|
createdBy: auth.uid,
|
||||||
|
metadata: {
|
||||||
|
source: data?.source || "music_generation",
|
||||||
|
requestId:
|
||||||
|
typeof data?.requestId === "string" ? data.requestId : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { orderId };
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
onOrderCreated,
|
||||||
|
createSongOrder,
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ const { onRequest } = require("firebase-functions/v2/https");
|
|||||||
const { FieldValue } = require("firebase-admin/firestore");
|
const { FieldValue } = require("firebase-admin/firestore");
|
||||||
const { refList } = require("../index");
|
const { refList } = require("../index");
|
||||||
const refsList = refList;
|
const refsList = refList;
|
||||||
|
const { ORDER_TYPES, createOrderDocument } = require("./helpers/orders");
|
||||||
const {
|
const {
|
||||||
getStripeClient,
|
getStripeClient,
|
||||||
buildCheckoutLineItems,
|
buildCheckoutLineItems,
|
||||||
@@ -76,9 +77,7 @@ const COIN_PACK_PRODUCTS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const COIN_PACK_PRODUCT_IDS = COIN_PACK_PRODUCTS.map(
|
const COIN_PACK_PRODUCT_IDS = COIN_PACK_PRODUCTS.map((pack) => pack.productId);
|
||||||
(pack) => pack.productId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const COIN_PACK_PRODUCT_MAP = COIN_PACK_PRODUCTS.reduce(
|
const COIN_PACK_PRODUCT_MAP = COIN_PACK_PRODUCTS.reduce(
|
||||||
(acc, pack) => ({
|
(acc, pack) => ({
|
||||||
@@ -324,7 +323,7 @@ const handleCheckoutSessionCompleted = async (session, event) => {
|
|||||||
lastEventAt: getServerTimestamp(),
|
lastEventAt: getServerTimestamp(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { userRef } = await resolveUserContext({
|
const { uid, userRef } = await resolveUserContext({
|
||||||
metadata: session.metadata,
|
metadata: session.metadata,
|
||||||
customerId: session.customer,
|
customerId: session.customer,
|
||||||
});
|
});
|
||||||
@@ -378,17 +377,23 @@ const handleCheckoutSessionCompleted = async (session, event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alreadyGranted = Boolean(
|
const alreadyGranted = Boolean(
|
||||||
paymentSnapshot?.exists &&
|
paymentSnapshot?.exists && paymentSnapshot.data()?.coinPackGrantedAt,
|
||||||
paymentSnapshot.data()?.coinPackGrantedAt,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!alreadyGranted) {
|
if (!alreadyGranted) {
|
||||||
await userRef.set(
|
const targetUserId = uid || firebaseUid || userRef.id;
|
||||||
{
|
|
||||||
coins: FieldValue.increment(coinAmount),
|
await createOrderDocument({
|
||||||
|
userId: targetUserId,
|
||||||
|
type: ORDER_TYPES.COINS,
|
||||||
|
amount: coinAmount,
|
||||||
|
metadata: {
|
||||||
|
source: "STRIPE_CHECKOUT",
|
||||||
|
paymentId: session.id || null,
|
||||||
|
coinPackKey: session.metadata?.coinPackKey || null,
|
||||||
},
|
},
|
||||||
{ merge: true },
|
orderId: `stripe_${session.id}`,
|
||||||
);
|
});
|
||||||
|
|
||||||
await paymentDocRef.set(
|
await paymentDocRef.set(
|
||||||
{
|
{
|
||||||
@@ -409,9 +414,26 @@ const handleCustomerSubscriptionEvent = async (subscription, event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subscriptionPayload = buildSubscriptionPayload(subscription);
|
const subscriptionPayload = buildSubscriptionPayload(subscription);
|
||||||
|
if (!subscriptionPayload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { uid, userRef } = await resolveUserContext({
|
||||||
|
metadata: subscription.metadata,
|
||||||
|
customerId: subscription.customer,
|
||||||
|
});
|
||||||
|
|
||||||
|
let resolvedCustomerId = subscriptionPayload?.customerId || null;
|
||||||
|
if (!resolvedCustomerId && subscription.customer) {
|
||||||
|
resolvedCustomerId = subscription.customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackUid = extractFirebaseUid(subscription.metadata);
|
||||||
|
const resolvedUid = uid || fallbackUid || null;
|
||||||
|
|
||||||
await upsertPaymentDocument(subscription.id, {
|
await upsertPaymentDocument(subscription.id, {
|
||||||
customerId: subscription.customer || null,
|
userId: resolvedUid,
|
||||||
|
customerId: resolvedCustomerId,
|
||||||
subscriptionId: subscription.id || null,
|
subscriptionId: subscription.id || null,
|
||||||
status: subscription.status || null,
|
status: subscription.status || null,
|
||||||
mode: "subscription",
|
mode: "subscription",
|
||||||
@@ -420,17 +442,15 @@ const handleCustomerSubscriptionEvent = async (subscription, event) => {
|
|||||||
cancelAtPeriodEnd: subscriptionPayload?.cancelAtPeriodEnd ?? null,
|
cancelAtPeriodEnd: subscriptionPayload?.cancelAtPeriodEnd ?? null,
|
||||||
currentPeriodStart: subscriptionPayload?.currentPeriodStart || null,
|
currentPeriodStart: subscriptionPayload?.currentPeriodStart || null,
|
||||||
currentPeriodEnd: subscriptionPayload?.currentPeriodEnd || null,
|
currentPeriodEnd: subscriptionPayload?.currentPeriodEnd || null,
|
||||||
metadata: subscription.metadata || {},
|
metadata: {
|
||||||
|
...subscription.metadata,
|
||||||
|
stripeEventType: event?.type || null,
|
||||||
|
},
|
||||||
lastEventType: event?.type || null,
|
lastEventType: event?.type || null,
|
||||||
lastEventId: event?.id || null,
|
lastEventId: event?.id || null,
|
||||||
lastEventAt: getServerTimestamp(),
|
lastEventAt: getServerTimestamp(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { uid, userRef } = await resolveUserContext({
|
|
||||||
metadata: subscription.metadata,
|
|
||||||
customerId: subscription.customer,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userRef) {
|
if (!userRef) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[subscription-handleCustomerSubscriptionEvent] User not resolved",
|
"[subscription-handleCustomerSubscriptionEvent] User not resolved",
|
||||||
@@ -441,13 +461,11 @@ const handleCustomerSubscriptionEvent = async (subscription, event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lastEvent = buildEventSnapshot(event?.type, subscription.id);
|
const lastEvent = buildEventSnapshot(event?.type, subscription.id);
|
||||||
if (uid) {
|
if (resolvedUid) {
|
||||||
lastEvent.uid = uid;
|
lastEvent.uid = resolvedUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceMeta = getSubscriptionMetaFromPrice(
|
const priceMeta = getSubscriptionMetaFromPrice(subscriptionPayload?.priceId);
|
||||||
subscriptionPayload?.priceId,
|
|
||||||
);
|
|
||||||
const metadataLevel = subscription.metadata?.subscriptionLevel || null;
|
const metadataLevel = subscription.metadata?.subscriptionLevel || null;
|
||||||
const metadataPeriod =
|
const metadataPeriod =
|
||||||
subscription.metadata?.subscriptionBillingPeriod || null;
|
subscription.metadata?.subscriptionBillingPeriod || null;
|
||||||
@@ -468,6 +486,8 @@ const handleCustomerSubscriptionEvent = async (subscription, event) => {
|
|||||||
|
|
||||||
if (subscriptionPayload?.customerId) {
|
if (subscriptionPayload?.customerId) {
|
||||||
userUpdate.stripeCustomerId = subscriptionPayload.customerId;
|
userUpdate.stripeCustomerId = subscriptionPayload.customerId;
|
||||||
|
} else if (resolvedCustomerId) {
|
||||||
|
userUpdate.stripeCustomerId = resolvedCustomerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedLevel && isPremium) {
|
if (resolvedLevel && isPremium) {
|
||||||
@@ -733,9 +753,7 @@ const formatCoinPack = ({ product, price, fallback = {} }) => {
|
|||||||
|
|
||||||
const priceId =
|
const priceId =
|
||||||
(resolvedPrice && resolvedPrice.id) ||
|
(resolvedPrice && resolvedPrice.id) ||
|
||||||
(typeof product.default_price === "string"
|
(typeof product.default_price === "string" ? product.default_price : null);
|
||||||
? product.default_price
|
|
||||||
: null);
|
|
||||||
|
|
||||||
const rawCoinAmount =
|
const rawCoinAmount =
|
||||||
product?.metadata?.coins ??
|
product?.metadata?.coins ??
|
||||||
@@ -753,8 +771,7 @@ const formatCoinPack = ({ product, price, fallback = {} }) => {
|
|||||||
priceId,
|
priceId,
|
||||||
name: product.name || "",
|
name: product.name || "",
|
||||||
description: product.description || "",
|
description: product.description || "",
|
||||||
coinAmount:
|
coinAmount: Number.isFinite(coinAmount) && coinAmount > 0 ? coinAmount : 0,
|
||||||
Number.isFinite(coinAmount) && coinAmount > 0 ? coinAmount : 0,
|
|
||||||
currency:
|
currency:
|
||||||
resolvedPrice?.currency ||
|
resolvedPrice?.currency ||
|
||||||
(typeof resolvedPrice?.currency === "string"
|
(typeof resolvedPrice?.currency === "string"
|
||||||
@@ -824,9 +841,7 @@ const listCoinPacks = onCall({ region: REGION }, async () => {
|
|||||||
|
|
||||||
let resolvedPrice = null;
|
let resolvedPrice = null;
|
||||||
if (typeof product?.default_price === "string") {
|
if (typeof product?.default_price === "string") {
|
||||||
resolvedPrice = await stripe.prices.retrieve(
|
resolvedPrice = await stripe.prices.retrieve(product.default_price);
|
||||||
product.default_price,
|
|
||||||
);
|
|
||||||
} else if (
|
} else if (
|
||||||
product?.default_price &&
|
product?.default_price &&
|
||||||
typeof product.default_price === "object"
|
typeof product.default_price === "object"
|
||||||
|
|||||||
+10
-7
@@ -4,6 +4,7 @@ const {
|
|||||||
onDocumentCreated,
|
onDocumentCreated,
|
||||||
} = require("firebase-functions/firestore");
|
} = require("firebase-functions/firestore");
|
||||||
const { refList } = require("../index");
|
const { refList } = require("../index");
|
||||||
|
const { ORDER_TYPES, createOrderDocument } = require("./helpers/orders");
|
||||||
const { deleteFolder } = require("../helpers/firebase");
|
const { deleteFolder } = require("../helpers/firebase");
|
||||||
const { Resend } = require("resend");
|
const { Resend } = require("resend");
|
||||||
const { welcomeTemplate } = require("../helpers/email");
|
const { welcomeTemplate } = require("../helpers/email");
|
||||||
@@ -56,14 +57,16 @@ exports.onUserCreated = onDocumentCreated("users/{userID}", async (event) => {
|
|||||||
lastName = "",
|
lastName = "",
|
||||||
} = event?.data?.data() || {};
|
} = event?.data?.data() || {};
|
||||||
|
|
||||||
|
const userId = event?.params?.userID;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await event.data.ref.set(
|
await createOrderDocument({
|
||||||
{
|
userId,
|
||||||
coins: admin.firestore.FieldValue.increment(10),
|
type: ORDER_TYPES.GIFT,
|
||||||
coinWelcomeGrantedAt: admin.firestore.FieldValue.serverTimestamp(),
|
amount: 10,
|
||||||
},
|
metadata: { reason: "WELCOME_BONUS" },
|
||||||
{ merge: true },
|
orderId: `welcome_${userId}`,
|
||||||
);
|
});
|
||||||
} catch (coinError) {
|
} catch (coinError) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[users-onUserCreated] Unable to grant welcome coins",
|
"[users-onUserCreated] Unable to grant welcome coins",
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
export const strings = {
|
export const strings = {
|
||||||
writing: {
|
writing: {
|
||||||
steps: {
|
steps: {
|
||||||
contextTitle: "Étape 2: Quel est le contexte ?",
|
contextTitle: "Quel est le contexte ?",
|
||||||
contextSubtitle:
|
emotionTitle: "Quelle émotion veux-tu transmettre ?",
|
||||||
"Ajoute quelques détails pour nous guider (ex.: anniversaire, équipe de sport ou d’entreprise…)",
|
|
||||||
contextExamples:
|
|
||||||
"Exemples : « Anniversaire de Clara – ton joyeux et pop », « Hymne d’équipe – style rap énergique », « Esprit d’entreprise – ambiance motivante »",
|
|
||||||
emotionTitle: "Étape 3: Quelle émotion veux-tu transmettre ?",
|
|
||||||
emotionSubtitle: "Sélectionne une seule intention émotionnelle.",
|
emotionSubtitle: "Sélectionne une seule intention émotionnelle.",
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
|
|||||||
+2
-13
@@ -48,16 +48,7 @@ export default ({
|
|||||||
const { currentUID = null, currentUserData = null } = useUserData?.() || {};
|
const { currentUID = null, currentUserData = null } = useUserData?.() || {};
|
||||||
|
|
||||||
const coinBalance = React.useMemo(() => {
|
const coinBalance = React.useMemo(() => {
|
||||||
const data = currentUserData || {};
|
const value = currentUserData?.coins;
|
||||||
const candidates = [
|
|
||||||
data?.coins,
|
|
||||||
data?.coinBalance,
|
|
||||||
data?.coin,
|
|
||||||
data?.wallet?.coins,
|
|
||||||
data?.wallet?.coinBalance,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const value of candidates) {
|
|
||||||
if (typeof value === "number" && Number.isFinite(value)) {
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -67,10 +58,8 @@ export default ({
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}, [currentUserData]);
|
}, [currentUserData?.coins]);
|
||||||
|
|
||||||
const formattedCoins = React.useMemo(() => {
|
const formattedCoins = React.useMemo(() => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import EditProfile from "../screens/Profile/EditProfile";
|
|||||||
import Follows from "../screens/Profile/Follows";
|
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 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";
|
||||||
@@ -271,6 +272,11 @@ const baseScreens = [
|
|||||||
name: Routes.Settings,
|
name: Routes.Settings,
|
||||||
component: Settings,
|
component: Settings,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: Routes.OrderHistory,
|
||||||
|
component: OrderHistory,
|
||||||
|
title: "Historique de commandes",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: Routes.Follows,
|
name: Routes.Follows,
|
||||||
component: Follows,
|
component: Follows,
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export const Routes = {
|
|||||||
SingerProfile: "SingerProfile",
|
SingerProfile: "SingerProfile",
|
||||||
EditProfile: "EditProfile",
|
EditProfile: "EditProfile",
|
||||||
Settings: "Settings",
|
Settings: "Settings",
|
||||||
|
OrderHistory: "OrderHistory",
|
||||||
ChangeEmailAddress: "ChangeEmailAddress",
|
ChangeEmailAddress: "ChangeEmailAddress",
|
||||||
ChangePassword: "ChangePassword",
|
ChangePassword: "ChangePassword",
|
||||||
Notifications: "Notifications",
|
Notifications: "Notifications",
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
FlatList,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
import firebase from "../../config/firebase";
|
||||||
|
import { background } from "../../assets";
|
||||||
|
import Page from "../../layouts/Page";
|
||||||
|
import { useUser } from "../../providers/UserDataProvider";
|
||||||
|
import { gutters, Palette } from "../../styles";
|
||||||
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
|
|
||||||
|
const ORDER_TYPE_LABELS = {
|
||||||
|
GIFT: "Crédit offert",
|
||||||
|
COINS: "Achat de coins",
|
||||||
|
SONG: "Génération de musique",
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_LABELS = {
|
||||||
|
PENDING: "En cours",
|
||||||
|
APPLIED: "Confirmée",
|
||||||
|
REJECTED: "Refusée",
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCoins = (amount) => {
|
||||||
|
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new Intl.NumberFormat("fr-FR", {
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(amount);
|
||||||
|
} catch (_error) {
|
||||||
|
return `${amount}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) {
|
||||||
|
return "En attente de confirmation";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return date.toLocaleString("fr-FR", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
} catch (_error) {
|
||||||
|
return date.toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapOrderType = (type) => ORDER_TYPE_LABELS[type] || "Opération";
|
||||||
|
|
||||||
|
const mapStatus = (status) => STATUS_LABELS[status] || "Inconnue";
|
||||||
|
|
||||||
|
const OrderHistory = () => {
|
||||||
|
const { currentUID } = useUser() || {};
|
||||||
|
const [orders, setOrders] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUID) {
|
||||||
|
setOrders([]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const unsubscribe = firebase
|
||||||
|
.firestore()
|
||||||
|
.collection("orders")
|
||||||
|
.where("userId", "==", currentUID)
|
||||||
|
.orderBy("createdAt", "desc")
|
||||||
|
.onSnapshot(
|
||||||
|
(snapshot) => {
|
||||||
|
const nextOrders = snapshot.docs.map((doc) => {
|
||||||
|
const rawData = doc.data() || {};
|
||||||
|
const createdAt =
|
||||||
|
typeof rawData.createdAt?.toDate === "function"
|
||||||
|
? rawData.createdAt.toDate()
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
id: doc.id,
|
||||||
|
...rawData,
|
||||||
|
createdAt,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setOrders(nextOrders);
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
(firestoreError) => {
|
||||||
|
console.warn(
|
||||||
|
"[OrderHistory] Unable to load orders",
|
||||||
|
firestoreError?.message || firestoreError,
|
||||||
|
);
|
||||||
|
setError(
|
||||||
|
firestoreError?.message ||
|
||||||
|
"Impossible de récupérer l'historique des commandes.",
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (typeof unsubscribe === "function") {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [currentUID]);
|
||||||
|
|
||||||
|
const renderOrder = useCallback(({ item }) => {
|
||||||
|
const amountValue =
|
||||||
|
typeof item.amount === "number" && Number.isFinite(item.amount)
|
||||||
|
? item.amount
|
||||||
|
: 0;
|
||||||
|
const isPositive = amountValue > 0;
|
||||||
|
const amountLabel = `${isPositive ? "+" : ""}${formatCoins(amountValue)} ${
|
||||||
|
amountValue === 1 ? "coin" : "coins"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const status = item.status || "PENDING";
|
||||||
|
|
||||||
|
const details = (() => {
|
||||||
|
if (item.type === "GIFT" && item.metadata?.reason) {
|
||||||
|
switch (item.metadata.reason) {
|
||||||
|
case "WELCOME_BONUS":
|
||||||
|
return "Crédit de bienvenue";
|
||||||
|
default:
|
||||||
|
return item.metadata.reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.type === "SONG" && item.songId) {
|
||||||
|
return `Musique ${item.songId}`;
|
||||||
|
}
|
||||||
|
if (item.type === "COINS" && item.metadata?.paymentId) {
|
||||||
|
return `Paiement ${item.metadata.paymentId}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.orderCard}>
|
||||||
|
<View style={styles.orderHeader}>
|
||||||
|
<Text style={styles.orderTitle}>{mapOrderType(item.type)}</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.orderAmount,
|
||||||
|
isPositive ? styles.amountPositive : styles.amountNegative,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{amountLabel}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const keyExtractor = useCallback((item) => item.id, []);
|
||||||
|
|
||||||
|
const listEmptyComponent = useMemo(() => {
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!currentUID) {
|
||||||
|
return (
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
Connecte-toi pour consulter l'historique de tes commandes.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return <Text style={styles.errorText}>{error}</Text>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
Aucune opération de coins pour le moment.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}, [loading, currentUID, error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
headerType="NAVIGATION"
|
||||||
|
title="Historique de commandes"
|
||||||
|
backgroundImg={background.profileBG}
|
||||||
|
contentContainerStyle={{ paddingBottom: gutters * 2 }}
|
||||||
|
containerStyle={{
|
||||||
|
backgroundColor: "rgba(0,0,0,0.4)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={styles.container}>
|
||||||
|
{loading ? (
|
||||||
|
<View style={styles.loaderContainer}>
|
||||||
|
<ActivityIndicator color={Palette.white} size="large" />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<FlatList
|
||||||
|
data={orders}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
renderItem={renderOrder}
|
||||||
|
contentContainerStyle={[
|
||||||
|
styles.listContent,
|
||||||
|
orders.length === 0 && styles.emptyListContent,
|
||||||
|
]}
|
||||||
|
ItemSeparatorComponent={() => <View style={{ height: 12 }} />}
|
||||||
|
ListEmptyComponent={listEmptyComponent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: gutters,
|
||||||
|
paddingTop: gutters,
|
||||||
|
},
|
||||||
|
loaderContainer: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
listContent: {
|
||||||
|
paddingBottom: gutters * 2,
|
||||||
|
},
|
||||||
|
emptyListContent: {
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
orderCard: {
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: Palette.ultraLightWhite,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "rgba(255, 255, 255, 0.08)",
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
|
},
|
||||||
|
amountPositive: {
|
||||||
|
color: Palette.green,
|
||||||
|
},
|
||||||
|
amountNegative: {
|
||||||
|
color: Palette.red,
|
||||||
|
},
|
||||||
|
orderMetaRow: {
|
||||||
|
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,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
color: Palette.white,
|
||||||
|
},
|
||||||
|
orderBalance: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
color: Palette.grayMid,
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
textAlign: "center",
|
||||||
|
color: Palette.grayMid,
|
||||||
|
fontSize: 15,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
textAlign: "center",
|
||||||
|
color: Palette.red,
|
||||||
|
fontSize: 15,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default OrderHistory;
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { SheetManager } from "react-native-actions-sheet";
|
import { SheetManager } from "react-native-actions-sheet";
|
||||||
|
import { Feather } from "@expo/vector-icons";
|
||||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
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";
|
||||||
@@ -33,7 +34,7 @@ import useLayoutType from "../../hooks/useLayoutType";
|
|||||||
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
|
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
import { push } from "../../navigation/NavigationService";
|
import { navigate, push } from "../../navigation/NavigationService";
|
||||||
import { useUser } from "../../providers/UserDataProvider";
|
import { useUser } from "../../providers/UserDataProvider";
|
||||||
import { Palette } from "../../styles";
|
import { Palette } from "../../styles";
|
||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
@@ -83,6 +84,9 @@ const Profile = () => {
|
|||||||
const onPressMenu = (item) => {
|
const onPressMenu = (item) => {
|
||||||
setSelected(item);
|
setSelected(item);
|
||||||
};
|
};
|
||||||
|
const handleOpenSettings = () => {
|
||||||
|
navigate(Routes.Settings);
|
||||||
|
};
|
||||||
|
|
||||||
// Chargement des données utilisateur si on consulte un autre profil
|
// Chargement des données utilisateur si on consulte un autre profil
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -340,8 +344,8 @@ const Profile = () => {
|
|||||||
}}
|
}}
|
||||||
rightComponent={() =>
|
rightComponent={() =>
|
||||||
!isWeb && isSelf ? (
|
!isWeb && isSelf ? (
|
||||||
<PressableScale onPress={() => SheetManager.show("ProfileSettings")}>
|
<PressableScale onPress={handleOpenSettings}>
|
||||||
<Image source={icons.more} style={{ ...size({ size: 24 }) }} />
|
<Feather name="settings" size={24} color={Palette.white} />
|
||||||
</PressableScale>
|
</PressableScale>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
@@ -363,18 +367,13 @@ const Profile = () => {
|
|||||||
<PressableScale
|
<PressableScale
|
||||||
style={{
|
style={{
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
}}
|
|
||||||
onPress={() => SheetManager.show("ProfileSettings")}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={icons.more}
|
|
||||||
style={{
|
|
||||||
...size({ size: 24 }),
|
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: 10,
|
right: 10,
|
||||||
top: 10,
|
top: 10,
|
||||||
}}
|
}}
|
||||||
/>
|
onPress={handleOpenSettings}
|
||||||
|
>
|
||||||
|
<Feather name="settings" size={24} color={Palette.white} />
|
||||||
</PressableScale>
|
</PressableScale>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ const SETTINGS = [
|
|||||||
navigate(Routes.Language);
|
navigate(Routes.Language);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Historique de commandes",
|
||||||
|
action: () => {
|
||||||
|
navigate(Routes.OrderHistory);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Découvrir les packs",
|
title: "Découvrir les packs",
|
||||||
action: () => {
|
action: () => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
|||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
import AppAlert from "../../components/Alert";
|
import AppAlert from "../../components/Alert";
|
||||||
import firebase, { usersRef } from "../../config/firebase";
|
import firebase, { getFunctionsClient } from "../../config/firebase";
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
@@ -50,16 +50,7 @@ const ComposeSong = () => {
|
|||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const coinBalance = useMemo(() => {
|
const coinBalance = useMemo(() => {
|
||||||
const data = currentUserData || {};
|
const value = currentUserData?.coins;
|
||||||
const candidates = [
|
|
||||||
data?.coins,
|
|
||||||
data?.coinBalance,
|
|
||||||
data?.coin,
|
|
||||||
data?.wallet?.coins,
|
|
||||||
data?.wallet?.coinBalance,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const value of candidates) {
|
|
||||||
if (typeof value === "number" && Number.isFinite(value)) {
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -69,10 +60,8 @@ const ComposeSong = () => {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}, [currentUserData]);
|
}, [currentUserData?.coins]);
|
||||||
|
|
||||||
const formattedCoinBalance = useMemo(() => {
|
const formattedCoinBalance = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
@@ -154,24 +143,29 @@ const ComposeSong = () => {
|
|||||||
throw new Error("Utilisateur introuvable. Merci de réessayer.");
|
throw new Error("Utilisateur introuvable. Merci de réessayer.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const decrement = firebase.firestore.FieldValue.increment(
|
try {
|
||||||
-MUSIC_GENERATION_COIN_COST,
|
const functionsClient = getFunctionsClient();
|
||||||
);
|
const createSongOrder =
|
||||||
const timestamp = firebase.firestore.FieldValue.serverTimestamp();
|
functionsClient.httpsCallable("orders-createSongOrder");
|
||||||
|
|
||||||
await usersRef.doc(currentUID).set(
|
await createSongOrder({
|
||||||
{
|
amount: -MUSIC_GENERATION_COIN_COST,
|
||||||
coins: decrement,
|
songId: selectedProjectId || null,
|
||||||
coinBalance: decrement,
|
source: "music_generation",
|
||||||
wallet: {
|
});
|
||||||
coins: decrement,
|
} catch (error) {
|
||||||
coinBalance: decrement,
|
const message =
|
||||||
},
|
typeof error?.message === "string"
|
||||||
lastCoinSpendAt: timestamp,
|
? error.message.replace(
|
||||||
lastCoinSpendAmount: MUSIC_GENERATION_COIN_COST,
|
/^functions\.https\.HttpsError:\s*/iu,
|
||||||
},
|
"",
|
||||||
{ merge: true },
|
)
|
||||||
|
: null;
|
||||||
|
throw new Error(
|
||||||
|
message ||
|
||||||
|
"Une erreur est survenue lors de la création de la commande de crédits.",
|
||||||
);
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmGeneration = async () => {
|
const handleConfirmGeneration = async () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
|||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
import AppAlert from "../../components/Alert";
|
import AppAlert from "../../components/Alert";
|
||||||
import firebase, { usersRef } from "../../config/firebase";
|
import firebase, { getFunctionsClient } from "../../config/firebase";
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
@@ -53,16 +53,7 @@ const ComposeSong = () => {
|
|||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const coinBalance = useMemo(() => {
|
const coinBalance = useMemo(() => {
|
||||||
const data = currentUserData || {};
|
const value = currentUserData?.coins;
|
||||||
const candidates = [
|
|
||||||
data?.coins,
|
|
||||||
data?.coinBalance,
|
|
||||||
data?.coin,
|
|
||||||
data?.wallet?.coins,
|
|
||||||
data?.wallet?.coinBalance,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const value of candidates) {
|
|
||||||
if (typeof value === "number" && Number.isFinite(value)) {
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -72,10 +63,8 @@ const ComposeSong = () => {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}, [currentUserData]);
|
}, [currentUserData?.coins]);
|
||||||
|
|
||||||
const formattedCoinBalance = useMemo(() => {
|
const formattedCoinBalance = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
@@ -209,25 +198,30 @@ const ComposeSong = () => {
|
|||||||
throw new Error("Utilisateur introuvable. Merci de réessayer.");
|
throw new Error("Utilisateur introuvable. Merci de réessayer.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const decrement = firebase.firestore.FieldValue.increment(
|
try {
|
||||||
-MUSIC_GENERATION_COIN_COST,
|
const functionsClient = getFunctionsClient();
|
||||||
);
|
const createSongOrder =
|
||||||
const timestamp = firebase.firestore.FieldValue.serverTimestamp();
|
functionsClient.httpsCallable("orders-createSongOrder");
|
||||||
|
|
||||||
await usersRef.doc(currentUID).set(
|
await createSongOrder({
|
||||||
{
|
amount: -MUSIC_GENERATION_COIN_COST,
|
||||||
coins: decrement,
|
songId: selectedProjectId || null,
|
||||||
coinBalance: decrement,
|
source: "music_generation",
|
||||||
wallet: {
|
});
|
||||||
coins: decrement,
|
} catch (error) {
|
||||||
coinBalance: decrement,
|
const message =
|
||||||
},
|
typeof error?.message === "string"
|
||||||
lastCoinSpendAt: timestamp,
|
? error.message.replace(
|
||||||
lastCoinSpendAmount: MUSIC_GENERATION_COIN_COST,
|
/^functions\.https\.HttpsError:\s*/iu,
|
||||||
},
|
"",
|
||||||
{ merge: true },
|
)
|
||||||
|
: null;
|
||||||
|
throw new Error(
|
||||||
|
message ||
|
||||||
|
"Une erreur est survenue lors de la création de la commande de crédits.",
|
||||||
);
|
);
|
||||||
}, [currentUID]);
|
}
|
||||||
|
}, [currentUID, selectedProjectId]);
|
||||||
|
|
||||||
const handleConfirmGeneration = useCallback(async () => {
|
const handleConfirmGeneration = useCallback(async () => {
|
||||||
if (isProcessingConfirmation) return;
|
if (isProcessingConfirmation) return;
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
import {
|
||||||
|
Platform,
|
||||||
|
Pressable,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { BaseButton } from "../../components/Button";
|
||||||
import ItemContainer from "../../components/ItemContainer/ItemContainer";
|
import ItemContainer from "../../components/ItemContainer/ItemContainer";
|
||||||
import ListSelection from "../../components/ListSelection/ListSelection";
|
import ListSelection from "../../components/ListSelection/ListSelection";
|
||||||
|
import Overlay from "../../components/Overlay";
|
||||||
import { GOALS, OTHER_OBJECTIVE_OPTION } from "../../data/data";
|
import { GOALS, OTHER_OBJECTIVE_OPTION } from "../../data/data";
|
||||||
import { strings } from "../../constants/strings";
|
import { strings } from "../../constants/strings";
|
||||||
import { Palette } from "../../styles";
|
import { Palette } from "../../styles";
|
||||||
@@ -15,10 +24,22 @@ const Goals = ({
|
|||||||
otherObjective,
|
otherObjective,
|
||||||
setOtherObjective,
|
setOtherObjective,
|
||||||
}) => {
|
}) => {
|
||||||
|
const normalizedOtherObjective =
|
||||||
|
typeof otherObjective === "string" ? otherObjective : "";
|
||||||
|
|
||||||
const [internalSelected, setInternalSelected] = useState(null);
|
const [internalSelected, setInternalSelected] = useState(null);
|
||||||
|
const [otherObjectiveModalVisible, setOtherObjectiveModalVisible] =
|
||||||
|
useState(false);
|
||||||
|
const [previousSelection, setPreviousSelection] = useState(null);
|
||||||
|
const [previousOtherObjective, setPreviousOtherObjective] = useState(
|
||||||
|
normalizedOtherObjective
|
||||||
|
);
|
||||||
|
|
||||||
const selected = selectedProp ?? internalSelected;
|
const selected = selectedProp ?? internalSelected;
|
||||||
const setSelected = setSelectedProp ?? setInternalSelected;
|
const setSelectedBase = setSelectedProp ?? setInternalSelected;
|
||||||
|
|
||||||
|
const trimmedOtherObjective = normalizedOtherObjective.trim();
|
||||||
|
const hasOtherObjectiveValue = trimmedOtherObjective.length > 0;
|
||||||
|
|
||||||
const goalsOptions = useMemo(() => GOALS ?? [], []);
|
const goalsOptions = useMemo(() => GOALS ?? [], []);
|
||||||
|
|
||||||
@@ -27,16 +48,63 @@ const Goals = ({
|
|||||||
const trimmed = value?.trim() ?? "";
|
const trimmed = value?.trim() ?? "";
|
||||||
if (trimmed.length) {
|
if (trimmed.length) {
|
||||||
if (selected !== OTHER_OBJECTIVE_OPTION) {
|
if (selected !== OTHER_OBJECTIVE_OPTION) {
|
||||||
setSelected(OTHER_OBJECTIVE_OPTION);
|
setSelectedBase(OTHER_OBJECTIVE_OPTION);
|
||||||
}
|
}
|
||||||
} else if (selected === OTHER_OBJECTIVE_OPTION) {
|
} else if (selected === OTHER_OBJECTIVE_OPTION) {
|
||||||
setSelected(null);
|
setSelectedBase(null);
|
||||||
|
setPreviousSelection(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Selection handled by ListSelection
|
const handleGoalSelect = (value) => {
|
||||||
|
if (value === OTHER_OBJECTIVE_OPTION) {
|
||||||
|
setPreviousSelection(selected ?? null);
|
||||||
|
setPreviousOtherObjective(normalizedOtherObjective);
|
||||||
|
setSelectedBase(value);
|
||||||
|
setOtherObjectiveModalVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedBase(value);
|
||||||
|
setOtherObjectiveModalVisible(false);
|
||||||
|
setPreviousSelection(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenOtherObjectiveModal = () => {
|
||||||
|
setPreviousSelection(selected ?? null);
|
||||||
|
setPreviousOtherObjective(normalizedOtherObjective);
|
||||||
|
setOtherObjectiveModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelOtherObjective = () => {
|
||||||
|
setOtherObjectiveModalVisible(false);
|
||||||
|
setOtherObjective?.(previousOtherObjective);
|
||||||
|
if (selected === OTHER_OBJECTIVE_OPTION) {
|
||||||
|
setSelectedBase(previousSelection ?? null);
|
||||||
|
}
|
||||||
|
setPreviousSelection(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmOtherObjective = () => {
|
||||||
|
if (!hasOtherObjectiveValue) {
|
||||||
|
const fallback =
|
||||||
|
previousSelection && previousSelection !== OTHER_OBJECTIVE_OPTION
|
||||||
|
? previousSelection
|
||||||
|
: null;
|
||||||
|
setOtherObjective?.(previousOtherObjective);
|
||||||
|
setSelectedBase(fallback);
|
||||||
|
setOtherObjectiveModalVisible(false);
|
||||||
|
setPreviousSelection(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selected !== OTHER_OBJECTIVE_OPTION) {
|
||||||
|
setSelectedBase(OTHER_OBJECTIVE_OPTION);
|
||||||
|
}
|
||||||
|
setOtherObjectiveModalVisible(false);
|
||||||
|
setPreviousSelection(OTHER_OBJECTIVE_OPTION);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<ScrollView contentContainerStyle={{ flex: 1, gap: 16, marginTop: 16 }}>
|
<ScrollView contentContainerStyle={{ flex: 1, gap: 16, marginTop: 16 }}>
|
||||||
<View style={{ gap: 10 }}>
|
<View style={{ gap: 10 }}>
|
||||||
<CreateLyricsHeader
|
<CreateLyricsHeader
|
||||||
@@ -53,21 +121,72 @@ const Goals = ({
|
|||||||
options={goalsOptions}
|
options={goalsOptions}
|
||||||
variant="simple"
|
variant="simple"
|
||||||
selected={selected}
|
selected={selected}
|
||||||
setSelected={setSelected}
|
setSelected={handleGoalSelect}
|
||||||
contentContainerStyle={styles.contentContainer}
|
contentContainerStyle={styles.contentContainer}
|
||||||
itemContainerStyle={styles.itemContainer}
|
itemContainerStyle={styles.itemContainer}
|
||||||
itemTextStyle={styles.itemText}
|
itemTextStyle={styles.itemText}
|
||||||
/>
|
/>
|
||||||
</ItemContainer>
|
</ItemContainer>
|
||||||
</View>
|
</View>
|
||||||
|
{selected === OTHER_OBJECTIVE_OPTION ? (
|
||||||
|
<View style={styles.otherObjectiveSummary}>
|
||||||
|
<View style={{ flex: 1, gap: 4 }}>
|
||||||
|
<Text style={styles.summaryLabel}>
|
||||||
|
{strings.writing.labels.otherObjective}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.summaryValue,
|
||||||
|
!hasOtherObjectiveValue && styles.summaryPlaceholder,
|
||||||
|
]}
|
||||||
|
numberOfLines={3}
|
||||||
|
>
|
||||||
|
{hasOtherObjectiveValue
|
||||||
|
? trimmedOtherObjective
|
||||||
|
: strings.writing.labels.otherObjectivePlaceholder}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Pressable
|
||||||
|
style={styles.summaryButton}
|
||||||
|
onPress={handleOpenOtherObjectiveModal}
|
||||||
|
>
|
||||||
|
<Text style={styles.summaryButtonText}>
|
||||||
|
{hasOtherObjectiveValue ? "Modifier" : "Ajouter"}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</ScrollView>
|
||||||
|
<Overlay
|
||||||
|
isVisible={otherObjectiveModalVisible}
|
||||||
|
setIsVisible={handleCancelOtherObjective}
|
||||||
|
>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<Text style={styles.modalTitle}>
|
||||||
|
{strings.writing.labels.otherObjective}
|
||||||
|
</Text>
|
||||||
<CustomInput
|
<CustomInput
|
||||||
label={strings.writing.labels.otherObjective}
|
|
||||||
placeholder={strings.writing.labels.otherObjectivePlaceholder}
|
placeholder={strings.writing.labels.otherObjectivePlaceholder}
|
||||||
value={otherObjective}
|
value={otherObjective}
|
||||||
setValue={handleOtherObjectiveChange}
|
setValue={handleOtherObjectiveChange}
|
||||||
height={Platform.OS === "web" ? 160 : undefined}
|
height={Platform.OS === "web" ? 160 : undefined}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
<View style={styles.modalActions}>
|
||||||
|
<BaseButton
|
||||||
|
type="secondary"
|
||||||
|
text="Annuler"
|
||||||
|
onPress={handleCancelOtherObjective}
|
||||||
|
containerStyle={styles.modalActionButton}
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
text="Valider"
|
||||||
|
onPress={handleConfirmOtherObjective}
|
||||||
|
containerStyle={styles.modalActionButton}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Overlay>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,4 +231,62 @@ const styles = StyleSheet.create({
|
|||||||
fontFamily: FONT_FAMILY.InterRegular,
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
|
otherObjectiveSummary: {
|
||||||
|
backgroundColor: Palette.ultraLightWhite,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 12,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
summaryLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
},
|
||||||
|
summaryValue: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
summaryPlaceholder: {
|
||||||
|
color: Palette.grayMid,
|
||||||
|
fontStyle: "italic",
|
||||||
|
},
|
||||||
|
summaryButton: {
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderRadius: 999,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Palette.white,
|
||||||
|
backgroundColor: "#FFFFFF10",
|
||||||
|
},
|
||||||
|
summaryButtonText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
width: "90%",
|
||||||
|
maxWidth: 420,
|
||||||
|
padding: 20,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: Palette.darkPurple,
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
modalActions: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
modalActionButton: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { View, StyleSheet } from "react-native";
|
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import CreateLyricsHeader from "./components/CreateLyricsHeader";
|
import CreateLyricsHeader from "./components/CreateLyricsHeader";
|
||||||
import { OTHER_STYLE_OPTION, SONG_STYLE } from "../../data/data";
|
import { OTHER_STYLE_OPTION, SONG_STYLE } from "../../data/data";
|
||||||
@@ -7,6 +7,8 @@ import CustomInput from "./components/CustomInput";
|
|||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
import ItemContainer from "../../components/ItemContainer/ItemContainer";
|
import ItemContainer from "../../components/ItemContainer/ItemContainer";
|
||||||
import ListSelection from "../../components/ListSelection/ListSelection";
|
import ListSelection from "../../components/ListSelection/ListSelection";
|
||||||
|
import { BaseButton } from "../../components/Button";
|
||||||
|
import Overlay from "../../components/Overlay";
|
||||||
|
|
||||||
const SongStyle = ({
|
const SongStyle = ({
|
||||||
selected: selectedProp,
|
selected: selectedProp,
|
||||||
@@ -14,9 +16,21 @@ const SongStyle = ({
|
|||||||
otherStyle,
|
otherStyle,
|
||||||
setOtherStyle,
|
setOtherStyle,
|
||||||
}) => {
|
}) => {
|
||||||
|
const normalizedOtherStyle =
|
||||||
|
typeof otherStyle === "string" ? otherStyle : "";
|
||||||
|
|
||||||
const [internalSelected, setInternalSelected] = useState(null);
|
const [internalSelected, setInternalSelected] = useState(null);
|
||||||
|
const [otherStyleModalVisible, setOtherStyleModalVisible] = useState(false);
|
||||||
|
const [previousSelection, setPreviousSelection] = useState(null);
|
||||||
|
const [previousOtherStyle, setPreviousOtherStyle] = useState(
|
||||||
|
normalizedOtherStyle
|
||||||
|
);
|
||||||
|
|
||||||
const selected = selectedProp ?? internalSelected;
|
const selected = selectedProp ?? internalSelected;
|
||||||
const setSelected = setSelectedProp ?? setInternalSelected;
|
const setSelectedBase = setSelectedProp ?? setInternalSelected;
|
||||||
|
|
||||||
|
const trimmedOtherStyle = normalizedOtherStyle.trim();
|
||||||
|
const hasOtherStyleValue = trimmedOtherStyle.length > 0;
|
||||||
|
|
||||||
const songStyleOptions = useMemo(() => SONG_STYLE ?? [], []);
|
const songStyleOptions = useMemo(() => SONG_STYLE ?? [], []);
|
||||||
|
|
||||||
@@ -25,16 +39,63 @@ const SongStyle = ({
|
|||||||
const trimmed = value?.trim() ?? "";
|
const trimmed = value?.trim() ?? "";
|
||||||
if (trimmed.length) {
|
if (trimmed.length) {
|
||||||
if (selected !== OTHER_STYLE_OPTION) {
|
if (selected !== OTHER_STYLE_OPTION) {
|
||||||
setSelected(OTHER_STYLE_OPTION);
|
setSelectedBase(OTHER_STYLE_OPTION);
|
||||||
}
|
}
|
||||||
} else if (selected === OTHER_STYLE_OPTION) {
|
} else if (selected === OTHER_STYLE_OPTION) {
|
||||||
setSelected(null);
|
setSelectedBase(null);
|
||||||
|
setPreviousSelection(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Selection handled by ListSelection
|
const handleStyleSelect = (value) => {
|
||||||
|
if (value === OTHER_STYLE_OPTION) {
|
||||||
|
setPreviousSelection(selected ?? null);
|
||||||
|
setPreviousOtherStyle(normalizedOtherStyle);
|
||||||
|
setSelectedBase(value);
|
||||||
|
setOtherStyleModalVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedBase(value);
|
||||||
|
setOtherStyleModalVisible(false);
|
||||||
|
setPreviousSelection(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenOtherStyleModal = () => {
|
||||||
|
setPreviousSelection(selected ?? null);
|
||||||
|
setPreviousOtherStyle(normalizedOtherStyle);
|
||||||
|
setOtherStyleModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelOtherStyle = () => {
|
||||||
|
setOtherStyleModalVisible(false);
|
||||||
|
setOtherStyle?.(previousOtherStyle);
|
||||||
|
if (selected === OTHER_STYLE_OPTION) {
|
||||||
|
setSelectedBase(previousSelection ?? null);
|
||||||
|
}
|
||||||
|
setPreviousSelection(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmOtherStyle = () => {
|
||||||
|
if (!hasOtherStyleValue) {
|
||||||
|
const fallback =
|
||||||
|
previousSelection && previousSelection !== OTHER_STYLE_OPTION
|
||||||
|
? previousSelection
|
||||||
|
: null;
|
||||||
|
setOtherStyle?.(previousOtherStyle);
|
||||||
|
setSelectedBase(fallback);
|
||||||
|
setOtherStyleModalVisible(false);
|
||||||
|
setPreviousSelection(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selected !== OTHER_STYLE_OPTION) {
|
||||||
|
setSelectedBase(OTHER_STYLE_OPTION);
|
||||||
|
}
|
||||||
|
setOtherStyleModalVisible(false);
|
||||||
|
setPreviousSelection(OTHER_STYLE_OPTION);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<View style={{ flex: 1, gap: 16, marginTop: 16 }}>
|
<View style={{ flex: 1, gap: 16, marginTop: 16 }}>
|
||||||
<View style={{ gap: 10 }}>
|
<View style={{ gap: 10 }}>
|
||||||
<CreateLyricsHeader
|
<CreateLyricsHeader
|
||||||
@@ -46,19 +107,69 @@ const SongStyle = ({
|
|||||||
options={songStyleOptions}
|
options={songStyleOptions}
|
||||||
variant="titleDescription"
|
variant="titleDescription"
|
||||||
selected={selected}
|
selected={selected}
|
||||||
setSelected={setSelected}
|
setSelected={handleStyleSelect}
|
||||||
contentContainerStyle={styles.contentContainer}
|
contentContainerStyle={styles.contentContainer}
|
||||||
itemContainerStyle={styles.itemContainer}
|
itemContainerStyle={styles.itemContainer}
|
||||||
/>
|
/>
|
||||||
</ItemContainer>
|
</ItemContainer>
|
||||||
</View>
|
</View>
|
||||||
|
{selected === OTHER_STYLE_OPTION ? (
|
||||||
|
<View style={styles.otherStyleSummary}>
|
||||||
|
<View style={{ flex: 1, gap: 4 }}>
|
||||||
|
<Text style={styles.summaryLabel}>
|
||||||
|
Tu as un autre style de chanson ?
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.summaryValue,
|
||||||
|
!hasOtherStyleValue && styles.summaryPlaceholder,
|
||||||
|
]}
|
||||||
|
numberOfLines={3}
|
||||||
|
>
|
||||||
|
{hasOtherStyleValue ? trimmedOtherStyle : "Décrire le style"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Pressable
|
||||||
|
style={styles.summaryButton}
|
||||||
|
onPress={handleOpenOtherStyleModal}
|
||||||
|
>
|
||||||
|
<Text style={styles.summaryButtonText}>
|
||||||
|
{hasOtherStyleValue ? "Modifier" : "Ajouter"}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
<Overlay
|
||||||
|
isVisible={otherStyleModalVisible}
|
||||||
|
setIsVisible={handleCancelOtherStyle}
|
||||||
|
>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<Text style={styles.modalTitle}>
|
||||||
|
Tu as un autre style de chanson ?
|
||||||
|
</Text>
|
||||||
<CustomInput
|
<CustomInput
|
||||||
label="Tu as un autre style de chanson ?"
|
|
||||||
placeholder="Décrire le style"
|
placeholder="Décrire le style"
|
||||||
value={otherStyle}
|
value={otherStyle}
|
||||||
setValue={handleOtherStyleChange}
|
setValue={handleOtherStyleChange}
|
||||||
|
height={Platform.OS === "web" ? 160 : undefined}
|
||||||
|
/>
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<BaseButton
|
||||||
|
type="secondary"
|
||||||
|
text="Annuler"
|
||||||
|
onPress={handleCancelOtherStyle}
|
||||||
|
containerStyle={styles.modalActionButton}
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
text="Valider"
|
||||||
|
onPress={handleConfirmOtherStyle}
|
||||||
|
containerStyle={styles.modalActionButton}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
</Overlay>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,4 +201,62 @@ const styles = StyleSheet.create({
|
|||||||
fontFamily: FONT_FAMILY.InterRegular,
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
|
otherStyleSummary: {
|
||||||
|
backgroundColor: Palette.ultraLightWhite,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 12,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
summaryLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
},
|
||||||
|
summaryValue: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
summaryPlaceholder: {
|
||||||
|
color: Palette.grayMid,
|
||||||
|
fontStyle: "italic",
|
||||||
|
},
|
||||||
|
summaryButton: {
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderRadius: 999,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Palette.white,
|
||||||
|
backgroundColor: "#FFFFFF10",
|
||||||
|
},
|
||||||
|
summaryButtonText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
width: "90%",
|
||||||
|
maxWidth: 420,
|
||||||
|
padding: 20,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: Palette.darkPurple,
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
modalActions: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
modalActionButton: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user