onboarding tracking is now wired, the tour can be replayed from Settings, install‑banner reset is included, and empty states in Tasks/Members/Guest Messages now have guided CTAs.
What changed:
- Onboarding tracking: admin_app_opened on first authenticated dashboard load; event_created, branding_configured,
and invite_created on their respective actions.
- Tour replay: Settings now has an “Experience” section to replay the tour (clears tour seen flag and opens via ?tour=1).
- Empty states: Tasks, Members, and Guest Messages now include richer copy + quick actions.
- New helpers + copy: Tour storage helpers, new translations, and related UI wiring.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { resolveInstallBannerState } from './installBanner';
|
||||
import { resolveInstallBannerState, shouldShowInstallBanner } from './installBanner';
|
||||
|
||||
describe('resolveInstallBannerState', () => {
|
||||
it('returns null when already installed', () => {
|
||||
@@ -22,3 +22,21 @@ describe('resolveInstallBannerState', () => {
|
||||
expect(resolveInstallBannerState({ isInstalled: false, isStandalone: false, canInstall: false, isIos: false })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldShowInstallBanner', () => {
|
||||
it('returns null when dismissed', () => {
|
||||
const result = shouldShowInstallBanner(
|
||||
{ isInstalled: false, isStandalone: false, canInstall: true, isIos: true },
|
||||
true,
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns state when not dismissed', () => {
|
||||
const result = shouldShowInstallBanner(
|
||||
{ isInstalled: false, isStandalone: false, canInstall: true, isIos: false },
|
||||
false,
|
||||
);
|
||||
expect(result).toEqual({ variant: 'prompt' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@ export type InstallBannerInput = {
|
||||
isIos: boolean;
|
||||
};
|
||||
|
||||
export const INSTALL_BANNER_DISMISS_KEY = 'admin-install-banner-dismissed-v1';
|
||||
|
||||
export function resolveInstallBannerState(input: InstallBannerInput): InstallBannerState | null {
|
||||
if (input.isInstalled || input.isStandalone) {
|
||||
return null;
|
||||
@@ -26,3 +28,39 @@ export function resolveInstallBannerState(input: InstallBannerInput): InstallBan
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shouldShowInstallBanner(input: InstallBannerInput, dismissed: boolean): InstallBannerState | null {
|
||||
if (dismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolveInstallBannerState(input);
|
||||
}
|
||||
|
||||
export function getInstallBannerDismissed(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return window.localStorage.getItem(INSTALL_BANNER_DISMISS_KEY) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function setInstallBannerDismissed(value: boolean): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (value) {
|
||||
window.localStorage.setItem(INSTALL_BANNER_DISMISS_KEY, '1');
|
||||
} else {
|
||||
window.localStorage.removeItem(INSTALL_BANNER_DISMISS_KEY);
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { resolveTourStepKeys } from './mobileTour';
|
||||
import { getTourSeen, resolveTourStepKeys, setTourSeen, TOUR_STORAGE_KEY } from './mobileTour';
|
||||
|
||||
describe('resolveTourStepKeys', () => {
|
||||
it('includes the event step when there are no events', () => {
|
||||
@@ -10,3 +10,15 @@ describe('resolveTourStepKeys', () => {
|
||||
expect(resolveTourStepKeys(true)).toEqual(['qr', 'photos', 'push']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tour storage helpers', () => {
|
||||
it('stores and reads the seen flag', () => {
|
||||
setTourSeen(false);
|
||||
expect(getTourSeen()).toBe(false);
|
||||
|
||||
setTourSeen(true);
|
||||
expect(getTourSeen()).toBe(true);
|
||||
|
||||
window.localStorage.removeItem(TOUR_STORAGE_KEY);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export type TourStepKey = 'event' | 'qr' | 'photos' | 'push';
|
||||
|
||||
export const TOUR_STORAGE_KEY = 'admin-mobile-tour-v1';
|
||||
|
||||
export function resolveTourStepKeys(hasEvents: boolean): TourStepKey[] {
|
||||
if (hasEvents) {
|
||||
return ['qr', 'photos', 'push'];
|
||||
@@ -7,3 +9,31 @@ export function resolveTourStepKeys(hasEvents: boolean): TourStepKey[] {
|
||||
|
||||
return ['event', 'qr', 'photos', 'push'];
|
||||
}
|
||||
|
||||
export function getTourSeen(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return window.localStorage.getItem(TOUR_STORAGE_KEY) === 'seen';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function setTourSeen(seen: boolean): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (seen) {
|
||||
window.localStorage.setItem(TOUR_STORAGE_KEY, 'seen');
|
||||
} else {
|
||||
window.localStorage.removeItem(TOUR_STORAGE_KEY);
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user