Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions apps/web/src/features/auth/CalendarPermissionPrompt.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { useCalendarUiFlag } from '@app/features/calendar/use-calendar-ui-flag';
import {
useCalendarPromptAllowed,
useCalendarUiFlag,
} from '@app/features/calendar/use-calendar-ui-flag';
import { useKeyedPersistentToasts } from '@core/component/Toast/useKeyedPersistentToasts';
import { useAddInboxFlow } from '@core/email-link';
import { useEmailLinksQuery } from '@queries/email/link';
Expand All @@ -12,15 +15,20 @@ import { useEmailLinksQuery } from '@queries/email/link';
*
* Inboxes that also need a full reconnect are skipped: the reconnect prompt
* covers them, and reconnecting records the calendar grant anyway.
*
* Suppressed on phones unless `enable-calendar-prompt-mobile` says otherwise —
* the mobile toast layout can't present it without stranding the user. See
* `useCalendarPromptAllowed`.
*/
export function CalendarPermissionPrompt() {
const calendarUiEnabled = useCalendarUiFlag();
const promptAllowed = useCalendarPromptAllowed();
const linksQuery = useEmailLinksQuery();
const startAddInbox = useAddInboxFlow();

useKeyedPersistentToasts({
items: () =>
calendarUiEnabled()
calendarUiEnabled() && promptAllowed()
? (linksQuery.data?.links ?? []).filter(
(link) => link.needs_calendar_permission && !link.needs_reauth
)
Expand Down
47 changes: 47 additions & 0 deletions apps/web/src/features/calendar/use-calendar-ui-flag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
mobile: false,
flags: {} as Record<string, boolean>,
}));

vi.mock('@app/lib/analytics/posthog', () => ({
useFeatureFlag:
(key: string, opts?: { enabledOverride?: boolean }) => () => ({
enabled: opts?.enabledOverride ?? mocks.flags[key] ?? false,
}),
}));

vi.mock('@core/mobile/isMobile', () => ({
isMobile: () => mocks.mobile,
}));

vi.mock('@core/constant/featureFlags', () => ({
ENABLE_CALENDAR_UI_FLAG: 'enable-calendar-ui',
ENABLE_CALENDAR_UI_OVERRIDE: undefined,
ENABLE_CALENDAR_PROMPT_MOBILE_FLAG: 'enable-calendar-prompt-mobile',
ENABLE_CALENDAR_PROMPT_MOBILE_OVERRIDE: undefined,
}));

import { useCalendarPromptAllowed } from './use-calendar-ui-flag';

describe('useCalendarPromptAllowed', () => {
function allowed(state: { mobile: boolean; mobileFlag?: boolean }): boolean {
mocks.mobile = state.mobile;
mocks.flags = { 'enable-calendar-prompt-mobile': !!state.mobileFlag };
return useCalendarPromptAllowed()();
}

it('allows the prompt on desktop regardless of the mobile flag', () => {
expect(allowed({ mobile: false })).toBe(true);
expect(allowed({ mobile: false, mobileFlag: true })).toBe(true);
});

it('suppresses the prompt on mobile while the flag is off', () => {
expect(allowed({ mobile: true })).toBe(false);
});

it('allows the prompt on mobile once the flag is turned on', () => {
expect(allowed({ mobile: true, mobileFlag: true })).toBe(true);
});
});
15 changes: 15 additions & 0 deletions apps/web/src/features/calendar/use-calendar-ui-flag.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useFeatureFlag } from '@app/lib/analytics/posthog';
import {
ENABLE_CALENDAR_PROMPT_MOBILE_FLAG,
ENABLE_CALENDAR_PROMPT_MOBILE_OVERRIDE,
ENABLE_CALENDAR_UI_FLAG,
ENABLE_CALENDAR_UI_OVERRIDE,
} from '@core/constant/featureFlags';
import { isMobile } from '@core/mobile/isMobile';
import type { Accessor } from 'solid-js';

export function useCalendarUiFlag(): Accessor<boolean> {
Expand All @@ -11,3 +14,15 @@ export function useCalendarUiFlag(): Accessor<boolean> {
});
return () => flag().enabled;
}

/**
* Whether the "Enable calendar" prompt may surface on this device. Always true
* on desktop; on a phone it defers to `enable-calendar-prompt-mobile`, which is
* off until the mobile toast layout can present the prompt properly.
*/
export function useCalendarPromptAllowed(): Accessor<boolean> {
const mobileFlag = useFeatureFlag(ENABLE_CALENDAR_PROMPT_MOBILE_FLAG, {
enabledOverride: ENABLE_CALENDAR_PROMPT_MOBILE_OVERRIDE,
});
return () => !isMobile() || mobileFlag().enabled;
}
13 changes: 13 additions & 0 deletions apps/web/src/lib/core/constant/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,3 +563,16 @@ export const ENABLE_CALENDAR_UI_FLAG = 'enable-calendar-ui';
export const ENABLE_CALENDAR_UI_OVERRIDE =
getFeatureFlagOverride('ENABLE_CALENDAR_UI') ??
(DEV_MODE_ENV ? true : undefined);

// The "Enable calendar" prompt on phones. Off by default everywhere,
// including dev: the mobile toast layout drops the body and the close button,
// so the prompt lands as an undismissable one-line bar over the composer.
// Settings › Email keeps a per-inbox "Enable calendar" button, so nothing
// becomes unreachable while this is off. Flip it on in PostHog once the
// mobile layout is fixed, or locally with
// VITE_ENABLE_CALENDAR_PROMPT_MOBILE=true.
export const ENABLE_CALENDAR_PROMPT_MOBILE_FLAG =
'enable-calendar-prompt-mobile';
export const ENABLE_CALENDAR_PROMPT_MOBILE_OVERRIDE = getFeatureFlagOverride(
'ENABLE_CALENDAR_PROMPT_MOBILE'
);
Loading