Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
612c94d
feat: free tier signup incentives, telemetry, and top-up gating
huntcsg Feb 14, 2026
b4e1a07
fix: route free tier insufficient credits to subscribe CTA
huntcsg Feb 20, 2026
7917ce9
chore: satisfy knip in subscription dialog composable
huntcsg Feb 20, 2026
b039ea7
feat: show free tier explanation modal before pricing table
huntcsg Feb 21, 2026
41a87bb
wip
huntcsg Feb 21, 2026
9125fe1
refactor: idiomatic free tier support
huntcsg Feb 21, 2026
fe2d9f8
fix: free tier refactoring cleanup and test fixes
huntcsg Feb 21, 2026
1a925f1
fix: remove redundant free-tier guard from purchaseCredits
huntcsg Feb 21, 2026
14f3d42
fix: polish login/signup typography and consistency
huntcsg Feb 21, 2026
c2f07f9
fix: route free tier users through initial subscription checkout flow
huntcsg Feb 21, 2026
ca74601
fix: add isFreeTier to useSubscription mock in PricingTable tests
huntcsg Feb 21, 2026
fe30b9c
fix: clean up free-tier telemetry and enrich subscription tracking
huntcsg Feb 22, 2026
b8398f0
fix: show context-appropriate benefit copy for free-tier vs unsubscri…
huntcsg Feb 22, 2026
8e61c25
fix: use reactive localStorage signals for free-tier badge visibility
huntcsg Feb 22, 2026
6aebe14
fix: consolidate freeTierCredits to use getTierCredits('free')
huntcsg Feb 22, 2026
a9aab39
feat: simplify login page per design feedback
huntcsg Feb 23, 2026
eb0a411
feat: update login page free tier copy
huntcsg Feb 23, 2026
9975b44
fix: update free tier copy to 'Google', left-align signup teaser
huntcsg Feb 24, 2026
aa2163f
fix: show 'Add Credits' for free tier users with upsell on click
huntcsg Feb 24, 2026
d202d25
fix: remove dead trackLoginOpened and empty AuthPageOpenedMetadata
huntcsg Feb 24, 2026
c044392
fix: source isFreeTier from billing context and fix balance race on f…
huntcsg Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/registry-types/src/comfyRegistryTypes.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions src/components/topbar/CurrentUserPopoverLegacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,13 @@ vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
}))

// Mock the useSubscriptionDialog composable
const mockSubscriptionDialogShow = vi.fn()
const mockShowPricingTable = vi.fn()
vi.mock(
'@/platform/cloud/subscription/composables/useSubscriptionDialog',
() => ({
useSubscriptionDialog: vi.fn(() => ({
show: mockSubscriptionDialogShow,
show: vi.fn(),
showPricingTable: mockShowPricingTable,
hide: vi.fn()
}))
})
Expand Down Expand Up @@ -318,8 +319,8 @@ describe('CurrentUserPopoverLegacy', () => {

await plansPricingItem.trigger('click')

// Verify subscription dialog show was called
expect(mockSubscriptionDialogShow).toHaveBeenCalled()
// Verify showPricingTable was called
expect(mockShowPricingTable).toHaveBeenCalled()

// Verify close event was emitted
expect(wrapper.emitted('close')).toBeTruthy()
Expand Down
7 changes: 5 additions & 2 deletions src/components/topbar/CurrentUserPopoverLegacy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,10 @@ const formattedBalance = computed(() => {
const canUpgrade = computed(() => {
const tier = subscriptionTier.value
return (
tier === 'FOUNDERS_EDITION' || tier === 'STANDARD' || tier === 'CREATOR'
tier === 'FREE' ||
tier === 'FOUNDERS_EDITION' ||
tier === 'STANDARD' ||
tier === 'CREATOR'
)
})

Expand All @@ -205,7 +208,7 @@ const handleOpenUserSettings = () => {
}

const handleOpenPlansAndPricing = () => {
subscriptionDialog.show()
subscriptionDialog.showPricingTable()
emit('close')
}

Expand Down
5 changes: 5 additions & 0 deletions src/composables/billing/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ export interface BillingState {
* Equivalent to `subscription.value?.isActive ?? false`
*/
isActiveSubscription: ComputedRef<boolean>
/**
* Whether the current billing context has a FREE tier subscription.
* Workspace-aware: reflects the active workspace's tier, not the user's personal tier.
*/
isFreeTier: ComputedRef<boolean>
}

export interface BillingContext extends BillingState, BillingActions {
Expand Down
3 changes: 3 additions & 0 deletions src/composables/billing/useBillingContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ function useBillingContextInternal(): BillingContext {
toValue(activeContext.value.isActiveSubscription)
)

const isFreeTier = computed(() => subscription.value?.tier === 'FREE')

function getMaxSeats(tierKey: TierKey): number {
if (type.value === 'legacy') return 1

Expand Down Expand Up @@ -238,6 +240,7 @@ function useBillingContextInternal(): BillingContext {
isLoading,
error,
isActiveSubscription,
isFreeTier,
getMaxSeats,

initialize,
Expand Down
6 changes: 6 additions & 0 deletions src/composables/billing/useLegacyBilling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
const error = ref<string | null>(null)

const isActiveSubscription = computed(() => legacyIsActiveSubscription.value)
const isFreeTier = computed(() => subscriptionTier.value === 'FREE')

const subscription = computed<SubscriptionInfo | null>(() => {
if (!legacyIsActiveSubscription.value && !subscriptionTier.value) {
Expand Down Expand Up @@ -85,6 +86,10 @@ export function useLegacyBilling(): BillingState & BillingActions {
error.value = null
try {
await Promise.all([fetchStatus(), fetchBalance()])
// Re-fetch balance if free tier credits were just lazily granted
if (isFreeTier.value && balance.value?.amountMicros === 0) {
await fetchBalance()
}
isInitialized.value = true
} catch (err) {
error.value =
Expand Down Expand Up @@ -173,6 +178,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
isLoading,
error,
isActiveSubscription,
isFreeTier,

// Actions
initialize,
Expand Down
34 changes: 31 additions & 3 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1987,6 +1987,7 @@
"newUser": "New here?",
"userAvatar": "User Avatar",
"signUp": "Sign up",
"signUpFreeTierPromo": "New here? {signUp} with Google to get {credits} free credits every month.",
"emailLabel": "Email",
"emailPlaceholder": "Enter your email",
"passwordLabel": "Password",
Expand All @@ -2011,7 +2012,12 @@
"failed": "Login failed",
"insecureContextWarning": "This connection is insecure (HTTP) - your credentials may be intercepted by attackers if you proceed to login.",
"questionsContactPrefix": "Questions? Contact us at",
"noAssociatedUser": "There is no Comfy user associated with the provided API key"
"noAssociatedUser": "There is no Comfy user associated with the provided API key",
"useEmailInstead": "Use email instead",
"freeTierBadge": "Eligible for Free Tier",
"freeTierDescription": "Sign up with Google to get {credits} free credits every month. No card needed.",
"freeTierDescriptionGeneric": "Sign up with Google to get free credits every month. No card needed.",
"backToSocialLogin": "Sign up with Google or Github instead"
},
"signup": {
"title": "Create an account",
Expand All @@ -2025,7 +2031,8 @@
"signUpWithGoogle": "Sign up with Google",
"signUpWithGithub": "Sign up with Github",
"regionRestrictionChina": "In accordance with local regulatory requirements, our services are temporarily unavailable to users located in China.",
"personalDataConsentLabel": "I agree to the processing of my personal data."
"personalDataConsentLabel": "I agree to the processing of my personal data.",
"emailNotEligibleForFreeTier": "Email sign-up is not eligible for Free Tier."
},
"signOut": {
"signOut": "Log Out",
Expand Down Expand Up @@ -2216,10 +2223,15 @@
"invoiceHistory": "Invoice history",
"benefits": {
"benefit1": "Monthly credits for Partner Nodes — top up when needed",
"benefit2": "Up to 30 min runtime per job"
"benefit1FreeTier": "More monthly credits, top up anytime",
"benefit2": "Up to 1 hour runtime per job on Pro",
"benefit3": "Bring your own models (Creator & Pro)"
},
"yearlyDiscount": "20% DISCOUNT",
"tiers": {
"free": {
"name": "Free"
},
"founder": {
"name": "Founder's Edition"
},
Expand Down Expand Up @@ -2253,6 +2265,21 @@
"haveQuestions": "Have questions or wondering about enterprise?",
"contactUs": "Contact us",
"viewEnterprise": "View enterprise",
"freeTier": {
"title": "You're on the Free plan",
"description": "Your free plan includes {credits} credits each month to try Comfy Cloud.",
"descriptionGeneric": "Your free plan includes a monthly credit allowance to try Comfy Cloud.",
"nextRefresh": "Your credits refresh on {date}.",
"subscribeCta": "Subscribe for more",
"outOfCredits": {
"title": "You're out of free credits",
"subtitle": "Subscribe to unlock top-ups and more"
},
"topUpBlocked": {
"title": "Unlock top-ups and more"
},
"upgradeCta": "View plans"
},
"partnerNodesCredits": "Partner nodes pricing",
"plansAndPricing": "Plans & pricing",
"managePlan": "Manage plan",
Expand Down Expand Up @@ -2280,6 +2307,7 @@
"upgradeTo": "Upgrade to {plan}",
"changeTo": "Change to {plan}",
"maxDuration": {
"free": "30 min",
"standard": "30 min",
"creator": "30 min",
"pro": "1 hr",
Expand Down
109 changes: 74 additions & 35 deletions src/platform/cloud/onboarding/CloudLoginView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,30 @@
<div class="flex h-full items-center justify-center p-8">
<div class="max-w-screen p-2 lg:w-96">
<!-- Header -->
<div class="mt-6 mb-8 flex flex-col gap-4">
<div class="mb-8 flex flex-col gap-4">
<h1 class="my-0 text-xl leading-normal font-medium">
{{ t('auth.login.title') }}
</h1>
<p class="my-0 text-base">
<span class="text-muted">{{ t('auth.login.newUser') }}</span>
<i18n-t
v-if="isFreeTierEnabled"
keypath="auth.login.signUpFreeTierPromo"
tag="p"
class="my-0 text-base text-muted"
:plural="freeTierCredits ?? undefined"
>
<template #signUp>
<span
class="cursor-pointer text-blue-500"
@click="navigateToSignup"
>{{ t('auth.login.signUp') }}</span
>
</template>
<template #credits>{{ freeTierCredits }}</template>
</i18n-t>
<p v-else class="my-0 text-base text-muted">
{{ t('auth.login.newUser') }}
<span
class="ml-1 cursor-pointer text-blue-500"
class="cursor-pointer text-blue-500"
@click="navigateToSignup"
>{{ t('auth.login.signUp') }}</span
>
Expand All @@ -20,36 +36,49 @@
{{ t('auth.login.insecureContextWarning') }}
</Message>

<!-- Form -->
<CloudSignInForm :auth-error="authError" @submit="signInWithEmail" />

<!-- Divider -->
<Divider align="center" layout="horizontal" class="my-8">
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
</Divider>

<!-- Social Login Buttons -->
<div class="flex flex-col gap-6">
<Button
type="button"
class="h-10 bg-[#2d2e32]"
variant="secondary"
@click="signInWithGoogle"
>
<i class="pi pi-google mr-2"></i>
{{ t('auth.login.loginWithGoogle') }}
</Button>

<Button
type="button"
class="h-10 bg-[#2d2e32]"
variant="secondary"
@click="signInWithGithub"
>
<i class="pi pi-github mr-2"></i>
{{ t('auth.login.loginWithGithub') }}
</Button>
</div>
<template v-if="!showEmailForm">
<!-- OAuth Buttons (primary) -->
<div class="flex flex-col gap-4">
<Button type="button" class="h-10 w-full" @click="signInWithGoogle">
<i class="pi pi-google mr-2"></i>
{{ t('auth.login.loginWithGoogle') }}
</Button>

<Button
type="button"
class="h-10 bg-[#2d2e32]"
Comment thread
christian-byrne marked this conversation as resolved.
variant="secondary"
@click="signInWithGithub"
>
<i class="pi pi-github mr-2"></i>
{{ t('auth.login.loginWithGithub') }}
</Button>
</div>

<div class="mt-6 text-center">
<Button
variant="muted-textonly"
class="text-sm underline"
@click="switchToEmailForm"
>
{{ t('auth.login.useEmailInstead') }}
</Button>
</div>
</template>

<template v-else>
<CloudSignInForm :auth-error="authError" @submit="signInWithEmail" />

<div class="mt-4 text-center">
<Button
variant="muted-textonly"
class="text-sm underline"
@click="switchToSocialLogin"
>
{{ t('auth.login.backToSocialLogin') }}
</Button>
</div>
</template>

<!-- Terms & Contact -->
<p class="mt-5 text-sm text-gray-600">
Expand All @@ -75,7 +104,6 @@
</template>

<script setup lang="ts">
import Divider from 'primevue/divider'
import Message from 'primevue/message'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
Expand All @@ -84,6 +112,7 @@ import { useRoute, useRouter } from 'vue-router'
import Button from '@/components/ui/button/Button.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import CloudSignInForm from '@/platform/cloud/onboarding/components/CloudSignInForm.vue'
import { useFreeTierOnboarding } from '@/platform/cloud/onboarding/composables/useFreeTierOnboarding'
import { getSafePreviousFullPath } from '@/platform/cloud/onboarding/utils/previousFullPath'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { SignInData } from '@/schemas/signInSchema'
Expand All @@ -95,6 +124,16 @@ const authActions = useFirebaseAuthActions()
const isSecureContext = globalThis.isSecureContext
const authError = ref('')
const toastStore = useToastStore()
const showEmailForm = ref(false)
const { isFreeTierEnabled, freeTierCredits } = useFreeTierOnboarding()

function switchToEmailForm() {
showEmailForm.value = true
}

function switchToSocialLogin() {
showEmailForm.value = false
}

const navigateToSignup = async () => {
await router.push({ name: 'cloud-signup', query: route.query })
Expand Down
Loading
Loading