diff --git a/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateModal.stories.tsx b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateModal.stories.tsx
new file mode 100644
index 0000000000..d5d1c99756
--- /dev/null
+++ b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateModal.stories.tsx
@@ -0,0 +1,107 @@
+import { MemoryRouter } from 'react-router-dom'
+import { useDisclosure } from '@chakra-ui/react'
+import { GrowthBook, GrowthBookProvider } from '@growthbook/growthbook-react'
+import { Meta, StoryFn } from '@storybook/react'
+
+import { featureFlags } from 'formsg-shared/constants'
+
+import { getTemplateFormResponse } from '~/mocks/msw/handlers/admin-form/template-form'
+import { getUser, MOCK_USER, userHandlers } from '~/mocks/msw/handlers/user'
+
+import { fullScreenDecorator, LoggedInDecorator } from '~utils/storybook'
+
+import { UseTemplateModal, UseTemplateModalProps } from './UseTemplateModal'
+
+const MOCK_SOURCE_FORM_ID = '61540ece3d4a6e50ac0cc6ff'
+
+const baseMsw = [
+ ...userHandlers({ delay: 0 }),
+ getTemplateFormResponse({ delay: 0 }),
+]
+
+export default {
+ title: 'Pages/AdminFormPage/UseTemplateModal',
+ component: UseTemplateModal,
+ decorators: [
+ (storyFn) => {storyFn()},
+ fullScreenDecorator,
+ LoggedInDecorator,
+ ],
+ parameters: {
+ layout: 'fullscreen',
+ chromatic: { pauseAnimationAtEnd: true },
+ msw: baseMsw,
+ },
+} as Meta
+
+const Template: StoryFn = (args) => {
+ const modalProps = useDisclosure({ defaultIsOpen: true })
+ return (
+ console.log('close modal')}
+ />
+ )
+}
+
+export const Default = Template.bind({})
+
+const mrfCutoverOn = new GrowthBook({
+ features: { [featureFlags.mrfCutover]: { defaultValue: true } },
+})
+
+const withCutover = (Story: StoryFn) => (
+
+
+
+)
+
+export const MrfCutoverOn = Template.bind({})
+MrfCutoverOn.decorators = [withCutover]
+
+export const MrfCutoverOnChildrenBeta = Template.bind({})
+MrfCutoverOnChildrenBeta.decorators = [withCutover]
+MrfCutoverOnChildrenBeta.parameters = {
+ msw: [
+ getUser({
+ delay: 0,
+ mockUser: { ...MOCK_USER, betaFlags: { children: true } },
+ }),
+ getTemplateFormResponse({ delay: 0 }),
+ ],
+}
+
+export const MrfCutoverOnWebhookV1Beta = Template.bind({})
+MrfCutoverOnWebhookV1Beta.decorators = [withCutover]
+MrfCutoverOnWebhookV1Beta.parameters = {
+ msw: [
+ getUser({
+ delay: 0,
+ mockUser: {
+ ...MOCK_USER,
+ betaFlags: { createStorageModeForV1Webhook: true },
+ },
+ }),
+ getTemplateFormResponse({ delay: 0 }),
+ ],
+}
+
+export const MrfCutoverOnAllExceptions = Template.bind({})
+MrfCutoverOnAllExceptions.decorators = [withCutover]
+MrfCutoverOnAllExceptions.parameters = {
+ msw: [
+ getUser({
+ delay: 0,
+ mockUser: {
+ ...MOCK_USER,
+ betaFlags: {
+ children: true,
+ createStorageModeForV1Webhook: true,
+ },
+ },
+ }),
+ getTemplateFormResponse({ delay: 0 }),
+ ],
+}
diff --git a/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.cutover.test.tsx b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.cutover.test.tsx
new file mode 100644
index 0000000000..6c76b8e858
--- /dev/null
+++ b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.cutover.test.tsx
@@ -0,0 +1,130 @@
+import { useFeatureIsOn } from '@growthbook/growthbook-react'
+import { act, renderHook } from '@testing-library/react'
+
+import { FormResponseMode } from 'formsg-shared/types'
+
+import { useFormTemplate } from '~features/admin-form/common/queries'
+import { useUser } from '~features/user/queries'
+import { useEmailModeFeedbackMutation } from '~features/workspace/mutations'
+
+import { useUseTemplateMutations } from '../mutation'
+
+import { useUseTemplateWizardContext } from './UseTemplateWizardProvider'
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({ t: (k: string) => k }),
+}))
+vi.mock('@growthbook/growthbook-react', () => ({
+ useFeatureIsOn: vi.fn(),
+}))
+vi.mock('~features/admin-form/common/queries')
+vi.mock('~features/user/queries')
+vi.mock('~features/workspace/mutations')
+vi.mock('../mutation')
+
+const MOCK_FORM_ID = 'form123'
+const MOCK_USER_EMAIL = 'admin@example.com'
+
+const makeMockTemplateFormData = () => ({
+ spcpSession: null,
+ form: { title: 'Source', form_fields: [] },
+})
+
+describe('UseTemplateWizardProvider — cutover behaviour', () => {
+ const useStorageModeFormTemplateMutation = {
+ isLoading: false,
+ mutate: vi.fn(),
+ }
+ const useMultirespondentFormTemplateMutation = {
+ isLoading: false,
+ mutate: vi.fn(),
+ }
+ const useEmailModeFormTemplateMutation = { isLoading: false, mutate: vi.fn() }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+
+ vi.mocked(useFeatureIsOn).mockReturnValue(true)
+
+ vi.mocked(useFormTemplate).mockReturnValue({
+ data: makeMockTemplateFormData() as any,
+ isLoading: false,
+ } as any)
+
+ vi.mocked(useUser).mockReturnValue({
+ user: { email: MOCK_USER_EMAIL } as any,
+ } as any)
+
+ vi.mocked(useUseTemplateMutations).mockReturnValue({
+ useEmailModeFormTemplateMutation,
+ useStorageModeFormTemplateMutation,
+ useMultirespondentFormTemplateMutation,
+ } as any)
+
+ vi.mocked(useEmailModeFeedbackMutation).mockReturnValue({
+ emailModeFeedbackMutation: { isLoading: false, mutate: vi.fn() },
+ } as any)
+ })
+
+ it('defaults responseMode to Multirespondent when cutover is on', () => {
+ const { result } = renderHook(() =>
+ useUseTemplateWizardContext(MOCK_FORM_ID, vi.fn()),
+ )
+
+ expect(result.current.formMethods.getValues().responseMode).toBe(
+ FormResponseMode.Multirespondent,
+ )
+ })
+
+ it('fires the multirespondent template mutation on default submit', async () => {
+ const { result } = renderHook(() =>
+ useUseTemplateWizardContext(MOCK_FORM_ID, vi.fn()),
+ )
+
+ act(() => {
+ result.current.formMethods.setValue('title', 'New title')
+ })
+ await act(async () => {
+ await result.current.handleCreateStorageModeOrMultirespondentForm()
+ })
+
+ expect(useMultirespondentFormTemplateMutation.mutate).toHaveBeenCalledTimes(
+ 1,
+ )
+ expect(useMultirespondentFormTemplateMutation.mutate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ formIdToDuplicate: MOCK_FORM_ID,
+ title: 'New title',
+ responseMode: FormResponseMode.Multirespondent,
+ }),
+ expect.anything(),
+ )
+ expect(useStorageModeFormTemplateMutation.mutate).not.toHaveBeenCalled()
+ })
+
+ it('fires the storage template mutation with [user.email] fallback after escape hatch + submit', async () => {
+ const { result } = renderHook(() =>
+ useUseTemplateWizardContext(MOCK_FORM_ID, vi.fn()),
+ )
+
+ act(() => {
+ result.current.goToStorageModeDetails()
+ result.current.formMethods.setValue('title', 'New title')
+ })
+ await act(async () => {
+ await result.current.handleCreateStorageModeOrMultirespondentForm()
+ })
+
+ expect(useStorageModeFormTemplateMutation.mutate).toHaveBeenCalledTimes(1)
+ expect(useStorageModeFormTemplateMutation.mutate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ formIdToDuplicate: MOCK_FORM_ID,
+ title: 'New title',
+ responseMode: FormResponseMode.Encrypt,
+ emails: [MOCK_USER_EMAIL],
+ }),
+ expect.anything(),
+ )
+ expect(useMultirespondentFormTemplateMutation.mutate).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx
index 4cc12b5c5c..639618a889 100644
--- a/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx
+++ b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx
@@ -75,13 +75,14 @@ export const useUseTemplateWizardContext = (
if (!formId) return
switch (responseMode) {
case FormResponseMode.Encrypt: {
+ const cutoverDefaultEmails = adminEmail ? [adminEmail] : []
return useStorageModeFormTemplateMutation.mutate(
{
formIdToDuplicate: formId,
title,
responseMode,
publicKey: keypair.publicKey,
- emails: emails.filter(Boolean),
+ emails: emails ? emails.filter(Boolean) : cutoverDefaultEmails,
},
{
onSuccess: () => {