@servicetitan/standalone-root 1.1.0 → 1.2.0

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 (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/standalone-root-provider/standalone-root-provider.d.ts +7 -1
  3. package/dist/components/standalone-root-provider/standalone-root-provider.d.ts.map +1 -1
  4. package/dist/components/standalone-root-provider/standalone-root-provider.js +43 -9
  5. package/dist/components/standalone-root-provider/standalone-root-provider.js.map +1 -1
  6. package/dist/stores/standalone-root.store.d.ts +39 -7
  7. package/dist/stores/standalone-root.store.d.ts.map +1 -1
  8. package/dist/stores/standalone-root.store.js +297 -63
  9. package/dist/stores/standalone-root.store.js.map +1 -1
  10. package/dist/utils/constants.d.ts +4 -0
  11. package/dist/utils/constants.d.ts.map +1 -0
  12. package/dist/utils/constants.js +4 -0
  13. package/dist/utils/constants.js.map +1 -0
  14. package/dist/utils/event-emitter.d.ts +9 -0
  15. package/dist/utils/event-emitter.d.ts.map +1 -0
  16. package/dist/utils/event-emitter.js +30 -0
  17. package/dist/utils/event-emitter.js.map +1 -0
  18. package/dist/utils/events.d.ts +4 -0
  19. package/dist/utils/events.d.ts.map +1 -0
  20. package/dist/utils/events.js +5 -0
  21. package/dist/utils/events.js.map +1 -0
  22. package/dist/utils/storage-keys.d.ts +8 -0
  23. package/dist/utils/storage-keys.d.ts.map +1 -0
  24. package/dist/utils/storage-keys.js +9 -0
  25. package/dist/utils/storage-keys.js.map +1 -0
  26. package/dist/utils/symbols.d.ts +4 -0
  27. package/dist/utils/symbols.d.ts.map +1 -0
  28. package/dist/utils/symbols.js +6 -0
  29. package/dist/utils/symbols.js.map +1 -0
  30. package/package.json +2 -2
  31. package/src/components/standalone-root-provider/standalone-root-provider.tsx +77 -14
  32. package/src/stores/standalone-root.store.ts +226 -83
  33. package/src/utils/constants.ts +4 -0
  34. package/src/utils/event-emitter.ts +27 -0
  35. package/src/utils/events.ts +3 -0
  36. package/src/utils/storage-keys.ts +9 -0
  37. package/src/utils/symbols.ts +6 -0
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -1,87 +1,83 @@
1
- import { inject, injectable, Store } from '@servicetitan/react-ioc';
2
1
  import { makeObservable, observable, runInAction } from 'mobx';
3
- import { TenantManagementApi, TenantModel } from '@servicetitan/standalone-tm-api';
4
- import { CurrentUserClientApi, UserDetails } from '@servicetitan/marketing-services-api/identity';
2
+
3
+ import {
4
+ CurrentUserClientApi,
5
+ PartitionInfo,
6
+ PartitionsClientApi,
7
+ UserDetails,
8
+ } from '@servicetitan/marketing-services-api/identity';
9
+ import { LoadStatus } from '@servicetitan/standalone-ui';
10
+ import { inject, injectable } from '@servicetitan/react-ioc';
5
11
  import {
6
12
  AppConfigApi,
7
13
  AppSettings,
8
14
  } from '@servicetitan/marketing-services-api/dist/common/app-config.mrk.api';
9
- import { LoadStatus } from '@servicetitan/standalone-ui';
10
-
11
- const symbolKey = 'standalone-root-symbol-key';
12
- const isInitializedSymbol: symbol = Symbol.for(symbolKey);
13
-
14
- const storageKey = 'standalone-root-cache';
15
- const storageKeyForTenant = `${storageKey}-tenant`;
16
- const storageKeyForConfig = `${storageKey}-config`;
17
- const storageKeyForUser = `${storageKey}-user`;
18
- const storageKeyForAppSettings = `${storageKey}-app-settings`;
19
- const storageKeyForLocale = `${storageKey}-locale`;
15
+ import { TenantManagementApi, TenantModel } from '@servicetitan/standalone-tm-api';
16
+ import { OnboardingApi, OnboardingResponse } from '@servicetitan/marketing-services-api/settings';
20
17
 
21
- const cultureKeyInCoreConfigs = 'Customers___Locations:Culture';
18
+ import { Events } from '../utils/events';
19
+ import { EventEmitter } from '../utils/event-emitter';
20
+ import {
21
+ storageKeyForUser,
22
+ storageKeyForLocale,
23
+ storageKeyForTenant,
24
+ storageKeyForOnboarding,
25
+ storageKeyForPartitions,
26
+ storageKeyForAppSettings,
27
+ storageKeyForTenantConfigs,
28
+ } from '../utils/storage-keys';
29
+ import { cultureKeyInCoreConfigs, defaultLocale, defaultTimezone } from '../utils/constants';
30
+ import { currentInstanceSymbol, eventEmitterSymbol, isInitializedSymbol } from '../utils/symbols';
22
31
 
23
32
  @injectable()
24
- export class StandaloneRootStore extends Store {
33
+ export class StandaloneRootStore {
25
34
  @observable loadStatus = LoadStatus.None;
26
35
  @observable tenant!: TenantModel;
27
36
  @observable tenantConfigs!: { [key: string]: string };
28
37
  @observable user!: UserDetails;
29
38
  @observable appSettings!: AppSettings;
30
- @observable timezone = 'UTC';
31
- @observable locale = 'en-US';
39
+ @observable onboarding!: OnboardingResponse;
40
+ @observable partitions!: PartitionInfo[];
41
+ @observable timezone = defaultTimezone;
42
+ @observable locale = defaultLocale;
43
+
44
+ private eventEmitter!: EventEmitter;
32
45
 
33
46
  constructor(
34
47
  @inject(TenantManagementApi) private readonly tenantManagementApi: TenantManagementApi,
35
48
  @inject(AppConfigApi) private readonly appConfigApi: AppConfigApi,
36
49
  @inject(CurrentUserClientApi) private readonly currentUserClientApi: CurrentUserClientApi,
50
+ @inject(OnboardingApi) private readonly onboardingApi: OnboardingApi,
51
+ @inject(PartitionsClientApi) private partitionsApi: PartitionsClientApi,
37
52
  ) {
38
- super();
53
+ this.initializeEventEmitter();
39
54
  makeObservable(this);
40
55
  }
41
56
 
42
- initialize = async () => {
57
+ initialize = async (forceRefresh: boolean, refreshOnboarding: boolean) => {
43
58
  try {
44
59
  runInAction(() => {
45
60
  this.loadStatus = LoadStatus.Loading;
46
61
  });
47
62
 
48
- const fetchData = async () => {
49
- const [
50
- tenantFetched,
51
- tenantConfigFetched,
52
- { data: userFetched },
53
- { data: appSettingsFetched },
54
- ] = await Promise.all([
55
- this.tenantManagementApi.client.tenant(undefined),
56
- this.tenantManagementApi.client.config(
57
- undefined,
58
- undefined,
59
- undefined,
60
- undefined,
61
- ),
62
- this.currentUserClientApi.getUserName(),
63
- this.appConfigApi.getAppSettings(),
63
+ const fetchAll = async () => {
64
+ await Promise.all([
65
+ this.fetchTenant(),
66
+ this.fetchTenantConfigs(),
67
+ this.fetchUser(),
68
+ this.fetchAppSettings(),
69
+ this.fetchOnboarding(),
70
+ this.fetchPartitions(),
64
71
  ]);
65
72
 
66
73
  const coreConfigs = await this.tenantManagementApi.client.configs2(
67
- tenantFetched.id,
68
- tenantFetched.id,
74
+ this.tenant.id,
75
+ this.tenant.id,
69
76
  undefined,
70
77
  [cultureKeyInCoreConfigs],
71
78
  );
72
- const localeFetched = coreConfigs[cultureKeyInCoreConfigs];
73
-
74
- localStorage.setItem(storageKeyForTenant, JSON.stringify(tenantFetched));
75
- localStorage.setItem(storageKeyForConfig, JSON.stringify(tenantConfigFetched));
76
- localStorage.setItem(storageKeyForUser, JSON.stringify(userFetched));
77
- localStorage.setItem(storageKeyForAppSettings, JSON.stringify(appSettingsFetched));
78
79
 
79
- runInAction(() => {
80
- this.tenant = tenantFetched;
81
- this.tenantConfigs = tenantConfigFetched;
82
- this.user = userFetched;
83
- this.appSettings = appSettingsFetched;
84
- });
80
+ const localeFetched = coreConfigs[cultureKeyInCoreConfigs];
85
81
 
86
82
  if (localeFetched) {
87
83
  localStorage.setItem(storageKeyForLocale, JSON.stringify(localeFetched));
@@ -95,44 +91,20 @@ export class StandaloneRootStore extends Store {
95
91
  };
96
92
 
97
93
  // checks if the root is initialized first time across the app
98
- if ((window as any)[isInitializedSymbol]) {
99
- const tenantCached = localStorage.getItem(storageKeyForTenant);
100
- const tenantConfigsCached = localStorage.getItem(storageKeyForConfig);
101
- const userCached = localStorage.getItem(storageKeyForUser);
102
- const appSettingsCached = localStorage.getItem(storageKeyForAppSettings);
103
- const localeCached = localStorage.getItem(storageKeyForLocale);
104
-
105
- if (
106
- tenantCached &&
107
- tenantConfigsCached &&
108
- userCached &&
109
- appSettingsCached &&
110
- localeCached
111
- ) {
112
- const tenantCachedParsed = JSON.parse(tenantCached) as typeof this.tenant;
113
- const tenantConfigsCachedParsed = JSON.parse(
114
- tenantConfigsCached,
115
- ) as typeof this.tenantConfigs;
116
- const userCachedParsed = JSON.parse(userCached) as typeof this.user;
117
- const appSettingsCachedParsed = JSON.parse(
118
- appSettingsCached,
119
- ) as typeof this.appSettings;
120
- const localeCachedParsed = JSON.parse(localeCached) as typeof this.locale;
94
+ if (this.isInitialized && !forceRefresh) {
95
+ const isSuccess = this.syncFromStorage();
121
96
 
122
- runInAction(() => {
123
- this.tenant = tenantCachedParsed;
124
- this.tenantConfigs = tenantConfigsCachedParsed;
125
- this.user = userCachedParsed;
126
- this.appSettings = appSettingsCachedParsed;
127
- this.locale = localeCachedParsed;
128
- });
97
+ if (isSuccess) {
98
+ if (refreshOnboarding) {
99
+ await this.fetchOnboarding();
100
+ }
129
101
  } else {
130
- await fetchData();
102
+ await fetchAll();
131
103
  }
132
104
  } else {
133
- await fetchData();
105
+ await fetchAll();
134
106
 
135
- (window as any)[isInitializedSymbol] = true;
107
+ this.setIsInitialized(true);
136
108
  }
137
109
 
138
110
  const timezoneFetched = this.tenantConfigs.TimeZone;
@@ -146,10 +118,181 @@ export class StandaloneRootStore extends Store {
146
118
  runInAction(() => {
147
119
  this.loadStatus = LoadStatus.Ok;
148
120
  });
121
+
122
+ this.registerGlobalSyncListener();
149
123
  } catch {
150
124
  runInAction(() => {
151
125
  this.loadStatus = LoadStatus.Error;
152
126
  });
153
127
  }
154
128
  };
129
+
130
+ refreshTenant = () => this.fetchTenant().then(this.emitGlobalSync);
131
+ refreshTenantConfigs = () => this.fetchTenantConfigs().then(this.emitGlobalSync);
132
+ refreshUser = () => this.fetchUser().then(this.emitGlobalSync);
133
+ refreshAppSettings = () => this.fetchAppSettings().then(this.emitGlobalSync);
134
+ refreshOnboarding = () => this.fetchOnboarding().then(this.emitGlobalSync);
135
+ refreshPartitions = () => this.fetchPartitions().then(this.emitGlobalSync);
136
+
137
+ get defaultMFEData() {
138
+ const { tenant, appSettings, timezone } = this;
139
+
140
+ return {
141
+ timezone,
142
+ partnerId: tenant.parentId,
143
+ partnerTenantId: tenant.externalId,
144
+ currentPartitionId: tenant.currentPartitionId,
145
+ featureFlagClientId: appSettings.featureFlagClientId,
146
+ };
147
+ }
148
+
149
+ unregisterGlobalSyncListener = () => {
150
+ this.eventEmitter.off(Events.SyncFromStorage, this.listenGlobalSync);
151
+ };
152
+
153
+ private initializeEventEmitter = () => {
154
+ const globalEventEmitter = (window as any)[eventEmitterSymbol];
155
+
156
+ if (globalEventEmitter) {
157
+ this.eventEmitter = globalEventEmitter;
158
+ } else {
159
+ (window as any)[eventEmitterSymbol] = new EventEmitter();
160
+ this.eventEmitter = (window as any)[eventEmitterSymbol];
161
+ }
162
+ };
163
+
164
+ private registerGlobalSyncListener = () => {
165
+ this.eventEmitter.on(Events.SyncFromStorage, this.listenGlobalSync);
166
+ };
167
+
168
+ private listenGlobalSync = (instanceSymbol: symbol) => {
169
+ if (instanceSymbol !== currentInstanceSymbol) {
170
+ this.syncFromStorage();
171
+ }
172
+ };
173
+
174
+ private emitGlobalSync = () => {
175
+ this.eventEmitter.emit(Events.SyncFromStorage, currentInstanceSymbol);
176
+ };
177
+
178
+ private syncFromStorage = () => {
179
+ const tenantCached = localStorage.getItem(storageKeyForTenant);
180
+ const tenantConfigsCached = localStorage.getItem(storageKeyForTenantConfigs);
181
+ const userCached = localStorage.getItem(storageKeyForUser);
182
+ const appSettingsCached = localStorage.getItem(storageKeyForAppSettings);
183
+ const onboardingCached = localStorage.getItem(storageKeyForOnboarding);
184
+ const partitionsCached = localStorage.getItem(storageKeyForPartitions);
185
+ const localeCached = localStorage.getItem(storageKeyForLocale);
186
+
187
+ if (
188
+ tenantCached &&
189
+ tenantConfigsCached &&
190
+ userCached &&
191
+ appSettingsCached &&
192
+ onboardingCached &&
193
+ partitionsCached &&
194
+ localeCached
195
+ ) {
196
+ const tenantCachedParsed = JSON.parse(tenantCached) as typeof this.tenant;
197
+ const tenantConfigsCachedParsed = JSON.parse(
198
+ tenantConfigsCached,
199
+ ) as typeof this.tenantConfigs;
200
+ const userCachedParsed = JSON.parse(userCached) as typeof this.user;
201
+ const appSettingsCachedParsed = JSON.parse(
202
+ appSettingsCached,
203
+ ) as typeof this.appSettings;
204
+ const onboardingCachedParsed = JSON.parse(onboardingCached) as typeof this.onboarding;
205
+ const partitionsCachedParsed = JSON.parse(partitionsCached) as typeof this.partitions;
206
+ const localeCachedParsed = JSON.parse(localeCached) as typeof this.locale;
207
+
208
+ runInAction(() => {
209
+ this.tenant = tenantCachedParsed;
210
+ this.tenantConfigs = tenantConfigsCachedParsed;
211
+ this.user = userCachedParsed;
212
+ this.appSettings = appSettingsCachedParsed;
213
+ this.onboarding = onboardingCachedParsed;
214
+ this.partitions = partitionsCachedParsed;
215
+ this.locale = localeCachedParsed;
216
+ });
217
+
218
+ return true;
219
+ }
220
+
221
+ return false;
222
+ };
223
+
224
+ private setIsInitialized = (value: boolean) => {
225
+ (window as any)[isInitializedSymbol] = value;
226
+ };
227
+
228
+ private get isInitialized() {
229
+ return !!(window as any)[isInitializedSymbol];
230
+ }
231
+
232
+ private fetchTenant = async () => {
233
+ const tenantFetched = await this.tenantManagementApi.client.tenant(undefined);
234
+
235
+ localStorage.setItem(storageKeyForTenant, JSON.stringify(tenantFetched));
236
+
237
+ runInAction(() => {
238
+ this.tenant = tenantFetched;
239
+ });
240
+ };
241
+
242
+ private fetchTenantConfigs = async () => {
243
+ const tenantConfigsFetched = await this.tenantManagementApi.client.config(
244
+ undefined,
245
+ undefined,
246
+ undefined,
247
+ undefined,
248
+ );
249
+
250
+ localStorage.setItem(storageKeyForTenantConfigs, JSON.stringify(tenantConfigsFetched));
251
+
252
+ runInAction(() => {
253
+ this.tenantConfigs = tenantConfigsFetched;
254
+ });
255
+ };
256
+
257
+ private fetchUser = async () => {
258
+ const { data: userFetched } = await this.currentUserClientApi.getUserName();
259
+
260
+ localStorage.setItem(storageKeyForUser, JSON.stringify(userFetched));
261
+
262
+ runInAction(() => {
263
+ this.user = userFetched;
264
+ });
265
+ };
266
+
267
+ private fetchAppSettings = async () => {
268
+ const { data: appSettingsFetched } = await this.appConfigApi.getAppSettings();
269
+
270
+ localStorage.setItem(storageKeyForAppSettings, JSON.stringify(appSettingsFetched));
271
+
272
+ runInAction(() => {
273
+ this.appSettings = appSettingsFetched;
274
+ });
275
+ };
276
+
277
+ private fetchOnboarding = async () => {
278
+ const { data: onboardingFetched } = await this.onboardingApi.getStatus();
279
+
280
+ localStorage.setItem(storageKeyForOnboarding, JSON.stringify(onboardingFetched));
281
+
282
+ runInAction(() => {
283
+ this.onboarding = onboardingFetched;
284
+ });
285
+ };
286
+
287
+ private fetchPartitions = async () => {
288
+ const { data: partitionsFetchedUnfiltered } =
289
+ await this.partitionsApi.userAssignedPartitions();
290
+ const partitionsFetched = partitionsFetchedUnfiltered.filter(Boolean) as PartitionInfo[];
291
+
292
+ localStorage.setItem(storageKeyForPartitions, JSON.stringify(partitionsFetched));
293
+
294
+ runInAction(() => {
295
+ this.partitions = partitionsFetched;
296
+ });
297
+ };
155
298
  }
@@ -0,0 +1,4 @@
1
+ export const defaultTimezone = 'UTC';
2
+ export const defaultLocale = 'en-US';
3
+
4
+ export const cultureKeyInCoreConfigs = 'Customers___Locations:Culture';
@@ -0,0 +1,27 @@
1
+ type Listener = (instanceSymbol: symbol) => void;
2
+
3
+ export class EventEmitter {
4
+ private events: Record<string, Listener[]> = {};
5
+
6
+ on(event: string, listener: Listener) {
7
+ if (!this.events[event]) {
8
+ this.events[event] = [];
9
+ }
10
+ this.events[event].push(listener);
11
+ return () => this.off(event, listener);
12
+ }
13
+
14
+ off(event: string, listener: Listener) {
15
+ if (!this.events[event]) {
16
+ return;
17
+ }
18
+ this.events[event] = this.events[event].filter(l => l !== listener);
19
+ }
20
+
21
+ emit(event: string, instanceSymbol: symbol) {
22
+ if (!this.events[event]) {
23
+ return;
24
+ }
25
+ this.events[event].forEach(listener => listener(instanceSymbol));
26
+ }
27
+ }
@@ -0,0 +1,3 @@
1
+ export enum Events {
2
+ SyncFromStorage = 'SyncFromStorage',
3
+ }
@@ -0,0 +1,9 @@
1
+ const storageKey = 'standalone-root-cache';
2
+
3
+ export const storageKeyForTenant = `${storageKey}-tenant`;
4
+ export const storageKeyForTenantConfigs = `${storageKey}-config`;
5
+ export const storageKeyForUser = `${storageKey}-user`;
6
+ export const storageKeyForAppSettings = `${storageKey}-app-settings`;
7
+ export const storageKeyForOnboarding = `${storageKey}-onboarding`;
8
+ export const storageKeyForPartitions = `${storageKey}-partitions`;
9
+ export const storageKeyForLocale = `${storageKey}-locale`;
@@ -0,0 +1,6 @@
1
+ const rootSymbolKey = 'standalone-root-symbol-key';
2
+ const eventEmitterSymbolKey = 'standalone-root-event-emitter-symbol-key';
3
+
4
+ export const isInitializedSymbol = Symbol.for(rootSymbolKey);
5
+ export const eventEmitterSymbol = Symbol.for(eventEmitterSymbolKey);
6
+ export const currentInstanceSymbol = Symbol();