• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ringcentral / ringcentral-js-widgets / 9984535799

18 Jul 2024 02:28AM UTC coverage: 63.055% (+1.7%) from 61.322%
9984535799

push

github

web-flow
misc: sync features and bugfixes from f39b7a45 (#1747)

* misc: sync features and bugfixes from ba8d789

* misc: add i18n-dayjs and react-hooks packages

* version to 0.15.0

* chore: update crius

* misc: sync from f39b7a45

* chore: fix tests

* chore: fix tests

* chore: run test with --updateSnapshot

9782 of 17002 branches covered (57.53%)

Branch coverage included in aggregate %.

2206 of 3368 new or added lines in 501 files covered. (65.5%)

219 existing lines in 52 files now uncovered.

16601 of 24839 relevant lines covered (66.83%)

178566.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.59
/packages/ringcentral-integration/modules/UserGuide/UserGuide.ts
1
import {
2
  action,
3
  computed,
4
  RcModuleV2,
5
  state,
6
  storage,
7
  track,
8
  watch,
9
} from '@ringcentral-integration/core';
10
import { includes } from 'ramda';
11

12
import { trackEvents } from '../../enums/trackEvents';
13
import { Module } from '../../lib/di';
14
import { proxify } from '../../lib/proxy/proxify';
15

16
import type {
17
  CarouselOptions,
18
  CarouselState,
19
  Deps,
20
  Guides,
21
} from './UserGuide.interface';
22

23
export const SUPPORTED_LOCALES = {
202✔
24
  'en-US': 'en-US',
25
  'fr-CA': 'fr-CA',
26
};
27

28
function getFileName(fileUrl: string) {
29
  return fileUrl.split('\\').pop()?.split('/').pop();
1✔
30
}
31

32
// Since file name has included file MD5, any file name change means file change
33
function anyFileDiff(urls1: string[], urls2: string[]) {
34
  const files1 = (urls1 ?? []).map((url) => getFileName(url));
711✔
35
  const files2 = (urls2 ?? []).map((url) => getFileName(url));
711✔
36
  return JSON.stringify(files1) !== JSON.stringify(files2);
711✔
37
}
38

39
@Module({
40
  name: 'UserGuide',
41
  deps: [
42
    'Auth',
43
    'Locale',
44
    'Storage',
45
    'Webphone',
46
    'AppFeatures',
47
    'Brand',
48
    { dep: 'UserGuideOptions', optional: true },
49
  ],
50
})
51
export class UserGuide extends RcModuleV2<Deps> {
52
  constructor(deps: Deps) {
53
    super({
346✔
54
      deps,
55
      enableCache: true,
56
      storageKey: 'UserGuide',
57
    });
58
  }
59

60
  @storage
61
  @state
62
  allGuides: { [key: string]: Guides } = {};
346✔
63

64
  @state
65
  carouselState: CarouselState = {
346✔
66
    curIdx: 0,
67
    entered: false,
68
    playing: false,
69
  };
70

71
  @state
72
  preLoadImageStatus = false;
346✔
73

74
  @state
75
  firstLogin = false;
346✔
76

77
  @action
78
  setPreLoadImageStatus() {
79
    this.preLoadImageStatus = true;
356✔
80
  }
81

82
  @action
83
  setGuides(guides: Guides) {
84
    const oldGuides = this.allGuides[this._deps.brand.code] ?? {};
356✔
85
    this.allGuides[this._deps.brand.code] = guides;
356✔
86
    for (const locale of Object.keys(SUPPORTED_LOCALES)) {
356✔
87
      if (anyFileDiff(guides[locale], oldGuides[locale])) {
711✔
88
        this.start({ firstLogin: true });
1✔
89
        break;
1✔
90
      }
91
    }
92
  }
93

94
  @action
95
  _migrateGuides() {
96
    if (!this.allGuides[this._deps.brand.code]) {
343!
97
      this.allGuides[this._deps.brand.code] = {};
343✔
98
    }
99
    Object.keys(SUPPORTED_LOCALES).forEach((locale) => {
343✔
100
      const allGuides: Guides = this.allGuides as any;
686✔
101
      if (allGuides[locale]) {
686✔
102
        this.allGuides[this._deps.brand.code][locale] = allGuides[locale];
2✔
103
        delete allGuides[locale];
2✔
104
      }
105
    });
106
  }
107

108
  @track((that: UserGuide, options: Required<CarouselOptions>) => {
109
    if (options.curIdx === 0 && options.playing) {
×
110
      return [trackEvents.whatsNew];
×
111
    }
112
  })
113
  @action
114
  setCarousel({ firstLogin, ...carouselState }: Required<CarouselOptions>) {
115
    this.carouselState = carouselState;
1✔
116
    this.firstLogin = firstLogin;
1✔
117
  }
118

119
  override async onInitSuccess() {
120
    if (this.hasPermission) {
346!
121
      await this.initUserGuide();
346✔
122
    }
123
  }
124

125
  override _shouldInit() {
126
    return !!(
183,182✔
127
      this.pending &&
429,676✔
128
      this._deps.auth.ready &&
129
      this._deps.auth.loggedIn &&
130
      this._deps.locale.ready &&
131
      this._deps.storage.ready &&
132
      this._deps.appFeatures.ready &&
133
      this._deps.brand.ready
134
    );
135
  }
136

137
  override _shouldReset() {
138
    return !!(
182,836✔
139
      this.ready &&
565,254✔
140
      (!this._deps.auth.ready ||
141
        !this._deps.locale.ready ||
142
        !this._deps.storage.ready ||
143
        !this._deps.appFeatures.ready ||
144
        !this._deps.brand.ready)
145
    );
146
  }
147

148
  override onInitOnce() {
149
    this._migrateGuides();
342✔
150
    // When there is an incoming call,
151
    // the guide should be dismissed
152
    watch(
342✔
153
      this,
154
      () => this._deps.webphone.ringSession,
141,635✔
155
      (ringSession) => {
156
        if (this._deps.webphone.ready && ringSession) {
299✔
157
          this.dismiss();
210✔
158
        }
159
      },
160
    );
161
    watch(
342✔
162
      this,
163
      () => this._deps.brand.brandConfig,
141,635✔
164
      async () => {
165
        if (this.hasPermission) {
9!
166
          await this.initUserGuide();
9✔
167
        }
168
      },
169
    );
170
  }
171

172
  @proxify
173
  async _preLoadImage(url: string) {
174
    await new Promise((resolve, reject) => {
×
175
      const img = new Image();
×
176
      img.src = url;
×
177
      img.onload = resolve;
×
178
      img.onerror = reject;
×
179
    });
180
  }
181

182
  @proxify
183
  async preLoadImage() {
184
    const url = this.guides[0];
355✔
185
    if (url) {
355!
NEW
186
      try {
×
NEW
187
        await this._preLoadImage(url);
×
188
      } catch (ex) {
NEW
189
        console.warn('[UserGuide] Preload image failed', ex);
×
190
      }
191
    }
192
    this.setPreLoadImageStatus();
355✔
193
  }
194

195
  resolveGuides(): Record<string, string[]> {
196
    const images =
197
      (this._deps.brand.brandConfig.assets?.guides as string[]) || [];
355✔
198

199
    const locales = Object.keys(SUPPORTED_LOCALES);
355✔
200
    return images.reduce<Record<string, string[]>>((acc, curr: string) => {
355✔
201
      locales.forEach((locale) => {
×
202
        if (!acc[locale]) {
×
203
          acc[locale] = [];
×
204
        }
205
        if (includes(locale, curr)) {
×
206
          acc[locale].push(curr);
×
207
        }
208
      });
209
      return acc;
×
210
    }, {});
211
  }
212

213
  @proxify
214
  async dismiss() {
215
    if (
210!
216
      this.carouselState.curIdx !== 0 ||
840✔
217
      this.carouselState.playing ||
218
      this.carouselState.entered ||
219
      this.firstLogin
220
    ) {
221
      this.updateCarousel({
×
222
        curIdx: 0,
223
        entered: false,
224
        playing: false,
225
        firstLogin: false,
226
      });
227
    }
228
  }
229

230
  @proxify
231
  async updateCarousel({
232
    curIdx,
233
    entered,
234
    playing,
235
    firstLogin = this.firstLogin,
×
236
  }: CarouselOptions) {
237
    this.setCarousel({
×
238
      curIdx,
239
      entered,
240
      playing,
241
      firstLogin,
242
    });
243
  }
244

245
  get hasPermission() {
246
    // For extensions without calling or read message permissions, most of the content in
247
    // the user guide is not applicable to them. So we should not show the user guide for
248
    // these extensions.
249
    return (
421✔
250
      this._deps.appFeatures.isCallingEnabled ||
421!
251
      this._deps.appFeatures.hasReadMessagesPermission
252
    );
253
  }
254

255
  @proxify
256
  async initUserGuide() {
257
    const guides = this.resolveGuides();
355✔
258
    // Determine if it needs to be displayed when first log in,
259
    // the principles behind this is to use webpack's file hash,
260
    // i.e. if any of the guide files is changed, the file name hash
261
    // will be changed as well, in this case, it will be displayed.
262
    if (guides) {
355!
263
      this.setGuides(guides);
355✔
264
      await this.preLoadImage();
355✔
265
    }
266
  }
267

268
  @proxify
269
  async start({ firstLogin = false } = {}) {
×
270
    // Start guides only when images are ready
271
    this.setCarousel({
×
272
      curIdx: 0,
273
      entered: true,
274
      playing: true,
275
      firstLogin,
276
    });
277
  }
278

279
  @computed((that: UserGuide) => [
59,661✔
280
    that._deps.locale.ready,
281
    that.allGuides[that._deps.brand.code],
282
    that._deps.locale.currentLocale,
283
  ])
284
  get guides() {
285
    if (!this._deps.locale.ready || !this._deps.auth.loggedIn) {
1,043✔
286
      return [];
505✔
287
    }
288
    const brandGuides = this.allGuides[this._deps.brand.code];
538✔
289
    if (brandGuides) {
538!
290
      const currentGuides = brandGuides[this._deps.locale.currentLocale];
538✔
291
      if (currentGuides && currentGuides.length > 0) {
538!
292
        return currentGuides;
×
293
      }
294
      return brandGuides[SUPPORTED_LOCALES['en-US']] || [];
538✔
295
    }
UNCOV
296
    return [];
×
297
  }
298

299
  get started() {
300
    return this.carouselState.entered && this.carouselState.playing;
×
301
  }
302
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc