Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -1971,6 +1971,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 @@ -1995,7 +1996,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 @@ -2009,7 +2015,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 @@ -2200,10 +2207,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 @@ -2237,6 +2249,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 @@ -2264,6 +2291,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]"
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