From c667118f10cee4b3f50e0a543cfffbe275927c3c Mon Sep 17 00:00:00 2001
From: Jed Fox <git@jedfox.com>
Date: Tue, 8 Aug 2023 19:35:22 -0700
Subject: [PATCH] Remove page-based modals in favor of existing state-based
 modal logic (#1270)

Co-authored-by: Trevor Farlow <trevdor@users.noreply.github.com>
---
 .../src/components/ActiveLocation.js          |  13 --
 .../src/components/FinancesApp.tsx            | 190 +++++++-----------
 .../desktop-client/src/components/Modals.tsx  |  42 ++++
 .../desktop-client/src/components/Page.tsx    |  52 +----
 .../src/components/accounts/Account.js        |  12 +-
 .../src/components/accounts/Header.js         |   2 +
 .../accounts/MobileAccountDetails.js          |   2 +
 .../src/components/manager/Modals.js          |   2 +-
 .../src/components/responsive/wide.ts         |   3 -
 .../components/schedules/DiscoverSchedules.js |  31 ++-
 .../src/components/schedules/EditSchedule.js  |  52 ++---
 .../src/components/schedules/LinkSchedule.js  |  19 +-
 .../schedules/PostsOfflineNotification.js     |  19 +-
 .../src/components/schedules/index.js         |  11 +-
 .../transactions/SelectedTransactions.js      |   7 +-
 .../transactions/TransactionList.js           |   5 +-
 packages/desktop-client/src/global-events.js  |   5 +-
 .../desktop-client/src/util/router-tools.tsx  |  71 +------
 packages/desktop-electron/menu.js             |   2 +-
 .../src/client/state-types/modals.d.ts        |   8 +
 packages/loot-core/typings/window.d.ts        |   3 -
 upcoming-release-notes/1270.md                |   6 +
 22 files changed, 206 insertions(+), 351 deletions(-)
 delete mode 100644 packages/desktop-client/src/components/ActiveLocation.js
 create mode 100644 upcoming-release-notes/1270.md

diff --git a/packages/desktop-client/src/components/ActiveLocation.js b/packages/desktop-client/src/components/ActiveLocation.js
deleted file mode 100644
index 65b8a050b..000000000
--- a/packages/desktop-client/src/components/ActiveLocation.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import React, { createContext, useContext } from 'react';
-
-let ActiveLocationContext = createContext(null);
-
-export function ActiveLocationProvider({ location, children }) {
-  return (
-    <ActiveLocationContext.Provider value={location} children={children} />
-  );
-}
-
-export function useActiveLocation() {
-  return useContext(ActiveLocationContext);
-}
diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx
index 4f5b0f8d2..e161aec5a 100644
--- a/packages/desktop-client/src/components/FinancesApp.tsx
+++ b/packages/desktop-client/src/components/FinancesApp.tsx
@@ -26,7 +26,7 @@ import PiggyBank from '../icons/v1/PiggyBank';
 import Wallet from '../icons/v1/Wallet';
 import { useResponsive } from '../ResponsiveProvider';
 import { theme, styles } from '../style';
-import { ExposeNavigate, StackedRoutes } from '../util/router-tools';
+import { ExposeNavigate } from '../util/router-tools';
 import { getIsOutdated, getLatestVersion } from '../util/versions';
 
 import BankSyncStatus from './BankSyncStatus';
@@ -40,7 +40,6 @@ import Notifications from './Notifications';
 import { ManagePayeesPage } from './payees/ManagePayeesPage';
 import Reports from './reports';
 import { NarrowAlternate, WideComponent } from './responsive';
-import PostsOfflineNotification from './schedules/PostsOfflineNotification';
 import Settings from './settings';
 import Titlebar, { TitlebarProvider } from './Titlebar';
 import { TransactionEdit } from './transactions/MobileTransaction';
@@ -73,120 +72,6 @@ function WideNotSupported({ children, redirectTo = '/budget' }) {
   return isNarrowWidth ? children : null;
 }
 
-function StackedRoutesInner({ location }) {
-  return (
-    <Routes location={location}>
-      <Route path="/" element={<Navigate to="/budget" replace />} />
-
-      <Route
-        path="/reports/*"
-        element={
-          <NarrowNotSupported>
-            {/* Has its own lazy loading logic */}
-            <Reports />
-          </NarrowNotSupported>
-        }
-      />
-
-      <Route path="/budget" element={<NarrowAlternate name="Budget" />} />
-
-      <Route
-        path="/schedules"
-        element={
-          <NarrowNotSupported>
-            <WideComponent name="Schedules" />
-          </NarrowNotSupported>
-        }
-      />
-
-      <Route
-        path="/schedule/edit"
-        element={
-          <NarrowNotSupported>
-            <WideComponent name="EditSchedule" />
-          </NarrowNotSupported>
-        }
-      />
-      <Route
-        path="/schedule/edit/:id"
-        element={
-          <NarrowNotSupported>
-            <WideComponent name="EditSchedule" />
-          </NarrowNotSupported>
-        }
-      />
-      <Route
-        path="/schedule/link"
-        element={
-          <NarrowNotSupported>
-            <WideComponent name="LinkSchedule" />
-          </NarrowNotSupported>
-        }
-      />
-      <Route
-        path="/schedule/discover"
-        element={
-          <NarrowNotSupported>
-            <WideComponent name="DiscoverSchedules" />
-          </NarrowNotSupported>
-        }
-      />
-
-      <Route
-        path="/schedule/posts-offline-notification"
-        element={<PostsOfflineNotification />}
-      />
-
-      <Route path="/payees" element={<ManagePayeesPage />} />
-      <Route path="/rules" element={<ManageRulesPage />} />
-      <Route path="/settings" element={<Settings />} />
-
-      {/* TODO: remove Nordigen route after v23.8.0 */}
-      <Route
-        path="/nordigen/link"
-        element={
-          <NarrowNotSupported>
-            <WideComponent name="GoCardlessLink" />
-          </NarrowNotSupported>
-        }
-      />
-      <Route
-        path="/gocardless/link"
-        element={
-          <NarrowNotSupported>
-            <WideComponent name="GoCardlessLink" />
-          </NarrowNotSupported>
-        }
-      />
-
-      <Route path="/accounts" element={<NarrowAlternate name="Accounts" />} />
-
-      <Route
-        path="/accounts/:id"
-        element={<NarrowAlternate name="Account" />}
-      />
-
-      <Route
-        path="/accounts/:id/transactions/:transactionId"
-        element={
-          <WideNotSupported>
-            <TransactionEdit />
-          </WideNotSupported>
-        }
-      />
-
-      <Route
-        path="/accounts/:id/transactions/new"
-        element={
-          <WideNotSupported>
-            <TransactionEdit />
-          </WideNotSupported>
-        }
-      />
-    </Routes>
-  );
-}
-
 function NavTab({ icon: TabIcon, name, path }) {
   return (
     <NavLink
@@ -311,9 +196,76 @@ function FinancesApp() {
               />
               <Notifications />
               <BankSyncStatus />
-              <StackedRoutes
-                render={location => <StackedRoutesInner location={location} />}
-              />
+
+              <Routes>
+                <Route path="/" element={<Navigate to="/budget" replace />} />
+
+                <Route
+                  path="/reports/*"
+                  element={
+                    <NarrowNotSupported>
+                      {/* Has its own lazy loading logic */}
+                      <Reports />
+                    </NarrowNotSupported>
+                  }
+                />
+
+                <Route
+                  path="/budget"
+                  element={<NarrowAlternate name="Budget" />}
+                />
+
+                <Route
+                  path="/schedules"
+                  element={
+                    <NarrowNotSupported>
+                      <WideComponent name="Schedules" />
+                    </NarrowNotSupported>
+                  }
+                />
+
+                <Route path="/payees" element={<ManagePayeesPage />} />
+                <Route path="/rules" element={<ManageRulesPage />} />
+                <Route path="/settings" element={<Settings />} />
+
+                <Route
+                  path="/gocardless/link"
+                  element={
+                    <NarrowNotSupported>
+                      <WideComponent name="GoCardlessLink" />
+                    </NarrowNotSupported>
+                  }
+                />
+
+                <Route
+                  path="/accounts"
+                  element={<NarrowAlternate name="Accounts" />}
+                />
+
+                <Route
+                  path="/accounts/:id"
+                  element={<NarrowAlternate name="Account" />}
+                />
+
+                <Route
+                  path="/accounts/:id/transactions/:transactionId"
+                  element={
+                    <WideNotSupported>
+                      <TransactionEdit />
+                    </WideNotSupported>
+                  }
+                />
+
+                <Route
+                  path="/accounts/:id/transactions/new"
+                  element={
+                    <WideNotSupported>
+                      <TransactionEdit />
+                    </WideNotSupported>
+                  }
+                />
+              </Routes>
+
               <Modals />
             </div>
 
diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx
index f37aa1043..43a8c4c7a 100644
--- a/packages/desktop-client/src/components/Modals.tsx
+++ b/packages/desktop-client/src/components/Modals.tsx
@@ -23,6 +23,10 @@ import ManageRulesModal from './modals/ManageRulesModal';
 import MergeUnusedPayees from './modals/MergeUnusedPayees';
 import PlaidExternalMsg from './modals/PlaidExternalMsg';
 import SelectLinkedAccounts from './modals/SelectLinkedAccounts';
+import DiscoverSchedules from './schedules/DiscoverSchedules';
+import ScheduleDetails from './schedules/EditSchedule';
+import ScheduleLink from './schedules/LinkSchedule';
+import PostsOfflineNotification from './schedules/PostsOfflineNotification';
 
 export default function Modals() {
   const modalStack = useSelector(state => state.modals.modalStack);
@@ -218,6 +222,44 @@ export default function Modals() {
             />
           );
 
+        case 'schedule-edit':
+          return (
+            <ScheduleDetails
+              key={name}
+              modalProps={modalProps}
+              id={options?.id || null}
+              actions={actions}
+            />
+          );
+
+        case 'schedule-link':
+          return (
+            <ScheduleLink
+              key={name}
+              modalProps={modalProps}
+              actions={actions}
+              transactionIds={options?.transactionIds}
+            />
+          );
+
+        case 'schedules-discover':
+          return (
+            <DiscoverSchedules
+              key={name}
+              modalProps={modalProps}
+              actions={actions}
+            />
+          );
+
+        case 'schedule-posts-offline-notification':
+          return (
+            <PostsOfflineNotification
+              key={name}
+              modalProps={modalProps}
+              actions={actions}
+            />
+          );
+
         default:
           console.error('Unknown modal:', name);
           return null;
diff --git a/packages/desktop-client/src/components/Page.tsx b/packages/desktop-client/src/components/Page.tsx
index ae638f952..8a2068bfc 100644
--- a/packages/desktop-client/src/components/Page.tsx
+++ b/packages/desktop-client/src/components/Page.tsx
@@ -1,30 +1,20 @@
-import React, { createContext, type ReactNode, useContext } from 'react';
-import { useNavigate } from 'react-router-dom';
+import React, { type ReactNode } from 'react';
 
 import { type CSSProperties } from 'glamor';
 
 import { useResponsive } from '../ResponsiveProvider';
 import { colors, styles } from '../style';
 
-import Modal from './common/Modal';
 import Text from './common/Text';
 import View from './common/View';
 
-let PageTypeContext = createContext({ type: 'page', current: undefined });
-
-export function PageTypeProvider({ type, current, children }) {
-  return (
-    <PageTypeContext.Provider value={{ type, current }}>
-      {children}
-    </PageTypeContext.Provider>
-  );
-}
-
-export function usePageType() {
-  return useContext(PageTypeContext);
-}
-
-function PageTitle({ name, style }) {
+function PageTitle({
+  name,
+  style,
+}: {
+  name: ReactNode;
+  style?: CSSProperties;
+}) {
   const { isNarrowWidth } = useResponsive();
 
   if (isNarrowWidth) {
@@ -69,40 +59,16 @@ function PageTitle({ name, style }) {
 
 export function Page({
   title,
-  modalSize,
   children,
   titleStyle,
 }: {
-  title: string;
-  modalSize?: string | { width: number; height?: number };
+  title: ReactNode;
   children: ReactNode;
   titleStyle?: CSSProperties;
 }) {
-  let { type, current } = usePageType();
-  let navigate = useNavigate();
   let { isNarrowWidth } = useResponsive();
   let HORIZONTAL_PADDING = isNarrowWidth ? 10 : 20;
 
-  if (type === 'modal') {
-    let size =
-      typeof modalSize === 'string'
-        ? modalSize === 'medium'
-          ? { width: 750, height: 600 }
-          : { width: 600 }
-        : modalSize;
-
-    return (
-      <Modal
-        title={title}
-        isCurrent={current}
-        size={size}
-        onClose={() => navigate(-1)}
-      >
-        {children}
-      </Modal>
-    );
-  }
-
   return (
     <View style={isNarrowWidth ? undefined : styles.page}>
       <PageTitle
diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js
index 638159a16..53bbe6761 100644
--- a/packages/desktop-client/src/components/accounts/Account.js
+++ b/packages/desktop-client/src/components/accounts/Account.js
@@ -28,7 +28,6 @@ import { applyChanges, groupById } from 'loot-core/src/shared/util';
 import { authorizeBank } from '../../gocardless';
 import { SelectedProviderWithItems } from '../../hooks/useSelected';
 import { styles, colors } from '../../style';
-import { useActiveLocation } from '../ActiveLocation';
 import Button from '../common/Button';
 import Text from '../common/Text';
 import View from '../common/View';
@@ -1144,6 +1143,7 @@ class AccountInternal extends PureComponent {
       hideFraction,
       addNotification,
       accountsSyncing,
+      pushModal,
       replaceModal,
       showExtraBalances,
       accountId,
@@ -1226,6 +1226,7 @@ class AccountInternal extends PureComponent {
                 filters={this.state.filters}
                 conditionsOp={this.state.conditionsOp}
                 savePrefs={this.props.savePrefs}
+                pushModal={this.props.pushModal}
                 onSearch={this.onSearch}
                 onShowTransactions={this.onShowTransactions}
                 onMenuSelect={this.onMenuSelect}
@@ -1301,6 +1302,7 @@ class AccountInternal extends PureComponent {
                       </View>
                     ) : null
                   }
+                  pushModal={pushModal}
                   onSort={this.onSort}
                   sortField={this.state.sort.field}
                   ascDesc={this.state.sort.ascDesc}
@@ -1342,7 +1344,6 @@ function AccountHack(props) {
 export default function Account() {
   let params = useParams();
   let location = useLocation();
-  let activeLocation = useActiveLocation();
 
   let state = useSelector(state => ({
     newTransactions: state.queries.newTransactions,
@@ -1403,12 +1404,9 @@ export default function Account() {
         <AccountHack
           {...state}
           {...actionCreators}
-          modalShowing={
-            state.modalShowing ||
-            !!(activeLocation.state && activeLocation.state.parent)
-          }
+          modalShowing={state.modalShowing}
           accountId={params.id}
-          categoryId={activeLocation?.state?.filter?.category}
+          categoryId={location?.state?.filter?.category}
           location={location}
           filtersList={filtersList}
         />
diff --git a/packages/desktop-client/src/components/accounts/Header.js b/packages/desktop-client/src/components/accounts/Header.js
index a22a232ed..b58ec8f4f 100644
--- a/packages/desktop-client/src/components/accounts/Header.js
+++ b/packages/desktop-client/src/components/accounts/Header.js
@@ -52,6 +52,7 @@ export function AccountHeader({
   filters,
   conditionsOp,
   savePrefs,
+  pushModal,
   onSearch,
   onAddTransaction,
   onShowTransactions,
@@ -259,6 +260,7 @@ export function AccountHeader({
               onUnlink={onBatchUnlink}
               onCreateRule={onCreateRule}
               onScheduleAction={onScheduleAction}
+              pushModal={pushModal}
             />
           )}
           <Button
diff --git a/packages/desktop-client/src/components/accounts/MobileAccountDetails.js b/packages/desktop-client/src/components/accounts/MobileAccountDetails.js
index 88281b6f0..4d95e5c4f 100644
--- a/packages/desktop-client/src/components/accounts/MobileAccountDetails.js
+++ b/packages/desktop-client/src/components/accounts/MobileAccountDetails.js
@@ -74,6 +74,7 @@ export default function AccountDetails({
   onLoadMore,
   onSearch,
   onSelectTransaction,
+  pushModal,
   // refreshControl
 }) {
   let allTransactions = useMemo(() => {
@@ -170,6 +171,7 @@ export default function AccountDetails({
         // refreshControl={refreshControl}
         onLoadMore={onLoadMore}
         onSelect={onSelectTransaction}
+        pushModal={pushModal}
       />
     </View>
   );
diff --git a/packages/desktop-client/src/components/manager/Modals.js b/packages/desktop-client/src/components/manager/Modals.js
index 2980749df..fa6451658 100644
--- a/packages/desktop-client/src/components/manager/Modals.js
+++ b/packages/desktop-client/src/components/manager/Modals.js
@@ -18,7 +18,7 @@ export default function Modals() {
   let isHidden = useSelector(state => state.modals.isHidden);
   let actions = useActions();
 
-  let stack = modalStack.map(({ name, options }, idx) => {
+  let stack = modalStack.map(({ name, options = {} }, idx) => {
     const modalProps = {
       onClose: actions.popModal,
       onPush: actions.pushModal,
diff --git a/packages/desktop-client/src/components/responsive/wide.ts b/packages/desktop-client/src/components/responsive/wide.ts
index cca8e725e..827a4203c 100644
--- a/packages/desktop-client/src/components/responsive/wide.ts
+++ b/packages/desktop-client/src/components/responsive/wide.ts
@@ -1,9 +1,6 @@
 export { default as Budget } from '../budget';
 
 export { default as Schedules } from '../schedules';
-export { default as EditSchedule } from '../schedules/EditSchedule';
-export { default as LinkSchedule } from '../schedules/LinkSchedule';
-export { default as DiscoverSchedules } from '../schedules/DiscoverSchedules';
 
 export { default as GoCardlessLink } from '../gocardless/GoCardlessLink';
 
diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.js b/packages/desktop-client/src/components/schedules/DiscoverSchedules.js
index de1a600bb..aab542cea 100644
--- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.js
+++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.js
@@ -1,5 +1,4 @@
 import React, { useState } from 'react';
-import { Navigate, useLocation, useNavigate } from 'react-router-dom';
 
 import q, { runQuery } from 'loot-core/src/client/query-helpers';
 import { send } from 'loot-core/src/platform/client/fetch';
@@ -12,12 +11,11 @@ import useSelected, {
 } from '../../hooks/useSelected';
 import useSendPlatformRequest from '../../hooks/useSendPlatformRequest';
 import { colors } from '../../style';
-import { getParent } from '../../util/router-tools';
 import { ButtonWithLoading } from '../common/Button';
+import Modal from '../common/Modal';
 import Paragraph from '../common/Paragraph';
 import Stack from '../common/Stack';
 import View from '../common/View';
-import { Page, usePageType } from '../Page';
 import { Table, TableHeader, Row, Field, SelectCell } from '../table';
 import DisplayId from '../util/DisplayId';
 
@@ -26,7 +24,6 @@ import { ScheduleAmountCell } from './SchedulesTable';
 let ROW_HEIGHT = 43;
 
 function DiscoverSchedulesTable({ schedules, loading }) {
-  let pageType = usePageType();
   let selectedItems = useSelectedItems();
   let dispatchSelected = useSelectedDispatch();
 
@@ -95,11 +92,10 @@ function DiscoverSchedulesTable({ schedules, loading }) {
       <Table
         rowHeight={ROW_HEIGHT}
         version="v2"
-        backgroundColor={pageType.type === 'modal' ? 'transparent' : undefined}
+        backgroundColor="transparent"
         style={{
           flex: 1,
-          backgroundColor:
-            pageType.type === 'modal' ? 'transparent' : undefined,
+          backgroundColor: 'transparent',
         }}
         items={schedules}
         loading={loading}
@@ -111,9 +107,7 @@ function DiscoverSchedulesTable({ schedules, loading }) {
   );
 }
 
-export default function DiscoverSchedules() {
-  let pageType = usePageType();
-  let navigate = useNavigate();
+export default function DiscoverSchedules({ modalProps, actions }) {
   let { data: schedules, isLoading } =
     useSendPlatformRequest('schedule/discover');
   if (!schedules) schedules = [];
@@ -122,11 +116,6 @@ export default function DiscoverSchedules() {
 
   let selectedInst = useSelected('discover-schedules', schedules, []);
 
-  let location = useLocation();
-  if (!getParent(location)) {
-    return <Navigate to="/schedules" replace />;
-  }
-
   async function onCreate() {
     let selected = schedules.filter(s => selectedInst.items.has(s.id));
     setCreating(true);
@@ -155,11 +144,15 @@ export default function DiscoverSchedules() {
     }
 
     setCreating(false);
-    navigate(-1);
+    actions.popModal();
   }
 
   return (
-    <Page title="Found schedules" modalSize={{ width: 850, height: 650 }}>
+    <Modal
+      title="Found schedules"
+      size={{ width: 850, height: 650 }}
+      {...modalProps}
+    >
       <Paragraph>
         We found some possible schedules in your current transactions. Select
         the ones you want to create.
@@ -180,7 +173,7 @@ export default function DiscoverSchedules() {
         justify="flex-end"
         style={{
           paddingTop: 20,
-          paddingBottom: pageType.type === 'modal' ? 0 : 20,
+          paddingBottom: 0,
         }}
       >
         <ButtonWithLoading
@@ -192,6 +185,6 @@ export default function DiscoverSchedules() {
           Create schedules
         </ButtonWithLoading>
       </Stack>
-    </Page>
+    </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/schedules/EditSchedule.js b/packages/desktop-client/src/components/schedules/EditSchedule.js
index 9548eb55d..528dc372a 100644
--- a/packages/desktop-client/src/components/schedules/EditSchedule.js
+++ b/packages/desktop-client/src/components/schedules/EditSchedule.js
@@ -1,6 +1,5 @@
 import React, { useEffect, useReducer } from 'react';
 import { useDispatch, useSelector } from 'react-redux';
-import { useParams, useNavigate } from 'react-router-dom';
 
 import { pushModal } from 'loot-core/src/client/actions/modals';
 import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees';
@@ -14,12 +13,12 @@ import { colors } from '../../style';
 import AccountAutocomplete from '../autocomplete/AccountAutocomplete';
 import PayeeAutocomplete from '../autocomplete/PayeeAutocomplete';
 import Button from '../common/Button';
+import Modal from '../common/Modal';
 import Stack from '../common/Stack';
 import Text from '../common/Text';
 import View from '../common/View';
 import { FormField, FormLabel, Checkbox } from '../forms';
 import { OpSelect } from '../modals/EditRule';
-import { Page } from '../Page';
 import DateSelect from '../select/DateSelect';
 import RecurringSchedulePicker from '../select/RecurringSchedulePicker';
 import { SelectedItemsButton } from '../table';
@@ -27,19 +26,6 @@ import SimpleTransactionsTable from '../transactions/SimpleTransactionsTable';
 import { AmountInput, BetweenAmountInput } from '../util/AmountInput';
 import GenericInput from '../util/GenericInput';
 
-function mergeFields(defaults, initial) {
-  let res = { ...defaults };
-  if (initial) {
-    // Only merge in fields from `initial` that exist in `defaults`
-    Object.keys(initial).forEach(key => {
-      if (key in defaults) {
-        res[key] = initial[key];
-      }
-    });
-  }
-  return res;
-}
-
 function updateScheduleConditions(schedule, fields) {
   let conds = extractScheduleConds(schedule._conditions);
 
@@ -80,11 +66,9 @@ function updateScheduleConditions(schedule, fields) {
   };
 }
 
-export default function ScheduleDetails() {
-  let { id, initialFields } = useParams();
+export default function ScheduleDetails({ modalProps, actions, id }) {
   let adding = id == null;
   let payees = useCachedPayees({ idKey: true });
-  let navigate = useNavigate();
   let globalDispatch = useDispatch();
   let dateFormat = useSelector(state => {
     return state.prefs.local.dateFormat || 'MM/dd/yyyy';
@@ -193,18 +177,15 @@ export default function ScheduleDetails() {
       schedule: null,
       upcomingDates: null,
       error: null,
-      fields: mergeFields(
-        {
-          payee: null,
-          account: null,
-          amount: null,
-          amountOp: null,
-          date: null,
-          posts_transaction: false,
-          name: null,
-        },
-        initialFields,
-      ),
+      fields: {
+        payee: null,
+        account: null,
+        amount: null,
+        amountOp: null,
+        date: null,
+        posts_transaction: false,
+        name: null,
+      },
       transactions: [],
       transactionsMode: adding ? 'matched' : 'linked',
     },
@@ -382,7 +363,7 @@ export default function ScheduleDetails() {
       if (adding) {
         await onLinkTransactions([...selectedInst.items], res.data);
       }
-      navigate(-1);
+      actions.popModal();
     }
   }
 
@@ -432,9 +413,10 @@ export default function ScheduleDetails() {
   let repeats = state.fields.date ? !!state.fields.date.frequency : false;
 
   return (
-    <Page
+    <Modal
       title={payee ? `Schedule: ${payee.name}` : 'Schedule'}
-      modalSize="medium"
+      size="medium"
+      {...modalProps}
     >
       <Stack direction="row" style={{ marginTop: 10 }}>
         <FormField style={{ flex: 1 }}>
@@ -772,13 +754,13 @@ export default function ScheduleDetails() {
         style={{ marginTop: 20 }}
       >
         {state.error && <Text style={{ color: colors.r4 }}>{state.error}</Text>}
-        <Button style={{ marginRight: 10 }} onClick={() => navigate(-1)}>
+        <Button style={{ marginRight: 10 }} onClick={actions.popModal}>
           Cancel
         </Button>
         <Button type="primary" onClick={onSave}>
           {adding ? 'Add' : 'Save'}
         </Button>
       </Stack>
-    </Page>
+    </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/schedules/LinkSchedule.js b/packages/desktop-client/src/components/schedules/LinkSchedule.js
index b33696755..af9724ae8 100644
--- a/packages/desktop-client/src/components/schedules/LinkSchedule.js
+++ b/packages/desktop-client/src/components/schedules/LinkSchedule.js
@@ -1,19 +1,20 @@
 import React, { useCallback, useState } from 'react';
-import { useLocation, useNavigate } from 'react-router-dom';
 
 import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
 import { send } from 'loot-core/src/platform/client/fetch';
 
+import Modal from '../common/Modal';
 import Search from '../common/Search';
 import Text from '../common/Text';
 import View from '../common/View';
-import { Page } from '../Page';
 
 import { SchedulesTable } from './SchedulesTable';
 
-export default function ScheduleLink() {
-  let location = useLocation();
-  let navigate = useNavigate();
+export default function ScheduleLink({
+  modalProps,
+  actions,
+  transactionIds: ids,
+}) {
   let scheduleData = useSchedules(
     useCallback(query => query.filter({ completed: false }), []),
   );
@@ -27,18 +28,16 @@ export default function ScheduleLink() {
   let { schedules, statuses } = scheduleData;
 
   async function onSelect(scheduleId) {
-    let { state } = location;
-    let ids = state.transactionIds;
     if (ids && ids.length > 0) {
       await send('transactions-batch-update', {
         updated: ids.map(id => ({ id, schedule: scheduleId })),
       });
     }
-    navigate(-1);
+    actions.popModal();
   }
 
   return (
-    <Page title="Link Schedule" modalSize="medium">
+    <Modal title="Link Schedule" size="medium" {...modalProps}>
       <View
         style={{ flexDirection: 'row', marginBottom: 20, alignItems: 'center' }}
       >
@@ -61,6 +60,6 @@ export default function ScheduleLink() {
         onSelect={onSelect}
         tableStyle={{ marginInline: -20 }}
       />
-    </Page>
+    </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.js b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.js
index 1c292825d..1261f938b 100644
--- a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.js
+++ b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.js
@@ -1,34 +1,29 @@
 import React from 'react';
-import { useLocation, useNavigate } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
 
 import { send } from 'loot-core/src/platform/client/fetch';
 
 import { colors } from '../../style';
 import Button from '../common/Button';
+import Modal from '../common/Modal';
 import Paragraph from '../common/Paragraph';
 import Stack from '../common/Stack';
 import Text from '../common/Text';
-import { Page } from '../Page';
 import DisplayId from '../util/DisplayId';
 
-export default function PostsOfflineNotification() {
+export default function PostsOfflineNotification({ modalProps, actions }) {
   let location = useLocation();
-  let navigate = useNavigate();
 
   let payees = (location.state && location.state.payees) || [];
   let plural = payees.length > 1;
 
-  function onClose() {
-    navigate(-1);
-  }
-
   async function onPost() {
     await send('schedule/force-run-service');
-    navigate(-1);
+    actions.popModal();
   }
 
   return (
-    <Page title="Post transactions?" modalSize="small">
+    <Modal title="Post transactions?" size="small" {...modalProps}>
       <Paragraph>
         {payees.length > 0 ? (
           <Text>
@@ -73,11 +68,11 @@ export default function PostsOfflineNotification() {
         style={{ marginTop: 20 }}
         spacing={2}
       >
-        <Button onClick={onClose}>Decide later</Button>
+        <Button onClick={actions.popModal}>Decide later</Button>
         <Button type="primary" onClick={onPost}>
           Post transactions
         </Button>
       </Stack>
-    </Page>
+    </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/schedules/index.js b/packages/desktop-client/src/components/schedules/index.js
index 05f6a2198..7ca41bf82 100644
--- a/packages/desktop-client/src/components/schedules/index.js
+++ b/packages/desktop-client/src/components/schedules/index.js
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
 import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
 import { send } from 'loot-core/src/platform/client/fetch';
 
-import { usePushModal } from '../../util/router-tools';
+import { useActions } from '../../hooks/useActions';
 import Button from '../common/Button';
 import Search from '../common/Search';
 import View from '../common/View';
@@ -12,8 +12,7 @@ import { Page } from '../Page';
 import { SchedulesTable, ROW_HEIGHT } from './SchedulesTable';
 
 export default function Schedules() {
-  let pushModal = usePushModal();
-
+  let { pushModal } = useActions();
   let [filter, setFilter] = useState('');
 
   let scheduleData = useSchedules();
@@ -25,15 +24,15 @@ export default function Schedules() {
   let { schedules, statuses } = scheduleData;
 
   function onEdit(id) {
-    pushModal(`/schedule/edit/${id}`);
+    pushModal('schedule-edit', { id });
   }
 
   function onAdd() {
-    pushModal('/schedule/edit');
+    pushModal('schedule-edit');
   }
 
   function onDiscover() {
-    pushModal('/schedule/discover');
+    pushModal('schedules-discover');
   }
 
   async function onAction(name, id) {
diff --git a/packages/desktop-client/src/components/transactions/SelectedTransactions.js b/packages/desktop-client/src/components/transactions/SelectedTransactions.js
index dc1bd5c0f..d29843896 100644
--- a/packages/desktop-client/src/components/transactions/SelectedTransactions.js
+++ b/packages/desktop-client/src/components/transactions/SelectedTransactions.js
@@ -1,7 +1,6 @@
 import React, { useMemo } from 'react';
 
 import { useSelectedItems } from '../../hooks/useSelected';
-import { usePushModal } from '../../util/router-tools';
 import Menu from '../common/Menu';
 import { SelectedItemsButton } from '../table';
 
@@ -16,8 +15,8 @@ export function SelectedTransactionsButton({
   onUnlink,
   onCreateRule,
   onScheduleAction,
+  pushModal,
 }) {
-  let pushModal = usePushModal();
   let selectedItems = useSelectedItems();
 
   let types = useMemo(() => {
@@ -130,11 +129,11 @@ export function SelectedTransactionsButton({
             }
 
             if (scheduleId) {
-              pushModal(`/schedule/edit/${scheduleId}`);
+              pushModal('schedule-edit', { id: scheduleId });
             }
             break;
           case 'link-schedule':
-            pushModal('/schedule/link', {
+            pushModal('schedule-link', {
               transactionIds: [...selectedItems],
             });
             break;
diff --git a/packages/desktop-client/src/components/transactions/TransactionList.js b/packages/desktop-client/src/components/transactions/TransactionList.js
index 908d6f025..c3eba6d14 100644
--- a/packages/desktop-client/src/components/transactions/TransactionList.js
+++ b/packages/desktop-client/src/components/transactions/TransactionList.js
@@ -12,7 +12,6 @@ import {
 import { getChangedValues, applyChanges } from 'loot-core/src/shared/util';
 
 import { theme } from '../../style';
-import { usePushModal } from '../../util/router-tools';
 
 import { TransactionTable } from './TransactionsTable';
 
@@ -78,6 +77,7 @@ export default function TransactionList({
   dateFormat,
   hideFraction,
   addNotification,
+  pushModal,
   renderEmpty,
   onSort,
   sortField,
@@ -89,7 +89,6 @@ export default function TransactionList({
 }) {
   let transactionsLatest = useRef();
   let navigate = useNavigate();
-  let pushModal = usePushModal();
 
   useLayoutEffect(() => {
     transactionsLatest.current = transactions;
@@ -163,7 +162,7 @@ export default function TransactionList({
   });
 
   let onNavigateToSchedule = useCallback(scheduleId => {
-    pushModal(`/schedule/edit/${scheduleId}`);
+    pushModal('schedule-edit', { id: scheduleId });
   });
 
   return (
diff --git a/packages/desktop-client/src/global-events.js b/packages/desktop-client/src/global-events.js
index b6e533b01..258e13ea2 100644
--- a/packages/desktop-client/src/global-events.js
+++ b/packages/desktop-client/src/global-events.js
@@ -36,10 +36,7 @@ export function handleGlobalEvents(actions, store) {
   });
 
   listen('schedules-offline', ({ payees }) => {
-    let pushModal = window.__pushModal;
-    if (pushModal) {
-      pushModal(`/schedule/posts-offline-notification`, { payees });
-    }
+    actions.pushModal(`schedule-posts-offline-notification`, { payees });
   });
 
   // This is experimental: we sync data locally automatically when
diff --git a/packages/desktop-client/src/util/router-tools.tsx b/packages/desktop-client/src/util/router-tools.tsx
index f370d90d5..21e89c061 100644
--- a/packages/desktop-client/src/util/router-tools.tsx
+++ b/packages/desktop-client/src/util/router-tools.tsx
@@ -1,75 +1,10 @@
-import { type ReactNode, useCallback, useLayoutEffect } from 'react';
-import {
-  type Location,
-  type To,
-  useLocation,
-  useNavigate,
-} from 'react-router-dom';
-
-import { ActiveLocationProvider } from '../components/ActiveLocation';
-import { PageTypeProvider } from '../components/Page';
-
-let VERSION = Date.now();
+import { useLayoutEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
 
 export function ExposeNavigate() {
   let navigate = useNavigate();
-  let pushModal = usePushModal();
   useLayoutEffect(() => {
     window.__navigate = navigate;
-    window.__pushModal = pushModal;
-  }, [navigate, pushModal]);
+  }, [navigate]);
   return null;
 }
-
-export function usePushModal() {
-  let navigate = useNavigate();
-  let location = useLocation();
-
-  return useCallback(
-    (path: To, stateProps: Record<string, unknown> = {}) =>
-      navigate(path, {
-        state: { parent: location, _version: VERSION, ...stateProps },
-      }),
-    [navigate, location],
-  );
-}
-
-export function getParent(location: Location): Location | null {
-  if (location.state?._version !== VERSION) {
-    return null;
-  }
-  return location.state?.parent || null;
-}
-
-export function StackedRoutes({
-  render,
-}: {
-  render: (loc: Location) => ReactNode;
-}) {
-  let location = useLocation();
-  let parent = getParent(location);
-
-  let locations = [location];
-  while (parent) {
-    locations.unshift(parent);
-    parent = getParent(parent);
-  }
-
-  let base = locations[0];
-  let stack = locations.slice(1);
-
-  return (
-    <ActiveLocationProvider location={locations[locations.length - 1]}>
-      {render(base)}
-      {stack.map((location, idx) => (
-        <PageTypeProvider
-          key={location.key}
-          type="modal"
-          current={idx === stack.length - 1}
-        >
-          {render(location)}
-        </PageTypeProvider>
-      ))}
-    </ActiveLocationProvider>
-  );
-}
diff --git a/packages/desktop-electron/menu.js b/packages/desktop-electron/menu.js
index 44cf69c6d..09d8fc9d7 100644
--- a/packages/desktop-electron/menu.js
+++ b/packages/desktop-electron/menu.js
@@ -131,7 +131,7 @@ function getMenu(isDev, createWindow) {
           enabled: false,
           click: function (menuItem, focusedWin) {
             focusedWin.webContents.executeJavaScript(
-              '__pushModal && __pushModal("/schedule/discover")',
+              'window.__actionsForMenu && window.__actionsForMenu.pushModal("schedules-discover")',
             );
           },
         },
diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts
index c4f1fac44..e44deedd3 100644
--- a/packages/loot-core/src/client/state-types/modals.d.ts
+++ b/packages/loot-core/src/client/state-types/modals.d.ts
@@ -85,6 +85,14 @@ type FinanceModals = {
   'budget-summary': {
     month: string;
   };
+
+  'schedule-edit': { id: string } | null;
+
+  'schedule-link': { transactionIds: string[] } | null;
+
+  'schedules-discover': null;
+
+  'schedule-posts-offline-notification': null;
 };
 
 export type PushModalAction = {
diff --git a/packages/loot-core/typings/window.d.ts b/packages/loot-core/typings/window.d.ts
index 00401f267..da199f9b0 100644
--- a/packages/loot-core/typings/window.d.ts
+++ b/packages/loot-core/typings/window.d.ts
@@ -1,5 +1,3 @@
-import { usePushModal } from '../../desktop-client/src/util/router-tools';
-
 export {};
 
 declare global {
@@ -19,6 +17,5 @@ declare global {
     };
 
     __navigate?: import('react-router').NavigateFunction;
-    __pushModal?: ReturnType<typeof usePushModal>;
   }
 }
diff --git a/upcoming-release-notes/1270.md b/upcoming-release-notes/1270.md
new file mode 100644
index 000000000..9fd440282
--- /dev/null
+++ b/upcoming-release-notes/1270.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [j-f1]
+---
+
+Remove second modal implementation
-- 
GitLab