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

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

View File

@@ -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
@@ -238,6 +240,7 @@ function useBillingContextInternal(): BillingContext {
isLoading,
error,
isActiveSubscription,
isFreeTier,
getMaxSeats,
initialize,

View File

@@ -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) {
@@ -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 =
@@ -173,6 +178,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
isLoading,
error,
isActiveSubscription,
isFreeTier,
// Actions
initialize,