From 9cfa034293b2758454d8d7d91b8c9ca663dba3c3 Mon Sep 17 00:00:00 2001 From: bymyself Date: Thu, 11 Dec 2025 06:43:26 -0800 Subject: [PATCH] feat: Add one-time notification for unified credit system Shows a popup notification to users with credits explaining the transition to Comfy Credits. Triggers on balance fetch, shows once per user, and auto-expires March 2026. --- .../auth/useFirebaseAuthActions.ts | 9 ++ src/platform/updates/common/releaseStore.ts | 95 ++++++++++++++++++- .../updates/components/WhatsNewPopup.vue | 31 ++++-- 3 files changed, 128 insertions(+), 7 deletions(-) diff --git a/src/composables/auth/useFirebaseAuthActions.ts b/src/composables/auth/useFirebaseAuthActions.ts index eed7eb021..dbb4fc8de 100644 --- a/src/composables/auth/useFirebaseAuthActions.ts +++ b/src/composables/auth/useFirebaseAuthActions.ts @@ -116,6 +116,15 @@ export const useFirebaseAuthActions = () => { const fetchBalance = wrapWithErrorHandlingAsync(async () => { const result = await authStore.fetchBalance() + + // Show partner node pricing notification if user has credits + if (result && result.amount_micros > 0) { + const { useReleaseStore } = + await import('@/platform/updates/common/releaseStore') + const releaseStore = useReleaseStore() + releaseStore.showPartnerNodePricingNotification() + } + // Top-up completion tracking happens in UsageLogsTable when events are fetched return result }, reportError) diff --git a/src/platform/updates/common/releaseStore.ts b/src/platform/updates/common/releaseStore.ts index 80c2bc05d..d2b559d1a 100644 --- a/src/platform/updates/common/releaseStore.ts +++ b/src/platform/updates/common/releaseStore.ts @@ -12,12 +12,22 @@ import { stringToLocale } from '@/utils/formatUtil' import { useReleaseService } from './releaseService' import type { ReleaseNote } from './releaseService' +interface CustomNotification { + id: string + title: string + content: string + learnMoreUrl?: string + storageKey?: string +} + // Store for managing release notes export const useReleaseStore = defineStore('release', () => { // State const releases = ref([]) const isLoading = ref(false) const error = ref(null) + const customNotifications = ref([]) + const currentCustomNotification = ref(null) // Services const releaseService = useReleaseService() @@ -172,6 +182,11 @@ export const useReleaseStore = defineStore('release', () => { }) const shouldShowPopup = computed(() => { + // Check for custom notification first + if (currentCustomNotification.value) { + return true + } + if (!isElectron() && !isCloud) { return false } @@ -281,6 +296,77 @@ export const useReleaseStore = defineStore('release', () => { } } + // Custom notification management + function addCustomNotification(notification: CustomNotification): void { + // Check if already dismissed via storage key + if ( + notification.storageKey && + localStorage.getItem(notification.storageKey) + ) { + return + } + + // Remove existing notification with same ID + customNotifications.value = customNotifications.value.filter( + (n) => n.id !== notification.id + ) + + // Add new notification and set as current + customNotifications.value.push(notification) + currentCustomNotification.value = notification + } + + // Public method for showing one-time notifications + function showPartnerNodePricingNotification(): void { + const STORAGE_KEY = 'comfy.notifications.partner-node-pricing-shown' + const EXPIRY_DATE = new Date('2026-03-01') + + // Skip if past expiry or already shown + if (new Date() > EXPIRY_DATE || localStorage.getItem(STORAGE_KEY)) { + return + } + + addCustomNotification({ + id: 'partner-node-pricing-change', + title: 'One credit system for all', + content: `# One credit system for all + +We've unified payments across Comfy. Everything now runs on Comfy Credits: + +- Partner Nodes (formerly API nodes) +- Cloud workflows + +Your existing Partner node balance has been converted into credits. + +Learn more about this change [here](https://blog.comfy.org/p/comfy-cloud-update-unified-credit-system)`, + learnMoreUrl: '', + storageKey: STORAGE_KEY + }) + } + + function dismissCustomNotification(notificationId: string): void { + const notification = customNotifications.value.find( + (n) => n.id === notificationId + ) + + if (notification) { + // Mark as dismissed in storage if storage key provided + if (notification.storageKey) { + localStorage.setItem(notification.storageKey, 'true') + } + + // Remove from custom notifications + customNotifications.value = customNotifications.value.filter( + (n) => n.id !== notificationId + ) + + // Clear current if it was the dismissed one + if (currentCustomNotification.value?.id === notificationId) { + currentCustomNotification.value = customNotifications.value[0] || null + } + } + } + // Initialize store async function initialize(): Promise { await fetchReleases() @@ -300,6 +386,13 @@ export const useReleaseStore = defineStore('release', () => { handleShowChangelog, handleWhatsNewSeen, fetchReleases, - initialize + initialize, + + // Custom notifications + customNotifications, + currentCustomNotification, + addCustomNotification, + dismissCustomNotification, + showPartnerNodePricingNotification } }) diff --git a/src/platform/updates/components/WhatsNewPopup.vue b/src/platform/updates/components/WhatsNewPopup.vue index 90c194031..60f46c2d6 100644 --- a/src/platform/updates/components/WhatsNewPopup.vue +++ b/src/platform/updates/components/WhatsNewPopup.vue @@ -26,8 +26,9 @@ class="modal-footer flex justify-between items-center gap-4 px-4 pb-4" > (() => { return releaseStore.recentRelease }) +// Get current content (custom notification or release) +const currentNotification = computed( + () => releaseStore.currentCustomNotification +) +const currentLearnMoreUrl = computed(() => { + if (currentNotification.value?.learnMoreUrl) { + return currentNotification.value.learnMoreUrl + } + return changelogUrl.value +}) + // Show popup when on latest version and not dismissed const shouldShow = computed( () => releaseStore.shouldShowPopup && !isDismissed.value @@ -97,12 +109,16 @@ const changelogUrl = computed(() => { }) const formattedContent = computed(() => { - if (!latestRelease.value?.content) { + // Use custom notification content first + const content = + currentNotification.value?.content || latestRelease.value?.content + + if (!content) { return DOMPurify.sanitize(`

${t('whatsNewPopup.noReleaseNotes')}

`) } try { - const markdown = latestRelease.value.content + const markdown = content // Check if content is meaningful (not just whitespace) const trimmedContent = markdown.trim() @@ -127,7 +143,7 @@ const formattedContent = computed(() => { } catch (error) { console.error('Error parsing markdown:', error) // Fallback to plain text with line breaks - sanitize the HTML we create - const fallbackContent = latestRelease.value.content.replace(/\n/g, '
') + const fallbackContent = content.replace(/\n/g, '
') return fallbackContent.trim() ? DOMPurify.sanitize(fallbackContent) : DOMPurify.sanitize(`

${t('whatsNewPopup.noReleaseNotes')}

`) @@ -144,8 +160,11 @@ const hide = () => { } const closePopup = async () => { - // Mark "what's new" seen when popup is closed - if (latestRelease.value) { + // Handle custom notification dismissal first + if (currentNotification.value) { + releaseStore.dismissCustomNotification(currentNotification.value.id) + } else if (latestRelease.value) { + // Mark "what's new" seen when popup is closed await releaseStore.handleWhatsNewSeen(latestRelease.value.version) } hide()