From 9f8f11f4240f349bda1726f64dff7d20b77cbd4a Mon Sep 17 00:00:00 2001 From: Felitendo Date: Mon, 14 Sep 2026 14:44:42 +0200 Subject: [PATCH] feat: browser microsoft sign-in, reachable offline and ely.by login --- README.md | 25 +- ...ove-advertising-and-Modrinth-upsells.patch | 235 ++++++++ patches/0002-Remove-advertising.patch | 117 ---- patches/0004-Add-offline-accounts.patch | 111 +++- patches/0005-Make-the-sidebars-foldable.patch | 20 +- patches/0006-Add-Ely.by-accounts.patch | 90 ++- ...icrosoft-in-the-player-s-own-browser.patch | 530 ++++++++++++++++++ scripts/check.sh | 14 + 8 files changed, 1003 insertions(+), 139 deletions(-) create mode 100644 patches/0002-Remove-advertising-and-Modrinth-upsells.patch delete mode 100644 patches/0002-Remove-advertising.patch create mode 100644 patches/0007-Sign-in-to-Microsoft-in-the-player-s-own-browser.patch diff --git a/README.md b/README.md index 4ac3bee..2f61df0 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,12 @@ works. | Patch | What it does | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `0001-Rename-the-app-to-Modrinth-Enhanced` | Product name, binary name, window title and version label. | -| `0002-Remove-advertising` | The sidebar ad slot, the "Upgrade to Modrinth+" nag and the ad cookie consent prompt. The ad webview is never created. | +| `0002-Remove-advertising-and-...` | The sidebar ad slot, both "Upgrade to Modrinth+" prompts and the ad cookie consent prompt. The ad webview is never created. | | `0003-Remove-telemetry` | PostHog analytics, Sentry crash reporting, the Tally survey embeds, and the playtime and server-play reports the launcher sends to Modrinth. | | `0004-Add-offline-accounts` | A way to add a Minecraft account that never contacts Microsoft or Mojang. | | `0005-Make-the-sidebars-foldable` | A switch for the Modrinth Servers button, a news section that folds away, and a title bar button that folds the right sidebar away. | | `0006-Add-Ely.by-accounts` | Sign in with Ely.by, launched through authlib-injector. | +| `0007-Sign-in-to-Microsoft-in-the-...` | Microsoft sign-in happens in your own browser instead of a webview, so your password manager works. | ### Offline accounts @@ -30,7 +31,20 @@ The player UUID is derived exactly the way Minecraft itself derives it — an MD launcher. Offline accounts can play singleplayer and join servers running in offline mode. Servers in online mode reject them, as they do in every other launcher. -Microsoft sign-in is untouched and still the default. +Both sit next to "Sign in to Microsoft" in the account card, and in the "Minecraft required" modal +you get when pressing Play with no account. + +### Microsoft sign-in + +Microsoft sign-in opens your own browser rather than a webview inside the launcher, so your +password manager, autofill and passkeys work, and you can see in the address bar that the page is +really Microsoft's. + +Microsoft cannot hand the result back: the client id the launcher uses is Minecraft's own, whose +only registered redirect is a fixed page on `login.live.com`, with no loopback address for the +launcher to listen on. So the browser lands on that page with the code in the address and you copy +the address into the launcher. The webview is still one click away for anyone the browser does not +work out for. ### Ely.by accounts @@ -62,6 +76,13 @@ None of the three reach Modrinth. Preference syncing maps a fixed list of named directions and these are not in it, so they are neither sent to your Modrinth account nor overwritten by another device. +### Modrinth+ + +Nothing in the app is gated behind Modrinth+. In upstream it decides whether the ad slot, the +consent prompt and the two "Upgrade to Modrinth+" prompts are shown, and nothing else — so removing +the advertising is the whole of it, and there is nothing further to unlock from here. Badges and +everything else a subscription buys are decided on Modrinth's servers. + ### What is *not* removed Download attribution still happens. It is a header on downloads you already asked for, and it is diff --git a/patches/0002-Remove-advertising-and-Modrinth-upsells.patch b/patches/0002-Remove-advertising-and-Modrinth-upsells.patch new file mode 100644 index 0000000..2e1d726 --- /dev/null +++ b/patches/0002-Remove-advertising-and-Modrinth-upsells.patch @@ -0,0 +1,235 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Modrinth Enhanced +Date: Mon, 14 Sep 2026 10:18:37 +0200 +Subject: [PATCH] Remove advertising and Modrinth+ upsells + +The sidebar ad slot, the ad cookie consent prompt and both "Upgrade to +Modrinth+" prompts are gone: + +* `showAd` and `adConsentAvailable` are pinned to false, which takes the + ad slot and the consent prompt out of the layout. +* The upgrade link above the ad slot and the entry in the account menu + are removed outright rather than left behind a false condition. +* The sidebar keeps upstream's `has-plus` class unconditionally, which + is what stops space being reserved at the bottom for an ad. +* With no upsell left to decide about, the request that asked Modrinth + whether this account has Modrinth+ goes too. + +The helpers in `helpers/ads.js` are additionally stubbed out, which +stops the Tauri `ads` plugin from ever being asked to spawn the ad +webview, no matter which call site reaches for it. + +Nothing else in the app is gated behind Modrinth+: it decided whether +ads and these prompts were shown, and nothing more. +--- + apps/app-frontend/src/App.vue | 55 ++++------------------- + apps/app-frontend/src/helpers/ads.js | 65 +++++++++------------------- + 2 files changed, 29 insertions(+), 91 deletions(-) + +diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue +index a2ae0e6..6e7b27d 100644 +--- a/apps/app-frontend/src/App.vue ++++ b/apps/app-frontend/src/App.vue +@@ -9,7 +9,6 @@ import { + VerboseLoggingFeature, + } from '@modrinth/api-client' + import { +- ArrowBigUpDashIcon, + ArrowLeftRightIcon, + ChevronLeftIcon, + ChevronRightIcon, +@@ -94,7 +93,6 @@ import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue' + import NavButton from '@/components/ui/NavButton.vue' + import OnboardingChecklist from '@/components/ui/onboarding-checklist/index.vue' + import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue' +-import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' + import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' + import SharedInstanceInviteHandler from '@/components/ui/shared-instances/shared-instance-invite-handler/index.vue' + import SplashScreen from '@/components/ui/SplashScreen.vue' +@@ -159,7 +157,6 @@ import { + syncedServersQueryOptions, + } from '@/helpers/synced-options' + import { syncedPackQueryOptions } from '@/helpers/synced-packs' +-import { hasActivePride26Midas, hasMidasBadge } from '@/helpers/user-campaigns.ts' + import { get_user_preferences } from '@/helpers/user-preferences.ts' + import { parse_modrinth_user_link } from '@/helpers/users' + import { +@@ -344,12 +341,6 @@ const tauriApiClient = new TauriModrinthClient({ + ], + }) + provideModrinthClient(tauriApiClient) +-const { data: authenticatedModrinthUser } = useQuery({ +- queryKey: computed(() => ['authenticated-user', 'campaigns', credentials.value?.user?.id]), +- queryFn: () => tauriApiClient.labrinth.users_v3.getAuthenticated(), +- enabled: () => !!credentials.value?.session, +- retry: false, +-}) + useQuery({ + queryKey: computed(() => instanceKeys.sharedEligibility(credentials.value?.user?.id)), + queryFn: can_current_user_use_shared_instances, +@@ -360,16 +351,13 @@ useQuery({ + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }) +-const hasPlus = computed( +- () => +- !!credentials.value?.user && +- (hasMidasBadge(credentials.value.user) || +- hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)), +-) +-const showAd = computed( +- () => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined, +-) +-const adConsentAvailable = computed(() => credentials.value !== undefined && !hasPlus.value) ++// Modrinth Enhanced ships without advertising, so the sidebar ad slot and the ++// ad cookie consent flow that only exists to serve it are both switched off. ++// The sidebar keeps upstream's `has-plus` class unconditionally for the same ++// reason: that class is what stops space being reserved at the bottom for an ++// ad and the gradient being drawn above it. ++const showAd = computed(() => false) ++const adConsentAvailable = computed(() => false) + providePageContext({ + hierarchicalSidebarAvailable: ref(true), + showAds: showAd, +@@ -694,10 +682,6 @@ const messages = defineMessages({ + id: 'app.restarting', + defaultMessage: 'Restarting...', + }, +- upgradeToModrinthPlus: { +- id: 'app.nav.upgrade-to-modrinth-plus', +- defaultMessage: 'Upgrade to Modrinth+', +- }, + news: { + id: 'app.news.title', + defaultMessage: 'News', +@@ -1529,16 +1513,6 @@ const modrinthAccountMenuOptions = computed(() => [ + icon: UserIcon, + action: () => router.push(`/user/${encodeURIComponent(credentials.value.user.username)}`), + }, +- { +- id: 'plus', +- label: formatMessage(messages.upgradeToModrinthPlus), +- icon: ArrowBigUpDashIcon, +- type: 'link', +- href: 'https://modrinth.plus?app', +- target: '_blank', +- tone: 'purple', +- shown: !hasPlus.value, +- }, + { + id: 'add-friend', + label: formatMessage(messages.addFriend), +@@ -2484,13 +2458,11 @@ provideAppUpdateDownloadProgress(appUpdateDownload) + + +
+
+ +
+
+- + + + +diff --git a/apps/app-frontend/src/helpers/ads.js b/apps/app-frontend/src/helpers/ads.js +index 8c85970..96005e6 100644 +--- a/apps/app-frontend/src/helpers/ads.js ++++ b/apps/app-frontend/src/helpers/ads.js +@@ -1,55 +1,32 @@ +-import { invoke } from '@tauri-apps/api/core' ++/** ++ * Modrinth Enhanced does not show advertising. ++ * ++ * Upstream these helpers forward to the Tauri `ads` plugin, which spawns a ++ * second webview that loads the ad network and tracks clicks on it. Every ++ * helper below is a no-op instead, so the plugin is never asked to create ++ * that webview and the consent flow that exists purely for ad cookies never ++ * has anything to consent to. ++ * ++ * The functions are kept — rather than removed along with their callers — so ++ * that upstream call sites keep working unchanged. ++ */ + +-export async function init_ads_window(overrideShown = false) { +- return await invoke('plugin:ads|init_ads_window', { +- overrideShown, +- dpr: window.devicePixelRatio, +- }) +-} +- +-let adsWindowHoldUpdate = Promise.resolve() +- +-async function update_ads_window_hold(acquire) { +- adsWindowHoldUpdate = adsWindowHoldUpdate +- .catch(() => {}) +- .then(() => +- invoke('plugin:ads|update_ads_window_hold', { +- acquire, +- dpr: window.devicePixelRatio, +- }), +- ) ++export async function init_ads_window() {} + +- return await adsWindowHoldUpdate +-} +- +-export async function take_ads_window_hold() { +- return await update_ads_window_hold(true) +-} ++export async function take_ads_window_hold() {} + +-export async function release_ads_window_hold() { +- return await update_ads_window_hold(false) +-} ++export async function release_ads_window_hold() {} + +-export async function hide_ads_window(reset) { +- return await invoke('plugin:ads|hide_ads_window', { reset }) +-} ++export async function hide_ads_window() {} + + export async function should_show_ads_consent_popup() { +- return await invoke('plugin:ads|should_show_ads_consent_popup') ++ return false + } + +-export async function perform_ads_consent_action(action) { +- return await invoke('plugin:ads|perform_ads_consent_action', { action }) +-} ++export async function perform_ads_consent_action() {} + +-export async function open_ads_consent_preferences() { +- return await invoke('plugin:ads|open_ads_consent_preferences') +-} ++export async function open_ads_consent_preferences() {} + +-export async function record_ads_click() { +- return await invoke('plugin:ads|record_ads_click') +-} ++export async function record_ads_click() {} + +-export async function open_ads_link(path, origin) { +- return await invoke('plugin:ads|open_link', { path, origin }) +-} ++export async function open_ads_link() {} diff --git a/patches/0002-Remove-advertising.patch b/patches/0002-Remove-advertising.patch deleted file mode 100644 index ca84739..0000000 --- a/patches/0002-Remove-advertising.patch +++ /dev/null @@ -1,117 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Modrinth Enhanced -Date: Mon, 14 Sep 2026 10:18:37 +0200 -Subject: [PATCH] Remove advertising - -The sidebar ad slot, the "Upgrade to Modrinth+" nag above it and the ad -cookie consent prompt are all driven by two computed flags in App.vue, -so pinning both to false takes the whole surface out of the layout. - -The helpers in `helpers/ads.js` are additionally stubbed out, which -stops the Tauri `ads` plugin from ever being asked to spawn the ad -webview, no matter which call site reaches for it. ---- - apps/app-frontend/src/App.vue | 8 ++-- - apps/app-frontend/src/helpers/ads.js | 65 +++++++++------------------- - 2 files changed, 25 insertions(+), 48 deletions(-) - -diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue -index a2ae0e6..33c29ce 100644 ---- a/apps/app-frontend/src/App.vue -+++ b/apps/app-frontend/src/App.vue -@@ -366,10 +366,10 @@ const hasPlus = computed( - (hasMidasBadge(credentials.value.user) || - hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)), - ) --const showAd = computed( -- () => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined, --) --const adConsentAvailable = computed(() => credentials.value !== undefined && !hasPlus.value) -+// Modrinth Enhanced ships without advertising, so the sidebar ad slot and the -+// ad cookie consent flow that only exists to serve it are both switched off. -+const showAd = computed(() => false) -+const adConsentAvailable = computed(() => false) - providePageContext({ - hierarchicalSidebarAvailable: ref(true), - showAds: showAd, -diff --git a/apps/app-frontend/src/helpers/ads.js b/apps/app-frontend/src/helpers/ads.js -index 8c85970..96005e6 100644 ---- a/apps/app-frontend/src/helpers/ads.js -+++ b/apps/app-frontend/src/helpers/ads.js -@@ -1,55 +1,32 @@ --import { invoke } from '@tauri-apps/api/core' -+/** -+ * Modrinth Enhanced does not show advertising. -+ * -+ * Upstream these helpers forward to the Tauri `ads` plugin, which spawns a -+ * second webview that loads the ad network and tracks clicks on it. Every -+ * helper below is a no-op instead, so the plugin is never asked to create -+ * that webview and the consent flow that exists purely for ad cookies never -+ * has anything to consent to. -+ * -+ * The functions are kept — rather than removed along with their callers — so -+ * that upstream call sites keep working unchanged. -+ */ - --export async function init_ads_window(overrideShown = false) { -- return await invoke('plugin:ads|init_ads_window', { -- overrideShown, -- dpr: window.devicePixelRatio, -- }) --} -- --let adsWindowHoldUpdate = Promise.resolve() -- --async function update_ads_window_hold(acquire) { -- adsWindowHoldUpdate = adsWindowHoldUpdate -- .catch(() => {}) -- .then(() => -- invoke('plugin:ads|update_ads_window_hold', { -- acquire, -- dpr: window.devicePixelRatio, -- }), -- ) -+export async function init_ads_window() {} - -- return await adsWindowHoldUpdate --} -- --export async function take_ads_window_hold() { -- return await update_ads_window_hold(true) --} -+export async function take_ads_window_hold() {} - --export async function release_ads_window_hold() { -- return await update_ads_window_hold(false) --} -+export async function release_ads_window_hold() {} - --export async function hide_ads_window(reset) { -- return await invoke('plugin:ads|hide_ads_window', { reset }) --} -+export async function hide_ads_window() {} - - export async function should_show_ads_consent_popup() { -- return await invoke('plugin:ads|should_show_ads_consent_popup') -+ return false - } - --export async function perform_ads_consent_action(action) { -- return await invoke('plugin:ads|perform_ads_consent_action', { action }) --} -+export async function perform_ads_consent_action() {} - --export async function open_ads_consent_preferences() { -- return await invoke('plugin:ads|open_ads_consent_preferences') --} -+export async function open_ads_consent_preferences() {} - --export async function record_ads_click() { -- return await invoke('plugin:ads|record_ads_click') --} -+export async function record_ads_click() {} - --export async function open_ads_link(path, origin) { -- return await invoke('plugin:ads|open_link', { path, origin }) --} -+export async function open_ads_link() {} diff --git a/patches/0004-Add-offline-accounts.patch b/patches/0004-Add-offline-accounts.patch index 80d8fd1..b7fd4d8 100644 --- a/patches/0004-Add-offline-accounts.patch +++ b/patches/0004-Add-offline-accounts.patch @@ -21,17 +21,25 @@ data when they are opened elsewhere. Usernames are validated the way Mojang validates them: 3 to 16 characters of letters, numbers and underscores. + +Two places had to be opened up for any of this to be reachable. The +account card was hidden until a Microsoft account had been added, which +left offline sign-in unreachable for exactly the people who want it, and +the "Minecraft required" modal - what you actually hit on pressing Play +with no account - offered Microsoft and nothing else. --- Cargo.lock | 1 + Cargo.toml | 1 + - .../src/components/ui/AccountsCard.vue | 25 ++++ + apps/app-frontend/src/App.vue | 10 +- + .../src/components/ui/AccountsCard.vue | 30 ++++ .../src/components/ui/OfflineAccountModal.vue | 136 ++++++++++++++++++ + .../MinecraftRequiredModal.vue | 26 +++- apps/app-frontend/src/helpers/auth.js | 13 ++ apps/app/src/api/auth.rs | 7 + packages/app-lib/Cargo.toml | 1 + packages/app-lib/src/api/minecraft_auth.rs | 39 +++++ packages/app-lib/src/state/minecraft_auth.rs | 68 +++++++++ - 9 files changed, 291 insertions(+) + 11 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/OfflineAccountModal.vue diff --git a/Cargo.lock b/Cargo.lock @@ -58,8 +66,29 @@ index a4a779c..a85f576 100644 modrinth-content-management = { path = "packages/modrinth-content-management" } modrinth-log = { path = "packages/modrinth-log" } modrinth-util = { path = "packages/modrinth-util" } +diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue +index 6e7b27d..532f104 100644 +--- a/apps/app-frontend/src/App.vue ++++ b/apps/app-frontend/src/App.vue +@@ -2472,10 +2472,12 @@ provideAppUpdateDownloadProgress(appUpdateDownload) + /> + +