From 19e89dfcd56eb2cf253f1bf8f5e4a24743b48389 Mon Sep 17 00:00:00 2001 From: Hariom Balhara <1780212+hariombalhara@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:18:35 +0530 Subject: [PATCH] fix: clear oversized images from onboarding store during migration (#27560) * fix: clear oversized images from onboarding store during migration This fixes the 429 errors caused by large images stored in IndexedDB before PR #27285 added cropping functionality. Changes: - Add version 1 to zustand persist config with migration function - Migration clears images exceeding thresholds: - Logos/avatars: 500KB (properly cropped are ~50-150KB) - Banners: 1.5MB (properly cropped are ~200-500KB) - Add comprehensive tests for migration logic When users refresh the page, the migration runs automatically and clears any oversized images, requiring them to re-upload using the new cropping flow. Co-Authored-By: hariom@cal.com * fixes * chore: remove migration test file Co-Authored-By: hariom@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../onboarding/store/onboarding-store.ts | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/apps/web/modules/onboarding/store/onboarding-store.ts b/apps/web/modules/onboarding/store/onboarding-store.ts index c6e045cca4..a45ff0f618 100644 --- a/apps/web/modules/onboarding/store/onboarding-store.ts +++ b/apps/web/modules/onboarding/store/onboarding-store.ts @@ -1,8 +1,23 @@ +import type { PersistOptions } from "zustand/middleware"; import { create } from "zustand"; import { persist } from "zustand/middleware"; import { onboardingIndexedDBStorage } from "./onboarding-storage"; +// Thresholds for detecting oversized images (in characters of base64 string) +// Images larger than these were uploaded before the cropping fix and should be cleared +const LOGO_SIZE_THRESHOLD = 500_000; // ~500KB - properly cropped logos are ~50-150KB +const BANNER_SIZE_THRESHOLD = 1_500_000; // ~1.5MB - properly cropped banners are ~200-500KB + +/** + * Checks if a base64 image string exceeds the given threshold. + * Returns true if the image should be cleared (is oversized). + */ +function isOversizedImage(imageData: string | null, threshold: number): boolean { + if (!imageData) return false; + return imageData.length > threshold; +} + export type PlanType = "personal" | "team" | "organization"; export type InviteRole = "MEMBER" | "ADMIN"; @@ -189,7 +204,42 @@ export const useOnboardingStore = create()( { name: "cal-onboarding-storage", // Storage key storage: onboardingIndexedDBStorage, // Use IndexedDB instead of localStorage for larger capacity - // Optional: Only persist certain fields + // Version 0: Had issue with Image sizes being too large. + // Version 1: We fixed that and also migrated to version-1 to facilitate one-time cleanup of all oversized images. + version: 1, + migrate: (persistedState, version) => { + if (version >= 1) { + return persistedState as OnboardingState; + } + const state = persistedState as OnboardingState; + + // Migration from version 0 (no version) to version 1: + // Clear oversized images that were uploaded before the cropping fix (PR #27285) + // These large images cause 429 errors when sent to the server + if (state.organizationBrand) { + if (isOversizedImage(state.organizationBrand.logo, LOGO_SIZE_THRESHOLD)) { + state.organizationBrand.logo = null; + } + if (isOversizedImage(state.organizationBrand.banner, BANNER_SIZE_THRESHOLD)) { + state.organizationBrand.banner = null; + } + } + + if (state.teamBrand) { + if (isOversizedImage(state.teamBrand.logo, LOGO_SIZE_THRESHOLD)) { + state.teamBrand.logo = null; + } + } + + if (state.personalDetails) { + if (isOversizedImage(state.personalDetails.avatar, LOGO_SIZE_THRESHOLD)) { + state.personalDetails.avatar = null; + } + } + + return state; + }, + // Only persist certain fields partialize: (state) => ({ selectedPlan: state.selectedPlan, organizationDetails: state.organizationDetails, @@ -204,6 +254,6 @@ export const useOnboardingStore = create()( teamId: state.teamId, personalDetails: state.personalDetails, }), - } + } as PersistOptions> ) );