diff options
Diffstat (limited to 'src/stores')
-rw-r--r-- | src/stores/FeaturesStore.js | 18 | ||||
-rw-r--r-- | src/stores/PaymentStore.js | 68 | ||||
-rw-r--r-- | src/stores/ServicesStore.js | 34 | ||||
-rw-r--r-- | src/stores/UserStore.js | 65 | ||||
-rw-r--r-- | src/stores/index.js | 6 |
5 files changed, 1 insertions, 190 deletions
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js index 2fee9bdda..ac623c258 100644 --- a/src/stores/FeaturesStore.js +++ b/src/stores/FeaturesStore.js | |||
@@ -1,15 +1,12 @@ | |||
1 | import { | 1 | import { |
2 | computed, | 2 | computed, |
3 | observable, | 3 | observable, |
4 | reaction, | ||
5 | runInAction, | 4 | runInAction, |
6 | } from 'mobx'; | 5 | } from 'mobx'; |
7 | 6 | ||
8 | import Store from './lib/Store'; | 7 | import Store from './lib/Store'; |
9 | import CachedRequest from './lib/CachedRequest'; | 8 | import CachedRequest from './lib/CachedRequest'; |
10 | 9 | ||
11 | import delayApp from '../features/delayApp'; | ||
12 | import spellchecker from '../features/spellchecker'; | ||
13 | import serviceProxy from '../features/serviceProxy'; | 10 | import serviceProxy from '../features/serviceProxy'; |
14 | import basicAuth from '../features/basicAuth'; | 11 | import basicAuth from '../features/basicAuth'; |
15 | import workspaces from '../features/workspaces'; | 12 | import workspaces from '../features/workspaces'; |
@@ -19,12 +16,9 @@ import publishDebugInfo from '../features/publishDebugInfo'; | |||
19 | import shareFranz from '../features/shareFranz'; | 16 | import shareFranz from '../features/shareFranz'; |
20 | import announcements from '../features/announcements'; | 17 | import announcements from '../features/announcements'; |
21 | import settingsWS from '../features/settingsWS'; | 18 | import settingsWS from '../features/settingsWS'; |
22 | import serviceLimit from '../features/serviceLimit'; | ||
23 | import communityRecipes from '../features/communityRecipes'; | 19 | import communityRecipes from '../features/communityRecipes'; |
24 | import todos from '../features/todos'; | 20 | import todos from '../features/todos'; |
25 | import appearance from '../features/appearance'; | 21 | import appearance from '../features/appearance'; |
26 | import planSelection from '../features/planSelection'; | ||
27 | import trialStatusBar from '../features/trialStatusBar'; | ||
28 | 22 | ||
29 | import { DEFAULT_FEATURES_CONFIG } from '../config'; | 23 | import { DEFAULT_FEATURES_CONFIG } from '../config'; |
30 | 24 | ||
@@ -43,13 +37,6 @@ export default class FeaturesStore extends Store { | |||
43 | 37 | ||
44 | await this.featuresRequest._promise; | 38 | await this.featuresRequest._promise; |
45 | setTimeout(this._setupFeatures.bind(this), 1); | 39 | setTimeout(this._setupFeatures.bind(this), 1); |
46 | |||
47 | // single key reaction | ||
48 | reaction(() => this.stores.user.data.isPremium, () => { | ||
49 | if (this.stores.user.isLoggedIn) { | ||
50 | this.featuresRequest.invalidate({ immediately: true }); | ||
51 | } | ||
52 | }); | ||
53 | } | 40 | } |
54 | 41 | ||
55 | @computed get anonymousFeatures() { | 42 | @computed get anonymousFeatures() { |
@@ -80,8 +67,6 @@ export default class FeaturesStore extends Store { | |||
80 | } | 67 | } |
81 | 68 | ||
82 | _setupFeatures() { | 69 | _setupFeatures() { |
83 | delayApp(this.stores, this.actions); | ||
84 | spellchecker(this.stores, this.actions); | ||
85 | serviceProxy(this.stores, this.actions); | 70 | serviceProxy(this.stores, this.actions); |
86 | basicAuth(this.stores, this.actions); | 71 | basicAuth(this.stores, this.actions); |
87 | workspaces(this.stores, this.actions); | 72 | workspaces(this.stores, this.actions); |
@@ -91,11 +76,8 @@ export default class FeaturesStore extends Store { | |||
91 | shareFranz(this.stores, this.actions); | 76 | shareFranz(this.stores, this.actions); |
92 | announcements(this.stores, this.actions); | 77 | announcements(this.stores, this.actions); |
93 | settingsWS(this.stores, this.actions); | 78 | settingsWS(this.stores, this.actions); |
94 | serviceLimit(this.stores, this.actions); | ||
95 | communityRecipes(this.stores, this.actions); | 79 | communityRecipes(this.stores, this.actions); |
96 | todos(this.stores, this.actions); | 80 | todos(this.stores, this.actions); |
97 | appearance(this.stores, this.actions); | 81 | appearance(this.stores, this.actions); |
98 | planSelection(this.stores, this.actions); | ||
99 | trialStatusBar(this.stores, this.actions); | ||
100 | } | 82 | } |
101 | } | 83 | } |
diff --git a/src/stores/PaymentStore.js b/src/stores/PaymentStore.js deleted file mode 100644 index 05bb5b3d0..000000000 --- a/src/stores/PaymentStore.js +++ /dev/null | |||
@@ -1,68 +0,0 @@ | |||
1 | import { action, observable, computed } from 'mobx'; | ||
2 | import { BrowserWindow, getCurrentWindow } from '@electron/remote'; | ||
3 | |||
4 | import Store from './lib/Store'; | ||
5 | import CachedRequest from './lib/CachedRequest'; | ||
6 | import Request from './lib/Request'; | ||
7 | |||
8 | export default class PaymentStore extends Store { | ||
9 | @observable plansRequest = new CachedRequest(this.api.payment, 'plans'); | ||
10 | |||
11 | @observable createHostedPageRequest = new Request(this.api.payment, 'getHostedPage'); | ||
12 | |||
13 | constructor(...args) { | ||
14 | super(...args); | ||
15 | |||
16 | this.actions.payment.createHostedPage.listen(this._createHostedPage.bind(this)); | ||
17 | this.actions.payment.upgradeAccount.listen(this._upgradeAccount.bind(this)); | ||
18 | } | ||
19 | |||
20 | @computed get plan() { | ||
21 | if (this.plansRequest.isError) { | ||
22 | return {}; | ||
23 | } | ||
24 | return this.plansRequest.execute().result || {}; | ||
25 | } | ||
26 | |||
27 | @action _createHostedPage({ planId }) { | ||
28 | const request = this.createHostedPageRequest.execute(planId); | ||
29 | |||
30 | return request; | ||
31 | } | ||
32 | |||
33 | @action _upgradeAccount({ planId, onCloseWindow = () => null }) { | ||
34 | let hostedPageURL = this.stores.features.features.subscribeURL; | ||
35 | |||
36 | const parsedUrl = new URL(hostedPageURL); | ||
37 | const params = new URLSearchParams(parsedUrl.search.slice(1)); | ||
38 | |||
39 | params.set('plan', planId); | ||
40 | |||
41 | hostedPageURL = this.stores.user.getAuthURL(`${parsedUrl.origin}${parsedUrl.pathname}?${params.toString()}`); | ||
42 | |||
43 | const win = new BrowserWindow({ | ||
44 | parent: getCurrentWindow(), | ||
45 | modal: true, | ||
46 | title: '🔒 Upgrade Your Franz Account', | ||
47 | width: 800, | ||
48 | height: window.innerHeight - 100, | ||
49 | maxWidth: 800, | ||
50 | minWidth: 600, | ||
51 | autoHideMenuBar: true, | ||
52 | webPreferences: { | ||
53 | nodeIntegration: true, | ||
54 | webviewTag: true, | ||
55 | enableRemoteModule: true, | ||
56 | contextIsolation: false, | ||
57 | }, | ||
58 | }); | ||
59 | win.loadURL(`file://${__dirname}/../index.html#/payment/${encodeURIComponent(hostedPageURL)}`); | ||
60 | |||
61 | win.on('closed', () => { | ||
62 | this.stores.user.getUserInfoRequest.invalidate({ immediately: true }); | ||
63 | this.stores.features.featuresRequest.invalidate({ immediately: true }); | ||
64 | |||
65 | onCloseWindow(); | ||
66 | }); | ||
67 | } | ||
68 | } | ||
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index 9b69cb7c6..9521f8493 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js | |||
@@ -18,8 +18,6 @@ import { matchRoute } from '../helpers/routing-helpers'; | |||
18 | import { isInTimeframe } from '../helpers/schedule-helpers'; | 18 | import { isInTimeframe } from '../helpers/schedule-helpers'; |
19 | import { getRecipeDirectory, getDevRecipeDirectory } from '../helpers/recipe-helpers'; | 19 | import { getRecipeDirectory, getDevRecipeDirectory } from '../helpers/recipe-helpers'; |
20 | import { workspaceStore } from '../features/workspaces'; | 20 | import { workspaceStore } from '../features/workspaces'; |
21 | import { serviceLimitStore } from '../features/serviceLimit'; | ||
22 | import { RESTRICTION_TYPES } from '../models/Service'; | ||
23 | import { KEEP_WS_LOADED_USID } from '../config'; | 21 | import { KEEP_WS_LOADED_USID } from '../config'; |
24 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; | 22 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; |
25 | 23 | ||
@@ -94,7 +92,6 @@ export default class ServicesStore extends Store { | |||
94 | this._saveActiveService.bind(this), | 92 | this._saveActiveService.bind(this), |
95 | this._logoutReaction.bind(this), | 93 | this._logoutReaction.bind(this), |
96 | this._handleMuteSettings.bind(this), | 94 | this._handleMuteSettings.bind(this), |
97 | this._restrictServiceAccess.bind(this), | ||
98 | this._checkForActiveService.bind(this), | 95 | this._checkForActiveService.bind(this), |
99 | ]); | 96 | ]); |
100 | 97 | ||
@@ -297,8 +294,6 @@ export default class ServicesStore extends Store { | |||
297 | async _createService({ | 294 | async _createService({ |
298 | recipeId, serviceData, redirect = true, skipCleanup = false, | 295 | recipeId, serviceData, redirect = true, skipCleanup = false, |
299 | }) { | 296 | }) { |
300 | if (serviceLimitStore.userHasReachedServiceLimit) return; | ||
301 | |||
302 | if (!this.stores.recipes.isInstalled(recipeId)) { | 297 | if (!this.stores.recipes.isInstalled(recipeId)) { |
303 | debug(`Recipe "${recipeId}" is not installed, installing recipe`); | 298 | debug(`Recipe "${recipeId}" is not installed, installing recipe`); |
304 | await this.stores.recipes._install({ recipeId }); | 299 | await this.stores.recipes._install({ recipeId }); |
@@ -961,35 +956,6 @@ export default class ServicesStore extends Store { | |||
961 | return serviceData; | 956 | return serviceData; |
962 | } | 957 | } |
963 | 958 | ||
964 | _restrictServiceAccess() { | ||
965 | const { features } = this.stores.features; | ||
966 | const { userHasReachedServiceLimit, serviceLimit } = this.stores.serviceLimit; | ||
967 | |||
968 | this.all.map((service, index) => { | ||
969 | if (userHasReachedServiceLimit) { | ||
970 | service.isServiceAccessRestricted = index >= serviceLimit; | ||
971 | |||
972 | if (service.isServiceAccessRestricted) { | ||
973 | service.restrictionType = RESTRICTION_TYPES.SERVICE_LIMIT; | ||
974 | |||
975 | debug('Restricting access to server due to service limit'); | ||
976 | } | ||
977 | } | ||
978 | |||
979 | if (service.isUsingCustomUrl) { | ||
980 | service.isServiceAccessRestricted = !features.isCustomUrlIncludedInCurrentPlan; | ||
981 | |||
982 | if (service.isServiceAccessRestricted) { | ||
983 | service.restrictionType = RESTRICTION_TYPES.CUSTOM_URL; | ||
984 | |||
985 | debug('Restricting access to server due to custom url'); | ||
986 | } | ||
987 | } | ||
988 | |||
989 | return service; | ||
990 | }); | ||
991 | } | ||
992 | |||
993 | _checkForActiveService() { | 959 | _checkForActiveService() { |
994 | if (!this.stores.router.location || this.stores.router.location.pathname.includes('auth/signup')) { | 960 | if (!this.stores.router.location || this.stores.router.location.pathname.includes('auth/signup')) { |
995 | return; | 961 | return; |
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index 7947e5a27..8a525c2ef 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js | |||
@@ -2,16 +2,13 @@ import { observable, computed, action } from 'mobx'; | |||
2 | import moment from 'moment'; | 2 | import moment from 'moment'; |
3 | import jwt from 'jsonwebtoken'; | 3 | import jwt from 'jsonwebtoken'; |
4 | import localStorage from 'mobx-localstorage'; | 4 | import localStorage from 'mobx-localstorage'; |
5 | import ms from 'ms'; | ||
6 | import { session } from '@electron/remote'; | 5 | import { session } from '@electron/remote'; |
7 | 6 | ||
8 | import { isDevMode } from '../environment'; | 7 | import { isDevMode } from '../environment'; |
9 | import Store from './lib/Store'; | 8 | import Store from './lib/Store'; |
10 | import Request from './lib/Request'; | 9 | import Request from './lib/Request'; |
11 | import CachedRequest from './lib/CachedRequest'; | 10 | import CachedRequest from './lib/CachedRequest'; |
12 | import { sleep } from '../helpers/async-helpers'; | 11 | import { TODOS_PARTITION_ID } from '../config'; |
13 | import { getPlan } from '../helpers/plan-helpers'; | ||
14 | import { PLANS, TODOS_PARTITION_ID } from '../config'; | ||
15 | 12 | ||
16 | const debug = require('debug')('Ferdi:UserStore'); | 13 | const debug = require('debug')('Ferdi:UserStore'); |
17 | 14 | ||
@@ -27,8 +24,6 @@ export default class UserStore extends Store { | |||
27 | 24 | ||
28 | SIGNUP_ROUTE = `${this.BASE_ROUTE}/signup`; | 25 | SIGNUP_ROUTE = `${this.BASE_ROUTE}/signup`; |
29 | 26 | ||
30 | PRICING_ROUTE = `${this.BASE_ROUTE}/signup/pricing`; | ||
31 | |||
32 | SETUP_ROUTE = `${this.BASE_ROUTE}/signup/setup`; | 27 | SETUP_ROUTE = `${this.BASE_ROUTE}/signup/setup`; |
33 | 28 | ||
34 | IMPORT_ROUTE = `${this.BASE_ROUTE}/signup/import`; | 29 | IMPORT_ROUTE = `${this.BASE_ROUTE}/signup/import`; |
@@ -45,8 +40,6 @@ export default class UserStore extends Store { | |||
45 | 40 | ||
46 | @observable passwordRequest = new Request(this.api.user, 'password'); | 41 | @observable passwordRequest = new Request(this.api.user, 'password'); |
47 | 42 | ||
48 | @observable activateTrialRequest = new Request(this.api.user, 'activateTrial'); | ||
49 | |||
50 | @observable inviteRequest = new Request(this.api.user, 'invite'); | 43 | @observable inviteRequest = new Request(this.api.user, 'invite'); |
51 | 44 | ||
52 | @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo'); | 45 | @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo'); |
@@ -71,8 +64,6 @@ export default class UserStore extends Store { | |||
71 | 64 | ||
72 | @observable hasCompletedSignup = false; | 65 | @observable hasCompletedSignup = false; |
73 | 66 | ||
74 | @observable hasActivatedTrial = false; | ||
75 | |||
76 | @observable userData = {}; | 67 | @observable userData = {}; |
77 | 68 | ||
78 | @observable actionStatus = []; | 69 | @observable actionStatus = []; |
@@ -93,7 +84,6 @@ export default class UserStore extends Store { | |||
93 | this.actions.user.retrievePassword.listen(this._retrievePassword.bind(this)); | 84 | this.actions.user.retrievePassword.listen(this._retrievePassword.bind(this)); |
94 | this.actions.user.logout.listen(this._logout.bind(this)); | 85 | this.actions.user.logout.listen(this._logout.bind(this)); |
95 | this.actions.user.signup.listen(this._signup.bind(this)); | 86 | this.actions.user.signup.listen(this._signup.bind(this)); |
96 | this.actions.user.activateTrial.listen(this._activateTrial.bind(this)); | ||
97 | this.actions.user.invite.listen(this._invite.bind(this)); | 87 | this.actions.user.invite.listen(this._invite.bind(this)); |
98 | this.actions.user.update.listen(this._update.bind(this)); | 88 | this.actions.user.update.listen(this._update.bind(this)); |
99 | this.actions.user.resetStatus.listen(this._resetStatus.bind(this)); | 89 | this.actions.user.resetStatus.listen(this._resetStatus.bind(this)); |
@@ -104,7 +94,6 @@ export default class UserStore extends Store { | |||
104 | this.registerReactions([ | 94 | this.registerReactions([ |
105 | this._requireAuthenticatedUser.bind(this), | 95 | this._requireAuthenticatedUser.bind(this), |
106 | this._getUserData.bind(this), | 96 | this._getUserData.bind(this), |
107 | this._resetTrialActivationState.bind(this), | ||
108 | ]); | 97 | ]); |
109 | } | 98 | } |
110 | 99 | ||
@@ -126,10 +115,6 @@ export default class UserStore extends Store { | |||
126 | return this.SIGNUP_ROUTE; | 115 | return this.SIGNUP_ROUTE; |
127 | } | 116 | } |
128 | 117 | ||
129 | get pricingRoute() { | ||
130 | return this.PRICING_ROUTE; | ||
131 | } | ||
132 | |||
133 | get setupRoute() { | 118 | get setupRoute() { |
134 | return this.SETUP_ROUTE; | 119 | return this.SETUP_ROUTE; |
135 | } | 120 | } |
@@ -172,31 +157,6 @@ export default class UserStore extends Store { | |||
172 | return this.data.team || null; | 157 | return this.data.team || null; |
173 | } | 158 | } |
174 | 159 | ||
175 | @computed get isPremium() { | ||
176 | return true; | ||
177 | } | ||
178 | |||
179 | @computed get isPremiumOverride() { | ||
180 | return ((!this.team || !this.team.plan) && this.isPremium) || (this.team && this.team.state === 'expired' && this.isPremium); | ||
181 | } | ||
182 | |||
183 | @computed get isPersonal() { | ||
184 | if (!this.team || !this.team.plan) return false; | ||
185 | const plan = getPlan(this.team.plan); | ||
186 | |||
187 | return plan === PLANS.PERSONAL; | ||
188 | } | ||
189 | |||
190 | @computed get isPro() { | ||
191 | return true; | ||
192 | // if (this.isPremiumOverride) return true; | ||
193 | |||
194 | // if (!this.team || (!this.team.plan || this.team.state === 'expired')) return false; | ||
195 | // const plan = getPlan(this.team.plan); | ||
196 | |||
197 | // return plan === PLANS.PRO || plan === PLANS.LEGACY; | ||
198 | } | ||
199 | |||
200 | @computed get legacyServices() { | 160 | @computed get legacyServices() { |
201 | return this.getLegacyServicesRequest.execute() || {}; | 161 | return this.getLegacyServicesRequest.execute() || {}; |
202 | } | 162 | } |
@@ -244,21 +204,6 @@ export default class UserStore extends Store { | |||
244 | this.actionStatus = request.result.status || []; | 204 | this.actionStatus = request.result.status || []; |
245 | } | 205 | } |
246 | 206 | ||
247 | @action async _activateTrial({ planId }) { | ||
248 | debug('activate trial', planId); | ||
249 | |||
250 | this.activateTrialRequest.execute({ | ||
251 | plan: planId, | ||
252 | }); | ||
253 | |||
254 | await this.activateTrialRequest._promise; | ||
255 | |||
256 | this.hasActivatedTrial = true; | ||
257 | |||
258 | this.stores.features.featuresRequest.invalidate({ immediately: true }); | ||
259 | this.stores.user.getUserInfoRequest.invalidate({ immediately: true }); | ||
260 | } | ||
261 | |||
262 | @action async _invite({ invites }) { | 207 | @action async _invite({ invites }) { |
263 | const data = invites.filter(invite => invite.email !== ''); | 208 | const data = invites.filter(invite => invite.email !== ''); |
264 | 209 | ||
@@ -386,14 +331,6 @@ export default class UserStore extends Store { | |||
386 | } | 331 | } |
387 | } | 332 | } |
388 | 333 | ||
389 | async _resetTrialActivationState() { | ||
390 | if (this.hasActivatedTrial) { | ||
391 | await sleep(ms('12s')); | ||
392 | |||
393 | this.hasActivatedTrial = false; | ||
394 | } | ||
395 | } | ||
396 | |||
397 | // Helpers | 334 | // Helpers |
398 | _parseToken(authToken) { | 335 | _parseToken(authToken) { |
399 | try { | 336 | try { |
diff --git a/src/stores/index.js b/src/stores/index.js index 4eeef7982..b6e481e8a 100644 --- a/src/stores/index.js +++ b/src/stores/index.js | |||
@@ -6,16 +6,13 @@ import ServicesStore from './ServicesStore'; | |||
6 | import RecipesStore from './RecipesStore'; | 6 | import RecipesStore from './RecipesStore'; |
7 | import RecipePreviewsStore from './RecipePreviewsStore'; | 7 | import RecipePreviewsStore from './RecipePreviewsStore'; |
8 | import UIStore from './UIStore'; | 8 | import UIStore from './UIStore'; |
9 | import PaymentStore from './PaymentStore'; | ||
10 | import NewsStore from './NewsStore'; | 9 | import NewsStore from './NewsStore'; |
11 | import RequestStore from './RequestStore'; | 10 | import RequestStore from './RequestStore'; |
12 | import GlobalErrorStore from './GlobalErrorStore'; | 11 | import GlobalErrorStore from './GlobalErrorStore'; |
13 | import { workspaceStore } from '../features/workspaces'; | 12 | import { workspaceStore } from '../features/workspaces'; |
14 | import { announcementsStore } from '../features/announcements'; | 13 | import { announcementsStore } from '../features/announcements'; |
15 | import { serviceLimitStore } from '../features/serviceLimit'; | ||
16 | import { communityRecipesStore } from '../features/communityRecipes'; | 14 | import { communityRecipesStore } from '../features/communityRecipes'; |
17 | import { todosStore } from '../features/todos'; | 15 | import { todosStore } from '../features/todos'; |
18 | import { planSelectionStore } from '../features/planSelection'; | ||
19 | 16 | ||
20 | export default (api, actions, router) => { | 17 | export default (api, actions, router) => { |
21 | const stores = {}; | 18 | const stores = {}; |
@@ -29,16 +26,13 @@ export default (api, actions, router) => { | |||
29 | recipes: new RecipesStore(stores, api, actions), | 26 | recipes: new RecipesStore(stores, api, actions), |
30 | recipePreviews: new RecipePreviewsStore(stores, api, actions), | 27 | recipePreviews: new RecipePreviewsStore(stores, api, actions), |
31 | ui: new UIStore(stores, api, actions), | 28 | ui: new UIStore(stores, api, actions), |
32 | payment: new PaymentStore(stores, api, actions), | ||
33 | news: new NewsStore(stores, api, actions), | 29 | news: new NewsStore(stores, api, actions), |
34 | requests: new RequestStore(stores, api, actions), | 30 | requests: new RequestStore(stores, api, actions), |
35 | globalError: new GlobalErrorStore(stores, api, actions), | 31 | globalError: new GlobalErrorStore(stores, api, actions), |
36 | workspaces: workspaceStore, | 32 | workspaces: workspaceStore, |
37 | announcements: announcementsStore, | 33 | announcements: announcementsStore, |
38 | serviceLimit: serviceLimitStore, | ||
39 | communityRecipes: communityRecipesStore, | 34 | communityRecipes: communityRecipesStore, |
40 | todos: todosStore, | 35 | todos: todosStore, |
41 | planSelection: planSelectionStore, | ||
42 | }); | 36 | }); |
43 | // Initialize all stores | 37 | // Initialize all stores |
44 | Object.keys(stores).forEach((name) => { | 38 | Object.keys(stores).forEach((name) => { |