From 074ec623f07be0fba0766a8e5913f14922ff4757 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Wed, 28 Jan 2026 14:48:28 -0800 Subject: [PATCH] FirebaseUID gating pending purchases --- .../composables/useSubscription.test.ts | 43 +++++++++++++++++-- .../composables/useSubscription.ts | 6 ++- .../utils/subscriptionCheckoutUtil.ts | 6 ++- .../utils/subscriptionPurchaseTracker.ts | 14 ++++-- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/platform/cloud/subscription/composables/useSubscription.test.ts b/src/platform/cloud/subscription/composables/useSubscription.test.ts index db6533d11..a9a2487a5 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.test.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.test.ts @@ -10,7 +10,8 @@ const { mockShowSubscriptionRequiredDialog, mockGetAuthHeader, mockPushDataLayerEvent, - mockTelemetry + mockTelemetry, + mockUserId } = vi.hoisted(() => ({ mockIsLoggedIn: { value: false }, mockReportError: vi.fn(), @@ -23,7 +24,8 @@ const { mockTelemetry: { trackSubscription: vi.fn(), trackMonthlySubscriptionCancelled: vi.fn() - } + }, + mockUserId: { value: 'user-123' } })) let scope: ReturnType | undefined @@ -89,7 +91,8 @@ vi.mock('@/services/dialogService', () => ({ vi.mock('@/stores/firebaseAuthStore', () => ({ useFirebaseAuthStore: vi.fn(() => ({ - getFirebaseAuthHeader: mockGetAuthHeader + getFirebaseAuthHeader: mockGetAuthHeader, + userId: mockUserId.value })), FirebaseAuthStoreError: class extends Error {} })) @@ -112,6 +115,7 @@ describe('useSubscription', () => { mockTelemetry.trackSubscription.mockReset() mockTelemetry.trackMonthlySubscriptionCancelled.mockReset() mockPushDataLayerEvent.mockReset() + mockUserId.value = 'user-123' mockPushDataLayerEvent.mockImplementation((event) => { const dataLayer = window.dataLayer ?? (window.dataLayer = []) dataLayer.push(event) @@ -249,6 +253,7 @@ describe('useSubscription', () => { localStorage.setItem( 'pending_subscription_purchase', JSON.stringify({ + firebaseUid: 'user-123', tierKey: 'creator', billingCycle: 'monthly', timestamp: Date.now() @@ -287,6 +292,38 @@ describe('useSubscription', () => { expect(localStorage.getItem('pending_subscription_purchase')).toBeNull() }) + it('ignores pending purchase when user does not match', async () => { + window.dataLayer = [] + localStorage.setItem( + 'pending_subscription_purchase', + JSON.stringify({ + firebaseUid: 'user-123', + tierKey: 'creator', + billingCycle: 'monthly', + timestamp: Date.now() + }) + ) + + mockUserId.value = 'user-456' + vi.mocked(global.fetch).mockResolvedValue({ + ok: true, + json: async () => ({ + is_active: true, + subscription_id: 'sub_123', + subscription_tier: 'CREATOR', + subscription_duration: 'MONTHLY' + }) + } as Response) + + mockIsLoggedIn.value = true + const { fetchStatus } = useSubscriptionWithScope() + + await fetchStatus() + + expect(window.dataLayer).toHaveLength(0) + expect(localStorage.getItem('pending_subscription_purchase')).toBeNull() + }) + it('should handle fetch errors gracefully', async () => { vi.mocked(global.fetch).mockResolvedValue({ ok: false, diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index e3ab00781..0f7c883d7 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -45,7 +45,7 @@ function useSubscriptionInternal() { const { reportError, accessBillingPortal } = useFirebaseAuthActions() const { showSubscriptionRequiredDialog } = useDialogService() - const { getFirebaseAuthHeader } = useFirebaseAuthStore() + const { getFirebaseAuthHeader, userId } = useFirebaseAuthStore() const { wrapWithErrorHandlingAsync } = useErrorHandling() const { isLoggedIn } = useCurrentUser() @@ -109,7 +109,9 @@ function useSubscriptionInternal() { ): void { if (!status?.is_active || !status.subscription_id) return - const pendingPurchase = getPendingSubscriptionPurchase() + if (!userId) return + + const pendingPurchase = getPendingSubscriptionPurchase(userId) if (!pendingPurchase) return const { tierKey, billingCycle } = pendingPurchase diff --git a/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts b/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts index 8be996ebc..7c9c0ec19 100644 --- a/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts +++ b/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts @@ -36,7 +36,7 @@ export async function performSubscriptionCheckout( ): Promise { if (!isCloud) return - const { getFirebaseAuthHeader } = useFirebaseAuthStore() + const { getFirebaseAuthHeader, userId } = useFirebaseAuthStore() const authHeader = await getFirebaseAuthHeader() if (!authHeader) { @@ -79,7 +79,9 @@ export async function performSubscriptionCheckout( const data = await response.json() if (data.checkout_url) { - startSubscriptionPurchaseTracking(tierKey, currentBillingCycle) + if (userId) { + startSubscriptionPurchaseTracking(tierKey, currentBillingCycle, userId) + } if (openInNewTab) { window.open(data.checkout_url, '_blank') } else { diff --git a/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts b/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts index 41bbb3484..9f962fff6 100644 --- a/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts +++ b/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts @@ -2,6 +2,7 @@ import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricin import type { BillingCycle } from './subscriptionTierRank' type PendingSubscriptionPurchase = { + firebaseUid: string tierKey: TierKey billingCycle: BillingCycle timestamp: number @@ -22,11 +23,14 @@ const safeRemove = (): void => { export function startSubscriptionPurchaseTracking( tierKey: TierKey, - billingCycle: BillingCycle + billingCycle: BillingCycle, + firebaseUid: string ): void { if (typeof window === 'undefined') return + if (!firebaseUid) return try { const payload: PendingSubscriptionPurchase = { + firebaseUid, tierKey, billingCycle, timestamp: Date.now() @@ -37,8 +41,11 @@ export function startSubscriptionPurchaseTracking( } } -export function getPendingSubscriptionPurchase(): PendingSubscriptionPurchase | null { +export function getPendingSubscriptionPurchase( + firebaseUid: string +): PendingSubscriptionPurchase | null { if (typeof window === 'undefined') return null + if (!firebaseUid) return null try { const raw = localStorage.getItem(STORAGE_KEY) @@ -50,8 +57,9 @@ export function getPendingSubscriptionPurchase(): PendingSubscriptionPurchase | return null } - const { tierKey, billingCycle, timestamp } = parsed + const { firebaseUid: storedUid, tierKey, billingCycle, timestamp } = parsed if ( + storedUid !== firebaseUid || !VALID_TIERS.includes(tierKey) || !VALID_CYCLES.includes(billingCycle) || typeof timestamp !== 'number'