feat: add Free subscription tier support (#8864)

## Summary

Add frontend support for a Free subscription tier — login/signup page
restructuring, telemetry instrumentation, and tier-aware billing gating.

## Changes

- **What**: 
- Restructure login/signup pages: OAuth buttons promoted as primary
sign-in method, email login available via progressive disclosure
- Add Free tier badge on Google sign-up button with dynamic credit count
from remote config
- Add `FREE` subscription tier to type system (tier pricing, tier rank,
registry types)
  - Add `isFreeTier` computed to `useSubscription()`
- Disable credit top-up for Free tier users (dialogService,
purchaseCredits, popover CTA)
- Show subscription/upgrade dialog instead of top-up dialog when Free
tier user hits out-of-credits
- Add funnel telemetry: `trackLoginOpened`, enrich `trackSignupOpened`
with `free_tier_badge_shown`, track email toggle clicks

## Review Focus

- Tier gating logic: Free tier users should see "Upgrade" instead of
"Add Credits" and never reach the top-up flow
- Telemetry event design for Mixpanel funnel analysis
- Progressive disclosure UX on login/signup pages

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8864-feat-add-Free-subscription-tier-support-3076d73d36508133b84ec5f0a67ccb03)
by [Unito](https://www.unito.io)
This commit is contained in:
Hunter
2026-02-24 23:28:51 -05:00
committed by GitHub
parent aee207f16c
commit 8c3738fb77
39 changed files with 720 additions and 221 deletions

View File

@@ -17,6 +17,7 @@ import type {
} from '@/stores/dialogStore'
import type { ComponentAttrs } from 'vue-component-type-helpers'
import type { SubscriptionDialogReason } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
// Lazy loaders for dialogs - components are loaded on first use
const lazyApiNodesSignInContent = () =>
@@ -274,8 +275,15 @@ export const useDialogService = () => {
async function showTopUpCreditsDialog(options?: {
isInsufficientCredits?: boolean
}) {
const { isActiveSubscription, type } = useBillingContext()
if (!isActiveSubscription.value) return
const { isActiveSubscription, isFreeTier, type } = useBillingContext()
if (!isActiveSubscription.value || isFreeTier.value) {
await showSubscriptionRequiredDialog({
reason: options?.isInsufficientCredits
? 'out_of_credits'
: 'top_up_blocked'
})
return
}
const component =
type.value === 'workspace'
@@ -392,7 +400,9 @@ export const useDialogService = () => {
})
}
async function showSubscriptionRequiredDialog() {
async function showSubscriptionRequiredDialog(options?: {
reason?: SubscriptionDialogReason
}) {
if (!isCloud || !window.__CONFIG__?.subscription_required) {
return
}
@@ -400,7 +410,7 @@ export const useDialogService = () => {
const { useSubscriptionDialog } =
await import('@/platform/cloud/subscription/composables/useSubscriptionDialog')
const { show } = useSubscriptionDialog()
show()
show(options)
}
// Workspace dialogs - dynamically imported to avoid bundling when feature flag is off