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.
This commit is contained in:
bymyself
2025-12-11 06:43:26 -08:00
parent d83c3122ab
commit 9cfa034293
3 changed files with 128 additions and 7 deletions

View File

@@ -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)

View File

@@ -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<ReleaseNote[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
const customNotifications = ref<CustomNotification[]>([])
const currentCustomNotification = ref<CustomNotification | null>(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<void> {
await fetchReleases()
@@ -300,6 +386,13 @@ export const useReleaseStore = defineStore('release', () => {
handleShowChangelog,
handleWhatsNewSeen,
fetchReleases,
initialize
initialize,
// Custom notifications
customNotifications,
currentCustomNotification,
addCustomNotification,
dismissCustomNotification,
showPartnerNodePricingNotification
}
})

View File

@@ -26,8 +26,9 @@
class="modal-footer flex justify-between items-center gap-4 px-4 pb-4"
>
<a
v-if="currentLearnMoreUrl"
class="learn-more-link flex items-center gap-2 text-sm font-normal py-1"
:href="changelogUrl"
:href="currentLearnMoreUrl"
target="_blank"
rel="noopener noreferrer"
@click="closePopup"
@@ -81,6 +82,17 @@ const latestRelease = computed<ReleaseNote | null>(() => {
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(`<p>${t('whatsNewPopup.noReleaseNotes')}</p>`)
}
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, '<br>')
const fallbackContent = content.replace(/\n/g, '<br>')
return fallbackContent.trim()
? DOMPurify.sanitize(fallbackContent)
: DOMPurify.sanitize(`<p>${t('whatsNewPopup.noReleaseNotes')}</p>`)
@@ -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()