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,
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<typeof effectScope> | 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,

View File

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

View File

@@ -36,7 +36,7 @@ export async function performSubscriptionCheckout(
): Promise<void> {
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 {

View File

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