@zeniai/client-epic-state 5.1.61 → 5.1.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/lib/coreEpics.js +3 -2
  2. package/lib/entity/chargeCard/chargeCard.d.ts +1 -1
  3. package/lib/entity/notificationRegistry/notificationRegistryPayload.d.ts +18 -0
  4. package/lib/entity/notificationRegistry/notificationRegistryPayload.js +28 -0
  5. package/lib/entity/notificationRegistry/notificationRegistryReducer.d.ts +10 -0
  6. package/lib/entity/notificationRegistry/notificationRegistryReducer.js +50 -0
  7. package/lib/entity/notificationRegistry/notificationRegistrySelector.d.ts +2 -0
  8. package/lib/entity/notificationRegistry/notificationRegistrySelector.js +10 -0
  9. package/lib/entity/notificationRegistry/notificationRegistryState.d.ts +32 -0
  10. package/lib/entity/notificationRegistry/notificationRegistryState.js +10 -0
  11. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  12. package/lib/entity/snackbar/snackbarTypes.js +3 -0
  13. package/lib/entity/tenant/clearAllEpic.d.ts +3 -1
  14. package/lib/entity/tenant/clearAllEpic.js +4 -0
  15. package/lib/entity/tenant/tenantReducer.d.ts +4 -1
  16. package/lib/entity/tenant/tenantReducer.js +8 -2
  17. package/lib/entity/tenant/tenantSelector.d.ts +1 -0
  18. package/lib/entity/tenant/tenantSelector.js +4 -0
  19. package/lib/epic.d.ts +5 -1
  20. package/lib/epic.js +5 -1
  21. package/lib/esm/coreEpics.js +3 -2
  22. package/lib/esm/entity/notificationRegistry/notificationRegistryPayload.js +24 -0
  23. package/lib/esm/entity/notificationRegistry/notificationRegistryReducer.js +46 -0
  24. package/lib/esm/entity/notificationRegistry/notificationRegistrySelector.js +6 -0
  25. package/lib/esm/entity/notificationRegistry/notificationRegistryState.js +5 -0
  26. package/lib/esm/entity/snackbar/snackbarTypes.js +3 -0
  27. package/lib/esm/entity/tenant/clearAllEpic.js +4 -0
  28. package/lib/esm/entity/tenant/tenantReducer.js +7 -1
  29. package/lib/esm/entity/tenant/tenantSelector.js +3 -0
  30. package/lib/esm/epic.js +5 -1
  31. package/lib/esm/index.js +7 -2
  32. package/lib/esm/reducer.js +6 -0
  33. package/lib/esm/view/companyView/companyViewReducer.js +68 -1
  34. package/lib/esm/view/companyView/epic/companyPassport/dismissCapitalizationOnboardingEpic.js +2 -1
  35. package/lib/esm/view/companyView/epic/companyPassport/projects/disableAccountingProjectsEpic.js +56 -0
  36. package/lib/esm/view/companyView/epic/companyPassport/projects/enableAccountingProjectsEpic.js +37 -0
  37. package/lib/esm/view/companyView/epic/companyPassport/projects/resumeEnableAccountingProjectsEpic.js +73 -0
  38. package/lib/esm/view/companyView/epic/companyPassport/updateAccountingClassesEnabledEpic.js +2 -1
  39. package/lib/esm/view/companyView/epic/companyPassport/updateCapitalizationAccountThresholdEpic.js +2 -1
  40. package/lib/esm/view/companyView/types/companyPassport/companyPassportViewState.js +5 -0
  41. package/lib/esm/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +116 -0
  42. package/lib/esm/view/notificationPreferencesView/notificationPreferencesEndpoint.js +5 -0
  43. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewPayload.js +36 -0
  44. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewReducer.js +222 -0
  45. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewSelector.js +25 -0
  46. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewState.js +4 -0
  47. package/lib/esm/view/settingsView/epic/fetchNotificationSettingsEpic.js +34 -2
  48. package/lib/esm/view/settingsView/epic/saveNotificationSettingsEpic.js +2 -1
  49. package/lib/esm/view/settingsView/settingsViewHelpers.js +3 -2
  50. package/lib/index.d.ts +8 -2
  51. package/lib/index.js +52 -32
  52. package/lib/reducer.d.ts +6 -0
  53. package/lib/reducer.js +6 -0
  54. package/lib/view/common/recurringViewHelper.d.ts +2 -2
  55. package/lib/view/companyView/companyViewReducer.d.ts +9 -1
  56. package/lib/view/companyView/companyViewReducer.js +70 -3
  57. package/lib/view/companyView/epic/companyPassport/dismissCapitalizationOnboardingEpic.js +2 -1
  58. package/lib/view/companyView/epic/companyPassport/projects/disableAccountingProjectsEpic.d.ts +10 -0
  59. package/lib/view/companyView/epic/companyPassport/projects/disableAccountingProjectsEpic.js +60 -0
  60. package/lib/view/companyView/epic/companyPassport/projects/enableAccountingProjectsEpic.d.ts +11 -0
  61. package/lib/view/companyView/epic/companyPassport/projects/enableAccountingProjectsEpic.js +41 -0
  62. package/lib/view/companyView/epic/companyPassport/projects/resumeEnableAccountingProjectsEpic.d.ts +13 -0
  63. package/lib/view/companyView/epic/companyPassport/projects/resumeEnableAccountingProjectsEpic.js +77 -0
  64. package/lib/view/companyView/epic/companyPassport/updateAccountingClassesEnabledEpic.js +2 -1
  65. package/lib/view/companyView/epic/companyPassport/updateCapitalizationAccountThresholdEpic.js +2 -1
  66. package/lib/view/companyView/types/companyPassport/companyPassportViewState.d.ts +2 -0
  67. package/lib/view/companyView/types/companyPassport/companyPassportViewState.js +5 -0
  68. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  69. package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.d.ts +40 -0
  70. package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +120 -0
  71. package/lib/view/notificationPreferencesView/notificationPreferencesEndpoint.d.ts +2 -0
  72. package/lib/view/notificationPreferencesView/notificationPreferencesEndpoint.js +9 -0
  73. package/lib/view/notificationPreferencesView/notificationPreferencesViewPayload.d.ts +7 -0
  74. package/lib/view/notificationPreferencesView/notificationPreferencesViewPayload.js +41 -0
  75. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.d.ts +30 -0
  76. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.js +226 -0
  77. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.d.ts +7 -0
  78. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.js +32 -0
  79. package/lib/view/notificationPreferencesView/notificationPreferencesViewState.d.ts +14 -0
  80. package/lib/view/notificationPreferencesView/notificationPreferencesViewState.js +8 -0
  81. package/lib/view/settingsView/epic/fetchNotificationSettingsEpic.d.ts +9 -1
  82. package/lib/view/settingsView/epic/fetchNotificationSettingsEpic.js +34 -2
  83. package/lib/view/settingsView/epic/saveNotificationSettingsEpic.js +2 -1
  84. package/lib/view/settingsView/settingsViewHelpers.d.ts +1 -1
  85. package/lib/view/settingsView/settingsViewHelpers.js +3 -2
  86. package/lib/view/settingsView/settingsViewPayload.d.ts +4 -0
  87. package/lib/view/settingsView/settingsViewState.d.ts +1 -1
  88. package/package.json +1 -1
@@ -0,0 +1,222 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+ import { mapPayloadToPreferences, } from './notificationPreferencesViewPayload';
3
+ import { emptyNotificationPreferences, } from './notificationPreferencesViewState';
4
+ export const initialState = {
5
+ cancelEpoch: 0,
6
+ localOverrides: emptyNotificationPreferences(),
7
+ preferences: emptyNotificationPreferences(),
8
+ saveEpoch: 0,
9
+ savePreferencesState: { fetchState: 'Not-Started' },
10
+ };
11
+ const mergePreferences = (base, patch) => {
12
+ const eventEnabledByChannel = {
13
+ ...base.eventEnabledByChannel,
14
+ };
15
+ Object.entries(patch.eventEnabledByChannel).forEach(([eventId, channels]) => {
16
+ eventEnabledByChannel[eventId] = {
17
+ ...(base.eventEnabledByChannel[eventId] ?? {}),
18
+ ...channels,
19
+ };
20
+ });
21
+ return {
22
+ eventEnabledByChannel,
23
+ groupFrequencyByGroupId: {
24
+ ...base.groupFrequencyByGroupId,
25
+ ...patch.groupFrequencyByGroupId,
26
+ },
27
+ };
28
+ };
29
+ // Remove only the (eventId, channel) and groupId keys that were actually saved
30
+ // to the server from `localOverrides` — AND whose current value still equals
31
+ // what was saved. Any new edits the user added while the PUT was in flight
32
+ // (either new keys not present in `saved`, OR same keys the user flipped back
33
+ // to a different value) are preserved so the next debounce cycle picks them up.
34
+ //
35
+ // Presence-only subtraction (the prior implementation) silently dropped a
36
+ // same-key re-edit that landed AFTER debounce fired but BEFORE the response
37
+ // returned. Concrete case:
38
+ // 1. toggle evt1.email = false → localOverrides = {evt1:{email:false}}
39
+ // 2. debounce PUT #1 fires with savedOverrides = {evt1:{email:false}}
40
+ // 3. user re-toggles evt1.email = true → localOverrides = {evt1:{email:true}}
41
+ // 4. PUT #1 success → subtract dropped `email` on presence, so the true
42
+ // re-edit disappeared and next-debounce found nothing to send.
43
+ const subtractSavedOverrides = (current, saved) => {
44
+ const eventEnabledByChannel = {};
45
+ Object.entries(current.eventEnabledByChannel).forEach(([eventId, channels]) => {
46
+ const savedChannels = saved.eventEnabledByChannel[eventId];
47
+ if (savedChannels == null) {
48
+ eventEnabledByChannel[eventId] = channels;
49
+ return;
50
+ }
51
+ const remainingChannels = {};
52
+ Object.keys(channels).forEach((channel) => {
53
+ // Preserve the key when it's a new channel OR when the user's
54
+ // current value differs from what was saved (same-key re-edit).
55
+ if (!(channel in savedChannels) ||
56
+ channels[channel] !== savedChannels[channel]) {
57
+ remainingChannels[channel] = channels[channel];
58
+ }
59
+ });
60
+ if (Object.keys(remainingChannels).length > 0) {
61
+ eventEnabledByChannel[eventId] = remainingChannels;
62
+ }
63
+ });
64
+ const groupFrequencyByGroupId = {};
65
+ Object.entries(current.groupFrequencyByGroupId).forEach(([groupId, frequency]) => {
66
+ if (saved.groupFrequencyByGroupId[groupId] !== frequency) {
67
+ groupFrequencyByGroupId[groupId] = frequency;
68
+ }
69
+ });
70
+ return { eventEnabledByChannel, groupFrequencyByGroupId };
71
+ };
72
+ const notificationPreferencesView = createSlice({
73
+ name: 'notificationPreferencesView',
74
+ initialState,
75
+ reducers: {
76
+ clearAllNotificationPreferencesView(draft) {
77
+ // Reset everything EXCEPT `cancelEpoch`, which must stay monotonic
78
+ // across the lifetime of the app instance. `initialState.cancelEpoch`
79
+ // is `0`; returning `initialState` verbatim would rewind the counter
80
+ // to zero, and any in-flight save whose `cancelEpochAtDispatch` was
81
+ // captured pre-clearAll (>0) would then satisfy the "captured >=
82
+ // current" epoch guard and merge stale edits into fresh preferences.
83
+ // Bumping instead of resetting also invalidates the abandoned save
84
+ // for free — Cursor Bugbot bc7876f1 followup.
85
+ const nextCancelEpoch = draft.cancelEpoch + 1;
86
+ // saveEpoch must also stay monotonic across clearAll for the same
87
+ // reason (Cursor Bugbot 3632510237). Rewinding to 0 would let a
88
+ // stale-in-flight GET whose `saveEpochAtDispatch` was captured >0
89
+ // satisfy the "captured >= current" epoch guard and clobber fresh
90
+ // preferences on any subsequent save.
91
+ const nextSaveEpoch = draft.saveEpoch + 1;
92
+ return {
93
+ ...initialState,
94
+ cancelEpoch: nextCancelEpoch,
95
+ saveEpoch: nextSaveEpoch,
96
+ };
97
+ },
98
+ clearNotificationPreferencesLocalOverrides(draft) {
99
+ // Discards in-flight per-event/per-group edits held in `localOverrides`
100
+ // without touching the fetched `preferences` snapshot. Used by the
101
+ // settings-page Cancel affordance so the effective preferences revert
102
+ // to the last server-known state without triggering a refetch race.
103
+ //
104
+ // Cancel/in-flight-save race: every Cancel bumps `cancelEpoch`
105
+ // monotonically. Any save that captured a lower epoch value at
106
+ // dispatch time is treated as stale by the Success/Failure reducer
107
+ // — its merge is skipped. Unconditional bump (vs the earlier
108
+ // counter model) survives switchMap abandonment and double-Cancel
109
+ // (Cursor Bugbot 4aa021f7): even if the abandoned save's
110
+ // Success/Failure never dispatches, subsequent legitimate saves
111
+ // still work because their captured epoch matches the current one.
112
+ draft.cancelEpoch += 1;
113
+ draft.localOverrides = emptyNotificationPreferences();
114
+ // Also drop `savePreferencesState` back to Not-Started so any
115
+ // "Saving…" affordance bound to `getNotificationPreferencesSaveState`
116
+ // clears on Cancel. Without this, a Cancel-during-save whose stale
117
+ // Success/Failure never fires (switchMap abandonment) leaves the
118
+ // selector reporting In-Progress forever — Cursor Bugbot bc7876f1.
119
+ draft.savePreferencesState = { fetchState: 'Not-Started' };
120
+ },
121
+ fetchNotificationPreferencesSuccess(draft, action) {
122
+ // Piggybacks on the settings-envelope fetch. The GET response is a full
123
+ // snapshot of the server-known preferences (empty maps included), so
124
+ // REPLACE `preferences` rather than merging — merging would leak keys
125
+ // the server has since removed. DO NOT touch `localOverrides`: a refetch
126
+ // that fires while the user has in-flight edits (pre-debounce) must not
127
+ // silently drop them; the save-success path is the only clearer.
128
+ // A `null`/`undefined` payload isn't a snapshot at all (e.g. the
129
+ // envelope key was omitted); no-op instead of wiping preferences.
130
+ if (action.payload.preferences == null) {
131
+ return;
132
+ }
133
+ // Stale-GET guard (Cursor Bugbot 3632510237): if the fetch epic
134
+ // captured a `saveEpoch` lower than the current one, a save has
135
+ // succeeded since this GET dispatched — the response is carrying
136
+ // the pre-PUT snapshot and would clobber the just-saved server
137
+ // truth back to the pre-edit state until the next refetch. Skip
138
+ // the replace; the store already reflects the fresher save.
139
+ if (action.payload.saveEpochAtDispatch < draft.saveEpoch) {
140
+ return;
141
+ }
142
+ draft.preferences = mapPayloadToPreferences(action.payload.preferences);
143
+ },
144
+ saveNotificationPreferences(draft) {
145
+ draft.savePreferencesState = { fetchState: 'In-Progress' };
146
+ },
147
+ saveNotificationPreferencesFailure(draft, action) {
148
+ // Stale failure from a cancelled-since save — swallow silently so
149
+ // the UI doesn't surface an error banner for a save the user
150
+ // already abandoned.
151
+ if (action.payload.cancelEpochAtDispatch < draft.cancelEpoch) {
152
+ return;
153
+ }
154
+ // Preserve `localOverrides` so the user's in-flight edits survive a
155
+ // transient network blip. UI should surface an error affordance and
156
+ // allow retry.
157
+ draft.savePreferencesState = {
158
+ fetchState: 'Error',
159
+ error: action.payload.error,
160
+ };
161
+ },
162
+ saveNotificationPreferencesSuccess(draft, action) {
163
+ // Preserve-on-partial: PUT /preferences is patch-semantic. The response
164
+ // echoes only the fields that changed, so merge into `preferences`
165
+ // instead of replacing — otherwise a single-channel toggle would wipe
166
+ // every other stored preference.
167
+ //
168
+ // Rollback invariant (F5): `serverEcho` may be `undefined` if the server
169
+ // rolls back to an older shape that omits the echo entirely. In that
170
+ // case fall back to merging `savedOverrides` into `preferences` — the
171
+ // PUT succeeded (2xx), so the values we sent are now server truth. If
172
+ // we merged an empty patch instead, `subtractSavedOverrides` would
173
+ // still strip the just-saved keys from `localOverrides`, leaving the
174
+ // effective preferences (preferences ∪ localOverrides) reverted to the
175
+ // pre-edit server snapshot until the next refetch.
176
+ //
177
+ // In-flight-edit invariant: `savedOverrides` is the exact snapshot the
178
+ // epic PUT to the server. Subtracting those keys from `localOverrides`
179
+ // (instead of clearing it wholesale) preserves any edits the user
180
+ // added AFTER the debounce fired but BEFORE the response landed. The
181
+ // next debounce cycle will pick them up. Clearing wholesale drops
182
+ // in-flight edits from both UI (once server echo lands) and server.
183
+ // Cancel/in-flight-save race: `cancelEpoch` monotonically increases
184
+ // on every Cancel. If the save captured a lower epoch at dispatch,
185
+ // the user has pressed Cancel since — skip the `preferences` merge
186
+ // and the `localOverrides` subtract (Cancel already emptied it).
187
+ // Do NOT flip fetchState so a subsequent legit save can still show
188
+ // its own In-Progress → Completed lifecycle. The server itself has
189
+ // accepted the write — reverting server-side is out of FE scope; UI
190
+ // reflects the cancelled intention until the next refetch merges
191
+ // whatever the server persisted.
192
+ if (action.payload.cancelEpochAtDispatch < draft.cancelEpoch) {
193
+ return;
194
+ }
195
+ // Bump `saveEpoch` so any settings-envelope GET that was in flight
196
+ // BEFORE this PUT resolved is treated as stale. Its captured epoch
197
+ // will be lower than the new value, so its reducer branch will
198
+ // skip the `preferences` replace. Cursor Bugbot 3632510237.
199
+ draft.saveEpoch += 1;
200
+ draft.savePreferencesState = { fetchState: 'Completed' };
201
+ const patch = action.payload.serverEcho == null
202
+ ? action.payload.savedOverrides
203
+ : mapPayloadToPreferences(action.payload.serverEcho);
204
+ draft.preferences = mergePreferences(draft.preferences, patch);
205
+ draft.localOverrides = subtractSavedOverrides(draft.localOverrides, action.payload.savedOverrides);
206
+ },
207
+ setGroupFrequency(draft, action) {
208
+ draft.localOverrides.groupFrequencyByGroupId[action.payload.groupId] =
209
+ action.payload.frequency;
210
+ },
211
+ toggleEventChannel(draft, action) {
212
+ const { channel, enabled, eventId } = action.payload;
213
+ const eventMap = {
214
+ ...(draft.localOverrides.eventEnabledByChannel[eventId] ?? {}),
215
+ };
216
+ eventMap[channel] = enabled;
217
+ draft.localOverrides.eventEnabledByChannel[eventId] = eventMap;
218
+ },
219
+ },
220
+ });
221
+ export const { clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, fetchNotificationPreferencesSuccess, saveNotificationPreferences, saveNotificationPreferencesFailure, saveNotificationPreferencesSuccess, setGroupFrequency, toggleEventChannel, } = notificationPreferencesView.actions;
222
+ export default notificationPreferencesView.reducer;
@@ -0,0 +1,25 @@
1
+ const getView = (state) => state.notificationPreferencesViewState;
2
+ export const getNotificationPreferencesSaveState = (state) => getView(state).savePreferencesState;
3
+ export const getEffectiveNotificationPreferences = (state) => {
4
+ const view = getView(state);
5
+ const mergedEventEnabledByChannel = { ...view.preferences.eventEnabledByChannel };
6
+ Object.entries(view.localOverrides.eventEnabledByChannel).forEach(([eventId, channels]) => {
7
+ mergedEventEnabledByChannel[eventId] = {
8
+ ...(view.preferences.eventEnabledByChannel[eventId] ?? {}),
9
+ ...channels,
10
+ };
11
+ });
12
+ return {
13
+ eventEnabledByChannel: mergedEventEnabledByChannel,
14
+ groupFrequencyByGroupId: {
15
+ ...view.preferences.groupFrequencyByGroupId,
16
+ ...view.localOverrides.groupFrequencyByGroupId,
17
+ },
18
+ };
19
+ };
20
+ export const getNotificationLocalOverrides = (state) => getView(state).localOverrides;
21
+ export const hasUnsavedNotificationPreferences = (state) => {
22
+ const overrides = getView(state).localOverrides;
23
+ return (Object.keys(overrides.eventEnabledByChannel).length > 0 ||
24
+ Object.keys(overrides.groupFrequencyByGroupId).length > 0);
25
+ };
@@ -0,0 +1,4 @@
1
+ export const emptyNotificationPreferences = () => ({
2
+ eventEnabledByChannel: {},
3
+ groupFrequencyByGroupId: {},
4
+ });
@@ -1,19 +1,51 @@
1
1
  import { from, of } from 'rxjs';
2
2
  import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { updateNotificationRegistry } from '../../../entity/notificationRegistry/notificationRegistryReducer';
3
4
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
5
+ import { notificationPreferencesUrl } from '../../notificationPreferencesView/notificationPreferencesEndpoint';
6
+ import { fetchNotificationPreferencesSuccess } from '../../notificationPreferencesView/notificationPreferencesViewReducer';
4
7
  import { fetchNotificationSettings, updateNotificationSettingsOnFailure, updateNotificationSettingsOnSuccess, } from '../settingsViewReducer';
5
8
  export const fetchNotificationSettingsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchNotificationSettings.match), switchMap(() => {
6
9
  const state = state$.value;
7
- const URL = `${zeniAPI.apiEndPoints.notificationMicroServiceBaseUrl}/1.0/notification-preferences`;
8
10
  const userEmail = state.tenantState.loggedInUser?.userEmail;
9
11
  if (userEmail == null) {
10
12
  return from([]);
11
13
  }
12
- return zeniAPI.getJSON(URL).pipe(mergeMap((response) => {
14
+ // Snapshot the save epoch at GET-dispatch time. The reducer compares
15
+ // the captured value against the current `saveEpoch` on response and
16
+ // skips the `preferences` replace when a save has landed in the
17
+ // meantime — the GET is carrying the pre-PUT snapshot and would
18
+ // otherwise clobber the fresh save. Cursor Bugbot 3632510237.
19
+ const saveEpochAtDispatch = state.notificationPreferencesViewState.saveEpoch;
20
+ // Snapshot the registry clear epoch at GET-dispatch time (same idiom as
21
+ // `saveEpochAtDispatch` above). The registry reducer compares it against
22
+ // the current `registryEpoch` and skips its write when a
23
+ // `clearAllNotificationRegistry` (tenant switch) has landed since — the
24
+ // GET is carrying the prior tenant's snapshot. Cursor Bugbot 67d6918e.
25
+ const registryEpochAtDispatch = state.notificationRegistryState.registryEpoch;
26
+ return zeniAPI
27
+ .getJSON(notificationPreferencesUrl(zeniAPI))
28
+ .pipe(mergeMap((response) => {
13
29
  if (isSuccessResponse(response) && response.data != null) {
14
30
  const updateActions = [
15
31
  updateNotificationSettingsOnSuccess(response.data, userEmail),
16
32
  ];
33
+ // Always dispatch — an absent `task_checklist_registry` (gate
34
+ // off) must CLEAR the prior tenant's registry, not leave it in
35
+ // place. The reducer clears on `undefined` and full-replaces
36
+ // otherwise, both guarded by the registry epoch so a stale GET
37
+ // that lands after a tenant switch is skipped. Cursor Bugbot
38
+ // 67d6918e.
39
+ updateActions.push(updateNotificationRegistry({
40
+ registry: response.data.task_checklist_registry ?? undefined,
41
+ registryEpochAtDispatch,
42
+ }));
43
+ if (response.data.task_checklist_preferences != null) {
44
+ updateActions.push(fetchNotificationPreferencesSuccess({
45
+ preferences: response.data.task_checklist_preferences,
46
+ saveEpochAtDispatch,
47
+ }));
48
+ }
17
49
  return of(...updateActions);
18
50
  }
19
51
  else {
@@ -2,9 +2,10 @@ import { of } from 'rxjs';
2
2
  import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
4
4
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
5
+ import { notificationPreferencesUrl } from '../../notificationPreferencesView/notificationPreferencesEndpoint';
5
6
  import { saveNotificationSettings, updateNotificationSettingsOnSaveFailure, updateNotificationSettingsOnSaveSuccess, } from '../settingsViewReducer';
6
7
  export const saveNotificationSettingsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(saveNotificationSettings.match), switchMap(() => {
7
- const URL = `${zeniAPI.apiEndPoints.notificationMicroServiceBaseUrl}/1.0/notification-preferences`;
8
+ const URL = notificationPreferencesUrl(zeniAPI);
8
9
  const { notificationSettings, notificationSettingsLocalData } = state$.value.settingsViewState.notification;
9
10
  const payload = toSaveNotificationSettingsPayload(notificationSettingsLocalData, notificationSettings);
10
11
  return zeniAPI
@@ -145,13 +145,14 @@ export const isMonthEndNotificationsSectionVisibleOnSettingsPage = (monthEndInsi
145
145
  return (monthEndInsightsNotificationSettings.enabled != null ||
146
146
  monthEndInsightsNotificationSettings.recipientEmailIds.includes(signedInUseEmail));
147
147
  };
148
- export const isNotificationsTabVisible = (notificationSettings, signedInUseEmail, isDebitCardFlagEnabled, currentTenant) => {
148
+ export const isNotificationsTabVisible = (notificationSettings, signedInUseEmail, isDebitCardFlagEnabled, currentTenant, hasTaskChecklistAccess) => {
149
149
  if (currentTenant == null) {
150
150
  return false;
151
151
  }
152
152
  const { reminderNotificationSettings, monthEndInsightsNotificationSettings } = notificationSettings;
153
153
  return ((isRemindersSectionVisibleOnSettingsPage(reminderNotificationSettings) ||
154
- isMonthEndNotificationsSectionVisibleOnSettingsPage(monthEndInsightsNotificationSettings, signedInUseEmail)) &&
154
+ isMonthEndNotificationsSectionVisibleOnSettingsPage(monthEndInsightsNotificationSettings, signedInUseEmail) ||
155
+ hasTaskChecklistAccess) &&
155
156
  (isTenantAccountingServicesEnabled(currentTenant) ||
156
157
  (isTenantBankingOnly(isDebitCardFlagEnabled, currentTenant) === false &&
157
158
  isTenantCardsOnly(currentTenant) === false)));
package/lib/index.d.ts CHANGED
@@ -106,6 +106,8 @@ import { updateCommentsNotifications, updateCommentsNotificationsStatuses } from
106
106
  import { NotificationWithAuthors } from './entity/notification/types/notificationSelectorTypes';
107
107
  import { Notification, NotificationActivity, NotificationEventData, NotificationMetaData, NotificationUpdates, NotificationValueFormat } from './entity/notification/types/notificationStateTypes';
108
108
  import { ExternalNotificationData, NotificationActivityType, NotificationGroup, NotificationIdentifierType, NotificationMode, NotificationStatus, NotificationUpdateValue, toNotificationModeStrict } from './entity/notification/types/notificationTypes';
109
+ import { getNotificationRegistry } from './entity/notificationRegistry/notificationRegistrySelector';
110
+ import { NotificationChannel, NotificationFrequency, NotificationRegistry, RegistryNotificationEvent, RegistryNotificationGroup, toNotificationChannel, toNotificationFrequency } from './entity/notificationRegistry/notificationRegistryState';
109
111
  import { getPlaidPaymentAccounts } from './entity/paymentAccount/paymentAccountSelector';
110
112
  import { Logo, PaymentAccount, VERIFIED_PAYMENT_ACCOUNT_PROVIDER_VERIFICATION_STATUS } from './entity/paymentAccount/paymentAccountState';
111
113
  import { PaymentInstrument } from './entity/paymentInstrument/paymentInstrument';
@@ -248,7 +250,7 @@ import { AuditSummaryData, CompanyMonthEndReportData, CompanyMonthReportTemplate
248
250
  import { fetchCockpitContext, fetchCompanyTaskManagerView, fetchTaskManagerMetrics, updateCompanyTaskManagerViewFilters } from './view/companyTaskManagerView/companyTaskManagerViewReducer';
249
251
  import { CompanyTaskManagerSelectorView, TaskWithCompanyDetail, getCompanyTaskManagerView } from './view/companyTaskManagerView/companyTaskManagerViewSelector';
250
252
  import { CompanyTaskManagerViewUIState, TaskManagerMetrics } from './view/companyTaskManagerView/companyTaskManagerViewState';
251
- import { clearCompanyView, companyManagementSaveUpdates, companyPassportClearDataInLocalStore, companyPassportSaveDataInLocalStore, deleteCompanyOfficerInLocalStore, dismissCapitalizationOnboarding, fetchAllCockpitViews, fetchCompanyManagementView, fetchCompanyPassportView, fetchCompanyPortfolioView, fetchManagementView, fetchOnboardingView, fetchParentSubsidiaryManagementView, fetchPortfolioView, fetchSubscriptionView, fetchZeniUsers, saveCompanyPassportDetails, saveIndustryAndIncDateInCompanyPassportLocalStore, updateAccountingClassesEnabled, updateCapitalizationAccountThreshold, updateCompanyDownloadState, updateCompanyManagementUIState, updateCompanyPassportLocalStoreData, updateCompanyPortfolioUIState } from './view/companyView/companyViewReducer';
253
+ import { clearCompanyView, companyManagementSaveUpdates, companyPassportClearDataInLocalStore, companyPassportSaveDataInLocalStore, deleteCompanyOfficerInLocalStore, dismissCapitalizationOnboarding, fetchAllCockpitViews, fetchCompanyManagementView, fetchCompanyPassportView, fetchCompanyPortfolioView, fetchManagementView, fetchOnboardingView, fetchParentSubsidiaryManagementView, fetchPortfolioView, fetchSubscriptionView, fetchZeniUsers, saveCompanyPassportDetails, saveIndustryAndIncDateInCompanyPassportLocalStore, updateAccountingClassesEnabled, enableAccountingProjects, resumeEnableAccountingProjects, disableAccountingProjects, clearQboProjectsReconnectRequest, updateCapitalizationAccountThreshold, updateCompanyDownloadState, updateCompanyManagementUIState, updateCompanyPassportLocalStoreData, updateCompanyPortfolioUIState } from './view/companyView/companyViewReducer';
252
254
  import { FilterCategoryValueType, canSendMonthEndEmailReport, shouldEnableCalendarPickerForLastReportSent } from './view/companyView/helpers/cockpitHelpers';
253
255
  import { getParentSubsidiaryManagementView } from './view/companyView/parentSubsidiaryView/parentSubsidiaryViewSelector';
254
256
  import { CompanyManagementView, CompanyWithUpdateStatusView, getAddonListZeniSku, getCompanyManagementView, getPlanListZeniSku } from './view/companyView/selector/companyManagementViewSelector';
@@ -335,6 +337,9 @@ import { fetchNetBurnOrIncomeStoryCard, updateNetBurnOrIncomeStoryCardSettings }
335
337
  import { getNetBurnOrIncomeStoryCardReport } from './view/netBurnOrIncomeStoryCard/netBurnOrIncomeStoryCardSelector';
336
338
  import { AccountingMethod, NetBurnOrIncomeStoryCardReport } from './view/netBurnOrIncomeStoryCard/netBurnOrIncomeStoryCardSelectorTypes';
337
339
  import { AverageMonthsCount, NetBurnOrIncomeRunway, NetBurnOrIncomeStoryCardState, TimeSpanIdForAverage } from './view/netBurnOrIncomeStoryCard/netBurnOrIncomeStoryCardState';
340
+ import { clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, setGroupFrequency, toggleEventChannel } from './view/notificationPreferencesView/notificationPreferencesViewReducer';
341
+ import { getEffectiveNotificationPreferences, getNotificationLocalOverrides, getNotificationPreferencesSaveState, hasUnsavedNotificationPreferences } from './view/notificationPreferencesView/notificationPreferencesViewSelector';
342
+ import { NotificationPreferences } from './view/notificationPreferencesView/notificationPreferencesViewState';
338
343
  import { fetchNotificationUnreadCount, fetchNotificationUnreadCountSuccess, fetchNotificationView, updateNotificationViewAllNotificationsStatus, updateNotificationViewCurrentTabAndSubTab, updateNotificationViewNotificationStatus, updateNotificationViewSubTab, updateNotificationViewTabState, updateNotificationViewUIState } from './view/notificationView/notificationViewReducer';
339
344
  import { NotificationView, getExternalNotificationsForSelectedSubTab, getNotificationView, getNotificationsForSelectedSubTab } from './view/notificationView/notificationViewSelector';
340
345
  import { NotificationSubTabType, NotificationTabState, NotificationTabType, NotificationViewUIState, toNotificationSubTabTypeStrict, toNotificationTabTypeStrict } from './view/notificationView/notificationViewState';
@@ -787,7 +792,7 @@ export { CockpitFilterCategory, CockpitFilters, DownloadCockpitTabOptions, Filte
787
792
  export { CockpitDownloadTabsID, CockpitTabsFileType, CockpitTabsID, ALL_COCKPIT_TABS_FILE_TYPES, ALL_COCKPIT_TABS_IDS, toCockpitTabsIDStrict, toCockpitTabsFileTypeStrict, };
788
793
  export { AllowedValueWithCode, AllowedValue, AllowedValueWithID, isAllowedValueWithCode, isAllowedValueWithID, };
789
794
  export { ManagementUIState, PortfolioUIState };
790
- export { fetchCompanyPassportView, CompanyPassportView, getCompanyPassportView, getCompanyPassportLocalStoreData, isCompanyPassportDataToBeSaved, companyPassportSaveDataInLocalStore, saveIndustryAndIncDateInCompanyPassportLocalStore, deleteCompanyOfficerInLocalStore, companyPassportClearDataInLocalStore, updateCompanyPassportLocalStoreData, saveCompanyPassportDetails, updateAccountingClassesEnabled, updateCapitalizationAccountThreshold, dismissCapitalizationOnboarding, clearCompanyView, UpdateActionType, CompanyPassportLocalStoreDataView, };
795
+ export { fetchCompanyPassportView, CompanyPassportView, getCompanyPassportView, getCompanyPassportLocalStoreData, isCompanyPassportDataToBeSaved, companyPassportSaveDataInLocalStore, saveIndustryAndIncDateInCompanyPassportLocalStore, deleteCompanyOfficerInLocalStore, companyPassportClearDataInLocalStore, updateCompanyPassportLocalStoreData, saveCompanyPassportDetails, updateAccountingClassesEnabled, enableAccountingProjects, resumeEnableAccountingProjects, disableAccountingProjects, clearQboProjectsReconnectRequest, updateCapitalizationAccountThreshold, dismissCapitalizationOnboarding, clearCompanyView, UpdateActionType, CompanyPassportLocalStoreDataView, };
791
796
  export { CompanyInfoLocalData, IncInfoLocalData, TaxDetailsLocalData, CompanyDetailsLocalData, PrimaryContactLocalData, CompanyOfficerLocalData, CompanyOfficerType, CompanyPassportLocalData, toCompanyPassportLocalData, };
792
797
  export type { CompanyOnboardingIndustryTypeCode, CompanyOnboardingSubIndustryTypeCode, CompanyPurposeOfAccountCode, CompanySourceOfFundsCode, CompanyTransactionVolumeCode, CompanyUsNexusTypeCode, };
793
798
  export { COMPANY_ONBOARDING_INDUSTRY_TYPE_CODES, COMPANY_ONBOARDING_SUB_INDUSTRY_CODES_BY_INDUSTRY, COMPANY_PURPOSE_OF_ACCOUNT_CODES, COMPANY_SOURCE_OF_FUNDS_CODES, COMPANY_TRANSACTION_VOLUME_CODES, COMPANY_US_NEXUS_TYPE_CODES, getCompanyOnboardingSubIndustryCodesForIndustry, };
@@ -903,6 +908,7 @@ export { ExternalNotificationData, NotificationGroup, NotificationActivityType,
903
908
  export { NotificationView, NotificationViewUIState, NotificationTabState, NotificationTabType, NotificationSubTabType, toNotificationSubTabTypeStrict, toNotificationTabTypeStrict, fetchNotificationView, fetchNotificationUnreadCount, fetchNotificationUnreadCountSuccess, updateNotificationViewAllNotificationsStatus, updateNotificationViewNotificationStatus, updateNotificationViewTabState, updateNotificationViewCurrentTabAndSubTab, updateNotificationViewSubTab, updateNotificationViewUIState, getNotificationView, getExternalNotificationsForSelectedSubTab, getNotificationsForSelectedSubTab, };
904
909
  export { clearFeatureNotificationView, fetchRegisteredInterests, notifyMeForFeature, getFeatureNotificationView, getRegisteredInterests, getRegisteredInterestsByFeature, isFeatureInterestRegistered, FeatureInterest, FeatureNotificationViewState, };
905
910
  export { pushToastNotification, ToastNotification, ToastNotificationPayload, getLastNotificationTime, getNotifications, };
911
+ export { getEffectiveNotificationPreferences, getNotificationLocalOverrides, getNotificationPreferencesSaveState, getNotificationRegistry, hasUnsavedNotificationPreferences, NotificationChannel, NotificationFrequency, NotificationPreferences, NotificationRegistry, RegistryNotificationEvent, RegistryNotificationGroup, clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, setGroupFrequency, toggleEventChannel, toNotificationChannel, toNotificationFrequency, };
906
912
  export { getReferralListView, getInviteFormView, ReferralListSelectorView, ReferralViewState, InviteCompanyLocalData, ReferralInvitation, ReferralListViewSortKey, ReferralViewUIState, toReferralListViewSortKeyType, ReferralStatus, ReferralAmountStatus, StatusTypes, AmountStatusTypes, DEFAULT_REFERRER_AMOUNT, fetchReferrals, sendReferralInvite, clearReferrals, saveReferralFormDataInLocalStore, updateReferralListSortUiState, resendReferralInvite, fetchRewardsPlan, RewardsPlanCardReport, getRewardsPlanCard, updateReferViewed, RewardsPlanData, };
907
913
  export { ALL_WEEK_DAYS, DayOfWeek, RecurringDatePickerOptions, RecurringFrequencyType, SEMI_WEEKLY_REQUIRED_DAYS_COUNT, getMinAllowedEndDate, getRecurringEndDateFromCount, toDayOfWeek, toRecurringFrequency, };
908
914
  export { fetchCockpitContext, fetchCompanyTaskManagerView, fetchTaskManagerMetrics, CompanyTaskManagerSelectorView, CompanyTaskManagerViewUIState, TaskManagerMetrics, getCompanyTaskManagerView, TaskWithCompanyDetail, createTaskFromTaskGroupTemplate, TaskGroupTemplate, };