fix: Use index DB instead of local storage for onboarding (#24798)
## What does this PR do? Implements an IndexedDB storage adapter for the onboarding store to replace localStorage. This allows storing much larger data (up to 50% of available disk space) compared to localStorage's limited quota, making it suitable for storing base64-encoded images like logos and banners during onboarding. The implementation includes a fallback to localStorage if IndexedDB is unavailable. Sometimes when you uploaded a large image or banner you get this error. Even if its within the bounds of 10mb (which is what we allow)  ## Visual Demo (For contributors especially) N/A - This is an infrastructure change with no visual component. ## Mandatory Tasks (DO NOT REMOVE) - [x] I have self-reviewed the code. - [x] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - [x] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? - Test the onboarding flow and verify that data persists between page refreshes - Test uploading large images during onboarding to ensure they're properly stored - Test in browsers with IndexedDB disabled to verify the localStorage fallback works correctly - Verify that the onboarding state is properly maintained when navigating through the onboarding steps ## Checklist - I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) - My code follows the style guidelines of this project - I have commented my code, particularly in hard-to-understand areas - I have checked if my changes generate no new warnings
This commit is contained in:
1 parent
b8098ed3ab
commit
69e4b12f52
2 files changed
+136
-1
No files matched your search
@@ -0,0 +1,132 @@
|
||||
import { createJSONStorage, type StateStorage } from "zustand/middleware";
|
||||
|
||||
/**
|
||||
* IndexedDB storage adapter for onboarding store.
|
||||
* Supports much larger storage limits than localStorage (typically 50% of available disk space),
|
||||
* making it suitable for storing large base64-encoded images (logos, banners, etc.).
|
||||
* Falls back to localStorage if IndexedDB is not available.
|
||||
*/
|
||||
const DB_NAME = "cal-onboarding-idb";
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = "keyval";
|
||||
|
||||
// Cache database connection to avoid opening/closing repeatedly
|
||||
let dbPromise: Promise<IDBDatabase> | null = null;
|
||||
|
||||
function getDB(): Promise<IDBDatabase> {
|
||||
if (dbPromise) {
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
dbPromise = new Promise((resolve, reject) => {
|
||||
if (typeof window === "undefined" || !window.indexedDB) {
|
||||
reject(new Error("IndexedDB is not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = window.indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => {
|
||||
dbPromise = null;
|
||||
reject(request.error);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME);
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
// Reset promise on close so we can reconnect if needed
|
||||
db.onclose = () => {
|
||||
dbPromise = null;
|
||||
};
|
||||
resolve(db);
|
||||
};
|
||||
});
|
||||
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
async function getItem(key: string): Promise<string | null> {
|
||||
try {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], "readonly");
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.get(key);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result ?? null);
|
||||
});
|
||||
} catch {
|
||||
// Fallback to localStorage if IndexedDB fails
|
||||
try {
|
||||
return window.localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setItem(key: string, value: string): Promise<void> {
|
||||
try {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], "readwrite");
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.put(value, key);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
} catch {
|
||||
// Fallback to localStorage if IndexedDB fails (will throw if quota exceeded)
|
||||
try {
|
||||
window.localStorage.setItem(key, value);
|
||||
} catch {
|
||||
// Storage quota exceeded even in fallback - not much we can do
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function removeItem(key: string): Promise<void> {
|
||||
try {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], "readwrite");
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.delete(key);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
} catch {
|
||||
// Fallback to localStorage if IndexedDB fails
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
} catch {
|
||||
// Ignore errors on remove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IndexedDB storage adapter that implements StateStorage interface.
|
||||
* This provides much larger storage capacity than localStorage, making it suitable for
|
||||
* storing large base64-encoded images (logos, banners, etc.).
|
||||
*/
|
||||
const indexedDBStateStorage: StateStorage = {
|
||||
getItem,
|
||||
setItem,
|
||||
removeItem,
|
||||
};
|
||||
|
||||
/**
|
||||
* IndexedDB storage adapter that implements the PersistStorage interface for zustand persist.
|
||||
* Uses createJSONStorage to handle JSON serialization/deserialization automatically.
|
||||
*/
|
||||
export const onboardingIndexedDBStorage = createJSONStorage(() => indexedDBStateStorage);
|
||||
@@ -1,6 +1,8 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
import { onboardingIndexedDBStorage } from "./onboarding-storage";
|
||||
|
||||
export type PlanType = "personal" | "team" | "organization";
|
||||
export type InviteRole = "MEMBER" | "ADMIN";
|
||||
|
||||
@@ -160,7 +162,8 @@ export const useOnboardingStore = create<OnboardingState>()(
|
||||
resetOnboarding: () => set(initialState),
|
||||
}),
|
||||
{
|
||||
name: "cal-onboarding-storage", // localStorage key
|
||||
name: "cal-onboarding-storage", // Storage key
|
||||
storage: onboardingIndexedDBStorage, // Use IndexedDB instead of localStorage for larger capacity
|
||||
// Optional: Only persist certain fields
|
||||
partialize: (state) => ({
|
||||
selectedPlan: state.selectedPlan,
|
||||
|
||||
Reference in new issue
Block a user