From 8e44f20eec650dd0becd45ab222e9a5f3a03174f Mon Sep 17 00:00:00 2001 From: Truman Gao <106889354+tdgao@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:31:10 -0600 Subject: [PATCH] feat: implement custom notification and popup components (#6768) * feat: implement custom popup * feat: add privacy setting * style fixes * more style fixes * feat: implement notification panel changes and better popup in website * feat: update popup button * fix: remove unnecessary modal props * fixes * pnpm prepr * remove devtools opening * update query to find buttons * update copy * update copy * fix: manage permissions depends on showAds booleans instead of managemetn availability --- apps/app-frontend/src/App.vue | 135 ++++++-- .../ui/settings/PrivacySettings.vue | 60 +++- apps/app-frontend/src/helpers/ads.js | 17 + .../app-frontend/src/locales/en-US/index.json | 18 + apps/app/build.rs | 5 + apps/app/src/api/ads-init.js | 284 ++++++++++++++- apps/app/src/api/ads.rs | 174 +++++++++- apps/frontend/src/app.vue | 2 + .../components/ui/AdsConsentNotification.vue | 327 ++++++++++++++++++ apps/frontend/src/error.vue | 1 + apps/frontend/src/locales/en-US/index.json | 15 + .../src/providers/setup/page-context.ts | 1 + apps/frontend/src/public/inmobi.js | 3 + .../src/components/nav/NotificationPanel.vue | 54 ++- .../components/nav/PopupNotificationPanel.vue | 13 +- packages/ui/src/providers/page-context.ts | 1 + .../ui/src/providers/popup-notifications.ts | 2 + .../ui/src/providers/web-notifications.ts | 17 +- .../stories/nav/NotificationPanel.stories.ts | 33 +- .../nav/PopupNotificationPanel.stories.ts | 34 ++ 20 files changed, 1142 insertions(+), 54 deletions(-) create mode 100644 apps/frontend/src/components/ui/AdsConsentNotification.vue diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 7910ca37a7..66e3024ed3 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -89,7 +89,14 @@ import SplashScreen from '@/components/ui/SplashScreen.vue' import WindowControls from '@/components/ui/WindowControls.vue' import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { config } from '@/config' -import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js' +import { + ads_consent_listener, + get_ads_consent_required, + hide_ads_window, + init_ads_window, + perform_ads_consent_action, + show_ads_window, +} from '@/helpers/ads.js' import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics' import { check_reachable } from '@/helpers/auth.js' import { get_user, get_version } from '@/helpers/cache.js' @@ -184,6 +191,8 @@ const { handleError, addNotification } = notificationManager const popupNotificationManager = new AppPopupNotificationManager() providePopupNotificationManager(popupNotificationManager) const { addPopupNotification } = popupNotificationManager +let adsConsentPopupId = null +let unlistenAdsConsent const appVersion = getVersion() const tauriApiClient = new TauriModrinthClient({ @@ -213,9 +222,20 @@ const { data: authenticatedModrinthUser } = useQuery({ enabled: () => !!credentials.value?.session, retry: 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) providePageContext({ hierarchicalSidebarAvailable: ref(true), - showAds: ref(false), + showAds: showAd, + adConsentAvailable, floatingActionBarOffsets: { left: ref(APP_LEFT_NAV_WIDTH), right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')), @@ -295,6 +315,12 @@ const authUnreachable = computed(() => { onMounted(async () => { await useCheckDisableMouseover() + try { + unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired) + handleAdsConsentRequired(await get_ads_consent_required()) + } catch (error) { + handleError(error) + } document.querySelector('body').addEventListener('click', handleClick) document.querySelector('body').addEventListener('auxclick', handleAuxClick) @@ -308,6 +334,7 @@ onUnmounted(async () => { unsubscribeSidebarToggle() clearDelayedUpdatePopup() + await unlistenAdsConsent?.() await unlistenUpdateDownload?.() }) @@ -332,8 +359,77 @@ const messages = defineMessages({ defaultMessage: 'Minecraft authentication servers may be down right now. Check your internet connection and try again later.', }, + adsConsentTitle: { + id: 'app.ads-consent.title', + defaultMessage: 'Your privacy and how ads support Modrinth', + }, + adsConsentBody: { + id: 'app.ads-consent.body', + defaultMessage: + 'Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance.', + }, + adsConsentManage: { + id: 'app.ads-consent.manage', + defaultMessage: 'Manage preferences', + }, + adsConsentReject: { + id: 'app.ads-consent.reject', + defaultMessage: 'Reject all', + }, + adsConsentAccept: { + id: 'app.ads-consent.accept', + defaultMessage: 'Accept all', + }, }) +function handleAdsConsentRequired(required) { + if (!required) { + if (adsConsentPopupId !== null) { + popupNotificationManager.removeNotification(adsConsentPopupId) + adsConsentPopupId = null + } + return + } + + if ( + adsConsentPopupId !== null && + popupNotificationManager.getNotifications().some((item) => item.id === adsConsentPopupId) + ) { + return + } + + const notification = addPopupNotification({ + title: formatMessage(messages.adsConsentTitle), + text: formatMessage(messages.adsConsentBody), + type: 'info', + hideIcon: true, + autoCloseMs: null, + dismissible: false, + buttons: [ + { + label: formatMessage(messages.adsConsentManage), + action: () => perform_ads_consent_action('manage').catch(handleError), + color: 'standard', + keepOpen: true, + }, + { + label: formatMessage(messages.adsConsentReject), + action: () => perform_ads_consent_action('reject').catch(handleError), + color: 'brand', + keepOpen: true, + }, + { + label: formatMessage(messages.adsConsentAccept), + action: () => perform_ads_consent_action('accept').catch(handleError), + color: 'brand', + keepOpen: true, + }, + ], + }) + + adsConsentPopupId = notification.id +} + async function setupApp() { const { native_decorations, @@ -403,7 +499,7 @@ async function setupApp() { addNotification({ title: 'Warning', text: e.message, - type: 'warn', + type: 'warning', }), ) @@ -703,17 +799,6 @@ async function logOut() { await fetchCredentials() } -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, -) - async function fetchIntercomToken() { const creds = await getCreds() if (!creds?.session) { @@ -740,13 +825,21 @@ async function fetchIntercomToken() { return await response.json() } -watch(showAd, () => { - if (!showAd.value) { - hide_ads_window(true) - } else { - init_ads_window(true) - } -}) +watch( + [showAd, adConsentAvailable], + async ([showAds, canManageConsent]) => { + if (showAds) { + await init_ads_window(true) + return + } + + await hide_ads_window(true) + if (canManageConsent) { + await init_ads_window() + } + }, + { immediate: true }, +) onMounted(() => { invoke('show_window') diff --git a/apps/app-frontend/src/components/ui/settings/PrivacySettings.vue b/apps/app-frontend/src/components/ui/settings/PrivacySettings.vue index 3482de94eb..b13cea773d 100644 --- a/apps/app-frontend/src/components/ui/settings/PrivacySettings.vue +++ b/apps/app-frontend/src/components/ui/settings/PrivacySettings.vue @@ -1,12 +1,44 @@