From da47b5d74168d6281a85cef8e6c283e21d8c1bf3 Mon Sep 17 00:00:00 2001 From: fetch Date: Tue, 16 Sep 2025 00:05:47 +0100 Subject: [PATCH 01/55] Try patching tauri config --- .github/workflows/theseus-build.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 64ae2b3349..e71e131520 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -1,8 +1,6 @@ name: Modrinth App build on: push: - branches: - - main tags: - 'v*' paths: @@ -60,6 +58,22 @@ jobs: node-version-file: .nvmrc cache: pnpm + - name: Generate tauri-dev.conf.json + run: | + GIT_HASH=$(git rev-parse --short HEAD) + cat > tauri-dev.conf.json < tauri.tmp && mv tauri.tmp tauri-dev.conf.json + - name: 🧰 Install Linux build dependencies if: startsWith(matrix.platform, 'ubuntu') run: | @@ -103,7 +117,7 @@ jobs: fi - name: 🔨 Build macOS app - run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json + run: ${{ github.ref == 'refs/heads/main' && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }} if: startsWith(matrix.platform, 'macos') env: ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }} From 7ddb2c05610851f1d09406c5ac435ea4d386ca45 Mon Sep 17 00:00:00 2001 From: fetch Date: Tue, 16 Sep 2025 00:07:55 +0100 Subject: [PATCH 02/55] Select branches --- .github/workflows/theseus-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index e71e131520..ffd6e365bb 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -1,6 +1,10 @@ name: Modrinth App build on: push: + branches: + - main + - prod + - '**/**' tags: - 'v*' paths: From 7a758c1f46f2f3cefc8d55fcb7d0c5171a211a48 Mon Sep 17 00:00:00 2001 From: fetch Date: Tue, 16 Sep 2025 00:13:33 +0100 Subject: [PATCH 03/55] Fix json --- .github/workflows/theseus-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index ffd6e365bb..36b8dd7bec 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -68,7 +68,7 @@ jobs: cat > tauri-dev.conf.json < Date: Tue, 16 Sep 2025 00:17:09 +0100 Subject: [PATCH 04/55] Correct paths --- .github/workflows/theseus-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 36b8dd7bec..9121e49892 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -65,7 +65,7 @@ jobs: - name: Generate tauri-dev.conf.json run: | GIT_HASH=$(git rev-parse --short HEAD) - cat > tauri-dev.conf.json < apps/app/tauri-dev.conf.json < tauri.tmp && mv tauri.tmp tauri-dev.conf.json + apps/app/tauri-dev.conf.json > apps/app/tauri.tmp && mv apps/app/tauri.tmp apps/app/tauri-dev.conf.json - name: 🧰 Install Linux build dependencies if: startsWith(matrix.platform, 'ubuntu') From 719e001a8f0d41422338a5efc40e0bb5ee4938d1 Mon Sep 17 00:00:00 2001 From: fetch Date: Tue, 16 Sep 2025 00:38:22 +0100 Subject: [PATCH 05/55] Fix MacOS builds --- .github/workflows/theseus-build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 9121e49892..35fa0ba0fc 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -164,7 +164,8 @@ jobs: target/release/bundle/appimage/Modrinth App_*.AppImage* target/release/bundle/deb/Modrinth App_*.deb* target/release/bundle/rpm/Modrinth App-*.rpm* - target/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz* - target/universal-apple-darwin/release/bundle/dmg/Modrinth App_*.dmg* + "target/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz*" + "target/universal-apple-darwin/release/bundle/macos/Modrinth App (dev build *).app.tar.gz" + "target/universal-apple-darwin/release/bundle/dmg/Modrinth App (dev build *).dmg" target/release/bundle/nsis/Modrinth App_*-setup.exe* target/release/bundle/nsis/Modrinth App_*-setup.nsis.zip* From b1a2a844344fdef441aa543cfd2e9fa8a47b608f Mon Sep 17 00:00:00 2001 From: fetch Date: Wed, 17 Sep 2025 17:01:24 +0100 Subject: [PATCH 06/55] Fix theseus build --- .github/workflows/theseus-build.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 35fa0ba0fc..9b0cfb57bf 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -67,16 +67,14 @@ jobs: GIT_HASH=$(git rev-parse --short HEAD) cat > apps/app/tauri-dev.conf.json < apps/app/tauri.tmp && mv apps/app/tauri.tmp apps/app/tauri-dev.conf.json - name: 🧰 Install Linux build dependencies if: startsWith(matrix.platform, 'ubuntu') @@ -164,8 +162,8 @@ jobs: target/release/bundle/appimage/Modrinth App_*.AppImage* target/release/bundle/deb/Modrinth App_*.deb* target/release/bundle/rpm/Modrinth App-*.rpm* - "target/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz*" - "target/universal-apple-darwin/release/bundle/macos/Modrinth App (dev build *).app.tar.gz" - "target/universal-apple-darwin/release/bundle/dmg/Modrinth App (dev build *).dmg" + target/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz* + target/universal-apple-darwin/release/bundle/macos/Modrinth App (dev-*)*.app.tar.gz + target/universal-apple-darwin/release/bundle/dmg/Modrinth App (dev-*)*.dmg target/release/bundle/nsis/Modrinth App_*-setup.exe* target/release/bundle/nsis/Modrinth App_*-setup.nsis.zip* From e1208ef8a1bee0c63b81596f75d09c291f22113a Mon Sep 17 00:00:00 2001 From: fetch Date: Wed, 17 Sep 2025 18:10:20 +0100 Subject: [PATCH 07/55] Don't hardcode ModrinthApp as app identifier --- apps/app-playground/src/main.rs | 2 +- apps/app/src/api/settings.rs | 7 +++-- apps/app/src/api/utils.rs | 10 ++++--- apps/app/src/main.rs | 6 +++-- packages/app-lib/src/api/settings.rs | 6 +++-- packages/app-lib/src/state/db.rs | 11 ++++---- packages/app-lib/src/state/dirs.rs | 39 ++++++++++++++++++---------- packages/app-lib/src/state/mod.rs | 15 +++++++---- 8 files changed, 62 insertions(+), 34 deletions(-) diff --git a/apps/app-playground/src/main.rs b/apps/app-playground/src/main.rs index 13da97d396..d450004c9b 100644 --- a/apps/app-playground/src/main.rs +++ b/apps/app-playground/src/main.rs @@ -41,7 +41,7 @@ async fn main() -> theseus::Result<()> { let _log_guard = theseus::start_logger(); // Initialize state - State::init().await?; + State::init("ModrinthApp".to_owned()).await?; let worlds = get_recent_worlds(4, EnumSet::all()).await?; for world in worlds { diff --git a/apps/app/src/api/settings.rs b/apps/app/src/api/settings.rs index 7a184536c7..5b302ef404 100644 --- a/apps/app/src/api/settings.rs +++ b/apps/app/src/api/settings.rs @@ -1,4 +1,5 @@ use crate::api::Result; +use tauri::Runtime; use theseus::prelude::*; pub fn init() -> tauri::plugin::TauriPlugin { @@ -28,7 +29,9 @@ pub async fn settings_set(settings: Settings) -> Result<()> { } #[tauri::command] -pub async fn cancel_directory_change() -> Result<()> { - settings::cancel_directory_change().await?; +pub async fn cancel_directory_change( + handle: tauri::AppHandle, +) -> Result<()> { + settings::cancel_directory_change(&handle.config().identifier).await?; Ok(()) } diff --git a/apps/app/src/api/utils.rs b/apps/app/src/api/utils.rs index 8a067b7ccf..4b3a6c4300 100644 --- a/apps/app/src/api/utils.rs +++ b/apps/app/src/api/utils.rs @@ -96,10 +96,12 @@ pub fn open_path(app: tauri::AppHandle, path: PathBuf) { #[tauri::command] pub fn show_launcher_logs_folder(app: tauri::AppHandle) { - let path = DirectoryInfo::launcher_logs_dir().unwrap_or_default(); - // failure to get folder just opens filesystem - // (ie: if in debug mode only and launcher_logs never created) - open_path(app, path); + if let Some(d) = DirectoryInfo::global_handle_if_ready() { + let path = d.launcher_logs_dir().unwrap_or_default(); + // failure to get folder just opens filesystem + // (ie: if in debug mode only and launcher_logs never created) + open_path(app, path) + } } // Get opening command diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 5799e51ce9..9b3bbfc198 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -21,6 +21,8 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { tracing::info!("Initializing app event state..."); theseus::EventState::init(app.clone()).await?; + let app_identifier = app.config().identifier.clone(); + #[cfg(feature = "updater")] 'updater: { if env::var("MODRINTH_EXTERNAL_UPDATE_PROVIDER").is_ok() { @@ -35,7 +37,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { let update_fut = updater.check(); tracing::info!("Initializing app state..."); - State::init().await?; + State::init(app_identifier).await?; let check_bar = theseus::init_loading( theseus::LoadingBarType::CheckingForUpdates, @@ -86,7 +88,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { #[cfg(not(feature = "updater"))] { - State::init().await?; + State::init(app_identifier).await?; } tracing::info!("Finished checking for updates!"); diff --git a/packages/app-lib/src/api/settings.rs b/packages/app-lib/src/api/settings.rs index 7619596832..b79e2e524c 100644 --- a/packages/app-lib/src/api/settings.rs +++ b/packages/app-lib/src/api/settings.rs @@ -23,10 +23,12 @@ pub async fn set(settings: Settings) -> crate::Result<()> { } #[tracing::instrument] -pub async fn cancel_directory_change() -> crate::Result<()> { +pub async fn cancel_directory_change( + app_identifier: &str, +) -> crate::Result<()> { // This is called to handle state initialization errors due to folder migrations // failing, so fetching a DB connection pool from `State::get` is not reliable here - let pool = crate::state::db::connect().await?; + let pool = crate::state::db::connect(app_identifier).await?; let mut settings = Settings::get(&pool).await?; if let Some(prev_custom_dir) = settings.prev_custom_dir { diff --git a/packages/app-lib/src/state/db.rs b/packages/app-lib/src/state/db.rs index de5464c4cd..b2a1805602 100644 --- a/packages/app-lib/src/state/db.rs +++ b/packages/app-lib/src/state/db.rs @@ -6,12 +6,13 @@ use sqlx::{Pool, Sqlite}; use std::str::FromStr; use std::time::Duration; -pub(crate) async fn connect() -> crate::Result> { - let settings_dir = DirectoryInfo::get_initial_settings_dir().ok_or( - crate::ErrorKind::FSError( +pub(crate) async fn connect( + app_identifier: &str, +) -> crate::Result> { + let settings_dir = DirectoryInfo::initial_settings_dir_path(app_identifier) + .ok_or(crate::ErrorKind::FSError( "Could not find valid config dir".to_string(), - ), - )?; + ))?; if !settings_dir.exists() { crate::util::io::create_dir_all(&settings_dir).await?; diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index 1f577f7c45..e413b3f94a 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -1,6 +1,7 @@ //! Theseus directory information use crate::LoadingBarType; use crate::event::emit::{emit_loading, init_loading}; +use crate::state::LAUNCHER_STATE; use crate::state::{JavaVersion, Profile, Settings}; use crate::util::fetch::IoSemaphore; use dashmap::DashSet; @@ -17,24 +18,35 @@ pub const METADATA_FOLDER_NAME: &str = "meta"; pub struct DirectoryInfo { pub settings_dir: PathBuf, // Base settings directory- app database pub config_dir: PathBuf, // Base config directory- instances, minecraft downloads, etc. Changeable as a setting. + pub app_identifier: String, } impl DirectoryInfo { + pub fn global_handle_if_ready() -> Option<&'static Self> { + LAUNCHER_STATE.get().map(|x| &x.directories) + } + + pub fn get_initial_settings_dir(&self) -> Option { + Self::initial_settings_dir_path(&self.app_identifier) + } + // Get the settings directory // init() is not needed for this function - pub fn get_initial_settings_dir() -> Option { + pub fn initial_settings_dir_path(app_identifier: &str) -> Option { Self::env_path("THESEUS_CONFIG_DIR") - .or_else(|| Some(dirs::data_dir()?.join("ModrinthApp"))) + .or_else(|| Some(dirs::data_dir()?.join(app_identifier))) } /// Get all paths needed for Theseus to operate properly #[tracing::instrument] - pub async fn init(config_dir: Option) -> crate::Result { - let settings_dir = Self::get_initial_settings_dir().ok_or( - crate::ErrorKind::FSError( + pub async fn init( + config_dir: Option, + app_identifier: &str, + ) -> crate::Result { + let settings_dir = Self::initial_settings_dir_path(app_identifier) + .ok_or(crate::ErrorKind::FSError( "Could not find valid settings dir".to_string(), - ), - )?; + ))?; fs::create_dir_all(&settings_dir).await.map_err(|err| { crate::ErrorKind::FSError(format!( @@ -48,6 +60,7 @@ impl DirectoryInfo { Ok(Self { settings_dir, config_dir, + app_identifier: app_identifier.to_owned(), }) } @@ -154,8 +167,8 @@ impl DirectoryInfo { } #[inline] - pub fn launcher_logs_dir() -> Option { - Self::get_initial_settings_dir() + pub fn launcher_logs_dir(&self) -> Option { + self.get_initial_settings_dir() .map(|d| d.join(LAUNCHER_LOGS_FOLDER_NAME)) } @@ -176,15 +189,15 @@ impl DirectoryInfo { settings: &mut Settings, exec: E, io_semaphore: &IoSemaphore, + app_identifier: &str, ) -> crate::Result<()> where E: sqlx::Executor<'a, Database = sqlx::Sqlite> + Copy, { - let app_dir = DirectoryInfo::get_initial_settings_dir().ok_or( - crate::ErrorKind::FSError( + let app_dir = DirectoryInfo::initial_settings_dir_path(app_identifier) + .ok_or(crate::ErrorKind::FSError( "Could not find valid config dir".to_string(), - ), - )?; + ))?; if let Some(ref prev_custom_dir) = settings.prev_custom_dir { let prev_dir = PathBuf::from(prev_custom_dir); diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index ab7a5e3e94..542d33d59a 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -80,9 +80,9 @@ pub struct State { } impl State { - pub async fn init() -> crate::Result<()> { + pub async fn init(app_identifier: String) -> crate::Result<()> { let state = LAUNCHER_STATE - .get_or_try_init(Self::initialize_state) + .get_or_try_init(move || Self::initialize_state(app_identifier)) .await?; tokio::task::spawn(async move { @@ -131,9 +131,11 @@ impl State { } #[tracing::instrument] - async fn initialize_state() -> crate::Result> { + async fn initialize_state( + app_identifier: String, + ) -> crate::Result> { tracing::info!("Connecting to app database"); - let pool = db::connect().await?; + let pool = db::connect(&app_identifier).await?; legacy_converter::migrate_legacy_data(&pool).await?; @@ -152,9 +154,12 @@ impl State { &mut settings, &pool, &io_semaphore, + &app_identifier, ) .await?; - let directories = DirectoryInfo::init(settings.custom_dir).await?; + + let directories = + DirectoryInfo::init(settings.custom_dir, &app_identifier).await?; let discord_rpc = DiscordGuard::init()?; From 210862c27a411c8b78c9bbea786753654e901003 Mon Sep 17 00:00:00 2001 From: fetch Date: Wed, 17 Sep 2025 18:42:14 +0100 Subject: [PATCH 08/55] Fix identifier use --- apps/app-playground/src/main.rs | 2 +- apps/app/src/main.rs | 7 +++++-- packages/app-lib/src/logger.rs | 8 +++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/app-playground/src/main.rs b/apps/app-playground/src/main.rs index d450004c9b..22f964f842 100644 --- a/apps/app-playground/src/main.rs +++ b/apps/app-playground/src/main.rs @@ -38,7 +38,7 @@ pub async fn authenticate_run() -> theseus::Result { async fn main() -> theseus::Result<()> { println!("Starting."); - let _log_guard = theseus::start_logger(); + let _log_guard = theseus::start_logger("ModrinthApp"); // Initialize state State::init("ModrinthApp".to_owned()).await?; diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 9b3bbfc198..014f522275 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -160,7 +160,10 @@ fn main() { RUST_LOG="theseus=trace" {run command} */ - let _log_guard = theseus::start_logger(); + + let tauri_context = tauri::generate_context!(); + + let _log_guard = theseus::start_logger(&tauri_context.config().identifier); tracing::info!("Initialized tracing subscriber. Loading Modrinth App!"); @@ -277,7 +280,7 @@ fn main() { ]); tracing::info!("Initializing app..."); - let app = builder.build(tauri::generate_context!()); + let app = builder.build(tauri_context); match app { Ok(app) => { diff --git a/packages/app-lib/src/logger.rs b/packages/app-lib/src/logger.rs index 30f0305314..83671582e5 100644 --- a/packages/app-lib/src/logger.rs +++ b/packages/app-lib/src/logger.rs @@ -18,7 +18,7 @@ // Handling for the live development logging // This will log to the console, and will not log to a file #[cfg(debug_assertions)] -pub fn start_logger() -> Option<()> { +pub fn start_logger(_app_identifier: &str) -> Option<()> { use tracing_subscriber::prelude::*; let filter = tracing_subscriber::EnvFilter::try_from_default_env() @@ -37,7 +37,7 @@ pub fn start_logger() -> Option<()> { // Handling for the live production logging // This will log to a file in the logs directory, and will not show any logs in the console #[cfg(not(debug_assertions))] -pub fn start_logger() -> Option<()> { +pub fn start_logger(app_identifier: &str) -> Option<()> { use crate::prelude::DirectoryInfo; use chrono::Local; use std::fs::OpenOptions; @@ -45,7 +45,9 @@ pub fn start_logger() -> Option<()> { use tracing_subscriber::prelude::*; // Initialize and get logs directory path - let logs_dir = if let Some(d) = DirectoryInfo::launcher_logs_dir() { + let logs_dir = if let Some(d) = + DirectoryInfo::launcher_logs_dir_path(app_identifier) + { d } else { eprintln!("Could not start logger"); From ba4112947e926b3b2cf8929f92f7d796d6908bdd Mon Sep 17 00:00:00 2001 From: fetch Date: Wed, 17 Sep 2025 19:18:52 +0100 Subject: [PATCH 09/55] Fix initial setup --- packages/app-lib/src/state/dirs.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index e413b3f94a..b098dbcac0 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -172,6 +172,12 @@ impl DirectoryInfo { .map(|d| d.join(LAUNCHER_LOGS_FOLDER_NAME)) } + #[inline] + pub fn launcher_logs_dir_path(app_identifier: &str) -> Option { + Self::initial_settings_dir_path(app_identifier) + .map(|d| d.join(LAUNCHER_LOGS_FOLDER_NAME)) + } + /// Get the cache directory for Theseus #[inline] pub fn caches_dir(&self) -> PathBuf { From cc4023a1144052598b268accc173936c21db58e8 Mon Sep 17 00:00:00 2001 From: fetch Date: Wed, 17 Sep 2025 19:29:45 +0100 Subject: [PATCH 10/55] Fix `updater` buidls --- apps/app/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 014f522275..da32f35249 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -26,7 +26,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { #[cfg(feature = "updater")] 'updater: { if env::var("MODRINTH_EXTERNAL_UPDATE_PROVIDER").is_ok() { - State::init().await?; + State::init(app_identifier).await?; break 'updater; } From 85a0378cd068a856bd5358bbb9ee8f79ea07c2eb Mon Sep 17 00:00:00 2001 From: fetch Date: Wed, 17 Sep 2025 19:48:00 +0100 Subject: [PATCH 11/55] Force bash --- .github/workflows/theseus-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 9b0cfb57bf..0264d6abb5 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -63,6 +63,7 @@ jobs: cache: pnpm - name: Generate tauri-dev.conf.json + shell: bash run: | GIT_HASH=$(git rev-parse --short HEAD) cat > apps/app/tauri-dev.conf.json < Date: Thu, 18 Sep 2025 16:55:39 +0100 Subject: [PATCH 12/55] Tweak build --- .github/workflows/theseus-build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 0264d6abb5..088d3f60a0 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -3,8 +3,6 @@ on: push: branches: - main - - prod - - '**/**' tags: - 'v*' paths: @@ -62,7 +60,7 @@ jobs: node-version-file: .nvmrc cache: pnpm - - name: Generate tauri-dev.conf.json + - name: 📄 Generate tauri-dev.conf.json shell: bash run: | GIT_HASH=$(git rev-parse --short HEAD) @@ -145,7 +143,7 @@ jobs: [System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream $env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD" $env:JAVA_HOME = "$env:JAVA_HOME_11_X64" - pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater' + ${{ github.ref == 'refs/heads/main' && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }} Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue if: startsWith(matrix.platform, 'windows') env: From c22dc2086e874adfb1f952766d2b9db2f03f6af7 Mon Sep 17 00:00:00 2001 From: teaSummer Date: Sat, 20 Sep 2025 20:07:30 +0800 Subject: [PATCH 13/55] fix(app): properly show all versions and notify loaders (#4395) * fix(app): properly show all versions and notify loaders * fix lint --- .../src/components/ui/NotificationItem.vue | 14 +++++++------- apps/frontend/src/pages/[type]/[id].vue | 6 ++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/frontend/src/components/ui/NotificationItem.vue b/apps/frontend/src/components/ui/NotificationItem.vue index 1e34a30274..662c98a79e 100644 --- a/apps/frontend/src/components/ui/NotificationItem.vue +++ b/apps/frontend/src/components/ui/NotificationItem.vue @@ -173,7 +173,7 @@ for @@ -391,12 +391,6 @@ const user = computed(() => props.notification.extra_data.user) const organization = computed(() => props.notification.extra_data.organization) const invitedBy = computed(() => props.notification.extra_data.invited_by) -const loaderCategories = computed(() => { - return tags.value.loaders.filter((loader) => { - return version.value?.loaders?.includes(loader.name) - }) -}) - const threadLink = computed(() => { if (report.value) { return `/dashboard/report/${report.value.id}` @@ -462,6 +456,12 @@ function getMessages() { } return messages } + +function getLoaderCategories(ver) { + return tags.value.loaders.filter((loader) => { + return ver?.loaders?.includes(loader.name) + }) +} diff --git a/apps/frontend/src/composables/avalara1099.ts b/apps/frontend/src/composables/avalara1099.ts new file mode 100644 index 0000000000..c34ab33a42 --- /dev/null +++ b/apps/frontend/src/composables/avalara1099.ts @@ -0,0 +1,187 @@ +import type { Ref } from 'vue' +import { computed, ref } from 'vue' + +export interface FormRequestAttributes { + form_type: 'W-9' | 'W-8BEN' | 'W-8BEN-E' | string + company_id: number + company_name: string + company_email: string + reference_id: string | null + form_id: string | null + signed_at: string | null + tin_match_status: string | null + expires_at: string | null +} + +export interface FormRequestLinks { + action_validate?: string + action_complete?: string + [key: string]: unknown +} + +export interface FormRequestData { + id: string + type: 'form_request' | string + attributes: FormRequestAttributes + links?: FormRequestLinks + [key: string]: unknown +} + +export interface FormRequestResponse { + data: FormRequestData + [key: string]: unknown +} + +export interface UseAvalara1099Options { + prefill?: Record + // Optional override for the origin (defaults to vendor CDN domain) + origin?: string + // Optional hook to further style the injected dialog/iframe + styleDialog?: (dialog: HTMLDialogElement, iframe: HTMLIFrameElement | null) => void + // Poll interval while waiting for global to appear + pollIntervalMs?: number + // Max time to wait for script before rejecting (ms); 0/undefined => no timeout + timeoutMs?: number +} + +interface AvalaraGlobal { + requestW9: ( + formRequest: FormRequestResponse | FormRequestData, + opts?: { prefill?: Record }, + ) => Promise | any + requestW8BEN: ( + formRequest: FormRequestResponse | FormRequestData, + opts?: { prefill?: Record }, + ) => Promise | any + requestW8BENE: ( + formRequest: FormRequestResponse | FormRequestData, + opts?: { prefill?: Record }, + ) => Promise | any + origin?: string + [key: string]: unknown +} + +declare global { + interface Window { + Avalara1099?: AvalaraGlobal + } +} + +const injectedKey = '__avalara1099_script_injected__' + +function ensureScriptInjected(origin: string) { + if (import.meta.server) return + const w = window as any + if (w[injectedKey]) return + w[injectedKey] = true + useHead({ + script: [ + { + src: `${origin.replace(/\/$/, '')}/api/request_form.js`, + crossorigin: 'anonymous', + type: 'module', + }, + ], + }) +} + +async function waitForAvalara(opts: { + pollIntervalMs: number + timeoutMs?: number + origin: string +}): Promise { + if (import.meta.server) throw new Error('Avalara 1099 is client-side only') + ensureScriptInjected(opts.origin) + const start = Date.now() + return await new Promise((resolve, reject) => { + const poll = () => { + const g = window.Avalara1099 + if (g) return resolve(g) + if (opts.timeoutMs && opts.timeoutMs > 0 && Date.now() - start > opts.timeoutMs) { + return reject(new Error('Timed out waiting for Avalara1099 script to load')) + } + setTimeout(poll, opts.pollIntervalMs) + } + poll() + }) +} + +export function useAvalara1099( + initial: FormRequestResponse | FormRequestData, + options: UseAvalara1099Options = {}, +) { + const origin = options.origin || 'https://www.track1099.com' + const pollIntervalMs = options.pollIntervalMs ?? 250 + const timeoutMs = options.timeoutMs + + const request: Ref = ref(initial) + const loading = ref(false) + const error: Ref = ref(null) + + const signedAt = computed(() => { + const data = (request.value as FormRequestResponse).data || request.value + return data.attributes?.signed_at ? new Date(data.attributes.signed_at) : null + }) + + const status = computed(() => (signedAt.value ? 'signed' : 'incomplete')) + + async function start(): Promise { + loading.value = true + error.value = null + try { + const g = await waitForAvalara({ pollIntervalMs, timeoutMs, origin }) + const data = (request.value as FormRequestResponse).data || request.value + const formType = data.attributes?.form_type + + // Defensive deep clone to strip proxies / non-cloneable refs before postMessage + // (DataCloneError guard) + let safeRequest: any + try { + safeRequest = JSON.parse(JSON.stringify(request.value)) + } catch { + // Fallback shallow copy + safeRequest = Array.isArray(request.value) + ? [...(request.value as any)] + : { ...(request.value as any) } + } + let safePrefill: any = undefined + if (options.prefill) { + try { + safePrefill = JSON.parse(JSON.stringify(options.prefill)) + } catch { + safePrefill = { ...options.prefill } + } + } + + let promise: any + if (formType === 'W-8BEN') { + promise = g.requestW8BEN(safeRequest, { prefill: safePrefill }) + } else if (formType === 'W-9') { + promise = g.requestW9(safeRequest, { prefill: safePrefill }) + } else if (formType === 'W-8BEN-E' || formType === 'W-8BEN E') { + promise = g.requestW8BENE(safeRequest, { prefill: safePrefill }) + } else { + throw new Error(`Unsupported form_type: ${formType}`) + } + + // The vendor promise resolves with an updated form request (signed state) + const newReq = await promise + request.value = newReq + return newReq + } catch (e) { + error.value = e + throw e + } finally { + loading.value = false + } + } + + return { + start, + request, + signedAt, + status, // 'signed' | 'incomplete' + loading, + error, + } +} diff --git a/apps/frontend/src/layouts/default.vue b/apps/frontend/src/layouts/default.vue index 99cb7eb82f..bd959ed922 100644 --- a/apps/frontend/src/layouts/default.vue +++ b/apps/frontend/src/layouts/default.vue @@ -27,6 +27,21 @@
+ + + + + + +
@@ -764,6 +784,7 @@ import { DownloadIcon, DropdownIcon, FileIcon, + FileTextIcon, GithubIcon, GlassesIcon, HamburgerIcon, @@ -805,6 +826,7 @@ import { IntlFormatted } from '@vintl/vintl/components' import TextLogo from '~/components/brand/TextLogo.vue' import CollectionCreateModal from '~/components/ui/CollectionCreateModal.vue' +import CreatorTaxFormModal from '~/components/ui/dashboard/CreatorTaxFormModal.vue' import ModalCreation from '~/components/ui/ModalCreation.vue' import OrganizationCreateModal from '~/components/ui/OrganizationCreateModal.vue' import TeleportOverflowMenu from '~/components/ui/servers/TeleportOverflowMenu.vue' @@ -826,6 +848,43 @@ const route = useNativeRoute() const router = useNativeRouter() const link = config.public.siteUrl + route.path.replace(/\/+$/, '') +const { data: payoutBalance } = await useAsyncData('payout/balance', () => + useBaseFetch('payout/balance', { apiVersion: 3 }), +) + +const showTaxComplianceBanner = computed(() => { + const bal = payoutBalance.value + if (!bal) return false + const thresholdMet = (bal.withdrawn_ytd ?? 0) >= 600 + const status = bal.form_completion_status ?? 'unknown' + const isComplete = status === 'complete' + return !!auth.value.user && thresholdMet && !isComplete +}) + +const taxBannerMessages = defineMessages({ + title: { + id: 'layout.banner.tax.title', + defaultMessage: 'Tax form required', + }, + description: { + id: 'layout.banner.tax.description', + defaultMessage: + 'You’ve already withdrawn over $600 from Modrinth this year. To comply with tax regulations, you need to complete a tax form. Your withdrawals are paused until this form is submitted.', + }, + action: { + id: 'layout.banner.tax.action', + defaultMessage: 'Complete tax form', + }, + close: { id: 'common.close', defaultMessage: 'Close' }, +}) + +const taxFormModalRef = ref(null) +function openTaxForm(e) { + if (taxFormModalRef.value && taxFormModalRef.value.startTaxForm) { + taxFormModalRef.value.startTaxForm(e) + } +} + const basePopoutId = useId() async function handleResendEmailVerification() { try { diff --git a/apps/frontend/src/locales/en-US/index.json b/apps/frontend/src/locales/en-US/index.json index 57a69e83f8..0633ab28f5 100644 --- a/apps/frontend/src/locales/en-US/index.json +++ b/apps/frontend/src/locales/en-US/index.json @@ -1,4 +1,10 @@ { + "action.cancel": { + "message": "Cancel" + }, + "action.continue": { + "message": "Continue" + }, "admin.billing.error.not-found": { "message": "User not found" }, @@ -413,6 +419,15 @@ "collection.title": { "message": "{name} - Collection" }, + "common.close": { + "message": "Close" + }, + "common.no": { + "message": "No" + }, + "common.yes": { + "message": "Yes" + }, "dashboard.collections.button.create-new": { "message": "Create new" }, @@ -425,6 +440,30 @@ "dashboard.collections.long-title": { "message": "Your collections" }, + "dashboard.creator-tax-form-modal.entity.description": { + "message": "A foreign entity means a business entity organized outside the United States (such as a non-US corporation, partnership, or LLC)." + }, + "dashboard.creator-tax-form-modal.entity.foreign-entity": { + "message": "Foreign entity" + }, + "dashboard.creator-tax-form-modal.entity.private-individual": { + "message": "Private individual" + }, + "dashboard.creator-tax-form-modal.entity.question": { + "message": "Are you a private individual or part of a foreign entity?" + }, + "dashboard.creator-tax-form-modal.header": { + "message": "Tax form" + }, + "dashboard.creator-tax-form-modal.security.description": { + "message": "Modrinth uses third-party provider Track1099 to securely collect and store your tax forms. Learn more here." + }, + "dashboard.creator-tax-form-modal.security.header": { + "message": "Security practices" + }, + "dashboard.creator-tax-form-modal.us-citizen.question": { + "message": "Are you a US citizen?" + }, "error.collection.404.list_item.1": { "message": "You may have mistyped the collection's URL." }, @@ -728,6 +767,15 @@ "layout.banner.subscription-payment-failed.title": { "message": "Billing action required." }, + "layout.banner.tax.action": { + "message": "Complete tax form" + }, + "layout.banner.tax.description": { + "message": "You’ve already withdrawn over $600 from Modrinth this year. To comply with tax regulations, you need to complete a tax form. Your withdrawals are paused until this form is submitted." + }, + "layout.banner.tax.title": { + "message": "Tax form required" + }, "layout.banner.verify-email.action": { "message": "Re-send verification email" }, diff --git a/apps/frontend/src/pages/dashboard/revenue/index.vue b/apps/frontend/src/pages/dashboard/revenue/index.vue index 8c5c2e5847..3deb63d127 100644 --- a/apps/frontend/src/pages/dashboard/revenue/index.vue +++ b/apps/frontend/src/pages/dashboard/revenue/index.vue @@ -68,23 +68,37 @@
- - - Withdraw - + + + + Withdraw + + - - - View transfer history - + + + + View transfer history + +
+

+ You have withdrawn over $600 this year. To continue withdrawing, you must complete a tax + form. +

+

By uploading projects to Modrinth and withdrawing money from your account, you agree to the Rewards Program Terms. For more @@ -101,10 +115,12 @@ email {{ auth.user.payout_data.paypal_address }}

- + + + @@ -148,7 +165,7 @@ import { UnknownIcon, XIcon, } from '@modrinth/assets' -import { injectNotificationManager } from '@modrinth/ui' +import { ButtonStyled, injectNotificationManager } from '@modrinth/ui' import { formatDate } from '@modrinth/utils' import dayjs from 'dayjs' import { computed } from 'vue' @@ -163,6 +180,12 @@ const { data: userBalance } = await useAsyncData(`payout/balance`, () => useBaseFetch(`payout/balance`, { apiVersion: 3 }), ) +const blockedByTax = computed(() => { + const status = userBalance.value?.form_completion_status ?? 'unknown' + const thresholdMet = (userBalance.value?.withdrawn_ytd ?? 0) >= 600 + return thresholdMet && status !== 'complete' +}) + const deadlineEnding = computed(() => { let deadline = dayjs().subtract(2, 'month').endOf('month').add(60, 'days') if (deadline.isBefore(dayjs().startOf('day'))) { diff --git a/apps/frontend/src/pages/dashboard/revenue/withdraw.vue b/apps/frontend/src/pages/dashboard/revenue/withdraw.vue index b7f86899ae..9401ad9db3 100644 --- a/apps/frontend/src/pages/dashboard/revenue/withdraw.vue +++ b/apps/frontend/src/pages/dashboard/revenue/withdraw.vue @@ -135,6 +135,10 @@ +

+ You have withdrawn over $600 this year. To continue withdrawing, you must complete a tax form. +

+