FirebaseUID gating pending purchases

This commit is contained in:
Benjamin Lu
2026-01-28 14:48:28 -08:00
parent 2db667bde4
commit 074ec623f0
4 changed files with 59 additions and 10 deletions

View File

@@ -10,7 +10,8 @@ const {
mockShowSubscriptionRequiredDialog, mockShowSubscriptionRequiredDialog,
mockGetAuthHeader, mockGetAuthHeader,
mockPushDataLayerEvent, mockPushDataLayerEvent,
mockTelemetry mockTelemetry,
mockUserId
} = vi.hoisted(() => ({ } = vi.hoisted(() => ({
mockIsLoggedIn: { value: false }, mockIsLoggedIn: { value: false },
mockReportError: vi.fn(), mockReportError: vi.fn(),
@@ -23,7 +24,8 @@ const {
mockTelemetry: { mockTelemetry: {
trackSubscription: vi.fn(), trackSubscription: vi.fn(),
trackMonthlySubscriptionCancelled: vi.fn() trackMonthlySubscriptionCancelled: vi.fn()
} },
mockUserId: { value: 'user-123' }
})) }))
let scope: ReturnType<typeof effectScope> | undefined let scope: ReturnType<typeof effectScope> | undefined
@@ -89,7 +91,8 @@ vi.mock('@/services/dialogService', () => ({
vi.mock('@/stores/firebaseAuthStore', () => ({ vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: vi.fn(() => ({ useFirebaseAuthStore: vi.fn(() => ({
getFirebaseAuthHeader: mockGetAuthHeader getFirebaseAuthHeader: mockGetAuthHeader,
userId: mockUserId.value
})), })),
FirebaseAuthStoreError: class extends Error {} FirebaseAuthStoreError: class extends Error {}
})) }))
@@ -112,6 +115,7 @@ describe('useSubscription', () => {
mockTelemetry.trackSubscription.mockReset() mockTelemetry.trackSubscription.mockReset()
mockTelemetry.trackMonthlySubscriptionCancelled.mockReset() mockTelemetry.trackMonthlySubscriptionCancelled.mockReset()
mockPushDataLayerEvent.mockReset() mockPushDataLayerEvent.mockReset()
mockUserId.value = 'user-123'
mockPushDataLayerEvent.mockImplementation((event) => { mockPushDataLayerEvent.mockImplementation((event) => {
const dataLayer = window.dataLayer ?? (window.dataLayer = []) const dataLayer = window.dataLayer ?? (window.dataLayer = [])
dataLayer.push(event) dataLayer.push(event)
@@ -249,6 +253,7 @@ describe('useSubscription', () => {
localStorage.setItem( localStorage.setItem(
'pending_subscription_purchase', 'pending_subscription_purchase',
JSON.stringify({ JSON.stringify({
firebaseUid: 'user-123',
tierKey: 'creator', tierKey: 'creator',
billingCycle: 'monthly', billingCycle: 'monthly',
timestamp: Date.now() timestamp: Date.now()
@@ -287,6 +292,38 @@ describe('useSubscription', () => {
expect(localStorage.getItem('pending_subscription_purchase')).toBeNull() 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 () => { it('should handle fetch errors gracefully', async () => {
vi.mocked(global.fetch).mockResolvedValue({ vi.mocked(global.fetch).mockResolvedValue({
ok: false, ok: false,

View File

@@ -45,7 +45,7 @@ function useSubscriptionInternal() {
const { reportError, accessBillingPortal } = useFirebaseAuthActions() const { reportError, accessBillingPortal } = useFirebaseAuthActions()
const { showSubscriptionRequiredDialog } = useDialogService() const { showSubscriptionRequiredDialog } = useDialogService()
const { getFirebaseAuthHeader } = useFirebaseAuthStore() const { getFirebaseAuthHeader, userId } = useFirebaseAuthStore()
const { wrapWithErrorHandlingAsync } = useErrorHandling() const { wrapWithErrorHandlingAsync } = useErrorHandling()
const { isLoggedIn } = useCurrentUser() const { isLoggedIn } = useCurrentUser()
@@ -109,7 +109,9 @@ function useSubscriptionInternal() {
): void { ): void {
if (!status?.is_active || !status.subscription_id) return if (!status?.is_active || !status.subscription_id) return
const pendingPurchase = getPendingSubscriptionPurchase() if (!userId) return
const pendingPurchase = getPendingSubscriptionPurchase(userId)
if (!pendingPurchase) return if (!pendingPurchase) return
const { tierKey, billingCycle } = pendingPurchase const { tierKey, billingCycle } = pendingPurchase

View File

@@ -36,7 +36,7 @@ export async function performSubscriptionCheckout(
): Promise<void> { ): Promise<void> {
if (!isCloud) return if (!isCloud) return
const { getFirebaseAuthHeader } = useFirebaseAuthStore() const { getFirebaseAuthHeader, userId } = useFirebaseAuthStore()
const authHeader = await getFirebaseAuthHeader() const authHeader = await getFirebaseAuthHeader()
if (!authHeader) { if (!authHeader) {
@@ -79,7 +79,9 @@ export async function performSubscriptionCheckout(
const data = await response.json() const data = await response.json()
if (data.checkout_url) { if (data.checkout_url) {
startSubscriptionPurchaseTracking(tierKey, currentBillingCycle) if (userId) {
startSubscriptionPurchaseTracking(tierKey, currentBillingCycle, userId)
}
if (openInNewTab) { if (openInNewTab) {
window.open(data.checkout_url, '_blank') window.open(data.checkout_url, '_blank')
} else { } else {

View File

@@ -2,6 +2,7 @@ import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricin
import type { BillingCycle } from './subscriptionTierRank' import type { BillingCycle } from './subscriptionTierRank'
type PendingSubscriptionPurchase = { type PendingSubscriptionPurchase = {
firebaseUid: string
tierKey: TierKey tierKey: TierKey
billingCycle: BillingCycle billingCycle: BillingCycle
timestamp: number timestamp: number
@@ -22,11 +23,14 @@ const safeRemove = (): void => {
export function startSubscriptionPurchaseTracking( export function startSubscriptionPurchaseTracking(
tierKey: TierKey, tierKey: TierKey,
billingCycle: BillingCycle billingCycle: BillingCycle,
firebaseUid: string
): void { ): void {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
if (!firebaseUid) return
try { try {
const payload: PendingSubscriptionPurchase = { const payload: PendingSubscriptionPurchase = {
firebaseUid,
tierKey, tierKey,
billingCycle, billingCycle,
timestamp: Date.now() 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 (typeof window === 'undefined') return null
if (!firebaseUid) return null
try { try {
const raw = localStorage.getItem(STORAGE_KEY) const raw = localStorage.getItem(STORAGE_KEY)
@@ -50,8 +57,9 @@ export function getPendingSubscriptionPurchase(): PendingSubscriptionPurchase |
return null return null
} }
const { tierKey, billingCycle, timestamp } = parsed const { firebaseUid: storedUid, tierKey, billingCycle, timestamp } = parsed
if ( if (
storedUid !== firebaseUid ||
!VALID_TIERS.includes(tierKey) || !VALID_TIERS.includes(tierKey) ||
!VALID_CYCLES.includes(billingCycle) || !VALID_CYCLES.includes(billingCycle) ||
typeof timestamp !== 'number' typeof timestamp !== 'number'