Compare commits
25 Commits
fetch/incl
...
cal/dev-64
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46fdb29ba6 | ||
|
|
a3839461cf | ||
|
|
858c7e393f | ||
|
|
0278241006 | ||
|
|
3afb682fc6 | ||
|
|
06f1df1995 | ||
|
|
3489771d2e | ||
|
|
448ae5a2b7 | ||
|
|
72340790e5 | ||
|
|
c9423fe478 | ||
|
|
02a850ae63 | ||
|
|
ede6d0c3cc | ||
|
|
7685989a8c | ||
|
|
4e8ebb5e5c | ||
|
|
3f77ab19ed | ||
|
|
d3d0c8c523 | ||
|
|
4e093131f3 | ||
|
|
6ca8a4e5fd | ||
|
|
63b15ded60 | ||
|
|
85e65aeffe | ||
|
|
ad44398492 | ||
|
|
a4ba41bf15 | ||
|
|
4441be5380 | ||
|
|
c0accb42fa | ||
|
|
7223c2b197 |
405
Cargo.lock
generated
405
Cargo.lock
generated
@@ -481,7 +481,7 @@ dependencies = [
|
||||
"serde_cbor",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -551,9 +551,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.23"
|
||||
version = "0.4.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07"
|
||||
checksum = "d615619615a650c571269c00dca41db04b9210037fa76ed8239f70404ab56985"
|
||||
dependencies = [
|
||||
"brotli 8.0.1",
|
||||
"bzip2",
|
||||
@@ -1354,7 +1354,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"fnv",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1412,9 +1412,9 @@ checksum = "93a719913643003b84bd13022b4b7e703c09342cd03b679c4641c7d2e50dc34d"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.38"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
|
||||
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -1422,9 +1422,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.38"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
|
||||
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1434,9 +1434,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.32"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -1452,9 +1452,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "clickhouse"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9894248c4c5a4402f76a56c273836a0c32547ec8a68166aedee7e01b7b8d102"
|
||||
checksum = "9a9a81a1dffadd762ee662635ce409232258ce9beebd7cc0fa227df0b5e7efc0"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"bytes",
|
||||
@@ -1474,7 +1474,7 @@ dependencies = [
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2000,7 +2000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2634,9 +2634,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.1"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-rs-sys",
|
||||
@@ -2992,7 +2992,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3681,22 +3681,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.11"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
|
||||
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.6.0",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3711,7 +3717,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.61.0",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4027,6 +4033,16 @@ version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
@@ -4377,12 +4393,12 @@ dependencies = [
|
||||
"tracing-actix-web",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
"validator",
|
||||
"webp",
|
||||
"woothee",
|
||||
"yaserde",
|
||||
"zip 3.0.0",
|
||||
"zip 4.0.0",
|
||||
"zxcvbn",
|
||||
]
|
||||
|
||||
@@ -4409,9 +4425,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "lettre"
|
||||
version = "0.11.16"
|
||||
version = "0.11.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87ffd14fa289730e3ad68edefdc31f603d56fe716ec38f2076bb7410e09147c2"
|
||||
checksum = "cb2a0354e9ece2fcdcf9fa53417f6de587230c0c248068eb058fa26c4a753179"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chumsky",
|
||||
@@ -4523,9 +4539,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libz-rs-sys"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a"
|
||||
checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
|
||||
dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
@@ -4753,7 +4769,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"yaup",
|
||||
@@ -4849,7 +4865,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"tagptr",
|
||||
"thiserror 1.0.69",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6719,9 +6735,9 @@ checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.15"
|
||||
version = "0.12.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64 0.22.1",
|
||||
@@ -6748,25 +6764,23 @@ dependencies = [
|
||||
"quinn",
|
||||
"rustls 0.23.27",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots 0.26.11",
|
||||
"windows-registry 0.4.0",
|
||||
"webpki-roots 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6854,7 +6868,7 @@ dependencies = [
|
||||
"rkyv_derive",
|
||||
"seahash",
|
||||
"tinyvec",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7056,7 +7070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"rustls-pemfile",
|
||||
"schannel",
|
||||
"security-framework 2.11.1",
|
||||
]
|
||||
@@ -7082,15 +7096,6 @@ dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.12.0"
|
||||
@@ -7200,7 +7205,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7449,7 +7454,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"url",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7474,9 +7479,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde-xml-rs"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "176b7ff880ab6ead7a020e773e9c096467fe347615a3e22ac29300cbdef67a5d"
|
||||
checksum = "53630160a98edebde0123eb4dfd0fce6adff091b2305db3154a9e920206eb510"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -7927,9 +7932,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
|
||||
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@@ -7940,9 +7945,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
|
||||
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -7978,9 +7983,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
|
||||
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -7991,9 +7996,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
|
||||
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -8010,16 +8015,15 @@ dependencies = [
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"syn 2.0.101",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
|
||||
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
@@ -8061,9 +8065,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
|
||||
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
@@ -8100,9 +8104,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
|
||||
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
@@ -8295,16 +8299,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.35.1"
|
||||
version = "0.35.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79251336d17c72d9762b8b54be4befe38d2db56fbbc0241396d70f173c39d47a"
|
||||
checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8380,8 +8384,8 @@ dependencies = [
|
||||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
]
|
||||
@@ -8469,7 +8473,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8519,7 +8523,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"url",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -8556,9 +8560,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.2.1"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dba4412f30eaff6f5d210e20383c2d6835593977402092e95b72497a4f8632fa"
|
||||
checksum = "e4976ac728ebc0487515aa956cfdf200abcc52b784e441493fc544bc6ce369c8"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"rust-ini",
|
||||
@@ -8570,15 +8574,15 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-registry 0.5.1",
|
||||
"windows-registry",
|
||||
"windows-result",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.2.1"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcaf6e5d6062423a0f711a23c2a573ccba222b6a16a9322d8499928f27e41376"
|
||||
checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
@@ -8594,9 +8598,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.2.1"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88371e340ad2f07409a3b68294abe73f20bc9c1bc1b631a31dc37a3d0161f682"
|
||||
checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
@@ -8612,14 +8616,13 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"url",
|
||||
"uuid 1.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.2.6"
|
||||
version = "2.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fdc6cb608e04b7d2b6d1f21e9444ad49245f6d03465ba53323d692d1ceb1a30"
|
||||
checksum = "66644b71a31ec1a8a52c4a16575edd28cf763c87cf4a7da24c884122b5c77097"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
@@ -8633,7 +8636,7 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.12",
|
||||
"url",
|
||||
"windows 0.60.0",
|
||||
"windows",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -8657,9 +8660,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.2.3"
|
||||
version = "2.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1320af4d866a7fb5f5721d299d14d0dd9e4e6bc0359ff3e263124a2bf6814efa"
|
||||
checksum = "97d0e07b40fb2eb13778e30778f5979347a2bf30e1b9d47f78ff7fe92d2e4b3d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8736,7 +8739,7 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.12",
|
||||
"url",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8762,7 +8765,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
"wry",
|
||||
]
|
||||
|
||||
@@ -8800,7 +8803,7 @@ dependencies = [
|
||||
"toml",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -8900,10 +8903,10 @@ dependencies = [
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
"whoami",
|
||||
"winreg 0.55.0",
|
||||
"zip 3.0.0",
|
||||
"zip 4.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8936,7 +8939,7 @@ dependencies = [
|
||||
"tracing-error",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9113,9 +9116,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.0"
|
||||
version = "1.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -9338,6 +9341,24 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower 0.5.2",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -9372,7 +9393,7 @@ dependencies = [
|
||||
"mutually_exclusive_features",
|
||||
"pin-project",
|
||||
"tracing",
|
||||
"uuid 1.16.0",
|
||||
"uuid 1.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9699,9 +9720,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.16.0"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
@@ -10052,9 +10073,9 @@ checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows-implement 0.60.0",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
]
|
||||
|
||||
@@ -10076,8 +10097,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295"
|
||||
dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10171,39 +10192,17 @@ dependencies = [
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529"
|
||||
dependencies = [
|
||||
"windows-collections 0.1.1",
|
||||
"windows-core 0.60.1",
|
||||
"windows-future 0.1.1",
|
||||
"windows-link",
|
||||
"windows-numerics 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
|
||||
dependencies = [
|
||||
"windows-collections 0.2.0",
|
||||
"windows-core 0.61.0",
|
||||
"windows-future 0.2.0",
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-numerics 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec"
|
||||
dependencies = [
|
||||
"windows-core 0.60.1",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10212,20 +10211,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.60.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247"
|
||||
dependencies = [
|
||||
"windows-implement 0.59.0",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10234,21 +10220,11 @@ version = "0.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0"
|
||||
dependencies = [
|
||||
"windows-core 0.60.1",
|
||||
"windows-link",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10257,21 +10233,10 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
@@ -10300,37 +10265,16 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed"
|
||||
dependencies = [
|
||||
"windows-core 0.60.1",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.1"
|
||||
@@ -10339,7 +10283,7 @@ checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.0",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10351,15 +10295,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.0"
|
||||
@@ -10444,29 +10379,13 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
"windows_i686_gnullvm 0.53.0",
|
||||
"windows_i686_msvc 0.53.0",
|
||||
"windows_x86_64_gnu 0.53.0",
|
||||
"windows_x86_64_gnullvm 0.53.0",
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-version"
|
||||
version = "0.1.4"
|
||||
@@ -10494,12 +10413,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -10518,12 +10431,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -10542,24 +10449,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -10578,12 +10473,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -10602,12 +10491,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -10626,12 +10509,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -10650,12 +10527,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
@@ -10771,8 +10642,8 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
@@ -11042,9 +10913,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "3.0.0"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
|
||||
checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
@@ -11059,9 +10930,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
|
||||
checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
|
||||
38
Cargo.toml
38
Cargo.toml
@@ -21,7 +21,7 @@ actix-web-prom = "0.10.0"
|
||||
actix-ws = "0.3.0"
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
ariadne = { path = "packages/ariadne" }
|
||||
async-compression = { version = "0.4.23", default-features = false }
|
||||
async-compression = { version = "0.4.24", default-features = false }
|
||||
async-recursion = "1.1.1"
|
||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||
"runtime-tokio-hyper-rustls",
|
||||
@@ -33,12 +33,12 @@ async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
||||
async-walkdir = "2.1.0"
|
||||
async_zip = "0.0.17"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.0"
|
||||
bitflags = "2.9.1"
|
||||
bytes = "1.10.1"
|
||||
censor = "0.3.0"
|
||||
chrono = "0.4.41"
|
||||
clap = "4.5.38"
|
||||
clickhouse = "0.13.2"
|
||||
clap = "4.5.40"
|
||||
clickhouse = "0.13.3"
|
||||
color-thief = "0.2.2"
|
||||
console-subscriber = "0.4.1"
|
||||
daedalus = { path = "packages/daedalus" }
|
||||
@@ -51,7 +51,7 @@ dotenvy = "0.15.7"
|
||||
dunce = "1.0.5"
|
||||
either = "1.15.0"
|
||||
enumset = "1.1.6"
|
||||
flate2 = "1.1.1"
|
||||
flate2 = "1.1.2"
|
||||
fs4 = { version = "0.13.1", default-features = false }
|
||||
futures = { version = "0.3.31", default-features = false }
|
||||
futures-util = "0.3.31"
|
||||
@@ -59,7 +59,7 @@ hex = "0.4.3"
|
||||
hickory-resolver = "0.25.2"
|
||||
hmac = "0.12.1"
|
||||
hyper-tls = "0.6.0"
|
||||
hyper-util = "0.1.11"
|
||||
hyper-util = "0.1.14"
|
||||
iana-time-zone = "0.1.63"
|
||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
||||
indexmap = "2.9.0"
|
||||
@@ -67,7 +67,7 @@ indicatif = "0.17.11"
|
||||
itertools = "0.14.0"
|
||||
jemalloc_pprof = "0.7.0"
|
||||
json-patch = { version = "4.0.0", default-features = false }
|
||||
lettre = { version = "0.11.16", default-features = false, features = [
|
||||
lettre = { version = "0.11.17", default-features = false, features = [
|
||||
"builder",
|
||||
"hostname",
|
||||
"pool",
|
||||
@@ -89,9 +89,9 @@ quartz_nbt = "0.2.9"
|
||||
quick-xml = "0.37.5"
|
||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||
redis = "0.31.0"
|
||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.15", default-features = false }
|
||||
reqwest = { version = "0.12.19", default-features = false }
|
||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
||||
"fail-on-err",
|
||||
"tags",
|
||||
@@ -113,7 +113,7 @@ sentry = { version = "0.38.1", default-features = false, features = [
|
||||
] }
|
||||
sentry-actix = "0.38.1"
|
||||
serde = "1.0.219"
|
||||
serde-xml-rs = "0.8.0" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
serde_bytes = "0.11.17"
|
||||
serde_cbor = "0.11.2"
|
||||
serde_ini = "0.2.0"
|
||||
@@ -123,16 +123,16 @@ sha1 = "0.10.6"
|
||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||
sha2 = "0.10.9"
|
||||
spdx = "0.10.8"
|
||||
sqlx = { version = "0.8.5", default-features = false }
|
||||
sysinfo = { version = "0.35.1", default-features = false }
|
||||
sqlx = { version = "0.8.6", default-features = false }
|
||||
sysinfo = { version = "0.35.2", default-features = false }
|
||||
tar = "0.4.44"
|
||||
tauri = "2.5.1"
|
||||
tauri-build = "2.2.0"
|
||||
tauri-plugin-deep-link = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.1"
|
||||
tauri-plugin-opener = "2.2.6"
|
||||
tauri-plugin-deep-link = "2.3.0"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
tauri-plugin-opener = "2.2.7"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-single-instance = "2.2.3"
|
||||
tauri-plugin-single-instance = "2.2.4"
|
||||
tauri-plugin-updater = { version = "2.7.1", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
"zip",
|
||||
@@ -143,7 +143,7 @@ theseus = { path = "packages/app-lib" }
|
||||
thiserror = "2.0.12"
|
||||
tikv-jemalloc-ctl = "0.6.0"
|
||||
tikv-jemallocator = "0.6.0"
|
||||
tokio = "1.45.0"
|
||||
tokio = "1.45.1"
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-util = "0.7.15"
|
||||
totp-rs = "5.7.0"
|
||||
@@ -153,14 +153,14 @@ tracing-error = "0.2.1"
|
||||
tracing-subscriber = "0.3.19"
|
||||
url = "2.5.4"
|
||||
urlencoding = "2.1.3"
|
||||
uuid = "1.16.0"
|
||||
uuid = "1.17.0"
|
||||
validator = "0.20.0"
|
||||
webp = { version = "0.3.0", default-features = false }
|
||||
whoami = "1.6.0"
|
||||
winreg = "0.55.0"
|
||||
woothee = "0.13.0"
|
||||
yaserde = "0.12.0"
|
||||
zip = { version = "3.0.0", default-features = false, features = [
|
||||
zip = { version = "4.0.0", default-features = false, features = [
|
||||
"bzip2",
|
||||
"deflate",
|
||||
"deflate64",
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
getWorldIdentifier,
|
||||
showWorldInFolder,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||
import {
|
||||
useRelativeTime,
|
||||
Avatar,
|
||||
@@ -108,20 +108,6 @@ const serverIncompatible = computed(
|
||||
props.serverStatus.version.protocol !== props.currentProtocol,
|
||||
)
|
||||
|
||||
function getPingLevel(ping: number) {
|
||||
if (ping < 150) {
|
||||
return 5
|
||||
} else if (ping < 300) {
|
||||
return 4
|
||||
} else if (ping < 600) {
|
||||
return 3
|
||||
} else if (ping < 1000) {
|
||||
return 2
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||
|
||||
const messages = defineMessages({
|
||||
|
||||
@@ -56,6 +56,7 @@ function show(world: SingleplayerWorld) {
|
||||
icon.value = world.icon
|
||||
displayStatus.value = world.display_status
|
||||
hideFromHome.value = world.display_status === 'hidden'
|
||||
removeIcon.value = false
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
|
||||
128
apps/frontend/src/components/ui/OptionGroup.vue
Normal file
128
apps/frontend/src/components/ui/OptionGroup.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<nav
|
||||
ref="scrollContainer"
|
||||
class="card-shadow experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||
>
|
||||
<button
|
||||
v-for="(option, index) in options"
|
||||
:key="`option-group-${index}`"
|
||||
ref="optionButtons"
|
||||
class="button-animation z-[1] flex flex-row items-center gap-2 rounded-full bg-transparent px-4 py-2 font-semibold"
|
||||
:class="{
|
||||
'text-button-textSelected': modelValue === option,
|
||||
'text-primary': modelValue !== option,
|
||||
}"
|
||||
@click="setOption(option)"
|
||||
>
|
||||
<slot :option="option" :selected="modelValue === option" />
|
||||
</button>
|
||||
<div
|
||||
class="navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full bg-button-bgSelected p-1"
|
||||
:style="{
|
||||
left: sliderLeftPx,
|
||||
top: sliderTopPx,
|
||||
right: sliderRightPx,
|
||||
bottom: sliderBottomPx,
|
||||
opacity: initialized ? 1 : 0,
|
||||
}"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
|
||||
const modelValue = defineModel<T>({ required: true });
|
||||
|
||||
const props = defineProps<{
|
||||
options: T[];
|
||||
}>();
|
||||
|
||||
const scrollContainer = ref<HTMLElement | null>(null);
|
||||
|
||||
const sliderLeft = ref(4);
|
||||
const sliderTop = ref(4);
|
||||
const sliderRight = ref(4);
|
||||
const sliderBottom = ref(4);
|
||||
|
||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`);
|
||||
const sliderTopPx = computed(() => `${sliderTop.value}px`);
|
||||
const sliderRightPx = computed(() => `${sliderRight.value}px`);
|
||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`);
|
||||
|
||||
const optionButtons = ref();
|
||||
|
||||
const initialized = ref(false);
|
||||
|
||||
function setOption(option: T) {
|
||||
modelValue.value = option;
|
||||
}
|
||||
|
||||
watch(modelValue, () => {
|
||||
startAnimation(props.options.indexOf(modelValue.value));
|
||||
});
|
||||
|
||||
function startAnimation(index: number) {
|
||||
const el = optionButtons.value[index];
|
||||
|
||||
if (!el || !el.offsetParent) return;
|
||||
|
||||
const newValues = {
|
||||
left: el.offsetLeft,
|
||||
top: el.offsetTop,
|
||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||
};
|
||||
|
||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||
sliderLeft.value = newValues.left;
|
||||
sliderRight.value = newValues.right;
|
||||
sliderTop.value = newValues.top;
|
||||
sliderBottom.value = newValues.bottom;
|
||||
} else {
|
||||
const delay = 200;
|
||||
|
||||
if (newValues.left < sliderLeft.value) {
|
||||
sliderLeft.value = newValues.left;
|
||||
setTimeout(() => {
|
||||
sliderRight.value = newValues.right;
|
||||
}, delay);
|
||||
} else {
|
||||
sliderRight.value = newValues.right;
|
||||
setTimeout(() => {
|
||||
sliderLeft.value = newValues.left;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
if (newValues.top < sliderTop.value) {
|
||||
sliderTop.value = newValues.top;
|
||||
setTimeout(() => {
|
||||
sliderBottom.value = newValues.bottom;
|
||||
}, delay);
|
||||
} else {
|
||||
sliderBottom.value = newValues.bottom;
|
||||
setTimeout(() => {
|
||||
sliderTop.value = newValues.top;
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
initialized.value = true;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startAnimation(props.options.indexOf(modelValue.value));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.navtabs-transition {
|
||||
transition:
|
||||
all 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
||||
}
|
||||
|
||||
.card-shadow {
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
</style>
|
||||
@@ -133,7 +133,7 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
||||
props,
|
||||
);
|
||||
} else {
|
||||
const returnTopN = 5;
|
||||
const returnTopN = 15;
|
||||
|
||||
const listEntries = series
|
||||
.map((value, index) => [
|
||||
|
||||
@@ -63,6 +63,7 @@ const props = defineProps<{
|
||||
loader: string | null;
|
||||
loader_version: string | null;
|
||||
};
|
||||
ignoreCurrentInstallation?: boolean;
|
||||
isInstalling?: boolean;
|
||||
}>();
|
||||
|
||||
|
||||
@@ -127,7 +127,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div
|
||||
v-if="!initialSetup"
|
||||
class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4"
|
||||
>
|
||||
<div class="flex w-full flex-row items-center justify-between">
|
||||
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
|
||||
Erase all data
|
||||
@@ -146,7 +149,10 @@
|
||||
<div class="font-bold">This does not affect your backups, which are stored off-site.</div>
|
||||
</div>
|
||||
|
||||
<BackupWarning :backup-link="`/servers/manage/${props.server?.serverId}/backups`" />
|
||||
<BackupWarning
|
||||
v-if="!initialSetup"
|
||||
:backup-link="`/servers/manage/${props.server?.serverId}/backups`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-start gap-4">
|
||||
@@ -217,6 +223,7 @@ const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
currentLoader: Loaders | undefined;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
initialSetup?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -313,7 +320,7 @@ const selectedLoaderVersions = computed<string[]>(() => {
|
||||
const loader = selectedLoader.value.toLowerCase();
|
||||
|
||||
if (loader === "paper") {
|
||||
return paperVersions.value[selectedMCVersion.value].map((x) => `${x}`) || [];
|
||||
return paperVersions.value[selectedMCVersion.value]?.map((x) => `${x}`) || [];
|
||||
}
|
||||
|
||||
if (loader === "purpur") {
|
||||
@@ -456,7 +463,7 @@ const handleReinstall = async () => {
|
||||
selectedLoader.value,
|
||||
selectedMCVersion.value,
|
||||
selectedLoader.value === "Vanilla" ? "" : selectedLoaderVersion.value,
|
||||
hardReset.value,
|
||||
props.initialSetup ? true : hardReset.value,
|
||||
);
|
||||
|
||||
emit("reinstall", {
|
||||
|
||||
278
apps/frontend/src/components/ui/servers/ServerInstallation.vue
Normal file
278
apps/frontend/src/components/ui/servers/ServerInstallation.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<LazyUiServersPlatformVersionSelectModal
|
||||
ref="versionSelectModal"
|
||||
:server="props.server"
|
||||
:current-loader="ignoreCurrentInstallation ? undefined : (data?.loader as Loaders)"
|
||||
:backup-in-progress="backupInProgress"
|
||||
:initial-setup="ignoreCurrentInstallation"
|
||||
@reinstall="emit('reinstall', $event)"
|
||||
/>
|
||||
|
||||
<LazyUiServersPlatformMrpackModal
|
||||
ref="mrpackModal"
|
||||
:server="props.server"
|
||||
@reinstall="emit('reinstall', $event)"
|
||||
/>
|
||||
|
||||
<LazyUiServersPlatformChangeModpackVersionModal
|
||||
ref="modpackVersionModal"
|
||||
:server="props.server"
|
||||
:project="data?.project"
|
||||
:versions="Array.isArray(versions) ? versions : []"
|
||||
:current-version="currentVersion"
|
||||
:current-version-id="data?.upstream?.version_id"
|
||||
:server-status="data?.status"
|
||||
@reinstall="emit('reinstall')"
|
||||
/>
|
||||
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<div v-if="data && versions" class="flex w-full flex-col">
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex select-none flex-col items-center justify-between gap-2 lg:flex-row">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<h2 class="m-0 text-lg font-bold text-contrast">Modpack</h2>
|
||||
<div
|
||||
v-if="updateAvailable"
|
||||
class="rounded-full bg-bg-orange px-2 py-1 text-xs font-medium text-orange"
|
||||
>
|
||||
<span>Update available</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.upstream" class="flex gap-4">
|
||||
<ButtonStyled>
|
||||
<button
|
||||
class="!w-full sm:!w-auto"
|
||||
:disabled="isInstalling"
|
||||
@click="mrpackModal.show()"
|
||||
>
|
||||
<UploadIcon class="size-4" /> Import .mrpack
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<!-- dumb hack to make a button link not a link -->
|
||||
<ButtonStyled>
|
||||
<template v-if="isInstalling">
|
||||
<button :disabled="isInstalling">
|
||||
<TransferIcon class="size-4" />
|
||||
Switch modpack
|
||||
</button>
|
||||
</template>
|
||||
<nuxt-link v-else :to="`/modpacks?sid=${props.server.serverId}`">
|
||||
<TransferIcon class="size-4" />
|
||||
Switch modpack
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.upstream" class="flex flex-col gap-2">
|
||||
<div
|
||||
v-if="versionsError || currentVersionError"
|
||||
class="rounded-2xl border border-solid border-red p-4 text-contrast"
|
||||
>
|
||||
<p class="m-0 font-bold">Something went wrong while loading your modpack.</p>
|
||||
<p class="m-0 mb-2 mt-1 text-sm">
|
||||
{{ versionsError || currentVersionError }}
|
||||
</p>
|
||||
<ButtonStyled>
|
||||
<button :disabled="isInstalling" @click="refreshData">Retry</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<NewProjectCard
|
||||
v-if="!versionsError && !currentVersionError"
|
||||
class="!cursor-default !bg-bg !filter-none"
|
||||
:project="projectCardData"
|
||||
:categories="data.project?.categories || []"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="isInstalling" @click="modpackVersionModal.show()">
|
||||
<SettingsIcon class="size-4" />
|
||||
Change version
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</NewProjectCard>
|
||||
</div>
|
||||
<div v-else class="flex w-full flex-col items-center gap-2 sm:w-fit sm:flex-row">
|
||||
<ButtonStyled>
|
||||
<nuxt-link
|
||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||
:class="{ disabled: backupInProgress }"
|
||||
class="!w-full sm:!w-auto"
|
||||
:to="`/modpacks?sid=${props.server.serverId}`"
|
||||
>
|
||||
<CompassIcon class="size-4" /> Find a modpack
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<span class="hidden sm:block">or</span>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||
:disabled="!!backupInProgress"
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="mrpackModal.show()"
|
||||
>
|
||||
<UploadIcon class="size-4" /> Upload .mrpack file
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="m-0 text-lg font-bold text-contrast">Platform</h2>
|
||||
<p class="m-0">Your server's platform is the software that runs mods and plugins.</p>
|
||||
<div v-if="data.upstream" class="mt-2 flex items-center gap-2">
|
||||
<InfoIcon class="hidden sm:block" />
|
||||
<span class="text-sm text-secondary">
|
||||
The current platform was automatically selected based on your modpack.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full flex-col gap-1 rounded-2xl"
|
||||
:class="{
|
||||
'pointer-events-none cursor-not-allowed select-none opacity-50':
|
||||
props.server.general?.status === 'installing',
|
||||
}"
|
||||
:tabindex="props.server.general?.status === 'installing' ? -1 : 0"
|
||||
>
|
||||
<UiServersLoaderSelector
|
||||
:data="
|
||||
ignoreCurrentInstallation
|
||||
? {
|
||||
loader: null,
|
||||
loader_version: null,
|
||||
}
|
||||
: data
|
||||
"
|
||||
:is-installing="isInstalling"
|
||||
@select-loader="selectLoader"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, NewProjectCard } from "@modrinth/ui";
|
||||
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { Loaders } from "~/types/servers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
ignoreCurrentInstallation?: boolean;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
reinstall: [any?];
|
||||
}>();
|
||||
|
||||
const isInstalling = computed(() => props.server.general?.status === "installing");
|
||||
|
||||
const versionSelectModal = ref();
|
||||
const mrpackModal = ref();
|
||||
const modpackVersionModal = ref();
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
|
||||
const {
|
||||
data: versions,
|
||||
error: versionsError,
|
||||
refresh: refreshVersions,
|
||||
} = await useAsyncData(
|
||||
`content-loader-versions-${data.value?.upstream?.project_id}`,
|
||||
async () => {
|
||||
if (!data.value?.upstream?.project_id) return [];
|
||||
try {
|
||||
const result = await useBaseFetch(`project/${data.value.upstream.project_id}/version`);
|
||||
return result || [];
|
||||
} catch (e) {
|
||||
console.error("couldnt fetch all versions:", e);
|
||||
throw new Error("Failed to load modpack versions.");
|
||||
}
|
||||
},
|
||||
{ default: () => [] },
|
||||
);
|
||||
|
||||
const {
|
||||
data: currentVersion,
|
||||
error: currentVersionError,
|
||||
refresh: refreshCurrentVersion,
|
||||
} = await useAsyncData(
|
||||
`content-loader-version-${data.value?.upstream?.version_id}`,
|
||||
async () => {
|
||||
if (!data.value?.upstream?.version_id) return null;
|
||||
try {
|
||||
const result = await useBaseFetch(`version/${data.value.upstream.version_id}`);
|
||||
return result || null;
|
||||
} catch (e) {
|
||||
console.error("couldnt fetch version:", e);
|
||||
throw new Error("Failed to load modpack version.");
|
||||
}
|
||||
},
|
||||
{ default: () => null },
|
||||
);
|
||||
|
||||
const projectCardData = computed(() => ({
|
||||
icon_url: data.value?.project?.icon_url,
|
||||
title: data.value?.project?.title,
|
||||
description: data.value?.project?.description,
|
||||
downloads: data.value?.project?.downloads,
|
||||
follows: data.value?.project?.followers,
|
||||
// @ts-ignore
|
||||
date_modified: currentVersion.value?.date_published || data.value?.project?.updated,
|
||||
}));
|
||||
|
||||
const selectLoader = (loader: string) => {
|
||||
versionSelectModal.value?.show(loader as Loaders);
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
await Promise.all([refreshVersions(), refreshCurrentVersion()]);
|
||||
};
|
||||
|
||||
const updateAvailable = computed(() => {
|
||||
// so sorry
|
||||
// @ts-ignore
|
||||
if (!data.value?.upstream || !versions.value?.length || !currentVersion.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const latestVersion = versions.value[0];
|
||||
// @ts-ignore
|
||||
return latestVersion.id !== currentVersion.value.id;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.server.general?.status,
|
||||
async (newStatus, oldStatus) => {
|
||||
if (oldStatus === "installing" && newStatus === "available") {
|
||||
await Promise.all([
|
||||
refreshVersions(),
|
||||
refreshCurrentVersion(),
|
||||
props.server.refresh(["general"]),
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stylized-toggle:checked::after {
|
||||
background: var(--color-accent-contrast) !important;
|
||||
}
|
||||
|
||||
.button-base:active {
|
||||
scale: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -43,7 +43,14 @@
|
||||
</div>
|
||||
<div v-else class="min-h-[20px]"></div>
|
||||
|
||||
<div
|
||||
v-if="isConfiguring"
|
||||
class="flex min-w-0 items-center gap-2 truncate text-sm font-semibold text-brand"
|
||||
>
|
||||
<SparklesIcon class="size-5 shrink-0" /> New server
|
||||
</div>
|
||||
<UiServersServerInfoLabels
|
||||
v-else
|
||||
:server-data="{ game, mc_version, loader, loader_version, net }"
|
||||
:show-game-label="showGameLabel"
|
||||
:show-loader-label="showLoaderLabel"
|
||||
@@ -73,7 +80,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, HammerIcon, LockIcon } from "@modrinth/assets";
|
||||
import { ChevronRightIcon, LockIcon, SparklesIcon } from "@modrinth/assets";
|
||||
import type { Project, Server } from "~/types/servers";
|
||||
|
||||
const props = defineProps<Partial<Server>>();
|
||||
@@ -106,4 +113,5 @@ if (import.meta.server && projectData.value?.icon_url) {
|
||||
}
|
||||
|
||||
const iconUrl = computed(() => projectData.value?.icon_url || undefined);
|
||||
const isConfiguring = computed(() => props.flows?.intro);
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { RightArrowIcon, SparklesIcon, UnknownIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, ServersSpecs } from "@modrinth/ui";
|
||||
import type { MessageDescriptor } from "@vintl/vintl";
|
||||
import { formatPrice } from "../../../../../../../packages/utils";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
const { formatMessage, locale } = useVIntl();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "select" | "scroll-to-faq"): void;
|
||||
@@ -18,8 +18,8 @@ const plans: Record<
|
||||
accentText: string;
|
||||
accentBg: string;
|
||||
name: MessageDescriptor;
|
||||
symbol: MessageDescriptor;
|
||||
description: MessageDescriptor;
|
||||
mostPopular: boolean;
|
||||
}
|
||||
> = {
|
||||
small: {
|
||||
@@ -30,15 +30,11 @@ const plans: Record<
|
||||
id: "servers.plan.small.name",
|
||||
defaultMessage: "Small",
|
||||
}),
|
||||
symbol: defineMessage({
|
||||
id: "servers.plan.small.symbol",
|
||||
defaultMessage: "S",
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: "servers.plan.small.description",
|
||||
defaultMessage:
|
||||
"Perfect for vanilla multiplayer, small friend groups, SMPs, and light modding.",
|
||||
defaultMessage: "Perfect for 1–5 friends with a few light mods.",
|
||||
}),
|
||||
mostPopular: false,
|
||||
},
|
||||
medium: {
|
||||
buttonColor: "green",
|
||||
@@ -48,14 +44,11 @@ const plans: Record<
|
||||
id: "servers.plan.medium.name",
|
||||
defaultMessage: "Medium",
|
||||
}),
|
||||
symbol: defineMessage({
|
||||
id: "servers.plan.medium.symbol",
|
||||
defaultMessage: "M",
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: "servers.plan.medium.description",
|
||||
defaultMessage: "Great for modded multiplayer and small communities.",
|
||||
defaultMessage: "Great for 6–15 players and multiple mods.",
|
||||
}),
|
||||
mostPopular: true,
|
||||
},
|
||||
large: {
|
||||
buttonColor: "purple",
|
||||
@@ -65,14 +58,11 @@ const plans: Record<
|
||||
id: "servers.plan.large.name",
|
||||
defaultMessage: "Large",
|
||||
}),
|
||||
symbol: defineMessage({
|
||||
id: "servers.plan.large.symbol",
|
||||
defaultMessage: "L",
|
||||
}),
|
||||
description: defineMessage({
|
||||
id: "servers.plan.large.description",
|
||||
defaultMessage: "Ideal for larger communities, modpacks, and heavy modding.",
|
||||
defaultMessage: "Ideal for 15–25 players, modpacks, or heavy modding.",
|
||||
}),
|
||||
mostPopular: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -83,42 +73,30 @@ const props = defineProps<{
|
||||
storage: number;
|
||||
cpus: number;
|
||||
price: number;
|
||||
interval: "monthly" | "quarterly" | "yearly";
|
||||
currency: string;
|
||||
isUsa: boolean;
|
||||
}>();
|
||||
|
||||
const outOfStock = computed(() => {
|
||||
return !props.capacity || props.capacity === 0;
|
||||
});
|
||||
|
||||
const lowStock = computed(() => {
|
||||
return !props.capacity || props.capacity < 8;
|
||||
});
|
||||
|
||||
const formattedRam = computed(() => {
|
||||
return props.ram / 1024;
|
||||
});
|
||||
|
||||
const formattedStorage = computed(() => {
|
||||
return props.storage / 1024;
|
||||
});
|
||||
|
||||
const sharedCpus = computed(() => {
|
||||
return props.cpus / 2;
|
||||
const billingMonths = computed(() => {
|
||||
if (props.interval === "yearly") {
|
||||
return 12;
|
||||
} else if (props.interval === "quarterly") {
|
||||
return 3;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="relative flex w-full flex-col justify-between pt-12 lg:w-1/3">
|
||||
<div
|
||||
v-if="lowStock"
|
||||
class="absolute left-0 right-0 top-[-2px] rounded-t-2xl p-4 text-center font-bold"
|
||||
:class="outOfStock ? 'bg-bg-red' : 'bg-bg-orange'"
|
||||
>
|
||||
<template v-if="outOfStock"> Out of stock! </template>
|
||||
<template v-else> Only {{ capacity }} left in stock! </template>
|
||||
</div>
|
||||
<li class="relative flex w-full flex-col justify-between">
|
||||
<div
|
||||
:style="
|
||||
plan === 'medium'
|
||||
plans[plan].mostPopular
|
||||
? {
|
||||
background: `radial-gradient(
|
||||
86.12% 101.64% at 95.97% 94.07%,
|
||||
@@ -131,55 +109,41 @@ const sharedCpus = computed(() => {
|
||||
: undefined
|
||||
"
|
||||
class="flex w-full flex-col justify-between gap-4 rounded-2xl bg-bg p-8 text-left"
|
||||
:class="{ '!rounded-t-none': lowStock }"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row flex-wrap items-center gap-3">
|
||||
<h1 class="m-0">{{ formatMessage(plans[plan].name) }}</h1>
|
||||
<div
|
||||
class="grid size-8 place-content-center rounded-full text-xs font-bold"
|
||||
:class="`${plans[plan].accentBg} ${plans[plan].accentText}`"
|
||||
v-if="plans[plan].mostPopular"
|
||||
class="rounded-full bg-brand-highlight px-2 py-1 text-xs font-bold text-brand"
|
||||
>
|
||||
{{ formatMessage(plans[plan].symbol) }}
|
||||
Most popular
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0">{{ formatMessage(plans[plan].description) }}</p>
|
||||
<div
|
||||
class="flex flex-row flex-wrap items-center gap-2 text-nowrap text-secondary xl:justify-between"
|
||||
>
|
||||
<p class="m-0">{{ formattedRam }} GB RAM</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">{{ formattedStorage }} GB SSD</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">{{ sharedCpus }} Shared CPUs</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-secondary">
|
||||
<SparklesIcon /> Bursts up to {{ cpus }} CPUs
|
||||
<nuxt-link
|
||||
v-tooltip="
|
||||
`CPU bursting allows your server to temporarily use additional threads to help mitigate TPS spikes. Click for more info.`
|
||||
"
|
||||
to="/servers#cpu-burst"
|
||||
@click="() => emit('scroll-to-faq')"
|
||||
>
|
||||
<UnknownIcon class="h-4 w-4 text-secondary opacity-80" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<span class="m-0 text-2xl font-bold text-contrast">
|
||||
${{ price / 100 }}<span class="text-lg font-semibold text-secondary">/month</span>
|
||||
{{ formatPrice(locale, price / billingMonths, currency, true) }}
|
||||
{{ isUsa ? "" : currency }}
|
||||
<span class="text-lg font-semibold text-secondary">
|
||||
/ month<template v-if="interval !== 'monthly'">, billed {{ interval }}</template>
|
||||
</span>
|
||||
</span>
|
||||
<p class="m-0 max-w-[18rem]">{{ formatMessage(plans[plan].description) }}</p>
|
||||
</div>
|
||||
<ButtonStyled
|
||||
:color="plans[plan].buttonColor"
|
||||
:type="plan === 'medium' ? 'standard' : 'highlight-colored-text'"
|
||||
:type="plans[plan].mostPopular ? 'standard' : 'highlight-colored-text'"
|
||||
size="large"
|
||||
>
|
||||
<span v-if="outOfStock" class="button-like disabled"> Out of Stock </span>
|
||||
<button v-else @click="() => emit('select')">
|
||||
Get Started
|
||||
<RightArrowIcon class="shrink-0" />
|
||||
</button>
|
||||
<button v-else @click="() => emit('select')">Select plan</button>
|
||||
</ButtonStyled>
|
||||
<ServersSpecs
|
||||
:ram="ram"
|
||||
:storage="storage"
|
||||
:cpus="cpus"
|
||||
:bursting-link="'/servers#cpu-burst'"
|
||||
@click-bursting-link="() => emit('scroll-to-faq')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -24,6 +24,7 @@ export const useUserCountry = () => {
|
||||
if (import.meta.client) {
|
||||
onMounted(() => {
|
||||
if (fromServer.value) return;
|
||||
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
|
||||
const lang = navigator.language || navigator.userLanguage || "";
|
||||
const region = lang.split("-")[1];
|
||||
if (region) {
|
||||
|
||||
@@ -49,7 +49,9 @@ export async function usePyroFetch<T>(path: string, options: PyroFetchOptions =
|
||||
|
||||
const fullUrl = override?.url
|
||||
? `https://${override.url}/${path.replace(/^\//, "")}`
|
||||
: `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`;
|
||||
: version === 0
|
||||
? `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`
|
||||
: `${base}/v${version}/${path.replace(/^\//, "")}`;
|
||||
|
||||
type HeadersRecord = Record<string, string>;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { $fetch, FetchError } from "ofetch";
|
||||
import type { ServerNotice } from "@modrinth/utils";
|
||||
import type { FilesystemOp, FSQueuedOp, WSBackupState, WSBackupTask } from "~/types/servers.ts";
|
||||
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
||||
|
||||
interface PyroFetchOptions {
|
||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
@@ -330,6 +331,9 @@ interface General {
|
||||
token: string;
|
||||
instance: string;
|
||||
};
|
||||
flows?: {
|
||||
intro?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface Allocation {
|
||||
@@ -1221,6 +1225,13 @@ const modules: any = {
|
||||
suspend: suspendServer,
|
||||
getMotd,
|
||||
setMotd,
|
||||
endIntro: async () => {
|
||||
await usePyroFetch(`servers/${internalServerReference.value.serverId}/flows/intro`, {
|
||||
method: "DELETE",
|
||||
version: 1,
|
||||
});
|
||||
await internalServerReference.value.refresh(["general"]);
|
||||
},
|
||||
},
|
||||
content: {
|
||||
get: async (serverId: string) => {
|
||||
@@ -1451,6 +1462,8 @@ type GeneralFunctions = {
|
||||
* @deprecated Use fs.downloadFile instead
|
||||
*/
|
||||
fetchConfigFile: (fileName: string) => Promise<any>;
|
||||
|
||||
endIntro: () => Promise<void>;
|
||||
};
|
||||
|
||||
type ContentFunctions = {
|
||||
|
||||
@@ -77,6 +77,9 @@ const errorMessages = computed(
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// TODO: REMOVE BEFORE MERGE
|
||||
console.log(props.error);
|
||||
|
||||
watch(route, () => {
|
||||
console.log(route);
|
||||
});
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
"message": "Subscribe to updates about Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "You’re now part of the awesome community of creators & explorers already building, downloading, and staying up-to-date with amazing mods."
|
||||
"message": "You’re now part of the awesome community of creators & explorers already building, downloading, and staying up-to-date with awazing mods."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "By creating an account, you have agreed to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>."
|
||||
@@ -350,11 +350,14 @@
|
||||
"layout.banner.add-email.button": {
|
||||
"message": "Visit account settings"
|
||||
},
|
||||
"layout.banner.add-email.description": {
|
||||
"message": "For security reasons, Modrinth needs you to register an email address to your account."
|
||||
},
|
||||
"layout.banner.build-fail.description": {
|
||||
"message": "This deploy of Modrinth's frontend failed to generate state from the API. This may be due to an outage or an error in configuration. Rebuild when the API is available. Error codes: {errors}; Current API URL is: {url}"
|
||||
},
|
||||
"layout.banner.build-fail.title": {
|
||||
"message": "Error generating state from API when building"
|
||||
"message": "Error generating state from API when building."
|
||||
},
|
||||
"layout.banner.staging.description": {
|
||||
"message": "The staging environment is completely separate from the production Modrinth database. This is used for testing and debugging purposes, and may be running in-development versions of the Modrinth backend or frontend newer than the production instance."
|
||||
@@ -365,12 +368,12 @@
|
||||
"layout.banner.subscription-payment-failed.button": {
|
||||
"message": "Update billing info"
|
||||
},
|
||||
"layout.banner.subscription-payment-failed.title": {
|
||||
"message": "Billing action required"
|
||||
},
|
||||
"layout.banner.subscription-payment-failed.description": {
|
||||
"message": "One or more subscriptions failed to renew. Please update your payment method to prevent losing access!"
|
||||
},
|
||||
"layout.banner.subscription-payment-failed.title": {
|
||||
"message": "Billing action required."
|
||||
},
|
||||
"layout.banner.verify-email.action": {
|
||||
"message": "Re-send verification email"
|
||||
},
|
||||
@@ -1047,32 +1050,23 @@
|
||||
"message": "No notices"
|
||||
},
|
||||
"servers.plan.large.description": {
|
||||
"message": "Ideal for larger communities, modpacks, and heavy modding."
|
||||
"message": "Ideal for 15–25 players, modpacks, or heavy modding."
|
||||
},
|
||||
"servers.plan.large.name": {
|
||||
"message": "Large"
|
||||
},
|
||||
"servers.plan.large.symbol": {
|
||||
"message": "L"
|
||||
},
|
||||
"servers.plan.medium.description": {
|
||||
"message": "Great for modded multiplayer and small communities."
|
||||
"message": "Great for 6–15 players and multiple mods."
|
||||
},
|
||||
"servers.plan.medium.name": {
|
||||
"message": "Medium"
|
||||
},
|
||||
"servers.plan.medium.symbol": {
|
||||
"message": "M"
|
||||
},
|
||||
"servers.plan.small.description": {
|
||||
"message": "Perfect for vanilla multiplayer, small friend groups, SMPs, and light modding."
|
||||
"message": "Perfect for 1–5 friends with a few light mods."
|
||||
},
|
||||
"servers.plan.small.name": {
|
||||
"message": "Small"
|
||||
},
|
||||
"servers.plan.small.symbol": {
|
||||
"message": "S"
|
||||
},
|
||||
"settings.billing.modal.cancel.action": {
|
||||
"message": "Cancel subscription"
|
||||
},
|
||||
|
||||
@@ -4,27 +4,28 @@
|
||||
data-pyro
|
||||
class="servers-hero relative isolate -mt-44 h-full min-h-screen pt-8"
|
||||
>
|
||||
<PurchaseModal
|
||||
v-if="showModal && selectedProduct && customer"
|
||||
:key="selectedProduct.id"
|
||||
<ModrinthServersPurchaseModal
|
||||
v-if="customer"
|
||||
:key="`purchase-modal-${customer.id}`"
|
||||
ref="purchaseModal"
|
||||
:product="selectedProduct"
|
||||
:country="country"
|
||||
:custom-server="customServer"
|
||||
:publishable-key="config.public.stripePublishableKey"
|
||||
:send-billing-request="
|
||||
:initiate-payment="
|
||||
async (body) =>
|
||||
await useBaseFetch('billing/payment', { internal: true, method: 'POST', body })
|
||||
"
|
||||
:fetch-payment-data="fetchPaymentData"
|
||||
:available-products="pyroProducts"
|
||||
:on-error="handleError"
|
||||
:customer="customer"
|
||||
:payment-methods="paymentMethods"
|
||||
:currency="selectedCurrency"
|
||||
:return-url="`${config.public.siteUrl}/servers/manage`"
|
||||
:server-name="`${auth?.user?.username}'s server`"
|
||||
:fetch-capacity-statuses="fetchCapacityStatuses"
|
||||
:out-of-stock-url="outOfStockUrl"
|
||||
@hidden="handleModalHidden"
|
||||
:fetch-capacity-statuses="fetchCapacityStatuses"
|
||||
:pings="regionPings"
|
||||
:regions="regions"
|
||||
:refresh-payment-methods="fetchPaymentData"
|
||||
:fetch-stock="fetchStock"
|
||||
/>
|
||||
|
||||
<section
|
||||
@@ -442,8 +443,8 @@
|
||||
Where are Modrinth Servers located? Can I choose a region?
|
||||
</summary>
|
||||
<p class="m-0 ml-6 leading-[160%]">
|
||||
Currently, Modrinth Servers are located on the east coast of the United States in
|
||||
Vint Hill, Virginia. More regions to come in the future!
|
||||
We have servers in both North America in Vint Hill, Virginia, and Europe in Limburg,
|
||||
Germany. More regions to come in the future!
|
||||
</p>
|
||||
</details>
|
||||
|
||||
@@ -497,98 +498,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="false"
|
||||
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
|
||||
>
|
||||
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
|
||||
<div class="mx-auto flex w-full max-w-7xl flex-col gap-8">
|
||||
<div class="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
class="relative w-fit rounded-full bg-highlight-green px-3 py-1 text-sm font-bold text-brand backdrop-blur-lg"
|
||||
>
|
||||
Server Locations
|
||||
</div>
|
||||
<h1 class="relative m-0 max-w-2xl text-4xl leading-[120%] md:text-7xl">
|
||||
Coast-to-Coast Coverage
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid size-8 place-content-center rounded-full bg-highlight-green">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="text-brand"
|
||||
>
|
||||
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="relative m-0 text-xl font-medium leading-[155%] md:text-2xl">
|
||||
US Coverage
|
||||
</h2>
|
||||
</div>
|
||||
<p
|
||||
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
|
||||
>
|
||||
With strategically placed servers in New York, California, Texas, Florida, and
|
||||
Washington, we ensure low latency connections for players across North America.
|
||||
Each location is equipped with high-performance hardware and DDoS protection.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid size-8 place-content-center rounded-full bg-highlight-blue">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="text-blue"
|
||||
>
|
||||
<path d="M12 2a10 10 0 1 0 10 10" />
|
||||
<path d="M18 13a6 6 0 0 0-6-6" />
|
||||
<path d="M13 2.05a10 10 0 0 1 2 2" />
|
||||
<path d="M19.5 8.5a10 10 0 0 1 2 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="relative m-0 text-xl font-medium leading-[155%] md:text-2xl">
|
||||
Global Expansion
|
||||
</h2>
|
||||
</div>
|
||||
<p
|
||||
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
|
||||
>
|
||||
We're expanding to Europe and Asia-Pacific regions soon, bringing Modrinth's
|
||||
seamless hosting experience worldwide. Join our Discord to stay updated on new
|
||||
region launches.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Globe />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="plan"
|
||||
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
|
||||
@@ -596,19 +505,35 @@
|
||||
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
|
||||
<div class="mx-auto flex w-full max-w-7xl flex-col items-center gap-8 text-center">
|
||||
<h1 class="relative m-0 text-4xl leading-[120%] md:text-7xl">
|
||||
Start your server on Modrinth
|
||||
There's a server for everyone
|
||||
</h1>
|
||||
<h2
|
||||
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
|
||||
>
|
||||
{{
|
||||
isAtCapacity && !loggedOut
|
||||
? "We are currently at capacity. Please try again later."
|
||||
: "There's a plan for everyone! Choose the one that fits your needs."
|
||||
}}
|
||||
</h2>
|
||||
<p class="m-0 flex items-center gap-1">
|
||||
Available in North America and Europe for wide coverage.
|
||||
</p>
|
||||
|
||||
<ul class="m-0 mt-8 flex w-full flex-col gap-8 p-0 lg:flex-row">
|
||||
<div class="grid grid-cols-[1fr_auto_1fr] items-center gap-3">
|
||||
<span></span>
|
||||
<OptionGroup v-slot="{ option }" v-model="billingPeriod" :options="billingPeriods">
|
||||
<template v-if="option === 'monthly'"> Pay monthly </template>
|
||||
<span v-else-if="option === 'quarterly'"> Pay quarterly </span>
|
||||
<span v-else-if="option === 'yearly'"> Pay yearly </span>
|
||||
</OptionGroup>
|
||||
<template v-if="billingPeriods.includes('quarterly')">
|
||||
<button
|
||||
v-if="billingPeriod !== 'quarterly'"
|
||||
class="bg-transparent p-0 text-sm font-medium text-brand hover:underline active:scale-95"
|
||||
@click="billingPeriod = 'quarterly'"
|
||||
>
|
||||
Save 16% with quarterly billing!
|
||||
</button>
|
||||
<span v-else class="bg-transparent p-0 text-sm font-medium text-brand">
|
||||
Save 16% with quarterly billing!
|
||||
</span>
|
||||
</template>
|
||||
<span v-else></span>
|
||||
</div>
|
||||
|
||||
<ul class="m-0 flex w-full grid-cols-3 flex-col gap-8 p-0 lg:grid">
|
||||
<ServerPlanSelector
|
||||
:capacity="capacityStatuses?.small?.available"
|
||||
plan="small"
|
||||
@@ -616,9 +541,12 @@
|
||||
:storage="plans.small.metadata.storage"
|
||||
:cpus="plans.small.metadata.cpu"
|
||||
:price="
|
||||
plans.small?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
|
||||
?.monthly
|
||||
plans.small?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
|
||||
?.intervals?.[billingPeriod]
|
||||
"
|
||||
:interval="billingPeriod"
|
||||
:currency="selectedCurrency"
|
||||
:is-usa="country.toLowerCase() === 'us'"
|
||||
@select="selectProduct('small')"
|
||||
@scroll-to-faq="scrollToFaq()"
|
||||
/>
|
||||
@@ -629,9 +557,12 @@
|
||||
:storage="plans.medium.metadata.storage"
|
||||
:cpus="plans.medium.metadata.cpu"
|
||||
:price="
|
||||
plans.medium?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
|
||||
?.monthly
|
||||
plans.medium?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
|
||||
?.intervals?.[billingPeriod]
|
||||
"
|
||||
:interval="billingPeriod"
|
||||
:currency="selectedCurrency"
|
||||
:is-usa="country.toLowerCase() === 'us'"
|
||||
@select="selectProduct('medium')"
|
||||
@scroll-to-faq="scrollToFaq()"
|
||||
/>
|
||||
@@ -641,10 +572,13 @@
|
||||
:storage="plans.large.metadata.storage"
|
||||
:cpus="plans.large.metadata.cpu"
|
||||
:price="
|
||||
plans.large?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
|
||||
?.monthly
|
||||
plans.large?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
|
||||
?.intervals?.[billingPeriod]
|
||||
"
|
||||
:currency="selectedCurrency"
|
||||
:is-usa="country.toLowerCase() === 'us'"
|
||||
plan="large"
|
||||
:interval="billingPeriod"
|
||||
@select="selectProduct('large')"
|
||||
@scroll-to-faq="scrollToFaq()"
|
||||
/>
|
||||
@@ -654,10 +588,9 @@
|
||||
class="mb-24 flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left lg:flex-row lg:gap-0"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="m-0">Build your own</h1>
|
||||
<h1 class="m-0">Know exactly what you need?</h1>
|
||||
<h2 class="m-0 text-base font-normal text-primary">
|
||||
If you're a more technical server administrator, you can pick your own RAM and storage
|
||||
options.
|
||||
Pick a customized plan with just the specs you need.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -666,7 +599,7 @@
|
||||
>
|
||||
<ButtonStyled color="standard" size="large">
|
||||
<button class="w-full md:w-fit" @click="selectProduct('custom')">
|
||||
Build your own
|
||||
Get started
|
||||
<RightArrowIcon class="shrink-0" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -679,7 +612,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ButtonStyled, PurchaseModal } from "@modrinth/ui";
|
||||
import { ButtonStyled, ModrinthServersPurchaseModal } from "@modrinth/ui";
|
||||
import {
|
||||
BoxIcon,
|
||||
GameIcon,
|
||||
@@ -691,8 +624,11 @@ import {
|
||||
} from "@modrinth/assets";
|
||||
import { products } from "~/generated/state.json";
|
||||
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
|
||||
import Globe from "~/components/ui/servers/Globe.vue";
|
||||
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
|
||||
import OptionGroup from "~/components/ui/OptionGroup.vue";
|
||||
|
||||
const billingPeriods = ref(["monthly", "quarterly"]);
|
||||
const billingPeriod = ref(billingPeriods.value.includes("quarterly") ? "quarterly" : "monthly");
|
||||
|
||||
const pyroProducts = products.filter((p) => p.metadata.type === "pyro");
|
||||
const pyroPlanProducts = pyroProducts.filter(
|
||||
@@ -711,16 +647,6 @@ useSeoMeta({
|
||||
ogDescription: description,
|
||||
});
|
||||
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
src: "https://js.stripe.com/v3/",
|
||||
defer: true,
|
||||
async: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auth = await useAuth();
|
||||
const data = useNuxtApp();
|
||||
const config = useRuntimeConfig();
|
||||
@@ -740,6 +666,7 @@ const isDeleting = ref(false);
|
||||
const typingSpeed = 75;
|
||||
const deletingSpeed = 25;
|
||||
const pauseTime = 2000;
|
||||
const selectedCurrency = ref("USD");
|
||||
|
||||
const loggedOut = computed(() => !auth.value.user);
|
||||
const outOfStockUrl = "https://discord.modrinth.com";
|
||||
@@ -754,6 +681,16 @@ const { data: hasServers } = await useAsyncData("ServerListCountCheck", async ()
|
||||
}
|
||||
});
|
||||
|
||||
function fetchStock(region, request) {
|
||||
return usePyroFetch(`stock?region=${region.shortcode}`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
...request,
|
||||
},
|
||||
bypassAuth: true,
|
||||
}).then((res) => res.available);
|
||||
}
|
||||
|
||||
async function fetchCapacityStatuses(customProduct = null) {
|
||||
try {
|
||||
const productsToCheck = customProduct?.metadata
|
||||
@@ -841,23 +778,6 @@ const handleError = (err) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleModalHidden = () => {
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
watch(selectedProduct, async (newProduct) => {
|
||||
if (newProduct) {
|
||||
showModal.value = false;
|
||||
await nextTick();
|
||||
showModal.value = true;
|
||||
modalKey.value++;
|
||||
await nextTick();
|
||||
if (purchaseModal.value && purchaseModal.value.show) {
|
||||
purchaseModal.value.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchPaymentData() {
|
||||
if (!auth.value.user) return;
|
||||
try {
|
||||
@@ -954,8 +874,10 @@ const selectProduct = async (product) => {
|
||||
modalKey.value++;
|
||||
await nextTick();
|
||||
|
||||
if (purchaseModal.value && purchaseModal.value.show) {
|
||||
purchaseModal.value.show();
|
||||
if (product === "custom") {
|
||||
purchaseModal.value?.show(billingPeriod.value);
|
||||
} else {
|
||||
purchaseModal.value?.show(billingPeriod.value, selectedProduct.value);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -966,9 +888,82 @@ const planQuery = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const regions = ref([]);
|
||||
const regionPings = ref([]);
|
||||
|
||||
function pingRegions() {
|
||||
usePyroFetch("regions", {
|
||||
method: "GET",
|
||||
version: 1,
|
||||
bypassAuth: true,
|
||||
}).then((res) => {
|
||||
regions.value = res;
|
||||
regions.value.forEach((region) => {
|
||||
runPingTest(region);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const PING_COUNT = 20;
|
||||
const PING_INTERVAL = 200;
|
||||
const MAX_PING_TIME = 1000;
|
||||
|
||||
function runPingTest(region, index = 1) {
|
||||
if (index > 10) {
|
||||
regionPings.value.push({
|
||||
region: region.shortcode,
|
||||
ping: -1,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const wsUrl = `wss://${region.shortcode}${index}.${region.zone}/pingtest`;
|
||||
try {
|
||||
const socket = new WebSocket(wsUrl);
|
||||
const pings = [];
|
||||
|
||||
socket.onopen = () => {
|
||||
for (let i = 0; i < PING_COUNT; i++) {
|
||||
setTimeout(() => {
|
||||
socket.send(performance.now());
|
||||
}, i * PING_INTERVAL);
|
||||
}
|
||||
setTimeout(
|
||||
() => {
|
||||
socket.close();
|
||||
|
||||
const median = Math.round([...pings].sort((a, b) => a - b)[Math.floor(pings.length / 2)]);
|
||||
if (median) {
|
||||
regionPings.value.push({
|
||||
region: region.shortcode,
|
||||
ping: median,
|
||||
});
|
||||
}
|
||||
},
|
||||
PING_COUNT * PING_INTERVAL + MAX_PING_TIME,
|
||||
);
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
pings.push(performance.now() - event.data);
|
||||
};
|
||||
|
||||
socket.onerror = (event) => {
|
||||
console.error(
|
||||
`Failed to connect pingtest WebSocket with ${wsUrl}, trying index ${index + 1}:`,
|
||||
event,
|
||||
);
|
||||
runPingTest(region, index + 1);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to connect pingtest WebSocket with ${wsUrl}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startTyping();
|
||||
planQuery();
|
||||
pingRegions();
|
||||
});
|
||||
|
||||
watch(customer, (newCustomer) => {
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
class="server-action-buttons-anim flex w-fit flex-shrink-0"
|
||||
>
|
||||
<UiServersPanelServerActionButton
|
||||
v-if="!serverData.flows?.intro"
|
||||
class="flex-shrink-0"
|
||||
:is-online="isServerRunning"
|
||||
:is-actioning="isActioning"
|
||||
@@ -220,7 +221,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="serverData.flows?.intro"
|
||||
class="flex items-center gap-2 font-semibold text-secondary"
|
||||
>
|
||||
<SettingsIcon /> Configuring server...
|
||||
</div>
|
||||
<UiServersServerInfoLabels
|
||||
v-else
|
||||
:server-data="serverData"
|
||||
:show-game-label="showGameLabel"
|
||||
:show-loader-label="showLoaderLabel"
|
||||
@@ -231,149 +239,189 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-pyro-navigation
|
||||
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
|
||||
>
|
||||
<UiNavTabs :links="navLinks" />
|
||||
</div>
|
||||
|
||||
<div data-pyro-mount class="h-full w-full flex-1">
|
||||
<template v-if="serverData.flows?.intro">
|
||||
<div
|
||||
v-if="error"
|
||||
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
||||
v-if="serverData?.status === 'installing'"
|
||||
class="w-50 h-50 flex items-center justify-center gap-2 text-center text-lg font-bold"
|
||||
>
|
||||
<div class="flex flex-row gap-4">
|
||||
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
|
||||
<div class="flex flex-col gap-2 leading-[150%]">
|
||||
<div class="flex items-center gap-3">
|
||||
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
|
||||
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
|
||||
</div>
|
||||
<LazyUiServersPanelSpinner class="size-10 animate-spin" /> Setting up your server...
|
||||
</div>
|
||||
<div v-else>
|
||||
<h2 class="my-4 text-xl font-extrabold">
|
||||
What would you like to install on your new server?
|
||||
</h2>
|
||||
|
||||
<div v-if="errorTitle.toLocaleLowerCase() === 'installation error'" class="font-normal">
|
||||
<div
|
||||
v-if="errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'"
|
||||
>
|
||||
An invalid loader or Minecraft version was specified and could not be installed.
|
||||
<ul class="m-0 mt-4 p-0 pl-4">
|
||||
<li>
|
||||
If this version of Minecraft was released recently, please check if Modrinth
|
||||
Servers supports it.
|
||||
</li>
|
||||
<li>
|
||||
If you've installed a modpack, it may have been packaged incorrectly or may not
|
||||
be compatible with the loader.
|
||||
</li>
|
||||
<li>
|
||||
Your server may need to be reinstalled with a valid mod loader and version. You
|
||||
can change the loader by clicking the "Change Loader" button.
|
||||
</li>
|
||||
<li>
|
||||
If you're stuck, please contact Modrinth Support with the information below:
|
||||
</li>
|
||||
</ul>
|
||||
<ButtonStyled>
|
||||
<button class="mt-2" @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
|
||||
An internal error occurred while installing your server. Don't fret — try
|
||||
reinstalling your server, and if the problem persists, please contact Modrinth
|
||||
support with your server's debug information.
|
||||
</div>
|
||||
<div v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'">
|
||||
An error occurred while installing your server because Modrinth Servers does not
|
||||
support the version of Minecraft or the loader you specified. Try reinstalling your
|
||||
server with a different version or loader, and if the problem persists, please
|
||||
contact Modrinth Support with your server's debug information.
|
||||
<ServerInstallation
|
||||
:server="server"
|
||||
:backup-in-progress="backupInProgress"
|
||||
ignore-current-installation
|
||||
@reinstall="onReinstall"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
data-pyro-navigation
|
||||
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
|
||||
>
|
||||
<UiNavTabs :links="navLinks" />
|
||||
</div>
|
||||
|
||||
<div data-pyro-mount class="h-full w-full flex-1">
|
||||
<div
|
||||
v-if="error"
|
||||
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-4">
|
||||
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
|
||||
<div class="flex flex-col gap-2 leading-[150%]">
|
||||
<div class="flex items-center gap-3">
|
||||
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
|
||||
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorTitle === 'Installation error'"
|
||||
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
||||
v-if="errorTitle.toLocaleLowerCase() === 'installation error'"
|
||||
class="font-normal"
|
||||
>
|
||||
<ButtonStyled v-if="errorLog">
|
||||
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red" type="standard">
|
||||
<NuxtLink
|
||||
class="whitespace-pre"
|
||||
:to="`/servers/manage/${serverId}/options/loader`"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
Change Loader
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
<div
|
||||
v-if="
|
||||
errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'
|
||||
"
|
||||
>
|
||||
An invalid loader or Minecraft version was specified and could not be installed.
|
||||
<ul class="m-0 mt-4 p-0 pl-4">
|
||||
<li>
|
||||
If this version of Minecraft was released recently, please check if Modrinth
|
||||
Servers supports it.
|
||||
</li>
|
||||
<li>
|
||||
If you've installed a modpack, it may have been packaged incorrectly or may
|
||||
not be compatible with the loader.
|
||||
</li>
|
||||
<li>
|
||||
Your server may need to be reinstalled with a valid mod loader and version.
|
||||
You can change the loader by clicking the "Change Loader" button.
|
||||
</li>
|
||||
<li>
|
||||
If you're stuck, please contact Modrinth Support with the information below:
|
||||
</li>
|
||||
</ul>
|
||||
<ButtonStyled>
|
||||
<button class="mt-2" @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
|
||||
An internal error occurred while installing your server. Don't fret — try
|
||||
reinstalling your server, and if the problem persists, please contact Modrinth
|
||||
support with your server's debug information.
|
||||
</div>
|
||||
<div
|
||||
v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'"
|
||||
>
|
||||
An error occurred while installing your server because Modrinth Servers does not
|
||||
support the version of Minecraft or the loader you specified. Try reinstalling
|
||||
your server with a different version or loader, and if the problem persists,
|
||||
please contact Modrinth Support with your server's debug information.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorTitle === 'Installation error'"
|
||||
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
||||
>
|
||||
<ButtonStyled v-if="errorLog">
|
||||
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red" type="standard">
|
||||
<NuxtLink
|
||||
class="whitespace-pre"
|
||||
:to="`/servers/manage/${serverId}/options/loader`"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
Change Loader
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isConnected && !isReconnecting && !isLoading"
|
||||
data-pyro-server-ws-error
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
|
||||
>
|
||||
<IssuesIcon class="size-5 text-red" />
|
||||
Something went wrong...
|
||||
</div>
|
||||
<div
|
||||
v-if="!isConnected && !isReconnecting && !isLoading"
|
||||
data-pyro-server-ws-error
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
|
||||
>
|
||||
<IssuesIcon class="size-5 text-red" />
|
||||
Something went wrong...
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isReconnecting"
|
||||
data-pyro-server-ws-reconnecting
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
|
||||
>
|
||||
<UiServersPanelSpinner />
|
||||
Hang on, we're reconnecting to your server.
|
||||
</div>
|
||||
<div
|
||||
v-if="isReconnecting"
|
||||
data-pyro-server-ws-reconnecting
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
|
||||
>
|
||||
<UiServersPanelSpinner />
|
||||
Hang on, we're reconnecting to your server.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="serverData.status === 'installing'"
|
||||
data-pyro-server-installing
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-blue p-4 text-sm text-contrast"
|
||||
>
|
||||
<UiServersServerIcon :image="serverData.image" class="!h-10 !w-10" />
|
||||
<div
|
||||
v-if="serverData.status === 'installing'"
|
||||
data-pyro-server-installing
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-blue p-4 text-sm text-contrast"
|
||||
>
|
||||
<UiServersServerIcon :image="serverData.image" class="!h-10 !w-10" />
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-bold"> We're preparing your server! </span>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<UiServersPanelSpinner class="!h-3 !w-3" /> <LazyUiServersInstallingTicker />
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-bold"> We're preparing your server! </span>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<UiServersPanelSpinner class="!h-3 !w-3" /> <LazyUiServersInstallingTicker />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtPage
|
||||
:route="route"
|
||||
:is-connected="isConnected"
|
||||
:is-ws-auth-incorrect="isWSAuthIncorrect"
|
||||
:is-server-running="isServerRunning"
|
||||
:stats="stats"
|
||||
:server-power-state="serverPowerState"
|
||||
:power-state-details="powerStateDetails"
|
||||
:socket="socket"
|
||||
:server="server"
|
||||
:backup-in-progress="backupInProgress"
|
||||
@reinstall="onReinstall"
|
||||
/>
|
||||
</div>
|
||||
<NuxtPage
|
||||
:route="route"
|
||||
:is-connected="isConnected"
|
||||
:is-ws-auth-incorrect="isWSAuthIncorrect"
|
||||
:is-server-running="isServerRunning"
|
||||
:stats="stats"
|
||||
:server-power-state="serverPowerState"
|
||||
:power-state-details="powerStateDetails"
|
||||
:socket="socket"
|
||||
:server="server"
|
||||
:backup-in-progress="backupInProgress"
|
||||
@reinstall="onReinstall"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="flags.advancedDebugInfo"
|
||||
class="experimental-styles-within relative mx-auto mt-6 box-border w-full min-w-0 max-w-[1280px] px-6"
|
||||
>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Server data</h2>
|
||||
<pre class="markdown-body w-full overflow-auto rounded-2xl bg-bg-raised p-4 text-sm">{{
|
||||
JSON.stringify(server, null, " ")
|
||||
}}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import {
|
||||
SettingsIcon,
|
||||
CopyIcon,
|
||||
IssuesIcon,
|
||||
LeftArrowIcon,
|
||||
@@ -392,6 +440,7 @@ import type { ServerState, Stats, WSEvent, WSInstallationResultEvent } from "~/t
|
||||
import { usePyroConsole } from "~/store/console.ts";
|
||||
import { type Backup } from "~/composables/pyroServers.ts";
|
||||
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
||||
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
|
||||
|
||||
const app = useNuxtApp() as unknown as { $notify: any };
|
||||
|
||||
@@ -401,6 +450,7 @@ const isLoading = ref(true);
|
||||
const reconnectInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
||||
const isFirstMount = ref(true);
|
||||
const isMounted = ref(true);
|
||||
const flags = useFeatureFlags();
|
||||
|
||||
const INTERCOM_APP_ID = ref("ykeritl9");
|
||||
const auth = (await useAuth()) as unknown as {
|
||||
@@ -812,6 +862,10 @@ const newLoaderVersion = ref<string | null>(null);
|
||||
const newMCVersion = ref<string | null>(null);
|
||||
|
||||
const onReinstall = (potentialArgs: any) => {
|
||||
if (serverData.value?.flows?.intro) {
|
||||
server.general?.endIntro();
|
||||
}
|
||||
|
||||
if (!serverData.value) return;
|
||||
|
||||
serverData.value.status = "installing";
|
||||
@@ -1131,6 +1185,10 @@ onMounted(() => {
|
||||
connectWebSocket();
|
||||
}
|
||||
|
||||
if (server.general?.flows?.intro && server.general?.project) {
|
||||
server.general?.endIntro();
|
||||
}
|
||||
|
||||
if (username.value && email.value && userId.value && createdAt.value) {
|
||||
const currentUser = auth.value?.user as any;
|
||||
const matches =
|
||||
|
||||
@@ -1,162 +1,15 @@
|
||||
<template>
|
||||
<LazyUiServersPlatformVersionSelectModal
|
||||
ref="versionSelectModal"
|
||||
<ServerInstallation
|
||||
:server="props.server"
|
||||
:current-loader="data?.loader as Loaders"
|
||||
:backup-in-progress="backupInProgress"
|
||||
@reinstall="emit('reinstall', $event)"
|
||||
/>
|
||||
|
||||
<LazyUiServersPlatformMrpackModal
|
||||
ref="mrpackModal"
|
||||
:server="props.server"
|
||||
@reinstall="emit('reinstall', $event)"
|
||||
/>
|
||||
|
||||
<LazyUiServersPlatformChangeModpackVersionModal
|
||||
ref="modpackVersionModal"
|
||||
:server="props.server"
|
||||
:project="data?.project"
|
||||
:versions="Array.isArray(versions) ? versions : []"
|
||||
:current-version="currentVersion"
|
||||
:current-version-id="data?.upstream?.version_id"
|
||||
:server-status="data?.status"
|
||||
:backup-in-progress="props.backupInProgress"
|
||||
@reinstall="emit('reinstall')"
|
||||
/>
|
||||
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<div v-if="data && versions" class="flex w-full flex-col">
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex select-none flex-col items-center justify-between gap-2 lg:flex-row">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<h2 class="m-0 text-lg font-bold text-contrast">Modpack</h2>
|
||||
<div
|
||||
v-if="updateAvailable"
|
||||
class="rounded-full bg-bg-orange px-2 py-1 text-xs font-medium text-orange"
|
||||
>
|
||||
<span>Update available</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.upstream" class="flex gap-4">
|
||||
<ButtonStyled>
|
||||
<button
|
||||
class="!w-full sm:!w-auto"
|
||||
:disabled="isInstalling"
|
||||
@click="mrpackModal.show()"
|
||||
>
|
||||
<UploadIcon class="size-4" /> Import .mrpack
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<!-- dumb hack to make a button link not a link -->
|
||||
<ButtonStyled>
|
||||
<template v-if="isInstalling">
|
||||
<button :disabled="isInstalling">
|
||||
<TransferIcon class="size-4" />
|
||||
Switch modpack
|
||||
</button>
|
||||
</template>
|
||||
<nuxt-link v-else :to="`/modpacks?sid=${props.server.serverId}`">
|
||||
<TransferIcon class="size-4" />
|
||||
Switch modpack
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.upstream" class="flex flex-col gap-2">
|
||||
<div
|
||||
v-if="versionsError || currentVersionError"
|
||||
class="rounded-2xl border border-solid border-red p-4 text-contrast"
|
||||
>
|
||||
<p class="m-0 font-bold">Something went wrong while loading your modpack.</p>
|
||||
<p class="m-0 mb-2 mt-1 text-sm">
|
||||
{{ versionsError || currentVersionError }}
|
||||
</p>
|
||||
<ButtonStyled>
|
||||
<button :disabled="isInstalling" @click="refreshData">Retry</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<NewProjectCard
|
||||
v-if="!versionsError && !currentVersionError"
|
||||
class="!cursor-default !bg-bg !filter-none"
|
||||
:project="projectCardData"
|
||||
:categories="data.project?.categories || []"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="isInstalling" @click="modpackVersionModal.show()">
|
||||
<SettingsIcon class="size-4" />
|
||||
Change version
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</NewProjectCard>
|
||||
</div>
|
||||
<div v-else class="flex w-full flex-col items-center gap-2 sm:w-fit sm:flex-row">
|
||||
<ButtonStyled>
|
||||
<nuxt-link
|
||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||
:class="{ disabled: backupInProgress }"
|
||||
class="!w-full sm:!w-auto"
|
||||
:to="`/modpacks?sid=${props.server.serverId}`"
|
||||
>
|
||||
<CompassIcon class="size-4" /> Find a modpack
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<span class="hidden sm:block">or</span>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||
:disabled="!!backupInProgress"
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="mrpackModal.show()"
|
||||
>
|
||||
<UploadIcon class="size-4" /> Upload .mrpack file
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="m-0 text-lg font-bold text-contrast">Platform</h2>
|
||||
<p class="m-0">Your server's platform is the software that runs mods and plugins.</p>
|
||||
<div v-if="data.upstream" class="mt-2 flex items-center gap-2">
|
||||
<InfoIcon class="hidden sm:block" />
|
||||
<span class="text-sm text-secondary">
|
||||
The current platform was automatically selected based on your modpack.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full flex-col gap-1 rounded-2xl"
|
||||
:class="{
|
||||
'pointer-events-none cursor-not-allowed select-none opacity-50':
|
||||
props.server.general?.status === 'installing',
|
||||
}"
|
||||
:tabindex="props.server.general?.status === 'installing' ? -1 : 0"
|
||||
>
|
||||
<UiServersLoaderSelector
|
||||
:data="data"
|
||||
:is-installing="isInstalling"
|
||||
@select-loader="selectLoader"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, NewProjectCard } from "@modrinth/ui";
|
||||
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { Loaders } from "~/types/servers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
@@ -166,104 +19,4 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
reinstall: [any?];
|
||||
}>();
|
||||
|
||||
const isInstalling = computed(() => props.server.general?.status === "installing");
|
||||
|
||||
const versionSelectModal = ref();
|
||||
const mrpackModal = ref();
|
||||
const modpackVersionModal = ref();
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
|
||||
const {
|
||||
data: versions,
|
||||
error: versionsError,
|
||||
refresh: refreshVersions,
|
||||
} = await useAsyncData(
|
||||
`content-loader-versions-${data.value?.upstream?.project_id}`,
|
||||
async () => {
|
||||
if (!data.value?.upstream?.project_id) return [];
|
||||
try {
|
||||
const result = await useBaseFetch(`project/${data.value.upstream.project_id}/version`);
|
||||
return result || [];
|
||||
} catch (e) {
|
||||
console.error("couldnt fetch all versions:", e);
|
||||
throw new Error("Failed to load modpack versions.");
|
||||
}
|
||||
},
|
||||
{ default: () => [] },
|
||||
);
|
||||
|
||||
const {
|
||||
data: currentVersion,
|
||||
error: currentVersionError,
|
||||
refresh: refreshCurrentVersion,
|
||||
} = await useAsyncData(
|
||||
`content-loader-version-${data.value?.upstream?.version_id}`,
|
||||
async () => {
|
||||
if (!data.value?.upstream?.version_id) return null;
|
||||
try {
|
||||
const result = await useBaseFetch(`version/${data.value.upstream.version_id}`);
|
||||
return result || null;
|
||||
} catch (e) {
|
||||
console.error("couldnt fetch version:", e);
|
||||
throw new Error("Failed to load modpack version.");
|
||||
}
|
||||
},
|
||||
{ default: () => null },
|
||||
);
|
||||
|
||||
const projectCardData = computed(() => ({
|
||||
icon_url: data.value?.project?.icon_url,
|
||||
title: data.value?.project?.title,
|
||||
description: data.value?.project?.description,
|
||||
downloads: data.value?.project?.downloads,
|
||||
follows: data.value?.project?.followers,
|
||||
// @ts-ignore
|
||||
date_modified: currentVersion.value?.date_published || data.value?.project?.updated,
|
||||
}));
|
||||
|
||||
const selectLoader = (loader: string) => {
|
||||
versionSelectModal.value?.show(loader as Loaders);
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
await Promise.all([refreshVersions(), refreshCurrentVersion()]);
|
||||
};
|
||||
|
||||
const updateAvailable = computed(() => {
|
||||
// so sorry
|
||||
// @ts-ignore
|
||||
if (!data.value?.upstream || !versions.value?.length || !currentVersion.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const latestVersion = versions.value[0];
|
||||
// @ts-ignore
|
||||
return latestVersion.id !== currentVersion.value.id;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.server.general?.status,
|
||||
async (newStatus, oldStatus) => {
|
||||
if (oldStatus === "installing" && newStatus === "available") {
|
||||
await Promise.all([
|
||||
refreshVersions(),
|
||||
refreshCurrentVersion(),
|
||||
props.server.refresh(["general"]),
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stylized-toggle:checked::after {
|
||||
background: var(--color-accent-contrast) !important;
|
||||
}
|
||||
|
||||
.button-base:active {
|
||||
scale: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
data-pyro-server-list-root
|
||||
class="experimental-styles-within relative mx-auto flex min-h-screen w-full max-w-[1280px] flex-col px-6"
|
||||
class="experimental-styles-within relative mx-auto mb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6"
|
||||
>
|
||||
<div
|
||||
v-if="hasError || fetchError"
|
||||
@@ -89,7 +89,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul v-if="filteredData.length > 0" class="m-0 flex flex-col gap-4 p-0">
|
||||
<ul
|
||||
v-if="filteredData.length > 0 || isPollingForNewServers"
|
||||
class="m-0 flex flex-col gap-4 p-0"
|
||||
>
|
||||
<UiServersServerListing
|
||||
v-for="server in filteredData"
|
||||
:key="server.server_id"
|
||||
@@ -102,6 +105,7 @@
|
||||
:mc_version="server.mc_version"
|
||||
:upstream="server.upstream"
|
||||
:net="server.net"
|
||||
:flows="server.flows"
|
||||
/>
|
||||
<LazyUiServersServerListingSkeleton v-if="isPollingForNewServers" />
|
||||
</ul>
|
||||
@@ -133,6 +137,7 @@ interface ServerResponse {
|
||||
servers: Server[];
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const hasError = ref(false);
|
||||
const isPollingForNewServers = ref(false);
|
||||
@@ -163,11 +168,19 @@ const fuse = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
function introToTop(array: Server[]): Server[] {
|
||||
return array.slice().sort((a, b) => {
|
||||
return Number(b.flows?.intro) - Number(a.flows?.intro);
|
||||
});
|
||||
}
|
||||
|
||||
const filteredData = computed(() => {
|
||||
if (!searchInput.value.trim()) {
|
||||
return serverList.value;
|
||||
return introToTop(serverList.value);
|
||||
}
|
||||
return fuse.value ? fuse.value.search(searchInput.value).map((result) => result.item) : [];
|
||||
return fuse.value
|
||||
? introToTop(fuse.value.search(searchInput.value).map((result) => result.item))
|
||||
: [];
|
||||
});
|
||||
|
||||
const previousServerList = ref<Server[]>([]);
|
||||
@@ -179,6 +192,7 @@ const checkForNewServers = async () => {
|
||||
if (JSON.stringify(previousServerList.value) !== JSON.stringify(serverList.value)) {
|
||||
isPollingForNewServers.value = false;
|
||||
clearInterval(intervalId);
|
||||
router.replace({ query: {} });
|
||||
} else if (refreshCount.value >= 5) {
|
||||
isPollingForNewServers.value = false;
|
||||
clearInterval(intervalId);
|
||||
|
||||
@@ -353,6 +353,21 @@
|
||||
Upgrade
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-if="
|
||||
getPyroCharge(subscription) &&
|
||||
getPyroCharge(subscription).status !== 'cancelled' &&
|
||||
getPyroCharge(subscription).status !== 'failed'
|
||||
"
|
||||
color="purple"
|
||||
color-fill="text"
|
||||
>
|
||||
<button @click="showPyroIntervalChange(subscription)">
|
||||
<TransferIcon />
|
||||
<!-- TODO: Make this attractive af for monthly subscribers -->
|
||||
Change billing interval
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="
|
||||
getPyroCharge(subscription) &&
|
||||
@@ -412,6 +427,31 @@
|
||||
:payment-methods="paymentMethods"
|
||||
:return-url="`${config.public.siteUrl}/settings/billing`"
|
||||
/>
|
||||
<PurchaseModal
|
||||
v-if="currentProduct"
|
||||
ref="pyroIntervalModal"
|
||||
:product="[currentProduct]"
|
||||
:country="country"
|
||||
custom-server
|
||||
interval-change-only
|
||||
:existing-subscription="currentSubscription"
|
||||
:existing-plan="currentProduct"
|
||||
:publishable-key="config.public.stripePublishableKey"
|
||||
:send-billing-request="
|
||||
async (body) => {
|
||||
await useBaseFetch(`billing/subscription/${currentSubscription.id}`, {
|
||||
internal: true,
|
||||
method: 'PATCH',
|
||||
body,
|
||||
});
|
||||
}
|
||||
"
|
||||
:renewal-date="currentSubRenewalDate"
|
||||
:on-error="handleError"
|
||||
:customer="customer"
|
||||
:payment-methods="paymentMethods"
|
||||
:return-url="`${config.public.siteUrl}/settings/billing`"
|
||||
/>
|
||||
<PurchaseModal
|
||||
ref="pyroPurchaseModal"
|
||||
:product="upgradeProducts"
|
||||
@@ -444,39 +484,13 @@
|
||||
:return-url="`${config.public.siteUrl}/servers/manage`"
|
||||
:server-name="`${auth?.user?.username}'s server`"
|
||||
/>
|
||||
<NewModal ref="addPaymentMethodModal">
|
||||
<template #title>
|
||||
<span class="text-lg font-extrabold text-contrast">
|
||||
{{ formatMessage(messages.paymentMethodTitle) }}
|
||||
</span>
|
||||
</template>
|
||||
<div class="min-h-[16rem] md:w-[600px]">
|
||||
<div
|
||||
v-show="loadingPaymentMethodModal !== 2"
|
||||
class="flex min-h-[16rem] items-center justify-center"
|
||||
>
|
||||
<AnimatedLogo class="w-[80px]" />
|
||||
</div>
|
||||
<div v-show="loadingPaymentMethodModal === 2" class="min-h-[16rem] p-1">
|
||||
<div id="address-element"></div>
|
||||
<div id="payment-element" class="mt-4"></div>
|
||||
</div>
|
||||
<div v-show="loadingPaymentMethodModal === 2" class="input-group mt-auto pt-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="loadingAddMethod" @click="submit">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.paymentMethodAdd) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.addPaymentMethodModal.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
<AddPaymentMethodModal
|
||||
ref="addPaymentMethodModal"
|
||||
:publishable-key="config.public.stripePublishableKey"
|
||||
:return-url="`${config.public.siteUrl}/settings/billing`"
|
||||
:create-setup-intent="createSetupIntent"
|
||||
:on-error="handleError"
|
||||
/>
|
||||
<div class="header__row">
|
||||
<div class="header__title">
|
||||
<h2 class="text-2xl">{{ formatMessage(messages.paymentMethodTitle) }}</h2>
|
||||
@@ -590,9 +604,8 @@
|
||||
<script setup>
|
||||
import {
|
||||
ConfirmModal,
|
||||
NewModal,
|
||||
AddPaymentMethodModal,
|
||||
OverflowMenu,
|
||||
AnimatedLogo,
|
||||
PurchaseModal,
|
||||
ButtonStyled,
|
||||
CopyCode,
|
||||
@@ -617,7 +630,7 @@ import {
|
||||
UpdatedIcon,
|
||||
HistoryIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { calculateSavings, formatPrice, createStripeElements, getCurrency } from "@modrinth/utils";
|
||||
import { calculateSavings, formatPrice, getCurrency } from "@modrinth/utils";
|
||||
import { ref, computed } from "vue";
|
||||
import { products } from "~/generated/state.json";
|
||||
|
||||
@@ -754,19 +767,6 @@ const paymentMethodTypes = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
let stripe = null;
|
||||
let elements = null;
|
||||
|
||||
function loadStripe() {
|
||||
try {
|
||||
if (!stripe) {
|
||||
stripe = Stripe(config.public.stripePublishableKey);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading Stripe:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const [
|
||||
{ data: paymentMethods, refresh: refreshPaymentMethods },
|
||||
{ data: charges, refresh: refreshCharges },
|
||||
@@ -842,69 +842,16 @@ const primaryPaymentMethodId = computed(() => {
|
||||
});
|
||||
|
||||
const addPaymentMethodModal = ref();
|
||||
const loadingPaymentMethodModal = ref(0);
|
||||
async function addPaymentMethod() {
|
||||
try {
|
||||
loadingPaymentMethodModal.value = 0;
|
||||
addPaymentMethodModal.value.show();
|
||||
|
||||
const result = await useBaseFetch("billing/payment_method", {
|
||||
internal: true,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
loadStripe();
|
||||
const {
|
||||
elements: elementsVal,
|
||||
addressElement,
|
||||
paymentElement,
|
||||
} = createStripeElements(stripe, paymentMethods.value, {
|
||||
clientSecret: result.client_secret,
|
||||
});
|
||||
|
||||
elements = elementsVal;
|
||||
paymentElement.on("ready", () => {
|
||||
loadingPaymentMethodModal.value += 1;
|
||||
});
|
||||
addressElement.on("ready", () => {
|
||||
loadingPaymentMethodModal.value += 1;
|
||||
});
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: err.data ? err.data.description : err,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
function addPaymentMethod() {
|
||||
addPaymentMethodModal.value.show(paymentMethods.value);
|
||||
}
|
||||
|
||||
const loadingAddMethod = ref(false);
|
||||
async function submit() {
|
||||
startLoading();
|
||||
loadingAddMethod.value = true;
|
||||
|
||||
loadStripe();
|
||||
const { error } = await stripe.confirmSetup({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: `${config.public.siteUrl}/settings/billing`,
|
||||
},
|
||||
async function createSetupIntent() {
|
||||
return await useBaseFetch("billing/payment_method", {
|
||||
internal: true,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (error && error.type !== "validation_error") {
|
||||
data.$notify({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: error.message,
|
||||
type: "error",
|
||||
});
|
||||
} else if (!error) {
|
||||
await refresh();
|
||||
addPaymentMethodModal.value.close();
|
||||
}
|
||||
loadingAddMethod.value = false;
|
||||
stopLoading();
|
||||
}
|
||||
|
||||
const removePaymentMethodIndex = ref();
|
||||
@@ -915,6 +862,18 @@ const oppositeInterval = computed(() =>
|
||||
midasCharge.value?.subscription_interval === "yearly" ? "monthly" : "yearly",
|
||||
);
|
||||
|
||||
async function showPyroIntervalChange(subscription) {
|
||||
currentSubscription.value = subscription;
|
||||
currentSubRenewalDate.value = getPyroCharge(subscription).due;
|
||||
currentProduct.value = getPyroProduct(subscription);
|
||||
|
||||
upgradeProducts.value = [currentProduct.value];
|
||||
upgradeProducts.value.metadata = { type: "pyro" };
|
||||
|
||||
await nextTick();
|
||||
pyroIntervalModal.value.show();
|
||||
}
|
||||
|
||||
async function switchMidasInterval(interval) {
|
||||
changingInterval.value = true;
|
||||
startLoading();
|
||||
@@ -1034,6 +993,7 @@ const getProductPrice = (product, interval) => {
|
||||
const modalCancel = ref(null);
|
||||
|
||||
const pyroPurchaseModal = ref();
|
||||
const pyroIntervalModal = ref();
|
||||
const currentSubscription = ref(null);
|
||||
const currentProduct = ref(null);
|
||||
const upgradeProducts = ref([]);
|
||||
|
||||
@@ -104,6 +104,9 @@ export interface Server {
|
||||
version_id: string;
|
||||
};
|
||||
motd: string;
|
||||
flows: {
|
||||
intro?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
|
||||
23
apps/labrinth/.sqlx/query-0d01a3991e7551a8b7936bf8f4cc1760d2e89af99dd71849eda35d6c6820aa43.json
generated
Normal file
23
apps/labrinth/.sqlx/query-0d01a3991e7551a8b7936bf8f4cc1760d2e89af99dd71849eda35d6c6820aa43.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH random_id_point AS (\n SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point\n )\n SELECT id FROM mods\n WHERE status = ANY($1)\n ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)\n LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0d01a3991e7551a8b7936bf8f4cc1760d2e89af99dd71849eda35d6c6820aa43"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id FROM mods TABLESAMPLE SYSTEM_ROWS($1) WHERE status = ANY($2)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1cefe4924d3c1f491739858ce844a22903d2dbe26f255219299f1833a10ce3d7"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
-- This index substantially brings down the cost of the query plan for the
|
||||
-- hot query at `labrinth::routes::v3::projects::random_projects_get`, from
|
||||
-- 354.04..363.39 to 171.33..180.68 (~2x improvement).
|
||||
--
|
||||
-- The numbers above were calculated in a clean PostgreSQL 17.5.0 container
|
||||
-- with 10k mods created with the SQL below.
|
||||
--
|
||||
-- WITH seq AS (SELECT n FROM GENERATE_SERIES(1, 10000) AS n)
|
||||
-- INSERT INTO mods (id, team_id, name, summary, icon_url, license_url, slug, status)
|
||||
-- SELECT n, 1, n, '', '', '', n, (ARRAY['approved', 'pending'])[n % 2 + 1] from seq;
|
||||
|
||||
CREATE INDEX mods_status ON mods(status);
|
||||
@@ -0,0 +1,12 @@
|
||||
-- The spatial query for retrieving random searchable projects is greatly sped
|
||||
-- up by this index on a fixture of 1M mods, bringing down the total cost of
|
||||
-- the query plan and runtime to be comparable to primary key lookups. See the
|
||||
-- `labrinth::routes::v3::projects::random_projects_get` function and the
|
||||
-- previous 20250608183828_random-project-index.sql migration for more details.
|
||||
--
|
||||
-- That previous migration created a non-spatial index for the status column which
|
||||
-- does not get used in the new spatial query, but may still be useful for other
|
||||
-- queries that filter mods by status.
|
||||
|
||||
CREATE INDEX mods_searchable_ids_gist ON mods USING gist (POINT(id, 0))
|
||||
WHERE status = ANY(ARRAY['approved', 'archived']);
|
||||
@@ -68,7 +68,7 @@ pub async fn init_oauth(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
Scopes::USER_AUTH_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -323,7 +323,7 @@ pub async fn accept_or_reject_client_scopes(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -15,7 +15,7 @@ pub async fn get_user_from_headers<'a, E>(
|
||||
executor: E,
|
||||
redis: &RedisPool,
|
||||
session_queue: &AuthQueue,
|
||||
required_scopes: Option<&[Scopes]>,
|
||||
required_scopes: Scopes,
|
||||
) -> Result<(Scopes, User), AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
@@ -33,12 +33,8 @@ where
|
||||
|
||||
let user = User::from_full(db_user);
|
||||
|
||||
if let Some(required_scopes) = required_scopes {
|
||||
for scope in required_scopes {
|
||||
if !scopes.contains(*scope) {
|
||||
return Err(AuthenticationError::InvalidCredentials);
|
||||
}
|
||||
}
|
||||
if !scopes.contains(required_scopes) {
|
||||
return Err(AuthenticationError::InvalidCredentials);
|
||||
}
|
||||
|
||||
Ok((scopes, user))
|
||||
@@ -175,7 +171,7 @@ pub async fn check_is_moderator_from_headers<'a, 'b, E>(
|
||||
executor: E,
|
||||
redis: &RedisPool,
|
||||
session_queue: &AuthQueue,
|
||||
required_scopes: Option<&[Scopes]>,
|
||||
required_scopes: Scopes,
|
||||
) -> Result<User, AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
|
||||
@@ -145,7 +145,7 @@ impl SubscriptionStatus {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum SubscriptionMetadata {
|
||||
Pyro { id: String },
|
||||
Pyro { id: String, region: Option<String> },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
||||
@@ -519,6 +519,9 @@ impl ProjectStatus {
|
||||
}
|
||||
|
||||
// Project can be displayed in search
|
||||
// IMPORTANT: if this is changed, make sure to update the `mods_searchable_ids_gist`
|
||||
// index in the DB to keep random project queries fast (see the
|
||||
// `20250609134334_spatial-random-project-index.sql` migration)
|
||||
pub fn is_searchable(&self) -> bool {
|
||||
matches!(self, ProjectStatus::Approved | ProjectStatus::Archived)
|
||||
}
|
||||
|
||||
@@ -55,10 +55,15 @@ pub async fn page_view_ingest(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user =
|
||||
get_user_from_headers(&req, &**pool, &redis, &session_queue, None)
|
||||
.await
|
||||
.ok();
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::empty(),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
let conn_info = req.connection_info().peer_addr().map(|x| x.to_string());
|
||||
|
||||
let url = Url::parse(&url_input.url).map_err(|_| {
|
||||
@@ -177,7 +182,7 @@ pub async fn playtime_ingest(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PERFORM_ANALYTICS]),
|
||||
Scopes::PERFORM_ANALYTICS,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ pub async fn subscriptions(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -161,7 +161,7 @@ pub async fn refund_charge(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -325,7 +325,7 @@ pub async fn edit_subscription(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -585,7 +585,7 @@ pub async fn user_customer(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -623,7 +623,7 @@ pub async fn charges(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -682,7 +682,7 @@ pub async fn add_payment_method_flow(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -736,7 +736,7 @@ pub async fn edit_payment_method(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -805,7 +805,7 @@ pub async fn remove_payment_method(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -892,7 +892,7 @@ pub async fn payment_methods(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -954,17 +954,19 @@ pub async fn active_servers(
|
||||
pub server_id: String,
|
||||
pub price_id: crate::models::ids::ProductPriceId,
|
||||
pub interval: PriceDuration,
|
||||
pub region: Option<String>,
|
||||
}
|
||||
|
||||
let server_ids = servers
|
||||
.into_iter()
|
||||
.filter_map(|x| {
|
||||
x.metadata.as_ref().map(|metadata| match metadata {
|
||||
SubscriptionMetadata::Pyro { id } => ActiveServer {
|
||||
SubscriptionMetadata::Pyro { id, region } => ActiveServer {
|
||||
user_id: x.user_id.into(),
|
||||
server_id: id.clone(),
|
||||
price_id: x.price_id.into(),
|
||||
interval: x.interval,
|
||||
region: region.clone(),
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1087,7 +1089,7 @@ pub async fn initiate_payment(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1764,8 +1766,10 @@ pub async fn stripe_webhook(
|
||||
{
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
if let Some(SubscriptionMetadata::Pyro { id }) =
|
||||
&subscription.metadata
|
||||
if let Some(SubscriptionMetadata::Pyro {
|
||||
id,
|
||||
region: _,
|
||||
}) = &subscription.metadata
|
||||
{
|
||||
client
|
||||
.post(format!(
|
||||
@@ -1880,6 +1884,7 @@ pub async fn stripe_webhook(
|
||||
subscription.metadata =
|
||||
Some(SubscriptionMetadata::Pyro {
|
||||
id: res.uuid,
|
||||
region: server_region,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2240,7 +2245,7 @@ pub async fn index_subscriptions(pool: PgPool, redis: RedisPool) {
|
||||
true
|
||||
}
|
||||
ProductMetadata::Pyro { .. } => {
|
||||
if let Some(SubscriptionMetadata::Pyro { id }) =
|
||||
if let Some(SubscriptionMetadata::Pyro { id, region: _ }) =
|
||||
&subscription.metadata
|
||||
{
|
||||
let res = reqwest::Client::new()
|
||||
|
||||
@@ -1243,7 +1243,7 @@ pub async fn delete_auth_provider(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
Scopes::USER_AUTH_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1663,7 +1663,7 @@ pub async fn begin_2fa_flow(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
Scopes::USER_AUTH_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1708,7 +1708,7 @@ pub async fn finish_2fa_flow(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
Scopes::USER_AUTH_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -2140,7 +2140,7 @@ pub async fn set_email(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
Scopes::USER_AUTH_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -2223,7 +2223,7 @@ pub async fn resend_verify_email(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
Scopes::USER_AUTH_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -2328,7 +2328,7 @@ pub async fn subscribe_newsletter(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
Scopes::USER_AUTH_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn export(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -39,7 +39,7 @@ pub async fn get_projects(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -82,7 +82,7 @@ pub async fn get_project_meta(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -234,7 +234,7 @@ pub async fn set_project_meta(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ pub async fn get_pats(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAT_READ]),
|
||||
Scopes::PAT_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -99,7 +99,7 @@ pub async fn create_pat(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAT_CREATE]),
|
||||
Scopes::PAT_CREATE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -174,7 +174,7 @@ pub async fn edit_pat(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAT_WRITE]),
|
||||
Scopes::PAT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -266,7 +266,7 @@ pub async fn delete_pat(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAT_DELETE]),
|
||||
Scopes::PAT_DELETE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -141,7 +141,7 @@ pub async fn list(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_READ]),
|
||||
Scopes::SESSION_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -178,7 +178,7 @@ pub async fn delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_DELETE]),
|
||||
Scopes::SESSION_DELETE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -212,10 +212,15 @@ pub async fn refresh(
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let current_user =
|
||||
get_user_from_headers(&req, &**pool, &redis, &session_queue, None)
|
||||
.await?
|
||||
.1;
|
||||
let current_user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::empty(),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let session = req
|
||||
.headers()
|
||||
.get(AUTHORIZATION)
|
||||
|
||||
@@ -88,7 +88,7 @@ pub async fn maven_metadata(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -294,7 +294,7 @@ pub async fn version_file(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -360,7 +360,7 @@ pub async fn version_file_sha1(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -405,7 +405,7 @@ pub async fn version_file_sha512(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
|
||||
@@ -51,7 +51,7 @@ pub async fn forge_updates(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
|
||||
@@ -82,7 +82,7 @@ pub async fn playtimes_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ANALYTICS]),
|
||||
Scopes::ANALYTICS,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)?;
|
||||
@@ -151,7 +151,7 @@ pub async fn views_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ANALYTICS]),
|
||||
Scopes::ANALYTICS,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)?;
|
||||
@@ -220,7 +220,7 @@ pub async fn downloads_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ANALYTICS]),
|
||||
Scopes::ANALYTICS,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)?;
|
||||
@@ -289,7 +289,7 @@ pub async fn revenue_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAYOUTS_READ]),
|
||||
Scopes::PAYOUTS_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)?;
|
||||
@@ -430,7 +430,7 @@ pub async fn countries_downloads_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ANALYTICS]),
|
||||
Scopes::ANALYTICS,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)?;
|
||||
@@ -503,7 +503,7 @@ pub async fn countries_views_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ANALYTICS]),
|
||||
Scopes::ANALYTICS,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)?;
|
||||
|
||||
@@ -71,7 +71,7 @@ pub async fn collection_create(
|
||||
&**client,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_CREATE]),
|
||||
Scopes::COLLECTION_CREATE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -156,7 +156,7 @@ pub async fn collections_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_READ]),
|
||||
Scopes::COLLECTION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -185,7 +185,7 @@ pub async fn collection_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_READ]),
|
||||
Scopes::COLLECTION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -231,7 +231,7 @@ pub async fn collection_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_WRITE]),
|
||||
Scopes::COLLECTION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -390,7 +390,7 @@ pub async fn collection_icon_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_WRITE]),
|
||||
Scopes::COLLECTION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -471,7 +471,7 @@ pub async fn delete_collection_icon(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_WRITE]),
|
||||
Scopes::COLLECTION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -528,7 +528,7 @@ pub async fn collection_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_DELETE]),
|
||||
Scopes::COLLECTION_DELETE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -36,7 +36,7 @@ pub async fn add_friend(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
Scopes::USER_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -154,7 +154,7 @@ pub async fn remove_friend(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
Scopes::USER_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -200,7 +200,7 @@ pub async fn friends(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_READ]),
|
||||
Scopes::USER_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -49,14 +49,12 @@ pub async fn images_add(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let mut context = ImageContext::from_str(&data.context, None);
|
||||
|
||||
let scopes = vec![context.relevant_scope()];
|
||||
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&scopes),
|
||||
context.relevant_scope(),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -40,7 +40,7 @@ pub async fn notifications_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::NOTIFICATION_READ]),
|
||||
Scopes::NOTIFICATION_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -82,7 +82,7 @@ pub async fn notification_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::NOTIFICATION_READ]),
|
||||
Scopes::NOTIFICATION_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -119,7 +119,7 @@ pub async fn notification_read(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::NOTIFICATION_WRITE]),
|
||||
Scopes::NOTIFICATION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -169,7 +169,7 @@ pub async fn notification_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::NOTIFICATION_WRITE]),
|
||||
Scopes::NOTIFICATION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -220,7 +220,7 @@ pub async fn notifications_read(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::NOTIFICATION_WRITE]),
|
||||
Scopes::NOTIFICATION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -273,7 +273,7 @@ pub async fn notifications_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::NOTIFICATION_WRITE]),
|
||||
Scopes::NOTIFICATION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -69,7 +69,7 @@ pub async fn get_user_clients(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -167,7 +167,7 @@ pub async fn oauth_client_create(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -228,7 +228,7 @@ pub async fn oauth_client_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -285,7 +285,7 @@ pub async fn oauth_client_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -363,7 +363,7 @@ pub async fn oauth_client_icon_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -430,7 +430,7 @@ pub async fn oauth_client_icon_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -477,7 +477,7 @@ pub async fn get_user_oauth_authorizations(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -507,7 +507,7 @@ pub async fn revoke_oauth_authorization(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -63,7 +63,7 @@ pub async fn organization_projects_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_READ, Scopes::PROJECT_READ]),
|
||||
Scopes::ORGANIZATION_READ | Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -127,7 +127,7 @@ pub async fn organization_create(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_CREATE]),
|
||||
Scopes::ORGANIZATION_CREATE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -224,7 +224,7 @@ pub async fn organization_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_READ]),
|
||||
Scopes::ORGANIZATION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -315,7 +315,7 @@ pub async fn organizations_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_READ]),
|
||||
Scopes::ORGANIZATION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -396,7 +396,7 @@ pub async fn organizations_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_WRITE]),
|
||||
Scopes::ORGANIZATION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -559,7 +559,7 @@ pub async fn organization_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_DELETE]),
|
||||
Scopes::ORGANIZATION_DELETE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -700,7 +700,7 @@ pub async fn organization_projects_add(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE, Scopes::ORGANIZATION_WRITE]),
|
||||
Scopes::PROJECT_WRITE | Scopes::ORGANIZATION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -863,7 +863,7 @@ pub async fn organization_projects_remove(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE, Scopes::ORGANIZATION_WRITE]),
|
||||
Scopes::PROJECT_WRITE | Scopes::ORGANIZATION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1051,7 +1051,7 @@ pub async fn organization_icon_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_WRITE]),
|
||||
Scopes::ORGANIZATION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1154,7 +1154,7 @@ pub async fn delete_organization_icon(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_WRITE]),
|
||||
Scopes::ORGANIZATION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -313,7 +313,7 @@ pub async fn user_payouts(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAYOUTS_READ]),
|
||||
Scopes::PAYOUTS_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -653,7 +653,7 @@ pub async fn cancel_payout(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAYOUTS_WRITE]),
|
||||
Scopes::PAYOUTS_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -783,7 +783,7 @@ pub async fn get_balance(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PAYOUTS_READ]),
|
||||
Scopes::PAYOUTS_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -342,7 +342,7 @@ async fn project_create_inner(
|
||||
pool,
|
||||
redis,
|
||||
session_queue,
|
||||
Some(&[Scopes::PROJECT_CREATE]),
|
||||
Scopes::PROJECT_CREATE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -94,14 +94,21 @@ pub async fn random_projects_get(
|
||||
})?;
|
||||
|
||||
let project_ids = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM mods TABLESAMPLE SYSTEM_ROWS($1) WHERE status = ANY($2)
|
||||
",
|
||||
count.count as i32,
|
||||
// IDs are randomly generated (see the `generate_ids` macro), so fetching a
|
||||
// number of mods nearest to a random point in the ID space is equivalent to
|
||||
// random sampling
|
||||
"WITH random_id_point AS (
|
||||
SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point
|
||||
)
|
||||
SELECT id FROM mods
|
||||
WHERE status = ANY($1)
|
||||
ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)
|
||||
LIMIT $2",
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
count.count as i32,
|
||||
)
|
||||
.fetch(&**pool)
|
||||
.map_ok(|m| db_ids::DBProjectId(m.id))
|
||||
@@ -139,7 +146,7 @@ pub async fn projects_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -168,7 +175,7 @@ pub async fn project_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -258,7 +265,7 @@ pub async fn project_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1014,7 +1021,7 @@ pub async fn dependency_list(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -1126,7 +1133,7 @@ pub async fn projects_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1423,7 +1430,7 @@ pub async fn project_icon_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1534,7 +1541,7 @@ pub async fn delete_project_icon(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1641,7 +1648,7 @@ pub async fn add_gallery_item(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1799,7 +1806,7 @@ pub async fn edit_gallery_item(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1966,7 +1973,7 @@ pub async fn delete_gallery_item(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -2075,7 +2082,7 @@ pub async fn project_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_DELETE]),
|
||||
Scopes::PROJECT_DELETE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -2178,7 +2185,7 @@ pub async fn project_follow(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
Scopes::USER_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -2258,7 +2265,7 @@ pub async fn project_unfollow(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
Scopes::USER_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -2334,7 +2341,7 @@ pub async fn project_get_organization(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ, Scopes::ORGANIZATION_READ]),
|
||||
Scopes::PROJECT_READ | Scopes::ORGANIZATION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
|
||||
@@ -58,7 +58,7 @@ pub async fn report_create(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::REPORT_CREATE]),
|
||||
Scopes::REPORT_CREATE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -254,7 +254,7 @@ pub async fn reports(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::REPORT_READ]),
|
||||
Scopes::REPORT_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -338,7 +338,7 @@ pub async fn reports_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::REPORT_READ]),
|
||||
Scopes::REPORT_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -364,7 +364,7 @@ pub async fn report_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::REPORT_READ]),
|
||||
Scopes::REPORT_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -406,7 +406,7 @@ pub async fn report_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::REPORT_WRITE]),
|
||||
Scopes::REPORT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -506,7 +506,7 @@ pub async fn report_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::REPORT_DELETE]),
|
||||
Scopes::REPORT_DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ pub async fn team_members_get_project(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -142,7 +142,7 @@ pub async fn team_members_get_organization(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_READ]),
|
||||
Scopes::ORGANIZATION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -222,7 +222,7 @@ pub async fn team_members_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -294,7 +294,7 @@ pub async fn teams_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -348,7 +348,7 @@ pub async fn join_team(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -437,7 +437,7 @@ pub async fn add_team_member(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -692,7 +692,7 @@ pub async fn edit_team_member(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -878,7 +878,7 @@ pub async fn transfer_ownership(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -1050,7 +1050,7 @@ pub async fn remove_team_member(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -285,7 +285,7 @@ pub async fn thread_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::THREAD_READ]),
|
||||
Scopes::THREAD_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -341,7 +341,7 @@ pub async fn threads_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::THREAD_READ]),
|
||||
Scopes::THREAD_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -379,7 +379,7 @@ pub async fn thread_send_message(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::THREAD_WRITE]),
|
||||
Scopes::THREAD_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -577,7 +577,7 @@ pub async fn message_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::THREAD_WRITE]),
|
||||
Scopes::THREAD_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn admin_user_email(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)?;
|
||||
@@ -114,7 +114,7 @@ pub async fn projects_list(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -150,7 +150,7 @@ pub async fn user_auth_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_READ]),
|
||||
Scopes::USER_READ,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -200,7 +200,7 @@ pub async fn user_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -231,7 +231,7 @@ pub async fn collections_list(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_READ]),
|
||||
Scopes::COLLECTION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -279,7 +279,7 @@ pub async fn orgs_list(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ]),
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -389,7 +389,7 @@ pub async fn user_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
Scopes::USER_WRITE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -561,7 +561,7 @@ pub async fn user_icon_edit(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
Scopes::USER_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -633,7 +633,7 @@ pub async fn user_icon_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
Scopes::USER_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -685,7 +685,7 @@ pub async fn user_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_DELETE]),
|
||||
Scopes::USER_DELETE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -726,7 +726,7 @@ pub async fn user_follows(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_READ]),
|
||||
Scopes::USER_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -768,7 +768,7 @@ pub async fn user_notifications(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::NOTIFICATION_READ]),
|
||||
Scopes::NOTIFICATION_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -169,7 +169,7 @@ async fn version_create_inner(
|
||||
pool,
|
||||
redis,
|
||||
session_queue,
|
||||
Some(&[Scopes::VERSION_CREATE]),
|
||||
Scopes::VERSION_CREATE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -599,7 +599,7 @@ async fn upload_file_to_version_inner(
|
||||
&**client,
|
||||
&redis,
|
||||
session_queue,
|
||||
Some(&[Scopes::VERSION_WRITE]),
|
||||
Scopes::VERSION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -46,7 +46,7 @@ pub async fn get_version_from_hash(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::VERSION_READ]),
|
||||
Scopes::VERSION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -132,7 +132,7 @@ pub async fn get_update_from_hash(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::VERSION_READ]),
|
||||
Scopes::VERSION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -231,7 +231,7 @@ pub async fn get_versions_from_hashes(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::VERSION_READ]),
|
||||
Scopes::VERSION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -285,7 +285,7 @@ pub async fn get_projects_from_hashes(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ, Scopes::VERSION_READ]),
|
||||
Scopes::PROJECT_READ | Scopes::VERSION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -437,7 +437,7 @@ pub async fn update_individual_files(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::VERSION_READ]),
|
||||
Scopes::VERSION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -569,7 +569,7 @@ pub async fn delete_file(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::VERSION_WRITE]),
|
||||
Scopes::VERSION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -699,7 +699,7 @@ pub async fn download_version(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::VERSION_READ]),
|
||||
Scopes::VERSION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
|
||||
@@ -80,7 +80,7 @@ pub async fn version_project_get_helper(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ, Scopes::VERSION_READ]),
|
||||
Scopes::PROJECT_READ | Scopes::VERSION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -145,7 +145,7 @@ pub async fn versions_get(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::VERSION_READ]),
|
||||
Scopes::VERSION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -184,7 +184,7 @@ pub async fn version_get_helper(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::VERSION_READ]),
|
||||
Scopes::VERSION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -281,7 +281,7 @@ pub async fn version_edit_helper(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::VERSION_WRITE]),
|
||||
Scopes::VERSION_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
@@ -736,7 +736,7 @@ pub async fn version_list(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_READ, Scopes::VERSION_READ]),
|
||||
Scopes::PROJECT_READ | Scopes::VERSION_READ,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
@@ -883,7 +883,7 @@ pub async fn version_delete(
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::VERSION_DELETE]),
|
||||
Scopes::VERSION_DELETE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
@@ -1,14 +1 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="absolute right-8 top-8 size-8"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 0 0 2.25-2.25V6.75a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 6.75v10.5a2.25 2.25 0 0 0 2.25 2.25Zm.75-12h9v9h-9v-9Z"
|
||||
/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cpu-icon lucide-cpu"><path d="M12 20v2"/><path d="M12 2v2"/><path d="M17 20v2"/><path d="M17 2v2"/><path d="M2 12h2"/><path d="M2 17h2"/><path d="M2 7h2"/><path d="M20 12h2"/><path d="M20 17h2"/><path d="M20 7h2"/><path d="M7 20v2"/><path d="M7 2v2"/><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="8" y="8" width="8" height="8" rx="1"/></svg>
|
||||
|
Before Width: | Height: | Size: 527 B After Width: | Height: | Size: 555 B |
1
packages/assets/icons/database.svg
Normal file
1
packages/assets/icons/database.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-database-icon lucide-database"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 21 19V5"/><path d="M3 12A9 3 0 0 0 21 12"/></svg>
|
||||
|
After Width: | Height: | Size: 348 B |
1
packages/assets/icons/memory-stick.svg
Normal file
1
packages/assets/icons/memory-stick.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-memory-stick-icon lucide-memory-stick"><path d="M6 19v-3"/><path d="M10 19v-3"/><path d="M14 19v-3"/><path d="M18 19v-3"/><path d="M8 11V9"/><path d="M16 11V9"/><path d="M12 11V9"/><path d="M2 15h20"/><path d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 0-3.837Z"/></svg>
|
||||
|
After Width: | Height: | Size: 535 B |
@@ -77,6 +77,7 @@ import _CopyrightIcon from './icons/copyright.svg?component'
|
||||
import _CrownIcon from './icons/crown.svg?component'
|
||||
import _CurrencyIcon from './icons/currency.svg?component'
|
||||
import _DashboardIcon from './icons/dashboard.svg?component'
|
||||
import _DatabaseIcon from './icons/database.svg?component'
|
||||
import _DownloadIcon from './icons/download.svg?component'
|
||||
import _DropdownIcon from './icons/dropdown.svg?component'
|
||||
import _EditIcon from './icons/edit.svg?component'
|
||||
@@ -125,6 +126,7 @@ import _LogOutIcon from './icons/log-out.svg?component'
|
||||
import _MailIcon from './icons/mail.svg?component'
|
||||
import _ManageIcon from './icons/manage.svg?component'
|
||||
import _MaximizeIcon from './icons/maximize.svg?component'
|
||||
import _MemoryStickIcon from './icons/memory-stick.svg?component'
|
||||
import _MessageIcon from './icons/message.svg?component'
|
||||
import _MicrophoneIcon from './icons/microphone.svg?component'
|
||||
import _MinimizeIcon from './icons/minimize.svg?component'
|
||||
@@ -295,6 +297,7 @@ export const CopyrightIcon = _CopyrightIcon
|
||||
export const CrownIcon = _CrownIcon
|
||||
export const CurrencyIcon = _CurrencyIcon
|
||||
export const DashboardIcon = _DashboardIcon
|
||||
export const DatabaseIcon = _DatabaseIcon
|
||||
export const DownloadIcon = _DownloadIcon
|
||||
export const DropdownIcon = _DropdownIcon
|
||||
export const EditIcon = _EditIcon
|
||||
@@ -344,6 +347,7 @@ export const LogOutIcon = _LogOutIcon
|
||||
export const MailIcon = _MailIcon
|
||||
export const ManageIcon = _ManageIcon
|
||||
export const MaximizeIcon = _MaximizeIcon
|
||||
export const MemoryStickIcon = _MemoryStickIcon
|
||||
export const MessageIcon = _MessageIcon
|
||||
export const MicrophoneIcon = _MicrophoneIcon
|
||||
export const MinimizeIcon = _MinimizeIcon
|
||||
|
||||
@@ -200,6 +200,8 @@ html {
|
||||
--color-platform-sponge: #f9e580;
|
||||
|
||||
--hover-brightness: 1.25;
|
||||
|
||||
--experimental-color-button-bg: #33363d;
|
||||
}
|
||||
|
||||
.oled-mode {
|
||||
@@ -257,7 +259,7 @@ html {
|
||||
}
|
||||
|
||||
.dark-experiments {
|
||||
--color-button-bg: #33363d;
|
||||
--color-button-bg: var(--experimental-color-button-bg);
|
||||
}
|
||||
|
||||
.dark-mode:not(.oled-mode),
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@stripe/stripe-js": "^7.3.1",
|
||||
"@vintl/unplugin": "^1.5.1",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"stripe": "^18.1.1",
|
||||
"tsconfig": "workspace:*",
|
||||
"typescript": "^5.4.5",
|
||||
"vue": "^3.5.13",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<ScrollablePanel v-if="search" class="h-[17rem]">
|
||||
<ScrollablePanel v-if="search">
|
||||
<Button
|
||||
v-for="(option, index) in filteredOptions"
|
||||
:key="`option-${index}`"
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<button
|
||||
v-for="(item, index) in items"
|
||||
:key="`radio-button-${index}`"
|
||||
class="p-0 py-2 px-2 border-0 flex gap-2 transition-all items-center cursor-pointer active:scale-95 hover:bg-button-bg rounded-xl"
|
||||
class="p-0 py-2 px-2 border-0 font-medium flex gap-2 transition-all items-center cursor-pointer active:scale-95 hover:bg-button-bg rounded-xl"
|
||||
:class="{
|
||||
'text-contrast font-medium bg-button-bg': selected === item,
|
||||
'text-contrast bg-button-bg': selected === item,
|
||||
'text-primary bg-transparent': selected !== item,
|
||||
}"
|
||||
@click="selected = item"
|
||||
|
||||
@@ -3,11 +3,18 @@
|
||||
<div
|
||||
class="wrapper-wrapper"
|
||||
:class="{
|
||||
'top-fade': !scrollableAtTop && !props.disableScrolling,
|
||||
'bottom-fade': !scrollableAtBottom && !props.disableScrolling,
|
||||
'top-fade': !scrollableAtTop && !disableScrolling,
|
||||
'bottom-fade': !scrollableAtBottom && !disableScrolling,
|
||||
}"
|
||||
>
|
||||
<div ref="scrollablePane" class="scrollable-pane" @scroll="onScroll">
|
||||
<div
|
||||
ref="scrollablePane"
|
||||
:class="{
|
||||
'max-h-[19rem]': !disableScrolling,
|
||||
}"
|
||||
class="scrollable-pane"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,7 +24,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
disableScrolling?: boolean
|
||||
}>(),
|
||||
|
||||
104
packages/ui/src/components/billing/AddPaymentMethod.vue
Normal file
104
packages/ui/src/components/billing/AddPaymentMethod.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { createStripeElements } from '@modrinth/utils'
|
||||
import ModalLoadingIndicator from '../modal/ModalLoadingIndicator.vue'
|
||||
import { loadStripe, type Stripe as StripsJs, type StripeElements } from '@stripe/stripe-js'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'startLoading' | 'stopLoading'): void
|
||||
}>()
|
||||
|
||||
export type SetupIntentResponse = {
|
||||
client_secret: string
|
||||
}
|
||||
|
||||
export type AddPaymentMethodProps = {
|
||||
publishableKey: string
|
||||
createSetupIntent: () => Promise<SetupIntentResponse>
|
||||
returnUrl: string
|
||||
onError: (error: Error) => void
|
||||
}
|
||||
|
||||
const props = defineProps<AddPaymentMethodProps>()
|
||||
|
||||
const elementsLoaded = ref<0 | 1 | 2>(0)
|
||||
const stripe = ref<StripsJs>()
|
||||
const elements = ref<StripeElements>()
|
||||
const error = ref(false)
|
||||
|
||||
function handleError(error: Error) {
|
||||
props.onError(error)
|
||||
error.value = true
|
||||
}
|
||||
|
||||
async function reload(paymentMethods: Stripe.PaymentMethod[]) {
|
||||
try {
|
||||
elementsLoaded.value = 0
|
||||
error.value = false
|
||||
|
||||
const result = await props.createSetupIntent()
|
||||
|
||||
stripe.value = await loadStripe(props.publishableKey)
|
||||
const {
|
||||
elements: newElements,
|
||||
addressElement,
|
||||
paymentElement,
|
||||
} = createStripeElements(stripe.value, paymentMethods, {
|
||||
clientSecret: result.client_secret,
|
||||
})
|
||||
|
||||
elements.value = newElements
|
||||
paymentElement.on('ready', () => {
|
||||
elementsLoaded.value += 1
|
||||
})
|
||||
addressElement.on('ready', () => {
|
||||
elementsLoaded.value += 1
|
||||
})
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function submit(): Promise<boolean> {
|
||||
emit('startLoading')
|
||||
|
||||
const result = await stripe.value.confirmSetup({
|
||||
elements: elements.value,
|
||||
confirmParams: {
|
||||
return_url: props.returnUrl,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(result)
|
||||
|
||||
const { error } = result
|
||||
|
||||
emit('stopLoading')
|
||||
if (error && error.type !== 'validation_error') {
|
||||
handleError(error.message)
|
||||
return false
|
||||
} else if (!error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
reload,
|
||||
submit,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-[16rem] flex flex-col gap-2 justify-center items-center">
|
||||
<div v-show="elementsLoaded < 2">
|
||||
<ModalLoadingIndicator :error="error">
|
||||
Loading...
|
||||
<template #error> Error loading Stripe payment UI. </template>
|
||||
</ModalLoadingIndicator>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div id="address-element"></div>
|
||||
<div id="payment-element" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
72
packages/ui/src/components/billing/AddPaymentMethodModal.vue
Normal file
72
packages/ui/src/components/billing/AddPaymentMethodModal.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, NewModal } from '../index'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import AddPaymentMethod from './AddPaymentMethod.vue'
|
||||
import type { AddPaymentMethodProps } from './AddPaymentMethod.vue'
|
||||
import { commonMessages } from '../../utils'
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const modal = useTemplateRef<InstanceType<typeof NewModal>>('modal')
|
||||
const addPaymentMethod = useTemplateRef<InstanceType<typeof AddPaymentMethod>>('addPaymentMethod')
|
||||
|
||||
const props = defineProps<AddPaymentMethodProps>()
|
||||
const loading = ref(false)
|
||||
|
||||
async function open(paymentMethods: Stripe.PaymentMethod[]) {
|
||||
modal.value?.show()
|
||||
await nextTick()
|
||||
await addPaymentMethod.value?.reload(paymentMethods)
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
addingPaymentMethod: {
|
||||
id: 'modal.add-payment-method.title',
|
||||
defaultMessage: 'Adding a payment method',
|
||||
},
|
||||
paymentMethodAdd: {
|
||||
id: 'modal.add-payment-method.action',
|
||||
defaultMessage: 'Add payment method',
|
||||
},
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
show: open,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal ref="modal">
|
||||
<template #title>
|
||||
<span class="text-lg font-extrabold text-contrast">
|
||||
{{ formatMessage(messages.addingPaymentMethod) }}
|
||||
</span>
|
||||
</template>
|
||||
<div class="w-[40rem] max-w-full">
|
||||
<AddPaymentMethod
|
||||
ref="addPaymentMethod"
|
||||
:publishable-key="props.publishableKey"
|
||||
:return-url="props.returnUrl"
|
||||
:create-setup-intent="props.createSetupIntent"
|
||||
:on-error="props.onError"
|
||||
@start-loading="loading = true"
|
||||
@stop-loading="loading = false"
|
||||
/>
|
||||
<div class="input-group mt-auto pt-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="loading" @click="addPaymentMethod.submit()">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.paymentMethodAdd) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="modal.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import Accordion from '../base/Accordion.vue'
|
||||
import { formatPrice } from '@modrinth/utils'
|
||||
import { useVIntl } from '@vintl/vintl'
|
||||
import { SpinnerIcon } from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { locale } = useVIntl()
|
||||
|
||||
export type BillingItem = {
|
||||
title: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
period?: string
|
||||
currency: string
|
||||
total: number
|
||||
billingItems: BillingItem[]
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const periodSuffix = computed(() => {
|
||||
return props.period ? ` / ${props.period}` : ''
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<Accordion
|
||||
class="rounded-2xl overflow-hidden bg-bg"
|
||||
button-class="bg-transparent p-0 w-full p-4 active:scale-[0.98] transition-transform duration-100"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-contrast font-bold">Total</div>
|
||||
<div class="text-right mr-1">
|
||||
<span class="text-primary font-bold">
|
||||
<template v-if="loading">
|
||||
<SpinnerIcon class="animate-spin size-4" />
|
||||
</template>
|
||||
<template v-else> {{ formatPrice(locale, total, currency) }} </template
|
||||
><span class="text-xs text-secondary">{{ periodSuffix }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 flex flex-col gap-4 bg-table-alternateRow">
|
||||
<div
|
||||
v-for="{ title, amount } in billingItems"
|
||||
:key="title"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<div class="font-semibold">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<template v-if="loading">
|
||||
<SpinnerIcon class="animate-spin size-4" />
|
||||
</template>
|
||||
<template v-else> {{ formatPrice(locale, amount, currency) }} </template
|
||||
><span class="text-xs text-secondary">{{ periodSuffix }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { CardIcon, CurrencyIcon, PayPalIcon, UnknownIcon } from '@modrinth/assets'
|
||||
import { commonMessages, paymentMethodMessages } from '../../utils'
|
||||
import type Stripe from 'stripe'
|
||||
import { useVIntl } from '@vintl/vintl'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
defineProps<{
|
||||
method: Stripe.PaymentMethod
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="'type' in method">
|
||||
<CardIcon v-if="method.type === 'card'" class="size-[1.5em]" />
|
||||
<CurrencyIcon v-else-if="method.type === 'cashapp'" class="size-[1.5em]" />
|
||||
<PayPalIcon v-else-if="method.type === 'paypal'" class="size-[1.5em]" />
|
||||
<UnknownIcon v-else class="size-[1.5em]" />
|
||||
<span v-if="method.type === 'card' && 'card' in method && method.card">
|
||||
{{
|
||||
formatMessage(commonMessages.paymentMethodCardDisplay, {
|
||||
card_brand:
|
||||
formatMessage(paymentMethodMessages[method.card.brand]) ??
|
||||
formatMessage(paymentMethodMessages.unknown),
|
||||
last_four: method.card.last4,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<template v-else>
|
||||
{{
|
||||
formatMessage(paymentMethodMessages[method.type]) ??
|
||||
formatMessage(paymentMethodMessages.unknown)
|
||||
}}
|
||||
</template>
|
||||
|
||||
<span v-if="method.type === 'cashapp' && 'cashapp' in method && method.cashapp">
|
||||
({{ method.cashapp.cashtag }})
|
||||
</span>
|
||||
<span v-else-if="method.type === 'paypal' && 'paypal' in method && method.paypal">
|
||||
({{ method.paypal.payer_email }})
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
@@ -0,0 +1,346 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, useTemplateRef, nextTick, watch } from 'vue'
|
||||
import NewModal from '../modal/NewModal.vue'
|
||||
import { type MessageDescriptor, useVIntl, defineMessage } from '@vintl/vintl'
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
LeftArrowIcon,
|
||||
RightArrowIcon,
|
||||
XIcon,
|
||||
CheckCircleIcon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type {
|
||||
CreatePaymentIntentRequest,
|
||||
CreatePaymentIntentResponse,
|
||||
ServerBillingInterval,
|
||||
ServerPlan,
|
||||
ServerRegion,
|
||||
ServerStockRequest,
|
||||
UpdatePaymentIntentRequest,
|
||||
UpdatePaymentIntentResponse,
|
||||
} from '../../utils/billing'
|
||||
import { ButtonStyled } from '../index'
|
||||
import type Stripe from 'stripe'
|
||||
|
||||
import { commonMessages } from '../../utils'
|
||||
import RegionSelector from './ServersPurchase1Region.vue'
|
||||
import PaymentMethodSelector from './ServersPurchase2PaymentMethod.vue'
|
||||
import ConfirmPurchase from './ServersPurchase3Review.vue'
|
||||
import { useStripe } from '../../composables/stripe'
|
||||
import ModalLoadingIndicator from '../modal/ModalLoadingIndicator.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
export type RegionPing = {
|
||||
region: string
|
||||
ping: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
publishableKey: string
|
||||
returnUrl: string
|
||||
paymentMethods: Stripe.PaymentMethod[]
|
||||
customer: Stripe.Customer
|
||||
currency: string
|
||||
pings: RegionPing[]
|
||||
regions: ServerRegion[]
|
||||
availableProducts: ServerPlan[]
|
||||
refreshPaymentMethods: () => Promise<void>
|
||||
fetchStock: (region: ServerRegion, request: ServerStockRequest) => Promise<number>
|
||||
initiatePayment: (
|
||||
body: CreatePaymentIntentRequest | UpdatePaymentIntentRequest,
|
||||
) => Promise<UpdatePaymentIntentResponse | CreatePaymentIntentResponse>
|
||||
onError: (err: Error) => void
|
||||
}>()
|
||||
|
||||
const modal = useTemplateRef<InstanceType<typeof NewModal>>('modal')
|
||||
const selectedPlan = ref<ServerPlan>()
|
||||
const selectedInterval = ref<ServerBillingInterval>('quarterly')
|
||||
const loading = ref(false)
|
||||
const selectedRegion = ref<string>()
|
||||
|
||||
const {
|
||||
initializeStripe,
|
||||
selectPaymentMethod,
|
||||
primaryPaymentMethodId,
|
||||
loadStripeElements,
|
||||
selectedPaymentMethod,
|
||||
inputtedPaymentMethod,
|
||||
createNewPaymentMethod,
|
||||
loadingElements,
|
||||
loadingElementsFailed,
|
||||
tax,
|
||||
total,
|
||||
paymentMethodLoading,
|
||||
reloadPaymentIntent,
|
||||
hasPaymentMethod,
|
||||
submitPayment,
|
||||
completingPurchase,
|
||||
} = useStripe(
|
||||
props.publishableKey,
|
||||
props.customer,
|
||||
props.paymentMethods,
|
||||
props.currency,
|
||||
selectedPlan,
|
||||
selectedInterval,
|
||||
selectedRegion,
|
||||
props.initiatePayment,
|
||||
props.onError,
|
||||
)
|
||||
|
||||
const customServer = ref<boolean>(false)
|
||||
const acceptedEula = ref<boolean>(false)
|
||||
const skipPaymentMethods = ref<boolean>(true)
|
||||
|
||||
type Step = 'region' | 'payment' | 'review'
|
||||
|
||||
const steps: Step[] = ['region', 'payment', 'review']
|
||||
|
||||
const titles: Record<Step, MessageDescriptor> = {
|
||||
region: defineMessage({ id: 'servers.purchase.step.region.title', defaultMessage: 'Region' }),
|
||||
payment: defineMessage({
|
||||
id: 'servers.purchase.step.payment.title',
|
||||
defaultMessage: 'Payment method',
|
||||
}),
|
||||
review: defineMessage({ id: 'servers.purchase.step.review.title', defaultMessage: 'Review' }),
|
||||
}
|
||||
|
||||
const currentRegion = computed(() => {
|
||||
return props.regions.find((region) => region.shortcode === selectedRegion.value)
|
||||
})
|
||||
|
||||
const currentPing = computed(() => {
|
||||
return props.pings.find((ping) => ping.region === currentRegion.value?.shortcode)?.ping
|
||||
})
|
||||
|
||||
const currentStep = ref<Step>()
|
||||
|
||||
const currentStepIndex = computed(() => (currentStep.value ? steps.indexOf(currentStep.value) : -1))
|
||||
const previousStep = computed(() => {
|
||||
const step = currentStep.value ? steps[steps.indexOf(currentStep.value) - 1] : undefined
|
||||
if (step === 'payment' && skipPaymentMethods.value && primaryPaymentMethodId.value) {
|
||||
return 'region'
|
||||
}
|
||||
return step
|
||||
})
|
||||
const nextStep = computed(() =>
|
||||
currentStep.value ? steps[steps.indexOf(currentStep.value) + 1] : undefined,
|
||||
)
|
||||
|
||||
const canProceed = computed(() => {
|
||||
switch (currentStep.value) {
|
||||
case 'region':
|
||||
return selectedRegion.value && selectedPlan.value && selectedInterval.value
|
||||
case 'payment':
|
||||
return selectedPaymentMethod.value || !loadingElements.value
|
||||
case 'review':
|
||||
return acceptedEula.value && hasPaymentMethod.value && !completingPurchase.value
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
async function beforeProceed(step: string) {
|
||||
switch (step) {
|
||||
case 'region':
|
||||
return true
|
||||
case 'payment':
|
||||
await initializeStripe()
|
||||
|
||||
if (primaryPaymentMethodId.value && skipPaymentMethods.value) {
|
||||
const paymentMethod = await props.paymentMethods.find(
|
||||
(x) => x.id === primaryPaymentMethodId.value,
|
||||
)
|
||||
await selectPaymentMethod(paymentMethod)
|
||||
await setStep('review', true)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
case 'review':
|
||||
if (selectedPaymentMethod.value) {
|
||||
return true
|
||||
} else {
|
||||
const token = await createNewPaymentMethod()
|
||||
return !!token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function afterProceed(step: string) {
|
||||
switch (step) {
|
||||
case 'region':
|
||||
break
|
||||
case 'payment':
|
||||
await loadStripeElements()
|
||||
break
|
||||
case 'review':
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async function setStep(step: Step | undefined, skipValidation = false) {
|
||||
if (!step) {
|
||||
await submitPayment(props.returnUrl)
|
||||
return
|
||||
}
|
||||
|
||||
if (!skipValidation && !canProceed.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (await beforeProceed(step)) {
|
||||
currentStep.value = step
|
||||
await nextTick()
|
||||
|
||||
await afterProceed(step)
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedPlan, () => {
|
||||
console.log(selectedPlan.value)
|
||||
})
|
||||
|
||||
function begin(interval: ServerBillingInterval, plan?: ServerPlan) {
|
||||
loading.value = false
|
||||
selectedPlan.value = plan
|
||||
selectedInterval.value = interval
|
||||
customServer.value = !selectedPlan.value
|
||||
selectedPaymentMethod.value = undefined
|
||||
currentStep.value = steps[0]
|
||||
skipPaymentMethods.value = true
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show: begin,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<NewModal ref="modal">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-1 font-bold text-secondary">
|
||||
<template v-for="(title, id, index) in titles" :key="id">
|
||||
<button
|
||||
v-if="index < currentStepIndex"
|
||||
class="bg-transparent active:scale-95 font-bold text-secondary p-0"
|
||||
@click="setStep(id, true)"
|
||||
>
|
||||
{{ formatMessage(title) }}
|
||||
</button>
|
||||
<span
|
||||
v-else
|
||||
:class="{
|
||||
'text-contrast': index === currentStepIndex,
|
||||
}"
|
||||
>
|
||||
{{ formatMessage(title) }}
|
||||
</span>
|
||||
<ChevronRightIcon
|
||||
v-if="index < steps.length - 1"
|
||||
class="h-5 w-5 text-secondary"
|
||||
stroke-width="3"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-[40rem] max-w-full">
|
||||
<RegionSelector
|
||||
v-if="currentStep === 'region'"
|
||||
v-model:region="selectedRegion"
|
||||
v-model:plan="selectedPlan"
|
||||
:regions="regions"
|
||||
:pings="pings"
|
||||
:custom="customServer"
|
||||
:available-products="availableProducts"
|
||||
:fetch-stock="fetchStock"
|
||||
/>
|
||||
<PaymentMethodSelector
|
||||
v-else-if="currentStep === 'payment' && selectedPlan && selectedInterval"
|
||||
:payment-methods="paymentMethods"
|
||||
:selected="selectedPaymentMethod"
|
||||
:loading-elements="loadingElements"
|
||||
:loading-elements-failed="loadingElementsFailed"
|
||||
@select="selectPaymentMethod"
|
||||
/>
|
||||
<ConfirmPurchase
|
||||
v-else-if="
|
||||
currentStep === 'review' &&
|
||||
hasPaymentMethod &&
|
||||
currentRegion &&
|
||||
selectedInterval &&
|
||||
selectedPlan
|
||||
"
|
||||
v-model:interval="selectedInterval"
|
||||
v-model:accepted-eula="acceptedEula"
|
||||
:currency="currency"
|
||||
:plan="selectedPlan"
|
||||
:region="currentRegion"
|
||||
:ping="currentPing"
|
||||
:loading="paymentMethodLoading"
|
||||
:selected-payment-method="selectedPaymentMethod || inputtedPaymentMethod"
|
||||
:tax="tax"
|
||||
:total="total"
|
||||
@change-payment-method="
|
||||
() => {
|
||||
skipPaymentMethods = false
|
||||
setStep('payment', true)
|
||||
}
|
||||
"
|
||||
@reload-payment-intent="reloadPaymentIntent"
|
||||
/>
|
||||
<div v-else>Something went wrong</div>
|
||||
<div
|
||||
v-show="
|
||||
selectedPaymentMethod === undefined &&
|
||||
currentStep === 'payment' &&
|
||||
selectedPlan &&
|
||||
selectedInterval
|
||||
"
|
||||
class="min-h-[16rem] flex flex-col gap-2 mt-2 p-4 bg-table-alternateRow rounded-xl justify-center items-center"
|
||||
>
|
||||
<div v-show="loadingElements">
|
||||
<ModalLoadingIndicator :error="loadingElementsFailed">
|
||||
Loading...
|
||||
<template #error> Error loading Stripe payment UI. </template>
|
||||
</ModalLoadingIndicator>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div id="address-element"></div>
|
||||
<div id="payment-element" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-between mt-4">
|
||||
<ButtonStyled>
|
||||
<button v-if="previousStep" @click="previousStep && setStep(previousStep, true)">
|
||||
<LeftArrowIcon /> {{ formatMessage(commonMessages.backButton) }}
|
||||
</button>
|
||||
<button v-else @click="modal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-tooltip="
|
||||
currentStep === 'review' && !acceptedEula
|
||||
? 'You must accept the Minecraft EULA to proceed.'
|
||||
: undefined
|
||||
"
|
||||
:disabled="!canProceed"
|
||||
@click="setStep(nextStep)"
|
||||
>
|
||||
<template v-if="currentStep === 'review'">
|
||||
<SpinnerIcon v-if="completingPurchase" class="animate-spin" />
|
||||
<CheckCircleIcon v-else />
|
||||
Subscribe
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ formatMessage(commonMessages.nextButton) }} <RightArrowIcon />
|
||||
</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
37
packages/ui/src/components/billing/PaymentMethodOption.vue
Normal file
37
packages/ui/src/components/billing/PaymentMethodOption.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { RadioButtonIcon, RadioButtonCheckedIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import type Stripe from 'stripe'
|
||||
import FormattedPaymentMethod from './FormattedPaymentMethod.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select'): void
|
||||
}>()
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
item: Stripe.PaymentMethod | undefined
|
||||
selected: boolean
|
||||
loading?: boolean
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center w-full gap-2 border-none p-3 text-primary rounded-xl transition-all duration-200 hover:bg-button-bg hover:brightness-[--hover-brightness] active:scale-[0.98] hover:cursor-pointer"
|
||||
:class="selected ? 'bg-button-bg' : 'bg-transparent'"
|
||||
@click="emit('select')"
|
||||
>
|
||||
<RadioButtonCheckedIcon v-if="selected" class="size-6 text-brand" />
|
||||
<RadioButtonIcon v-else class="size-6 text-secondary" />
|
||||
|
||||
<template v-if="item === undefined">
|
||||
<span>New payment method</span>
|
||||
</template>
|
||||
<FormattedPaymentMethod v-else-if="item" :method="item" />
|
||||
<SpinnerIcon v-if="loading" class="ml-auto size-4 text-secondary animate-spin" />
|
||||
</button>
|
||||
</template>
|
||||
@@ -2,7 +2,8 @@
|
||||
<NewModal ref="purchaseModal">
|
||||
<template #title>
|
||||
<span class="text-contrast text-xl font-extrabold">
|
||||
<template v-if="productType === 'midas'">Subscribe to Modrinth+!</template>
|
||||
<template v-if="intervalChangeOnly"> Change billing interval </template>
|
||||
<template v-else-if="productType === 'midas'">Subscribe to Modrinth+!</template>
|
||||
<template v-else-if="productType === 'pyro'">
|
||||
<template v-if="existingSubscription"> Upgrade server plan </template>
|
||||
<template v-else> Subscribe to Modrinth Servers! </template>
|
||||
@@ -11,11 +12,11 @@
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex items-center gap-1 pb-4">
|
||||
<template v-if="productType === 'pyro' && !projectId">
|
||||
<template v-if="!props.intervalChangeOnly && productType === 'pyro' && !projectId">
|
||||
<span
|
||||
:class="{
|
||||
'text-secondary': purchaseModalStep !== 0,
|
||||
'font-bold': purchaseModalStep === 0,
|
||||
'text-secondary': !isConfigStep,
|
||||
'font-bold': isConfigStep,
|
||||
}"
|
||||
>
|
||||
Configure
|
||||
@@ -25,8 +26,8 @@
|
||||
</template>
|
||||
<span
|
||||
:class="{
|
||||
'text-secondary': purchaseModalStep !== (productType === 'pyro' && !projectId ? 1 : 0),
|
||||
'font-bold': purchaseModalStep === (productType === 'pyro' && !projectId ? 1 : 0),
|
||||
'text-secondary': !isBillingStep,
|
||||
'font-bold': isBillingStep,
|
||||
}"
|
||||
>
|
||||
{{ productType === 'pyro' ? 'Billing' : 'Plan' }}
|
||||
@@ -37,8 +38,8 @@
|
||||
<ChevronRightIcon class="h-5 w-5 text-secondary" />
|
||||
<span
|
||||
:class="{
|
||||
'text-secondary': purchaseModalStep !== (productType === 'pyro' && !projectId ? 2 : 1),
|
||||
'font-bold': purchaseModalStep === (productType === 'pyro' && !projectId ? 2 : 1),
|
||||
'text-secondary': !isPaymentStep,
|
||||
'font-bold': isPaymentStep,
|
||||
}"
|
||||
>
|
||||
Payment
|
||||
@@ -46,17 +47,14 @@
|
||||
<ChevronRightIcon class="h-5 w-5 text-secondary" />
|
||||
<span
|
||||
:class="{
|
||||
'text-secondary': purchaseModalStep !== (productType === 'pyro' && !projectId ? 3 : 2),
|
||||
'font-bold': purchaseModalStep === (productType === 'pyro' && !projectId ? 3 : 2),
|
||||
'text-secondary': !isReviewStep,
|
||||
'font-bold': isReviewStep,
|
||||
}"
|
||||
>
|
||||
Review
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="productType === 'pyro' && !projectId && purchaseModalStep === 0"
|
||||
class="md:w-[600px] flex flex-col gap-4"
|
||||
>
|
||||
<div v-if="!props.intervalChangeOnly && isConfigStep" class="md:w-[600px] flex flex-col gap-4">
|
||||
<div v-if="!existingSubscription">
|
||||
<p class="my-2 text-lg font-bold">Configure your server</p>
|
||||
<div class="flex flex-col gap-4">
|
||||
@@ -182,10 +180,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 1 : 0)"
|
||||
class="md:w-[600px]"
|
||||
>
|
||||
<div v-if="isBillingStep" class="md:w-[600px]">
|
||||
<div>
|
||||
<p class="my-2 text-lg font-bold">Choose billing interval</p>
|
||||
<div v-if="existingPlan" class="flex flex-col gap-3 mb-4 text-secondary">
|
||||
@@ -214,10 +209,17 @@
|
||||
{{ interval }}
|
||||
</span>
|
||||
<span
|
||||
v-if="interval === 'yearly'"
|
||||
v-if="interval === 'yearly' || interval === 'quarterly'"
|
||||
class="rounded-full bg-brand px-2 py-1 font-bold text-brand-inverted"
|
||||
>
|
||||
SAVE {{ calculateSavings(price.prices.intervals.monthly, rawPrice) }}%
|
||||
SAVE
|
||||
{{
|
||||
calculateSavings(
|
||||
price.prices.intervals.monthly,
|
||||
rawPrice,
|
||||
interval === 'quarterly' ? 3 : 12,
|
||||
)
|
||||
}}%
|
||||
</span>
|
||||
<span class="ml-auto text-lg" :class="{ 'text-secondary': selectedPlan !== interval }">
|
||||
{{ formatPrice(locale, rawPrice, price.currency_code) }}
|
||||
@@ -242,9 +244,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template
|
||||
v-if="purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 2 : 1)"
|
||||
>
|
||||
<template v-if="isPaymentStep">
|
||||
<div
|
||||
v-show="loadingPaymentMethodModal !== 2"
|
||||
class="flex min-h-[16rem] items-center justify-center md:w-[600px]"
|
||||
@@ -256,12 +256,20 @@
|
||||
<div id="payment-element" class="mt-4"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 3 : 2)"
|
||||
class="md:w-[650px]"
|
||||
>
|
||||
<div v-if="isReviewStep" class="md:w-[650px]">
|
||||
<div v-if="props.intervalChangeOnly" class="r-4 rounded-xl bg-bg p-4">
|
||||
<p class="my-2 text-lg font-bold text-primary">Billing interval change</p>
|
||||
<div class="mb-2 flex justify-between">
|
||||
<span class="text-secondary">
|
||||
Current interval: {{ props.existingSubscription?.interval }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-2 flex justify-between">
|
||||
<span class="text-secondary"> New interval: {{ selectedPlan }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="mutatedProduct.metadata.type === 'pyro' && !existingSubscription"
|
||||
v-else-if="mutatedProduct.metadata.type === 'pyro' && !existingSubscription"
|
||||
class="r-4 rounded-xl bg-bg p-4 mb-4"
|
||||
>
|
||||
<p class="my-2 text-lg font-bold text-primary">Server details</p>
|
||||
@@ -422,7 +430,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="input-group push-right pt-4">
|
||||
<template v-if="purchaseModalStep === 0">
|
||||
<template v-if="!props.intervalChangeOnly && purchaseModalStep === 0">
|
||||
<button class="btn" @click="$refs.purchaseModal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
@@ -447,40 +455,21 @@
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 1 : 0)
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="btn"
|
||||
@click="
|
||||
purchaseModalStep =
|
||||
mutatedProduct.metadata.type === 'pyro' && !projectId ? 0 : purchaseModalStep
|
||||
"
|
||||
>
|
||||
<template v-else-if="isBillingStep">
|
||||
<button v-if="!props.intervalChangeOnly" class="btn" @click="purchaseModalStep = 0">
|
||||
Back
|
||||
</button>
|
||||
<button class="btn btn-primary" :disabled="paymentLoading" @click="beginPurchaseFlow(true)">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="paymentLoading || isSameInterval"
|
||||
@click="beginPurchaseFlow(true)"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
Select
|
||||
</button>
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 2 : 1)
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="btn"
|
||||
@click="
|
||||
() => {
|
||||
purchaseModalStep = mutatedProduct.metadata.type === 'pyro' && !projectId ? 1 : 0
|
||||
loadingPaymentMethodModal = 0
|
||||
paymentLoading = false
|
||||
}
|
||||
"
|
||||
>
|
||||
<template v-else-if="isPaymentStep">
|
||||
<button class="btn" @click="purchaseModalStep = props.intervalChangeOnly ? 0 : 1">
|
||||
Back
|
||||
</button>
|
||||
<button class="btn btn-primary" :disabled="paymentLoading" @click="validatePayment">
|
||||
@@ -488,11 +477,7 @@
|
||||
Continue
|
||||
</button>
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 3 : 2)
|
||||
"
|
||||
>
|
||||
<template v-else-if="isReviewStep">
|
||||
<button class="btn" @click="$refs.purchaseModal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
@@ -504,7 +489,7 @@
|
||||
@click="submitPayment"
|
||||
>
|
||||
<CheckCircleIcon />
|
||||
Subscribe
|
||||
{{ props.intervalChangeOnly ? 'Change Interval' : 'Subscribe' }}
|
||||
</button>
|
||||
<!-- Default Subscribe Button, so M+ still works -->
|
||||
<button v-else class="btn btn-primary" :disabled="paymentLoading" @click="submitPayment">
|
||||
@@ -632,10 +617,40 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
intervalChangeOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const productType = computed(() => (props.customServer ? 'pyro' : props.product.metadata.type))
|
||||
|
||||
const isConfigStep = computed(
|
||||
() =>
|
||||
productType.value === 'pyro' &&
|
||||
!props.projectId &&
|
||||
!props.intervalChangeOnly &&
|
||||
purchaseModalStep.value === 0,
|
||||
)
|
||||
|
||||
const isBillingStep = computed(
|
||||
() =>
|
||||
purchaseModalStep.value ===
|
||||
(props.intervalChangeOnly ? 0 : productType.value === 'pyro' && !props.projectId ? 1 : 0),
|
||||
)
|
||||
|
||||
const isPaymentStep = computed(
|
||||
() =>
|
||||
purchaseModalStep.value ===
|
||||
(props.intervalChangeOnly ? 1 : productType.value === 'pyro' && !props.projectId ? 2 : 1),
|
||||
)
|
||||
|
||||
const isReviewStep = computed(
|
||||
() =>
|
||||
purchaseModalStep.value ===
|
||||
(props.intervalChangeOnly ? 2 : productType.value === 'pyro' && !props.projectId ? 3 : 2),
|
||||
)
|
||||
|
||||
const messages = defineMessages({
|
||||
paymentMethodCardDisplay: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_card_display',
|
||||
@@ -742,6 +757,10 @@ const customServerConfig = reactive({
|
||||
})
|
||||
|
||||
const updateCustomServerProduct = () => {
|
||||
if (!props.product || !Array.isArray(props.product)) {
|
||||
return
|
||||
}
|
||||
|
||||
customMatchingProduct.value = props.product.find(
|
||||
(product) => product.metadata.ram === customServerConfig.ram,
|
||||
)
|
||||
@@ -782,6 +801,10 @@ const updateCustomServerStock = async () => {
|
||||
}
|
||||
|
||||
function updateRamValues() {
|
||||
if (!props.product || !Array.isArray(props.product)) {
|
||||
return
|
||||
}
|
||||
|
||||
const ramValues = props.product.map((product) => product.metadata.ram / 1024)
|
||||
customMinRam.value = Math.min(...ramValues)
|
||||
customMaxRam.value = Math.max(...ramValues)
|
||||
@@ -861,10 +884,19 @@ const sharedCpus = computed(() => {
|
||||
return (mutatedProduct.value?.metadata?.cpu ?? 0) / 2
|
||||
})
|
||||
|
||||
const isSameInterval = computed(() => {
|
||||
return (
|
||||
props.intervalChangeOnly &&
|
||||
props.existingSubscription &&
|
||||
selectedPlan.value === props.existingSubscription.interval
|
||||
)
|
||||
})
|
||||
|
||||
function nextStep() {
|
||||
if (
|
||||
mutatedProduct.value.metadata.type === 'pyro' &&
|
||||
!props.projectId &&
|
||||
!props.intervalChangeOnly &&
|
||||
purchaseModalStep.value === 0
|
||||
) {
|
||||
purchaseModalStep.value = 1
|
||||
@@ -884,13 +916,19 @@ async function beginPurchaseFlow(skip = false) {
|
||||
paymentLoading.value = true
|
||||
await refreshPayment(null, primaryPaymentMethodId.value)
|
||||
paymentLoading.value = false
|
||||
purchaseModalStep.value =
|
||||
mutatedProduct.value.metadata.type === 'pyro' && !props.projectId ? 3 : 2
|
||||
purchaseModalStep.value = props.intervalChangeOnly
|
||||
? 2
|
||||
: mutatedProduct.value.metadata.type === 'pyro' && !props.projectId
|
||||
? 3
|
||||
: 2
|
||||
} else {
|
||||
try {
|
||||
loadingPaymentMethodModal.value = 0
|
||||
purchaseModalStep.value =
|
||||
mutatedProduct.value.metadata.type === 'pyro' && !props.projectId ? 2 : 1
|
||||
purchaseModalStep.value = props.intervalChangeOnly
|
||||
? 1
|
||||
: mutatedProduct.value.metadata.type === 'pyro' && !props.projectId
|
||||
? 2
|
||||
: 1
|
||||
|
||||
await nextTick()
|
||||
|
||||
@@ -948,8 +986,11 @@ async function validatePayment() {
|
||||
|
||||
loadingPaymentMethodModal.value = 0
|
||||
confirmationToken.value = await createConfirmationToken()
|
||||
purchaseModalStep.value =
|
||||
mutatedProduct.value.metadata.type === 'pyro' && !props.projectId ? 3 : 2
|
||||
purchaseModalStep.value = props.intervalChangeOnly
|
||||
? 2
|
||||
: mutatedProduct.value.metadata.type === 'pyro' && !props.projectId
|
||||
? 3
|
||||
: 2
|
||||
paymentLoading.value = false
|
||||
}
|
||||
|
||||
@@ -999,7 +1040,7 @@ async function refreshPayment(confirmationId, paymentMethodId) {
|
||||
},
|
||||
)
|
||||
|
||||
if (!paymentIntentId.value) {
|
||||
if (result.payment_intent_id && !paymentIntentId.value) {
|
||||
paymentIntentId.value = result.payment_intent_id
|
||||
clientSecret.value = result.client_secret
|
||||
}
|
||||
|
||||
246
packages/ui/src/components/billing/ServersPurchase1Region.vue
Normal file
246
packages/ui/src/components/billing/ServersPurchase1Region.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<script setup lang="ts">
|
||||
import ServersRegionButton from './ServersRegionButton.vue'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { IntlFormatted } from '@vintl/vintl/components'
|
||||
import { onMounted, ref, computed, watch } from 'vue'
|
||||
import type { RegionPing } from './ModrinthServersPurchaseModal.vue'
|
||||
import type { ServerPlan, ServerRegion, ServerStockRequest } from '../../utils/billing'
|
||||
import ModalLoadingIndicator from '../modal/ModalLoadingIndicator.vue'
|
||||
import Slider from '../base/Slider.vue'
|
||||
import { SpinnerIcon, XIcon, InfoIcon } from '@modrinth/assets'
|
||||
import ServersSpecs from './ServersSpecs.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<{
|
||||
regions: ServerRegion[]
|
||||
pings: RegionPing[]
|
||||
fetchStock: (region: ServerRegion, request: ServerStockRequest) => Promise<number>
|
||||
custom: boolean
|
||||
availableProducts: ServerPlan[]
|
||||
}>()
|
||||
|
||||
const loading = ref(true)
|
||||
const checkingCustomStock = ref(false)
|
||||
const selectedPlan = defineModel<ServerPlan>('plan')
|
||||
const selectedRegion = defineModel<string>('region')
|
||||
|
||||
const regionOrder: string[] = ['us-vin', 'eu-lim']
|
||||
|
||||
const sortedRegions = computed(() => {
|
||||
return props.regions.slice().sort((a, b) => {
|
||||
return regionOrder.indexOf(a.shortcode) - regionOrder.indexOf(b.shortcode)
|
||||
})
|
||||
})
|
||||
|
||||
const selectedRam = ref<number>(-1)
|
||||
|
||||
const ramOptions = computed(() => {
|
||||
return props.availableProducts
|
||||
.map((product) => (product.metadata.ram ?? 0) / 1024)
|
||||
.filter((x) => x > 0)
|
||||
})
|
||||
|
||||
const minRam = computed(() => {
|
||||
return Math.min(...ramOptions.value)
|
||||
})
|
||||
const maxRam = computed(() => {
|
||||
return Math.max(...ramOptions.value)
|
||||
})
|
||||
|
||||
const lowestProduct = computed(() => {
|
||||
return (
|
||||
props.availableProducts.find(
|
||||
(product) => (product.metadata.ram ?? 0) / 1024 === minRam.value,
|
||||
) ?? props.availableProducts[0]
|
||||
)
|
||||
})
|
||||
|
||||
function updateRamStock(regionToCheck: string, newRam: number) {
|
||||
if (newRam > 0) {
|
||||
checkingCustomStock.value = true
|
||||
const plan = props.availableProducts.find(
|
||||
(product) => (product.metadata.ram ?? 0) / 1024 === newRam,
|
||||
)
|
||||
if (plan) {
|
||||
const region = sortedRegions.value.find((region) => region.shortcode === regionToCheck)
|
||||
if (region) {
|
||||
props
|
||||
.fetchStock(region, {
|
||||
cpu: plan.metadata.cpu ?? 0,
|
||||
memory_mb: plan.metadata.ram ?? 0,
|
||||
swap_mb: plan.metadata.swap ?? 0,
|
||||
storage_mb: plan.metadata.storage ?? 0,
|
||||
})
|
||||
.then((stock: number) => {
|
||||
if (stock > 0) {
|
||||
selectedPlan.value = plan
|
||||
} else {
|
||||
selectedPlan.value = undefined
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
checkingCustomStock.value = false
|
||||
})
|
||||
} else {
|
||||
checkingCustomStock.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedRam, (newRam: number) => {
|
||||
if (props.custom && selectedRegion.value) {
|
||||
updateRamStock(selectedRegion.value, newRam)
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedRegion, (newRegion: string | undefined) => {
|
||||
if (props.custom && newRegion) {
|
||||
updateRamStock(newRegion, selectedRam.value)
|
||||
}
|
||||
})
|
||||
|
||||
const currentStock = ref<{ [region: string]: number }>({})
|
||||
const bestPing = ref<string>()
|
||||
|
||||
const messages = defineMessages({
|
||||
prompt: {
|
||||
id: 'servers.region.prompt',
|
||||
defaultMessage: 'Where would you like your server to be located?',
|
||||
},
|
||||
regionUnsupported: {
|
||||
id: 'servers.region.region-unsupported',
|
||||
defaultMessage: `Region not listed? <link>Let us know where you'd like to see Modrinth Servers next!</link>`,
|
||||
},
|
||||
customPrompt: {
|
||||
id: 'servers.region.custom.prompt',
|
||||
defaultMessage: `How much RAM do you want your server to have?`,
|
||||
},
|
||||
})
|
||||
|
||||
async function updateStock() {
|
||||
currentStock.value = {}
|
||||
const capacityChecks = sortedRegions.value.map((region) =>
|
||||
props.fetchStock(
|
||||
region,
|
||||
selectedPlan.value
|
||||
? {
|
||||
cpu: selectedPlan.value?.metadata.cpu ?? 0,
|
||||
memory_mb: selectedPlan.value?.metadata.ram ?? 0,
|
||||
swap_mb: selectedPlan.value?.metadata.swap ?? 0,
|
||||
storage_mb: selectedPlan.value?.metadata.storage ?? 0,
|
||||
}
|
||||
: {
|
||||
cpu: lowestProduct.value.metadata.cpu ?? 0,
|
||||
memory_mb: lowestProduct.value.metadata.ram ?? 0,
|
||||
swap_mb: lowestProduct.value.metadata.swap ?? 0,
|
||||
storage_mb: lowestProduct.value.metadata.storage ?? 0,
|
||||
},
|
||||
),
|
||||
)
|
||||
const results = await Promise.all(capacityChecks)
|
||||
results.forEach((result, index) => {
|
||||
currentStock.value[sortedRegions.value[index].shortcode] = result
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// auto select region with lowest ping
|
||||
loading.value = true
|
||||
bestPing.value =
|
||||
props.pings.length > 0
|
||||
? props.pings.reduce((acc, cur) => {
|
||||
return acc.ping < cur.ping ? acc : cur
|
||||
})?.region
|
||||
: undefined
|
||||
selectedRegion.value = undefined
|
||||
selectedRam.value = minRam.value
|
||||
checkingCustomStock.value = true
|
||||
updateStock().then(() => {
|
||||
const firstWithStock = sortedRegions.value.find(
|
||||
(region) => currentStock.value[region.shortcode] > 0,
|
||||
)
|
||||
let stockedRegion = selectedRegion.value
|
||||
if (!stockedRegion) {
|
||||
stockedRegion =
|
||||
bestPing.value && currentStock.value[bestPing.value] > 0
|
||||
? bestPing.value
|
||||
: firstWithStock?.shortcode
|
||||
}
|
||||
selectedRegion.value = stockedRegion
|
||||
if (props.custom && stockedRegion) {
|
||||
updateRamStock(stockedRegion, minRam.value)
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalLoadingIndicator v-if="loading" class="flex py-40 justify-center">
|
||||
Checking availability...
|
||||
</ModalLoadingIndicator>
|
||||
<template v-else>
|
||||
<h2 class="mt-0 mb-4 text-xl font-bold text-contrast">
|
||||
{{ formatMessage(messages.prompt) }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<ServersRegionButton
|
||||
v-for="region in sortedRegions"
|
||||
:key="region.shortcode"
|
||||
v-model="selectedRegion"
|
||||
:region="region"
|
||||
:out-of-stock="currentStock[region.shortcode] === 0"
|
||||
:ping="pings.find((p) => p.region === region.shortcode)?.ping"
|
||||
:best-ping="bestPing === region.shortcode"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3 text-sm">
|
||||
<IntlFormatted :message-id="messages.regionUnsupported">
|
||||
<template #link="{ children }">
|
||||
<a
|
||||
class="text-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://surveys.modrinth.com/servers-region-waitlist"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
<template v-if="custom">
|
||||
<h2 class="mt-4 mb-2 text-xl font-bold text-contrast">
|
||||
{{ formatMessage(messages.customPrompt) }}
|
||||
</h2>
|
||||
<div>
|
||||
<Slider v-model="selectedRam" :min="minRam" :max="maxRam" :step="2" unit="GB" />
|
||||
<div class="bg-bg rounded-xl p-4 mt-4 text-secondary">
|
||||
<div v-if="checkingCustomStock" class="flex gap-2 items-center">
|
||||
<SpinnerIcon class="size-5 shrink-0 animate-spin" /> Checking availability...
|
||||
</div>
|
||||
<div v-else-if="selectedPlan">
|
||||
<ServersSpecs
|
||||
class="!flex-row justify-between"
|
||||
:ram="selectedPlan.metadata.ram ?? 0"
|
||||
:storage="selectedPlan.metadata.storage ?? 0"
|
||||
:cpus="selectedPlan.metadata.cpu ?? 0"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex gap-2 items-center">
|
||||
<XIcon class="size-5 shrink-0 text-red" /> Sorry, we don't have any plans available with
|
||||
{{ selectedRam }} GB RAM in this region.
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<InfoIcon class="hidden sm:block shrink-0 mt-1" />
|
||||
<span class="text-sm text-secondary">
|
||||
Storage and shared CPU count are currently not configurable independently, and are based
|
||||
on the amount of RAM you select.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type Stripe from 'stripe'
|
||||
import { useVIntl, defineMessages } from '@vintl/vintl'
|
||||
import PaymentMethodOption from './PaymentMethodOption.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', paymentMethod: Stripe.PaymentMethod | undefined): void
|
||||
}>()
|
||||
|
||||
defineProps<{
|
||||
paymentMethods: Stripe.PaymentMethod[]
|
||||
selected?: Stripe.PaymentMethod
|
||||
loadingElements: boolean
|
||||
loadingElementsFailed: boolean
|
||||
}>()
|
||||
|
||||
const messages = defineMessages({
|
||||
prompt: {
|
||||
id: 'servers.purchase.step.payment.prompt',
|
||||
defaultMessage: 'Select a payment method',
|
||||
},
|
||||
description: {
|
||||
id: 'servers.purchase.step.payment.description',
|
||||
defaultMessage: `You won't be charged yet.`,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="mt-0 mb-1 text-xl font-bold text-contrast">
|
||||
{{ formatMessage(messages.prompt) }}
|
||||
</h2>
|
||||
<p class="mt-0 mb-4 text-secondary">
|
||||
{{ formatMessage(messages.description) }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<PaymentMethodOption
|
||||
v-for="method in paymentMethods"
|
||||
:key="method.id"
|
||||
:item="method"
|
||||
:selected="selected?.id === method.id"
|
||||
@select="emit('select', method)"
|
||||
/>
|
||||
<PaymentMethodOption
|
||||
:loading="false"
|
||||
:item="undefined"
|
||||
:selected="selected === undefined"
|
||||
@select="emit('select', undefined)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
263
packages/ui/src/components/billing/ServersPurchase3Review.vue
Normal file
263
packages/ui/src/components/billing/ServersPurchase3Review.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { ServerBillingInterval, ServerPlan, ServerRegion } from '../../utils/billing'
|
||||
import TagItem from '../base/TagItem.vue'
|
||||
import ServersSpecs from './ServersSpecs.vue'
|
||||
import { formatPrice, getPingLevel } from '@modrinth/utils'
|
||||
import { useVIntl } from '@vintl/vintl'
|
||||
import { regionOverrides } from '../../utils/regions'
|
||||
import {
|
||||
EditIcon,
|
||||
RightArrowIcon,
|
||||
SignalIcon,
|
||||
SpinnerIcon,
|
||||
XIcon,
|
||||
RadioButtonIcon,
|
||||
RadioButtonCheckedIcon,
|
||||
ExternalIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type Stripe from 'stripe'
|
||||
import FormattedPaymentMethod from './FormattedPaymentMethod.vue'
|
||||
import ButtonStyled from '../base/ButtonStyled.vue'
|
||||
import Checkbox from '../base/Checkbox.vue'
|
||||
import ExpandableInvoiceTotal from './ExpandableInvoiceTotal.vue'
|
||||
|
||||
const vintl = useVIntl()
|
||||
const { locale, formatMessage } = vintl
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'changePaymentMethod' | 'reloadPaymentIntent'): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
plan: ServerPlan
|
||||
region: ServerRegion
|
||||
tax?: number
|
||||
total?: number
|
||||
currency: string
|
||||
ping?: number
|
||||
loading?: boolean
|
||||
selectedPaymentMethod: Stripe.PaymentMethod | undefined
|
||||
}>()
|
||||
|
||||
const interval = defineModel<ServerBillingInterval>('interval', { required: true })
|
||||
const acceptedEula = defineModel<boolean>('acceptedEula', { required: true })
|
||||
|
||||
const prices = computed(() => {
|
||||
return props.plan.prices.find((x) => x.currency_code === props.currency)
|
||||
})
|
||||
|
||||
const planName = computed(() => {
|
||||
if (!props.plan || !props.plan.metadata || props.plan.metadata.type !== 'pyro') return 'Unknown'
|
||||
const ram = props.plan.metadata.ram
|
||||
if (ram === 4096) return 'Small'
|
||||
if (ram === 6144) return 'Medium'
|
||||
if (ram === 8192) return 'Large'
|
||||
return 'Custom'
|
||||
})
|
||||
|
||||
const flag = computed(
|
||||
() =>
|
||||
regionOverrides[props.region.shortcode]?.flag ??
|
||||
`https://flagcdn.com/${props.region.country_code}.svg`,
|
||||
)
|
||||
const overrideTitle = computed(() => regionOverrides[props.region.shortcode]?.name)
|
||||
const title = computed(() =>
|
||||
overrideTitle.value ? formatMessage(overrideTitle.value) : props.region.display_name,
|
||||
)
|
||||
const locationSubtitle = computed(() =>
|
||||
overrideTitle.value ? props.region.display_name : undefined,
|
||||
)
|
||||
const pingLevel = computed(() => getPingLevel(props.ping ?? 0))
|
||||
|
||||
const period = computed(() => {
|
||||
if (interval.value === 'monthly') return 'month'
|
||||
if (interval.value === 'quarterly') return '3 months'
|
||||
if (interval.value === 'yearly') return 'year'
|
||||
return '???'
|
||||
})
|
||||
|
||||
const monthsInInterval: Record<ServerBillingInterval, number> = {
|
||||
monthly: 1,
|
||||
quarterly: 3,
|
||||
yearly: 12,
|
||||
}
|
||||
|
||||
function setInterval(newInterval: ServerBillingInterval) {
|
||||
interval.value = newInterval
|
||||
emit('reloadPaymentIntent')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid sm:grid-cols-[3fr_2fr] gap-4">
|
||||
<div class="bg-table-alternateRow p-4 rounded-2xl">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<LazyUiServersModrinthServersIcon class="flex h-5 w-fit" />
|
||||
<TagItem>{{ planName }}</TagItem>
|
||||
</div>
|
||||
<div>
|
||||
<ServersSpecs
|
||||
v-if="plan.metadata && plan.metadata.ram && plan.metadata.storage && plan.metadata.cpu"
|
||||
class="!grid sm:grid-cols-2"
|
||||
:ram="plan.metadata.ram"
|
||||
:storage="plan.metadata.storage"
|
||||
:cpus="plan.metadata.cpu"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bg-table-alternateRow p-4 rounded-2xl flex flex-col gap-2 items-center justify-center"
|
||||
>
|
||||
<img
|
||||
v-if="flag"
|
||||
class="aspect-[16/10] max-w-12 w-full object-cover rounded-md border-1 border-button-border border-solid"
|
||||
:src="flag"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="font-semibold">
|
||||
{{ title }}
|
||||
</span>
|
||||
<span class="text-xs flex items-center gap-1 text-secondary font-medium">
|
||||
<template v-if="locationSubtitle">
|
||||
<span>
|
||||
{{ locationSubtitle }}
|
||||
</span>
|
||||
<span v-if="ping !== -1">•</span>
|
||||
</template>
|
||||
<template v-if="ping !== -1">
|
||||
<SignalIcon
|
||||
v-if="ping"
|
||||
aria-hidden="true"
|
||||
:style="`--_signal-${pingLevel}: ${pingLevel <= 2 ? 'var(--color-red)' : pingLevel <= 4 ? 'var(--color-orange)' : 'var(--color-green)'}`"
|
||||
stroke-width="3px"
|
||||
class="shrink-0"
|
||||
/>
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
<template v-if="ping"> {{ ping }}ms </template>
|
||||
<span v-else> Testing connection... </span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 mt-4">
|
||||
<button
|
||||
:class="
|
||||
interval === 'monthly'
|
||||
? 'bg-button-bg border-transparent'
|
||||
: 'bg-transparent border-button-border'
|
||||
"
|
||||
class="rounded-2xl active:scale-[0.98] transition-transform duration-100 border-2 border-solid p-4 flex items-center gap-2"
|
||||
@click="setInterval('monthly')"
|
||||
>
|
||||
<RadioButtonCheckedIcon v-if="interval === 'monthly'" class="size-6 text-brand" />
|
||||
<RadioButtonIcon v-else class="size-6 text-secondary" />
|
||||
<div class="flex flex-col items-start gap-1 font-medium text-primary">
|
||||
<span class="flex items-center gap-1" :class="{ 'text-contrast': interval === 'monthly' }"
|
||||
>Pay monthly</span
|
||||
>
|
||||
<span class="text-sm text-secondary flex items-center gap-1"
|
||||
>{{ formatPrice(locale, prices?.prices.intervals['monthly'], currency, true) }} /
|
||||
month</span
|
||||
>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
:class="
|
||||
interval === 'quarterly'
|
||||
? 'bg-button-bg border-transparent'
|
||||
: 'bg-transparent border-button-border'
|
||||
"
|
||||
class="rounded-2xl active:scale-[0.98] transition-transform duration-100 border-2 border-solid p-4 flex items-center gap-2"
|
||||
@click="setInterval('quarterly')"
|
||||
>
|
||||
<RadioButtonCheckedIcon v-if="interval === 'quarterly'" class="size-6 text-brand" />
|
||||
<RadioButtonIcon v-else class="size-6 text-secondary" />
|
||||
<div class="flex flex-col items-start gap-1 font-medium text-primary">
|
||||
<span class="flex items-center gap-1" :class="{ 'text-contrast': interval === 'quarterly' }"
|
||||
>Pay quarterly
|
||||
<span class="text-xs font-bold text-brand px-1.5 py-0.5 rounded-full bg-brand-highlight"
|
||||
>{{ interval === 'quarterly' ? 'Saving' : 'Save' }} 16%</span
|
||||
></span
|
||||
>
|
||||
<span class="text-sm text-secondary flex items-center gap-1"
|
||||
>{{
|
||||
formatPrice(
|
||||
locale,
|
||||
(prices?.prices?.intervals?.['quarterly'] ?? 0) / monthsInInterval['quarterly'],
|
||||
currency,
|
||||
true,
|
||||
)
|
||||
}}
|
||||
/ month</span
|
||||
>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<ExpandableInvoiceTotal
|
||||
:period="period"
|
||||
:currency="currency"
|
||||
:loading="loading"
|
||||
:total="total ?? -1"
|
||||
:billing-items="
|
||||
total !== undefined && tax !== undefined
|
||||
? [
|
||||
{
|
||||
title: `Modrinth Servers (${planName})`,
|
||||
amount: total - tax,
|
||||
},
|
||||
{
|
||||
title: 'Tax',
|
||||
amount: tax,
|
||||
},
|
||||
]
|
||||
: []
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center pl-4 pr-2 py-3 bg-bg rounded-2xl gap-2 text-secondary">
|
||||
<template v-if="selectedPaymentMethod">
|
||||
<FormattedPaymentMethod :method="selectedPaymentMethod" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-2 text-red">
|
||||
<XIcon />
|
||||
No payment method selected
|
||||
</div>
|
||||
</template>
|
||||
<ButtonStyled size="small" type="transparent">
|
||||
<button class="ml-auto" @click="emit('changePaymentMethod')">
|
||||
<template v-if="selectedPaymentMethod"> <EditIcon /> Change </template>
|
||||
<template v-else> Select payment method <RightArrowIcon /> </template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<p class="m-0 mt-4 text-sm text-secondary">
|
||||
<span class="font-semibold"
|
||||
>By clicking "Subscribe", you are purchasing a recurring subscription.</span
|
||||
>
|
||||
<br />
|
||||
You'll be charged
|
||||
<SpinnerIcon v-if="loading" class="animate-spin relative top-0.5 mx-2" /><template v-else>{{
|
||||
formatPrice(locale, total, currency)
|
||||
}}</template>
|
||||
every {{ period }} plus applicable taxes starting today, until you cancel. You can cancel
|
||||
anytime from your settings page.
|
||||
</p>
|
||||
<div class="mt-2 flex items-center gap-1 text-sm">
|
||||
<Checkbox
|
||||
v-model="acceptedEula"
|
||||
label="I acknowledge that I have read and agree to the"
|
||||
description="I acknowledge that I have read and agree to the Minecraft EULA"
|
||||
/>
|
||||
<a
|
||||
href="https://www.minecraft.net/en-us/eula"
|
||||
target="_blank"
|
||||
class="text-brand underline hover:brightness-[--hover-brightness]"
|
||||
>Minecraft EULA<ExternalIcon class="size-3 shrink-0 ml-0.5 mb-0.5"
|
||||
/></a>
|
||||
</div>
|
||||
</template>
|
||||
90
packages/ui/src/components/billing/ServersRegionButton.vue
Normal file
90
packages/ui/src/components/billing/ServersRegionButton.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { useVIntl } from '@vintl/vintl'
|
||||
import { getPingLevel } from '@modrinth/utils'
|
||||
import { SignalIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
import type { ServerRegion } from '../../utils/billing'
|
||||
import { regionOverrides } from '../../utils/regions'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const currentRegion = defineModel<string | undefined>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
region: ServerRegion
|
||||
ping?: number
|
||||
bestPing?: boolean
|
||||
outOfStock?: boolean
|
||||
}>()
|
||||
|
||||
const isCurrentRegion = computed(() => currentRegion.value === props.region.shortcode)
|
||||
const flag = computed(
|
||||
() =>
|
||||
regionOverrides[props.region.shortcode]?.flag ??
|
||||
`https://flagcdn.com/${props.region.country_code}.svg`,
|
||||
)
|
||||
const overrideTitle = computed(() => regionOverrides[props.region.shortcode]?.name)
|
||||
const title = computed(() =>
|
||||
overrideTitle.value ? formatMessage(overrideTitle.value) : props.region.display_name,
|
||||
)
|
||||
const locationSubtitle = computed(() =>
|
||||
overrideTitle.value ? props.region.display_name : undefined,
|
||||
)
|
||||
const pingLevel = computed(() => getPingLevel(props.ping ?? 0))
|
||||
|
||||
function setRegion() {
|
||||
currentRegion.value = props.region.shortcode
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:disabled="outOfStock"
|
||||
class="rounded-2xl p-4 font-semibold transition-all border-2 border-solid flex flex-col items-center gap-3"
|
||||
:class="{
|
||||
'bg-button-bg border-transparent text-primary': !isCurrentRegion,
|
||||
'bg-brand-highlight border-brand text-contrast': isCurrentRegion,
|
||||
'opacity-50 cursor-not-allowed': outOfStock,
|
||||
'hover:text-contrast active:scale-95 hover:brightness-[--hover-brightness] focus-visible:brightness-[--hover-brightness] ':
|
||||
!outOfStock,
|
||||
}"
|
||||
@click="setRegion"
|
||||
>
|
||||
<img
|
||||
v-if="flag"
|
||||
class="aspect-[16/10] max-w-16 w-full object-cover rounded-md border-1 border-solid"
|
||||
:class="[
|
||||
isCurrentRegion ? 'border-brand' : 'border-button-border',
|
||||
{ 'saturate-[0.25]': outOfStock },
|
||||
]"
|
||||
:src="flag"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="flex flex-col gap-1 items-center">
|
||||
<span class="flex items-center gap-1 flex-wrap justify-center">
|
||||
{{ title }} <span v-if="outOfStock" class="text-sm text-secondary">(Out of stock)</span>
|
||||
</span>
|
||||
<span class="text-xs flex items-center gap-1 text-secondary font-medium">
|
||||
<template v-if="locationSubtitle">
|
||||
<span>
|
||||
{{ locationSubtitle }}
|
||||
</span>
|
||||
<span v-if="ping !== -1">•</span>
|
||||
</template>
|
||||
<template v-if="ping !== -1">
|
||||
<SignalIcon
|
||||
v-if="ping"
|
||||
aria-hidden="true"
|
||||
:style="`--_signal-${pingLevel}: ${pingLevel <= 2 ? 'var(--color-red)' : pingLevel <= 4 ? 'var(--color-orange)' : 'var(--color-green)'}`"
|
||||
stroke-width="3px"
|
||||
class="shrink-0"
|
||||
/>
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
<template v-if="ping"> {{ ping }}ms </template>
|
||||
<span v-else> Testing connection... </span>
|
||||
</template>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
60
packages/ui/src/components/billing/ServersSpecs.vue
Normal file
60
packages/ui/src/components/billing/ServersSpecs.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import AutoLink from '../base/AutoLink.vue'
|
||||
import { MemoryStickIcon, DatabaseIcon, CPUIcon, SparklesIcon, UnknownIcon } from '@modrinth/assets'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click-bursting-link'): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
ram: number
|
||||
storage: number
|
||||
cpus: number
|
||||
burstingLink?: string
|
||||
}>(),
|
||||
{
|
||||
burstingLink: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
const formattedRam = computed(() => {
|
||||
return props.ram / 1024
|
||||
})
|
||||
|
||||
const formattedStorage = computed(() => {
|
||||
return props.storage / 1024
|
||||
})
|
||||
|
||||
const sharedCpus = computed(() => {
|
||||
return props.cpus / 2
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ul class="m-0 flex list-none flex-col gap-2 px-0 text-sm leading-normal text-secondary">
|
||||
<li class="flex items-center gap-2">
|
||||
<MemoryStickIcon class="h-5 w-5 shrink-0" /> {{ formattedRam }} GB RAM
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<DatabaseIcon class="h-5 w-5 shrink-0" /> {{ formattedStorage }} GB Storage
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<CPUIcon class="h-5 w-5 shrink-0" /> {{ sharedCpus }} Shared CPUs
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<SparklesIcon class="h-5 w-5 shrink-0" /> Bursts up to {{ cpus }} CPUs
|
||||
<AutoLink
|
||||
v-if="burstingLink"
|
||||
v-tooltip="
|
||||
`CPU bursting allows your server to temporarily use additional threads to help mitigate TPS spikes. Click for more info.`
|
||||
"
|
||||
class="flex"
|
||||
:to="burstingLink"
|
||||
@click="() => emit('click-bursting-link')"
|
||||
>
|
||||
<UnknownIcon class="h-4 w-4 text-secondary opacity-80" />
|
||||
</AutoLink>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -96,6 +96,8 @@ export { default as SearchSidebarFilter } from './search/SearchSidebarFilter.vue
|
||||
|
||||
// Billing
|
||||
export { default as PurchaseModal } from './billing/PurchaseModal.vue'
|
||||
export { default as AddPaymentMethodModal } from './billing/AddPaymentMethodModal.vue'
|
||||
export { default as ModrinthServersPurchaseModal } from './billing/ModrinthServersPurchaseModal.vue'
|
||||
|
||||
// Version
|
||||
export { default as VersionChannelIndicator } from './version/VersionChannelIndicator.vue'
|
||||
@@ -107,3 +109,4 @@ export { default as ThemeSelector } from './settings/ThemeSelector.vue'
|
||||
|
||||
// Servers
|
||||
export { default as BackupWarning } from './servers/backups/BackupWarning.vue'
|
||||
export { default as ServersSpecs } from './billing/ServersSpecs.vue'
|
||||
|
||||
35
packages/ui/src/components/modal/ModalLoadingIndicator.vue
Normal file
35
packages/ui/src/components/modal/ModalLoadingIndicator.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { SpinnerIcon, XCircleIcon } from '@modrinth/assets'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
error?: boolean
|
||||
}>(),
|
||||
{
|
||||
error: false,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex items-center gap-2 font-semibold" :class="error ? 'text-red' : 'animate-pulse'">
|
||||
<XCircleIcon v-if="error" class="w-6 h-6" />
|
||||
<SpinnerIcon v-else class="w-6 h-6 animate-spin" />
|
||||
<slot v-if="error" name="error" />
|
||||
<slot v-else />
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.animate-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
scale: 1;
|
||||
}
|
||||
50% {
|
||||
scale: 0.95;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
403
packages/ui/src/composables/stripe.ts
Normal file
403
packages/ui/src/composables/stripe.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import type Stripe from 'stripe'
|
||||
import { type Stripe as StripeJs, loadStripe, type StripeElements } from '@stripe/stripe-js'
|
||||
import { computed, ref, type Ref } from 'vue'
|
||||
import type { ContactOption } from '@stripe/stripe-js/dist/stripe-js/elements/address'
|
||||
import type {
|
||||
ServerPlan,
|
||||
BasePaymentIntentResponse,
|
||||
ChargeRequestType,
|
||||
CreatePaymentIntentRequest,
|
||||
CreatePaymentIntentResponse,
|
||||
PaymentRequestType,
|
||||
ServerBillingInterval,
|
||||
UpdatePaymentIntentRequest,
|
||||
UpdatePaymentIntentResponse,
|
||||
} from '../utils/billing.ts'
|
||||
|
||||
// export type CreateElements = (
|
||||
// paymentMethods: Stripe.PaymentMethod[],
|
||||
// options: StripeElementsOptionsMode,
|
||||
// ) => {
|
||||
// elements: StripeElements
|
||||
// paymentElement: StripePaymentElement
|
||||
// addressElement: StripeAddressElement
|
||||
// }
|
||||
|
||||
export const useStripe = (
|
||||
publishableKey: string,
|
||||
customer: Stripe.Customer,
|
||||
paymentMethods: Stripe.PaymentMethod[],
|
||||
currency: string,
|
||||
product: Ref<ServerPlan | undefined>,
|
||||
interval: Ref<ServerBillingInterval>,
|
||||
region: Ref<string | undefined>,
|
||||
initiatePayment: (
|
||||
body: CreatePaymentIntentRequest | UpdatePaymentIntentRequest,
|
||||
) => Promise<CreatePaymentIntentResponse | UpdatePaymentIntentResponse>,
|
||||
onError: (err: Error) => void,
|
||||
) => {
|
||||
const stripe = ref<StripeJs | null>(null)
|
||||
|
||||
let elements: StripeElements | undefined = undefined
|
||||
const elementsLoaded = ref<0 | 1 | 2>(0)
|
||||
const loadingElementsFailed = ref<boolean>(false)
|
||||
|
||||
const paymentMethodLoading = ref(false)
|
||||
const loadingFailed = ref<string>()
|
||||
const paymentIntentId = ref<string>()
|
||||
const tax = ref<number>()
|
||||
const total = ref<number>()
|
||||
const confirmationToken = ref<string>()
|
||||
const submittingPayment = ref(false)
|
||||
const selectedPaymentMethod = ref<Stripe.PaymentMethod>()
|
||||
const inputtedPaymentMethod = ref<Stripe.PaymentMethod>()
|
||||
const clientSecret = ref<string>()
|
||||
const completingPurchase = ref<boolean>(false)
|
||||
|
||||
async function initialize() {
|
||||
stripe.value = await loadStripe(publishableKey)
|
||||
}
|
||||
|
||||
function createIntent(body: CreatePaymentIntentRequest): Promise<CreatePaymentIntentResponse> {
|
||||
return initiatePayment(body) as Promise<CreatePaymentIntentResponse>
|
||||
}
|
||||
|
||||
function updateIntent(body: UpdatePaymentIntentRequest): Promise<UpdatePaymentIntentResponse> {
|
||||
return initiatePayment(body) as Promise<UpdatePaymentIntentResponse>
|
||||
}
|
||||
|
||||
const planPrices = computed(() => {
|
||||
return product.value?.prices.find((x) => x.currency_code === currency)
|
||||
})
|
||||
|
||||
const createElements = (options) => {
|
||||
const styles = getComputedStyle(document.body)
|
||||
|
||||
if (!stripe.value) {
|
||||
throw new Error('Stripe API not yet loaded')
|
||||
}
|
||||
|
||||
elements = stripe.value.elements({
|
||||
appearance: {
|
||||
variables: {
|
||||
colorPrimary: styles.getPropertyValue('--color-brand'),
|
||||
colorBackground: styles.getPropertyValue('--experimental-color-button-bg'),
|
||||
colorText: styles.getPropertyValue('--color-base'),
|
||||
colorTextPlaceholder: styles.getPropertyValue('--color-secondary'),
|
||||
colorDanger: styles.getPropertyValue('--color-red'),
|
||||
fontFamily: styles.getPropertyValue('--font-standard'),
|
||||
spacingUnit: '0.25rem',
|
||||
borderRadius: '0.75rem',
|
||||
},
|
||||
},
|
||||
loader: 'never',
|
||||
...options,
|
||||
})
|
||||
|
||||
const paymentElement = elements.create('payment', {
|
||||
layout: {
|
||||
type: 'tabs',
|
||||
defaultCollapsed: false,
|
||||
},
|
||||
})
|
||||
paymentElement.mount('#payment-element')
|
||||
|
||||
const contacts: ContactOption[] = []
|
||||
|
||||
paymentMethods.forEach((method) => {
|
||||
const addr = method.billing_details?.address
|
||||
if (
|
||||
addr &&
|
||||
addr.line1 &&
|
||||
addr.city &&
|
||||
addr.postal_code &&
|
||||
addr.country &&
|
||||
addr.state &&
|
||||
method.billing_details.name
|
||||
) {
|
||||
contacts.push({
|
||||
address: {
|
||||
line1: addr.line1,
|
||||
line2: addr.line2 ?? undefined,
|
||||
city: addr.city,
|
||||
state: addr.state,
|
||||
postal_code: addr.postal_code,
|
||||
country: addr.country,
|
||||
},
|
||||
name: method.billing_details.name,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const addressElement = elements.create('address', {
|
||||
mode: 'billing',
|
||||
contacts: contacts.length > 0 ? contacts : undefined,
|
||||
})
|
||||
addressElement.mount('#address-element')
|
||||
|
||||
return { elements, paymentElement, addressElement }
|
||||
}
|
||||
|
||||
const primaryPaymentMethodId = computed<string | null>(() => {
|
||||
if (customer && customer.invoice_settings && customer.invoice_settings.default_payment_method) {
|
||||
const method = customer.invoice_settings.default_payment_method
|
||||
if (typeof method === 'string') {
|
||||
return method
|
||||
} else {
|
||||
return method.id
|
||||
}
|
||||
} else if (paymentMethods && paymentMethods[0] && paymentMethods[0].id) {
|
||||
return paymentMethods[0].id
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const loadStripeElements = async () => {
|
||||
loadingFailed.value = undefined
|
||||
try {
|
||||
if (!customer && primaryPaymentMethodId.value) {
|
||||
paymentMethodLoading.value = true
|
||||
await refreshPaymentIntent(primaryPaymentMethodId.value, false)
|
||||
paymentMethodLoading.value = false
|
||||
}
|
||||
|
||||
if (!selectedPaymentMethod.value) {
|
||||
elementsLoaded.value = 0
|
||||
|
||||
const {
|
||||
elements: newElements,
|
||||
addressElement,
|
||||
paymentElement,
|
||||
} = createElements({
|
||||
mode: 'payment',
|
||||
currency: currency.toLowerCase(),
|
||||
amount: product.value?.prices.find((x) => x.currency_code === currency)?.prices.intervals[
|
||||
interval.value
|
||||
],
|
||||
paymentMethodCreation: 'manual',
|
||||
setupFutureUsage: 'off_session',
|
||||
})
|
||||
|
||||
elements = newElements
|
||||
paymentElement.on('ready', () => {
|
||||
elementsLoaded.value += 1
|
||||
})
|
||||
addressElement.on('ready', () => {
|
||||
elementsLoaded.value += 1
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
loadingFailed.value = String(err)
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPaymentIntent(id: string, confirmation: boolean) {
|
||||
try {
|
||||
paymentMethodLoading.value = true
|
||||
if (!confirmation) {
|
||||
selectedPaymentMethod.value = paymentMethods.find((x) => x.id === id)
|
||||
}
|
||||
|
||||
const requestType: PaymentRequestType = confirmation
|
||||
? {
|
||||
type: 'confirmation_token',
|
||||
token: id,
|
||||
}
|
||||
: {
|
||||
type: 'payment_method',
|
||||
id: id,
|
||||
}
|
||||
|
||||
if (!product.value) {
|
||||
return handlePaymentError('No product selected')
|
||||
}
|
||||
|
||||
const charge: ChargeRequestType = {
|
||||
type: 'new',
|
||||
product_id: product.value?.id,
|
||||
interval: interval.value,
|
||||
}
|
||||
|
||||
let result: BasePaymentIntentResponse
|
||||
|
||||
if (paymentIntentId.value) {
|
||||
result = await updateIntent({
|
||||
...requestType,
|
||||
charge,
|
||||
existing_payment_intent: paymentIntentId.value,
|
||||
metadata: {
|
||||
type: 'pyro',
|
||||
server_region: region.value,
|
||||
source: {},
|
||||
},
|
||||
})
|
||||
console.log(`Updated payment intent: ${interval.value} for ${result.total}`)
|
||||
} else {
|
||||
;({
|
||||
payment_intent_id: paymentIntentId.value,
|
||||
client_secret: clientSecret.value,
|
||||
...result
|
||||
} = await createIntent({
|
||||
...requestType,
|
||||
charge,
|
||||
metadata: {
|
||||
type: 'pyro',
|
||||
server_region: region.value,
|
||||
source: {},
|
||||
},
|
||||
}))
|
||||
console.log(`Created payment intent: ${interval.value} for ${result.total}`)
|
||||
}
|
||||
|
||||
tax.value = result.tax
|
||||
total.value = result.total
|
||||
|
||||
if (confirmation) {
|
||||
confirmationToken.value = id
|
||||
if (result.payment_method) {
|
||||
inputtedPaymentMethod.value = result.payment_method
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
handlePaymentError(err as string)
|
||||
}
|
||||
paymentMethodLoading.value = false
|
||||
}
|
||||
|
||||
async function createConfirmationToken() {
|
||||
if (!elements) {
|
||||
return handlePaymentError('No elements')
|
||||
}
|
||||
if (!stripe.value) {
|
||||
return handlePaymentError('No stripe')
|
||||
}
|
||||
|
||||
const { error, confirmationToken: confirmation } = await stripe.value.createConfirmationToken({
|
||||
elements,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
handlePaymentError(error.message ?? 'Unknown error creating confirmation token')
|
||||
return
|
||||
}
|
||||
|
||||
return confirmation.id
|
||||
}
|
||||
|
||||
function handlePaymentError(err: string | Error) {
|
||||
paymentMethodLoading.value = false
|
||||
completingPurchase.value = false
|
||||
onError(typeof err === 'string' ? new Error(err) : err)
|
||||
}
|
||||
|
||||
async function createNewPaymentMethod() {
|
||||
paymentMethodLoading.value = true
|
||||
|
||||
if (!elements) {
|
||||
return handlePaymentError('No elements')
|
||||
}
|
||||
|
||||
const { error: submitError } = await elements.submit()
|
||||
|
||||
if (submitError) {
|
||||
return handlePaymentError(submitError.message ?? 'Unknown error creating payment method')
|
||||
}
|
||||
|
||||
const token = await createConfirmationToken()
|
||||
if (!token) {
|
||||
return handlePaymentError('Failed to create confirmation token')
|
||||
}
|
||||
await refreshPaymentIntent(token, true)
|
||||
|
||||
if (!planPrices.value) {
|
||||
return handlePaymentError('No plan prices')
|
||||
}
|
||||
if (!total.value) {
|
||||
return handlePaymentError('No total amount')
|
||||
}
|
||||
|
||||
elements.update({ currency: planPrices.value.currency_code.toLowerCase(), amount: total.value })
|
||||
|
||||
elementsLoaded.value = 0
|
||||
confirmationToken.value = token
|
||||
paymentMethodLoading.value = false
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
async function selectPaymentMethod(paymentMethod: Stripe.PaymentMethod | undefined) {
|
||||
selectedPaymentMethod.value = paymentMethod
|
||||
if (paymentMethod === undefined) {
|
||||
await loadStripeElements()
|
||||
} else {
|
||||
refreshPaymentIntent(paymentMethod.id, false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadingElements = computed(() => elementsLoaded.value < 2)
|
||||
|
||||
async function submitPayment(returnUrl: string) {
|
||||
completingPurchase.value = true
|
||||
const secert = clientSecret.value
|
||||
|
||||
if (!secert) {
|
||||
return handlePaymentError('No client secret')
|
||||
}
|
||||
|
||||
if (!stripe.value) {
|
||||
return handlePaymentError('No stripe')
|
||||
}
|
||||
|
||||
submittingPayment.value = true
|
||||
const { error } = await stripe.value.confirmPayment({
|
||||
clientSecret: secert,
|
||||
confirmParams: {
|
||||
confirmation_token: confirmationToken.value,
|
||||
return_url: `${returnUrl}?priceId=${product.value?.prices.find((x) => x.currency_code === currency)?.id}&plan=${interval.value}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (error) {
|
||||
handlePaymentError(error.message ?? 'Unknown error submitting payment')
|
||||
return false
|
||||
}
|
||||
submittingPayment.value = false
|
||||
completingPurchase.value = false
|
||||
return true
|
||||
}
|
||||
|
||||
async function reloadPaymentIntent() {
|
||||
console.log('selected:', selectedPaymentMethod.value)
|
||||
console.log('token:', confirmationToken.value)
|
||||
if (selectedPaymentMethod.value) {
|
||||
await refreshPaymentIntent(selectedPaymentMethod.value.id, false)
|
||||
} else if (confirmationToken.value) {
|
||||
await refreshPaymentIntent(confirmationToken.value, true)
|
||||
} else {
|
||||
throw new Error('No payment method selected')
|
||||
}
|
||||
}
|
||||
|
||||
const hasPaymentMethod = computed(() => selectedPaymentMethod.value || confirmationToken.value)
|
||||
|
||||
return {
|
||||
initializeStripe: initialize,
|
||||
selectPaymentMethod,
|
||||
reloadPaymentIntent,
|
||||
primaryPaymentMethodId,
|
||||
selectedPaymentMethod,
|
||||
inputtedPaymentMethod,
|
||||
hasPaymentMethod,
|
||||
createNewPaymentMethod,
|
||||
loadingElements,
|
||||
loadingElementsFailed,
|
||||
paymentMethodLoading,
|
||||
loadStripeElements,
|
||||
tax,
|
||||
total,
|
||||
submitPayment,
|
||||
completingPurchase,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"button.back": {
|
||||
"defaultMessage": "Back"
|
||||
},
|
||||
"button.cancel": {
|
||||
"defaultMessage": "Cancel"
|
||||
},
|
||||
@@ -23,6 +26,9 @@
|
||||
"button.edit": {
|
||||
"defaultMessage": "Edit"
|
||||
},
|
||||
"button.next": {
|
||||
"defaultMessage": "Next"
|
||||
},
|
||||
"button.open-folder": {
|
||||
"defaultMessage": "Open folder"
|
||||
},
|
||||
@@ -173,6 +179,12 @@
|
||||
"label.visit-your-profile": {
|
||||
"defaultMessage": "Visit your profile"
|
||||
},
|
||||
"modal.add-payment-method.action": {
|
||||
"defaultMessage": "Add payment method"
|
||||
},
|
||||
"modal.add-payment-method.title": {
|
||||
"defaultMessage": "Adding a payment method"
|
||||
},
|
||||
"notification.error.title": {
|
||||
"defaultMessage": "An error occurred"
|
||||
},
|
||||
@@ -485,6 +497,36 @@
|
||||
"servers.notice.undismissable": {
|
||||
"defaultMessage": "Undismissable"
|
||||
},
|
||||
"servers.purchase.step.payment.description": {
|
||||
"defaultMessage": "You won't be charged yet."
|
||||
},
|
||||
"servers.purchase.step.payment.prompt": {
|
||||
"defaultMessage": "Select a payment method"
|
||||
},
|
||||
"servers.purchase.step.payment.title": {
|
||||
"defaultMessage": "Payment method"
|
||||
},
|
||||
"servers.purchase.step.region.title": {
|
||||
"defaultMessage": "Region"
|
||||
},
|
||||
"servers.purchase.step.review.title": {
|
||||
"defaultMessage": "Review"
|
||||
},
|
||||
"servers.region.custom.prompt": {
|
||||
"defaultMessage": "How much RAM do you want your server to have?"
|
||||
},
|
||||
"servers.region.europe": {
|
||||
"defaultMessage": "Europe"
|
||||
},
|
||||
"servers.region.north-america": {
|
||||
"defaultMessage": "North America"
|
||||
},
|
||||
"servers.region.prompt": {
|
||||
"defaultMessage": "Where would you like your server to be located?"
|
||||
},
|
||||
"servers.region.region-unsupported": {
|
||||
"defaultMessage": "Region not listed? <link>Let us know where you'd like to see Modrinth Servers next!</link>"
|
||||
},
|
||||
"settings.account.title": {
|
||||
"defaultMessage": "Account and security"
|
||||
},
|
||||
|
||||
102
packages/ui/src/utils/billing.ts
Normal file
102
packages/ui/src/utils/billing.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type Stripe from 'stripe'
|
||||
|
||||
export type ServerBillingInterval = 'monthly' | 'yearly' | 'quarterly'
|
||||
|
||||
export interface ServerPlan {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
metadata: {
|
||||
type: string
|
||||
ram?: number
|
||||
cpu?: number
|
||||
storage?: number
|
||||
swap?: number
|
||||
}
|
||||
prices: {
|
||||
id: string
|
||||
currency_code: string
|
||||
prices: {
|
||||
intervals: {
|
||||
monthly: number
|
||||
yearly: number
|
||||
}
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface ServerStockRequest {
|
||||
cpu?: number
|
||||
memory_mb?: number
|
||||
swap_mb?: number
|
||||
storage_mb?: number
|
||||
}
|
||||
|
||||
export interface ServerRegion {
|
||||
shortcode: string
|
||||
country_code: string
|
||||
display_name: string
|
||||
lat: number
|
||||
lon: number
|
||||
}
|
||||
|
||||
/*
|
||||
Request types
|
||||
*/
|
||||
export type PaymentMethodRequest = {
|
||||
type: 'payment_method'
|
||||
id: string
|
||||
}
|
||||
|
||||
export type ConfirmationTokenRequest = {
|
||||
type: 'confirmation_token'
|
||||
token: string
|
||||
}
|
||||
|
||||
export type PaymentRequestType = PaymentMethodRequest | ConfirmationTokenRequest
|
||||
|
||||
export type ChargeRequestType =
|
||||
| {
|
||||
type: 'existing'
|
||||
id: string
|
||||
}
|
||||
| {
|
||||
type: 'new'
|
||||
product_id: string
|
||||
interval?: ServerBillingInterval
|
||||
}
|
||||
|
||||
export type CreatePaymentIntentRequest = PaymentRequestType & {
|
||||
charge: ChargeRequestType
|
||||
metadata?: {
|
||||
type: 'pyro'
|
||||
server_name?: string
|
||||
server_region?: string
|
||||
source: {
|
||||
loader?: string
|
||||
game_version?: string
|
||||
loader_version?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type UpdatePaymentIntentRequest = CreatePaymentIntentRequest & {
|
||||
existing_payment_intent: string
|
||||
}
|
||||
|
||||
/*
|
||||
Response types
|
||||
*/
|
||||
export type BasePaymentIntentResponse = {
|
||||
price_id: string
|
||||
tax: number
|
||||
total: number
|
||||
payment_method: Stripe.PaymentMethod
|
||||
}
|
||||
|
||||
export type UpdatePaymentIntentResponse = BasePaymentIntentResponse
|
||||
|
||||
export type CreatePaymentIntentResponse = BasePaymentIntentResponse & {
|
||||
payment_intent_id: string
|
||||
client_secret: string
|
||||
}
|
||||
@@ -17,6 +17,14 @@ export const commonMessages = defineMessages({
|
||||
id: 'button.continue',
|
||||
defaultMessage: 'Continue',
|
||||
},
|
||||
nextButton: {
|
||||
id: 'button.next',
|
||||
defaultMessage: 'Next',
|
||||
},
|
||||
backButton: {
|
||||
id: 'button.back',
|
||||
defaultMessage: 'Back',
|
||||
},
|
||||
copyIdButton: {
|
||||
id: 'button.copy-id',
|
||||
defaultMessage: 'Copy ID',
|
||||
@@ -205,6 +213,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'label.visit-your-profile',
|
||||
defaultMessage: 'Visit your profile',
|
||||
},
|
||||
paymentMethodCardDisplay: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_card_display',
|
||||
defaultMessage: '{card_brand} ending in {last_four}',
|
||||
},
|
||||
})
|
||||
|
||||
export const commonSettingsMessages = defineMessages({
|
||||
@@ -245,3 +257,51 @@ export const commonSettingsMessages = defineMessages({
|
||||
defaultMessage: 'Billing and subscriptions',
|
||||
},
|
||||
})
|
||||
|
||||
export const paymentMethodMessages = defineMessages({
|
||||
visa: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.visa',
|
||||
defaultMessage: 'Visa',
|
||||
},
|
||||
amex: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.amex',
|
||||
defaultMessage: 'American Express',
|
||||
},
|
||||
diners: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.diners',
|
||||
defaultMessage: 'Diners Club',
|
||||
},
|
||||
discover: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.discover',
|
||||
defaultMessage: 'Discover',
|
||||
},
|
||||
eftpos: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.eftpos',
|
||||
defaultMessage: 'EFTPOS',
|
||||
},
|
||||
jcb: { id: 'omorphia.component.purchase_modal.payment_method_type.jcb', defaultMessage: 'JCB' },
|
||||
mastercard: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.mastercard',
|
||||
defaultMessage: 'MasterCard',
|
||||
},
|
||||
unionpay: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.unionpay',
|
||||
defaultMessage: 'UnionPay',
|
||||
},
|
||||
paypal: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.paypal',
|
||||
defaultMessage: 'PayPal',
|
||||
},
|
||||
cashapp: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.cashapp',
|
||||
defaultMessage: 'Cash App',
|
||||
},
|
||||
amazon_pay: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.amazon_pay',
|
||||
defaultMessage: 'Amazon Pay',
|
||||
},
|
||||
unknown: {
|
||||
id: 'omorphia.component.purchase_modal.payment_method_type.unknown',
|
||||
defaultMessage: 'Unknown payment method',
|
||||
},
|
||||
})
|
||||
|
||||
16
packages/ui/src/utils/regions.ts
Normal file
16
packages/ui/src/utils/regions.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineMessage, type MessageDescriptor } from '@vintl/vintl'
|
||||
|
||||
export const regionOverrides = {
|
||||
'us-vin': {
|
||||
name: defineMessage({ id: 'servers.region.north-america', defaultMessage: 'North America' }),
|
||||
flag: 'https://flagcdn.com/us.svg',
|
||||
},
|
||||
'eu-lim': {
|
||||
name: defineMessage({ id: 'servers.region.europe', defaultMessage: 'Europe' }),
|
||||
flag: 'https://flagcdn.com/eu.svg',
|
||||
},
|
||||
'de-fra': {
|
||||
name: defineMessage({ id: 'servers.region.europe', defaultMessage: 'Europe' }),
|
||||
flag: 'https://flagcdn.com/eu.svg',
|
||||
},
|
||||
} satisfies Record<string, { name?: MessageDescriptor; flag?: string }>
|
||||
@@ -5,5 +5,6 @@
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "dom"],
|
||||
"noImplicitAny": false
|
||||
}
|
||||
},
|
||||
"types": ["@stripe/stripe-js"]
|
||||
}
|
||||
|
||||
@@ -61,23 +61,33 @@ export const getCurrency = (userCountry) => {
|
||||
return countryCurrency[userCountry] ?? 'USD'
|
||||
}
|
||||
|
||||
export const formatPrice = (locale, price, currency) => {
|
||||
const formatter = new Intl.NumberFormat(locale, {
|
||||
export const formatPrice = (locale, price, currency, trimZeros = false) => {
|
||||
let formatter = new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
})
|
||||
|
||||
const maxDigits = formatter.resolvedOptions().maximumFractionDigits
|
||||
|
||||
const convertedPrice = price / Math.pow(10, maxDigits)
|
||||
|
||||
let minimumFractionDigits = maxDigits
|
||||
|
||||
if (trimZeros && Number.isInteger(convertedPrice)) {
|
||||
minimumFractionDigits = 0
|
||||
}
|
||||
|
||||
formatter = new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits,
|
||||
})
|
||||
return formatter.format(convertedPrice)
|
||||
}
|
||||
|
||||
export const calculateSavings = (monthlyPlan, annualPlan) => {
|
||||
const monthlyAnnualized = monthlyPlan * 12
|
||||
export const calculateSavings = (monthlyPlan, plan, months = 12) => {
|
||||
const monthlyAnnualized = monthlyPlan * months
|
||||
|
||||
return Math.floor(((monthlyAnnualized - annualPlan) / monthlyAnnualized) * 100)
|
||||
return Math.floor(((monthlyAnnualized - plan) / monthlyAnnualized) * 100)
|
||||
}
|
||||
|
||||
export const createStripeElements = (stripe, paymentMethods, options) => {
|
||||
@@ -87,13 +97,13 @@ export const createStripeElements = (stripe, paymentMethods, options) => {
|
||||
appearance: {
|
||||
variables: {
|
||||
colorPrimary: styles.getPropertyValue('--color-brand'),
|
||||
colorBackground: styles.getPropertyValue('--color-bg'),
|
||||
colorBackground: styles.getPropertyValue('--experimental-color-button-bg'),
|
||||
colorText: styles.getPropertyValue('--color-base'),
|
||||
colorTextPlaceholder: styles.getPropertyValue('--color-secondary'),
|
||||
colorDanger: styles.getPropertyValue('--color-red'),
|
||||
fontFamily: styles.getPropertyValue('--font-standard'),
|
||||
spacingUnit: '0.25rem',
|
||||
borderRadius: '1rem',
|
||||
borderRadius: '0.75rem',
|
||||
},
|
||||
},
|
||||
loader: 'never',
|
||||
|
||||
@@ -177,8 +177,15 @@ export const formatCategory = (name) => {
|
||||
return 'Colored Lighting'
|
||||
} else if (name === 'optifine') {
|
||||
return 'OptiFine'
|
||||
} else if (name === 'bta-babric') {
|
||||
return 'BTA (Babric)'
|
||||
} else if (name === 'legacy-fabric') {
|
||||
return 'Legacy Fabric'
|
||||
} else if (name === 'java-agent') {
|
||||
return 'Java Agent'
|
||||
} else if (name === 'nilloader') {
|
||||
return 'NilLoader'
|
||||
}
|
||||
|
||||
return capitalizeString(name)
|
||||
}
|
||||
|
||||
@@ -341,3 +348,17 @@ export const getArrayOrString = (x: string[] | string): string[] => {
|
||||
return x
|
||||
}
|
||||
}
|
||||
|
||||
export function getPingLevel(ping: number) {
|
||||
if (ping < 120) {
|
||||
return 5
|
||||
} else if (ping < 200) {
|
||||
return 4
|
||||
} else if (ping < 300) {
|
||||
return 3
|
||||
} else if (ping < 400) {
|
||||
return 2
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
205
pnpm-lock.yaml
generated
205
pnpm-lock.yaml
generated
@@ -460,6 +460,9 @@ importers:
|
||||
'@formatjs/cli':
|
||||
specifier: ^6.2.12
|
||||
version: 6.2.12(@vue/compiler-core@3.5.13)(vue@3.5.13(typescript@5.5.4))
|
||||
'@stripe/stripe-js':
|
||||
specifier: ^7.3.1
|
||||
version: 7.3.1
|
||||
'@vintl/unplugin':
|
||||
specifier: ^1.5.1
|
||||
version: 1.5.2(@vue/compiler-core@3.5.13)(rollup@3.29.4)(vite@4.5.3(@types/node@22.4.1)(sass@1.77.6)(terser@5.31.6))(vue@3.5.13(typescript@5.5.4))(webpack@5.92.1)
|
||||
@@ -472,6 +475,9 @@ importers:
|
||||
eslint-config-custom:
|
||||
specifier: workspace:*
|
||||
version: link:../eslint-config-custom
|
||||
stripe:
|
||||
specifier: ^18.1.1
|
||||
version: 18.1.1(@types/node@22.4.1)
|
||||
tsconfig:
|
||||
specifier: workspace:*
|
||||
version: link:../tsconfig
|
||||
@@ -2338,6 +2344,10 @@ packages:
|
||||
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@stripe/stripe-js@7.3.1':
|
||||
resolution: {integrity: sha512-pTzb864TQWDRQBPLgSPFRoyjSDUqpCkbEgTzpsjiTjGz1Z5SxZNXJek28w1s6Dyry4CyW4/Izj5jHE/J9hCJYQ==}
|
||||
engines: {node: '>=12.16'}
|
||||
|
||||
'@stylistic/eslint-plugin@2.9.0':
|
||||
resolution: {integrity: sha512-OrDyFAYjBT61122MIY1a3SfEgy3YCMgt2vL4eoPmvTwDBwyQhAXurxNQznlRD/jESNfYWfID8Ej+31LljvF7Xg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -3313,10 +3323,18 @@ packages:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-bind@1.0.7:
|
||||
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-bound@1.0.4:
|
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-me-maybe@1.0.2:
|
||||
resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
|
||||
|
||||
@@ -3828,6 +3846,10 @@ packages:
|
||||
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
duplexer@0.1.2:
|
||||
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||
|
||||
@@ -3895,6 +3917,10 @@ packages:
|
||||
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-errors@1.3.0:
|
||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3909,6 +3935,10 @@ packages:
|
||||
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-set-tostringtag@2.0.3:
|
||||
resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4446,9 +4476,17 @@ packages:
|
||||
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-port-please@3.1.2:
|
||||
resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-stream@6.0.1:
|
||||
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4536,6 +4574,10 @@ packages:
|
||||
gopd@1.0.1:
|
||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
@@ -4573,6 +4615,10 @@ packages:
|
||||
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5211,6 +5257,10 @@ packages:
|
||||
markdown-table@3.0.3:
|
||||
resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mdast-util-definitions@6.0.0:
|
||||
resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
|
||||
|
||||
@@ -5646,6 +5696,10 @@ packages:
|
||||
resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
object-inspect@1.13.4:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
object-keys@1.1.1:
|
||||
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6209,6 +6263,10 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
qs@6.14.0:
|
||||
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -6563,10 +6621,26 @@ packages:
|
||||
shiki@1.29.2:
|
||||
resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==}
|
||||
|
||||
side-channel-list@1.0.0:
|
||||
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel-map@1.0.1:
|
||||
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel-weakmap@1.0.2:
|
||||
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel@1.0.6:
|
||||
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel@1.1.0:
|
||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
signal-exit@3.0.7:
|
||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
|
||||
@@ -6736,6 +6810,15 @@ packages:
|
||||
strip-literal@2.1.1:
|
||||
resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
|
||||
|
||||
stripe@18.1.1:
|
||||
resolution: {integrity: sha512-hlF0ripc2nJrihpsJZQDl3xirS7tpdpS7DlmSNLEDRW8j7Qr215y5DHOI3+aEY/lq6PG8y4GR1RZPtEoIoAs/g==}
|
||||
engines: {node: '>=12.*'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=12.x.x'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
style-mod@4.1.2:
|
||||
resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
|
||||
|
||||
@@ -9373,7 +9456,7 @@ snapshots:
|
||||
|
||||
'@nuxtjs/eslint-config-typescript@12.1.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
|
||||
'@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)
|
||||
eslint: 9.13.0(jiti@2.4.1)
|
||||
@@ -9386,10 +9469,10 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))':
|
||||
'@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))':
|
||||
dependencies:
|
||||
eslint: 9.13.0(jiti@2.4.1)
|
||||
eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-plugin-n: 15.7.0(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-plugin-node: 11.1.0(eslint@9.13.0(jiti@2.4.1))
|
||||
@@ -9852,6 +9935,8 @@ snapshots:
|
||||
|
||||
'@sindresorhus/merge-streams@2.3.0': {}
|
||||
|
||||
'@stripe/stripe-js@7.3.1': {}
|
||||
|
||||
'@stylistic/eslint-plugin@2.9.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)
|
||||
@@ -11242,7 +11327,7 @@ snapshots:
|
||||
|
||||
c12@2.0.1(magicast@0.3.5):
|
||||
dependencies:
|
||||
chokidar: 4.0.1
|
||||
chokidar: 4.0.3
|
||||
confbox: 0.1.8
|
||||
defu: 6.1.4
|
||||
dotenv: 16.4.5
|
||||
@@ -11259,6 +11344,11 @@ snapshots:
|
||||
|
||||
cac@6.7.14: {}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
call-bind@1.0.7:
|
||||
dependencies:
|
||||
es-define-property: 1.0.0
|
||||
@@ -11267,6 +11357,11 @@ snapshots:
|
||||
get-intrinsic: 1.2.4
|
||||
set-function-length: 1.2.2
|
||||
|
||||
call-bound@1.0.4:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
call-me-maybe@1.0.2: {}
|
||||
|
||||
callsites@3.1.0: {}
|
||||
@@ -11704,6 +11799,12 @@ snapshots:
|
||||
|
||||
dset@3.1.4: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
duplexer@0.1.2: {}
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
@@ -11801,6 +11902,8 @@ snapshots:
|
||||
dependencies:
|
||||
get-intrinsic: 1.2.4
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-module-lexer@1.5.4: {}
|
||||
@@ -11811,6 +11914,10 @@ snapshots:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-set-tostringtag@2.0.3:
|
||||
dependencies:
|
||||
get-intrinsic: 1.2.4
|
||||
@@ -11968,10 +12075,10 @@ snapshots:
|
||||
dependencies:
|
||||
eslint: 9.13.0(jiti@2.4.1)
|
||||
|
||||
eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1)):
|
||||
eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-n@15.7.0(eslint@9.13.0(jiti@2.4.1)))(eslint-plugin-promise@6.4.0(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1)):
|
||||
dependencies:
|
||||
eslint: 9.13.0(jiti@2.4.1)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-plugin-n: 15.7.0(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-plugin-promise: 6.4.0(eslint@9.13.0(jiti@2.4.1))
|
||||
|
||||
@@ -11997,7 +12104,7 @@ snapshots:
|
||||
debug: 4.4.0(supports-color@9.4.0)
|
||||
enhanced-resolve: 5.17.1
|
||||
eslint: 9.13.0(jiti@2.4.1)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint@9.13.0(jiti@2.4.1))
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.5
|
||||
@@ -12009,7 +12116,7 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.13.0(jiti@2.4.1)):
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
@@ -12020,16 +12127,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.13.0(jiti@2.4.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4)
|
||||
eslint: 9.13.0(jiti@2.4.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-es@3.0.1(eslint@9.13.0(jiti@2.4.1)):
|
||||
dependencies:
|
||||
eslint: 9.13.0(jiti@2.4.1)
|
||||
@@ -12069,7 +12166,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.13.0(jiti@2.4.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.15.0
|
||||
is-glob: 4.0.3
|
||||
@@ -12096,7 +12193,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.13.0(jiti@2.4.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.16.1(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.13.0(jiti@2.4.1))
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@9.13.0(jiti@2.4.1))(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@9.13.0(jiti@2.4.1)))(eslint@9.13.0(jiti@2.4.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.15.0
|
||||
is-glob: 4.0.3
|
||||
@@ -12672,8 +12769,26 @@ snapshots:
|
||||
has-symbols: 1.0.3
|
||||
hasown: 2.0.2
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-port-please@3.1.2: {}
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
get-stream@6.0.1: {}
|
||||
|
||||
get-stream@8.0.1: {}
|
||||
@@ -12693,7 +12808,7 @@ snapshots:
|
||||
citty: 0.1.6
|
||||
consola: 3.2.3
|
||||
defu: 6.1.4
|
||||
node-fetch-native: 1.6.4
|
||||
node-fetch-native: 1.6.6
|
||||
nypm: 0.3.12
|
||||
ohash: 1.1.4
|
||||
pathe: 1.1.2
|
||||
@@ -12782,6 +12897,8 @@ snapshots:
|
||||
dependencies:
|
||||
get-intrinsic: 1.2.4
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
grapheme-splitter@1.0.4: {}
|
||||
@@ -12829,6 +12946,8 @@ snapshots:
|
||||
|
||||
has-symbols@1.0.3: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.0.3
|
||||
@@ -13573,6 +13692,8 @@ snapshots:
|
||||
|
||||
markdown-table@3.0.3: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mdast-util-definitions@6.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@@ -14431,6 +14552,8 @@ snapshots:
|
||||
|
||||
object-inspect@1.13.2: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
object-keys@1.1.1: {}
|
||||
|
||||
object.assign@4.1.5:
|
||||
@@ -14932,6 +15055,10 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.5.4)
|
||||
|
||||
qs@6.14.0:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
queue-tick@1.0.1: {}
|
||||
@@ -15486,6 +15613,26 @@ snapshots:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
side-channel-list@1.0.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
|
||||
side-channel-map@1.0.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
|
||||
side-channel-weakmap@1.0.2:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
side-channel-map: 1.0.1
|
||||
|
||||
side-channel@1.0.6:
|
||||
dependencies:
|
||||
call-bind: 1.0.7
|
||||
@@ -15493,6 +15640,14 @@ snapshots:
|
||||
get-intrinsic: 1.2.4
|
||||
object-inspect: 1.13.2
|
||||
|
||||
side-channel@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
side-channel-list: 1.0.0
|
||||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
signal-exit@3.0.7: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
@@ -15671,6 +15826,12 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
||||
stripe@18.1.1(@types/node@22.4.1):
|
||||
dependencies:
|
||||
qs: 6.14.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.4.1
|
||||
|
||||
style-mod@4.1.2: {}
|
||||
|
||||
style-to-object@0.4.4:
|
||||
@@ -15994,7 +16155,7 @@ snapshots:
|
||||
dependencies:
|
||||
acorn: 8.14.0
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.14
|
||||
magic-string: 0.30.17
|
||||
unplugin: 1.16.0
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
Reference in New Issue
Block a user