@tma.js/sdk 1.2.1 → 1.4.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 (97) hide show
  1. package/dist/dts/index.d.ts +1 -1
  2. package/dist/dts/init/creators/createViewport.d.ts +2 -9
  3. package/dist/dts/init/init.d.ts +2 -0
  4. package/dist/dts/init/types.d.ts +7 -4
  5. package/dist/dts/launch-params/index.d.ts +1 -0
  6. package/dist/dts/launch-params/retrieveFromUrl.d.ts +6 -0
  7. package/dist/dts/launch-params/types.d.ts +12 -8
  8. package/dist/dts/types/platform.d.ts +1 -1
  9. package/dist/dts/viewport/index.d.ts +1 -0
  10. package/dist/dts/viewport/isStableViewportPlatform.d.ts +7 -0
  11. package/dist/index.cjs +1 -1
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.iife.js +1 -1
  14. package/dist/index.iife.js.map +1 -1
  15. package/dist/index.mjs +469 -467
  16. package/dist/index.mjs.map +1 -1
  17. package/package.json +2 -2
  18. package/src/__tests__/globals.ts +39 -0
  19. package/src/back-button/__tests__/BackButton.ts +129 -0
  20. package/src/bridge/__tests__/request.ts +236 -0
  21. package/src/bridge/env/__tests__/hasExternalNotify.ts +15 -0
  22. package/src/bridge/env/__tests__/hasWebviewProxy.ts +15 -0
  23. package/src/bridge/env/__tests__/isIframe.ts +30 -0
  24. package/src/bridge/events/__tests__/createEmitter.ts +143 -0
  25. package/src/bridge/events/__tests__/off.ts +34 -0
  26. package/src/bridge/events/__tests__/on.ts +49 -0
  27. package/src/bridge/events/__tests__/onTelegramEvent.ts +51 -0
  28. package/src/bridge/events/__tests__/once.ts +64 -0
  29. package/src/bridge/events/__tests__/singletonEmitter.ts +22 -0
  30. package/src/bridge/events/__tests__/subscribe.ts +49 -0
  31. package/src/bridge/events/__tests__/unsubscribe.ts +34 -0
  32. package/src/bridge/events/parsers/__tests__/clipboardTextReceived.ts +21 -0
  33. package/src/bridge/events/parsers/__tests__/invoiceClosed.ts +12 -0
  34. package/src/bridge/events/parsers/__tests__/popupClosed.ts +10 -0
  35. package/src/bridge/events/parsers/__tests__/qrTextReceived.ts +9 -0
  36. package/src/bridge/events/parsers/__tests__/theme-changed.ts +42 -0
  37. package/src/bridge/events/parsers/__tests__/viewportChanged.ts +49 -0
  38. package/src/bridge/methods/__tests__/createPostEvent.ts +37 -0
  39. package/src/bridge/methods/__tests__/postEvent.ts +137 -0
  40. package/src/classnames/__tests__/classNames.ts +20 -0
  41. package/src/classnames/__tests__/mergeClassNames.ts +21 -0
  42. package/src/closing-behavior/__tests__/ClosingBehavior.ts +86 -0
  43. package/src/colors/__tests__/isColorDark.ts +12 -0
  44. package/src/colors/__tests__/isRGB.ts +13 -0
  45. package/src/colors/__tests__/isRGBShort.ts +13 -0
  46. package/src/colors/__tests__/toRGB.ts +23 -0
  47. package/src/event-emitter/__tests__/EventEmitter.ts +145 -0
  48. package/src/haptic-feedback/__tests__/HapticFeedback.ts +68 -0
  49. package/src/index.ts +1 -0
  50. package/src/init/creators/__tests__/createViewport.ts +96 -0
  51. package/src/init/creators/createViewport.ts +60 -81
  52. package/src/init/init.ts +13 -15
  53. package/src/init/types.ts +8 -4
  54. package/src/init-data/__tests__/InitData.ts +98 -0
  55. package/src/init-data/__tests__/chatParser.ts +102 -0
  56. package/src/init-data/__tests__/initDataParser.ts +136 -0
  57. package/src/init-data/__tests__/parseInitData.ts +136 -0
  58. package/src/init-data/__tests__/userParser.ts +96 -0
  59. package/src/launch-params/__tests__/retrieveFromUrl.ts +19 -0
  60. package/src/launch-params/index.ts +1 -0
  61. package/src/launch-params/launchParamsParser.ts +4 -0
  62. package/src/launch-params/retrieveFromLocation.ts +2 -2
  63. package/src/launch-params/retrieveFromPerformance.ts +2 -7
  64. package/src/launch-params/retrieveFromUrl.ts +19 -0
  65. package/src/launch-params/types.ts +13 -8
  66. package/src/logger/__tests__/Logger.ts +107 -0
  67. package/src/main-button/__tests__/MainButton.ts +346 -0
  68. package/src/mini-app/__tests__/MiniApp.ts +140 -0
  69. package/src/misc/__tests__/isRecord.ts +21 -0
  70. package/src/navigation/HashNavigator/__tests__/HashNavigator.ts +144 -0
  71. package/src/navigation/HashNavigator/__tests__/drop.ts +42 -0
  72. package/src/navigation/HashNavigator/__tests__/go.ts +9 -0
  73. package/src/parsing/__tests__/ArrayValueParser.ts +18 -0
  74. package/src/parsing/__tests__/toRecord.ts +10 -0
  75. package/src/parsing/parsers/__tests__/array.ts +39 -0
  76. package/src/parsing/parsers/__tests__/boolean.ts +31 -0
  77. package/src/parsing/parsers/__tests__/date.ts +25 -0
  78. package/src/parsing/parsers/__tests__/json.ts +80 -0
  79. package/src/parsing/parsers/__tests__/number.ts +23 -0
  80. package/src/parsing/parsers/__tests__/rgb.ts +22 -0
  81. package/src/parsing/parsers/__tests__/searchParams.ts +105 -0
  82. package/src/parsing/parsers/__tests__/string.ts +25 -0
  83. package/src/popup/__tests__/Popup.ts +130 -0
  84. package/src/popup/__tests__/preparePopupParams.ts +85 -0
  85. package/src/supports/__tests__/supports.ts +123 -0
  86. package/src/theme-params/__tests__/keys.ts +19 -0
  87. package/src/theme-params/__tests__/parseThemeParams.ts +29 -0
  88. package/src/theme-params/__tests__/serializeThemeParams.ts +29 -0
  89. package/src/theme-params/__tests__/themeParamsParser.ts +29 -0
  90. package/src/timeout/__tests__/isTimeoutError.ts +9 -0
  91. package/src/timeout/__tests__/withTimeout.ts +28 -0
  92. package/src/types/platform.ts +2 -2
  93. package/src/version/__tests__/compareVersions.ts +19 -0
  94. package/src/viewport/__tests__/isStableViewportPlatform.ts +15 -0
  95. package/src/viewport/__tests__/utils.ts +12 -0
  96. package/src/viewport/index.ts +1 -0
  97. package/src/viewport/isStableViewportPlatform.ts +10 -0
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { ClosingBehavior } from '../ClosingBehavior';
4
+
5
+ describe('disable', () => {
6
+ it('should call "web_app_setup_closing_behavior" method with "need_confirmation" equal to false', () => {
7
+ const postEvent = vi.fn();
8
+ const confirmation = new ClosingBehavior(true, postEvent);
9
+
10
+ expect(postEvent).toHaveBeenCalledTimes(0);
11
+ confirmation.disableConfirmation();
12
+ expect(postEvent).toHaveBeenCalledTimes(1);
13
+ expect(postEvent).toHaveBeenCalledWith('web_app_setup_closing_behavior', { need_confirmation: false });
14
+ });
15
+
16
+ it('should emit "isConfirmationNeededChanged" event with false value', () => {
17
+ const confirmation = new ClosingBehavior(true, vi.fn());
18
+ const listener = vi.fn();
19
+
20
+ confirmation.on('change:isConfirmationNeeded', listener);
21
+ expect(listener).toHaveBeenCalledTimes(0);
22
+ confirmation.disableConfirmation();
23
+ expect(listener).toHaveBeenCalledTimes(1);
24
+ expect(listener).toHaveBeenCalledWith(false);
25
+ });
26
+ });
27
+
28
+ describe('enable', () => {
29
+ it('should call "web_app_setup_closing_behavior" method with "need_confirmation" equal to true', () => {
30
+ const postEvent = vi.fn();
31
+ const confirmation = new ClosingBehavior(false, postEvent);
32
+
33
+ expect(postEvent).toHaveBeenCalledTimes(0);
34
+ confirmation.enableConfirmation();
35
+ expect(postEvent).toHaveBeenCalledTimes(1);
36
+ expect(postEvent).toHaveBeenCalledWith('web_app_setup_closing_behavior', { need_confirmation: true });
37
+ });
38
+
39
+ it('should emit "isConfirmationNeededChanged" event with true value', () => {
40
+ const confirmation = new ClosingBehavior(false, vi.fn());
41
+ const listener = vi.fn();
42
+
43
+ confirmation.on('change:isConfirmationNeeded', listener);
44
+ expect(listener).toHaveBeenCalledTimes(0);
45
+ confirmation.enableConfirmation();
46
+ expect(listener).toHaveBeenCalledTimes(1);
47
+ expect(listener).toHaveBeenCalledWith(true);
48
+ });
49
+ });
50
+
51
+ describe('on', () => {
52
+ describe('"isConfirmationNeededChanged" event', () => {
53
+ it('should add event listener to event', () => {
54
+ const listener = vi.fn();
55
+ const confirmation = new ClosingBehavior(false, vi.fn());
56
+
57
+ confirmation.on('change:isConfirmationNeeded', listener);
58
+
59
+ expect(listener).toHaveBeenCalledTimes(0);
60
+ confirmation.enableConfirmation();
61
+ expect(listener).toHaveBeenCalledTimes(1);
62
+ });
63
+ });
64
+ });
65
+
66
+ describe('off', () => {
67
+ describe('"isConfirmationNeededChanged" event', () => {
68
+ it('should remove event listener from event', () => {
69
+ const listener = vi.fn();
70
+ const confirmation = new ClosingBehavior(false, vi.fn());
71
+
72
+ confirmation.on('change:isConfirmationNeeded', listener);
73
+
74
+ expect(listener).toHaveBeenCalledTimes(0);
75
+ confirmation.enableConfirmation();
76
+ expect(listener).toHaveBeenCalledTimes(1);
77
+
78
+ confirmation.off('change:isConfirmationNeeded', listener);
79
+ listener.mockClear();
80
+
81
+ expect(listener).toHaveBeenCalledTimes(0);
82
+ confirmation.disableConfirmation();
83
+ expect(listener).toHaveBeenCalledTimes(0);
84
+ });
85
+ });
86
+ });
@@ -0,0 +1,12 @@
1
+ import { expect, it } from 'vitest';
2
+
3
+ import { isColorDark } from '../isColorDark';
4
+
5
+ it('should return true in case, the value which is equal to Math.sqrt(0.299 * R * R + 0.587 * G * G + 0.114 * B * B) is less than 120 and false otherwise', () => {
6
+ expect(isColorDark('#17212b')).toBe(true);
7
+ expect(isColorDark('#f5f5f5')).toBe(false);
8
+ });
9
+
10
+ it('should throw an error in case, passed value has not convertable to RGB format', () => {
11
+ expect(() => isColorDark('abc')).toThrow();
12
+ });
@@ -0,0 +1,13 @@
1
+ import { expect, it } from 'vitest';
2
+
3
+ import { isRGB } from '../isRGB';
4
+
5
+ it('should return true for correct full RGB representation', () => {
6
+ expect(isRGB('#ffffff')).toBe(true);
7
+ });
8
+
9
+ it('should return false for any other value', () => {
10
+ ['abc', '#ffff', '#fff', '#fffffg'].forEach((v) => {
11
+ expect(isRGB(v)).toBe(false);
12
+ });
13
+ });
@@ -0,0 +1,13 @@
1
+ import { expect, it } from 'vitest';
2
+
3
+ import { isRGBShort } from '../isRGBShort';
4
+
5
+ it('should return true for correct short RGB representation', () => {
6
+ expect(isRGBShort('#fff')).toBe(true);
7
+ });
8
+
9
+ it('should return false for any other value', () => {
10
+ ['abc', '#ffff', '#ffffff', '#ggg'].forEach((v) => {
11
+ expect(isRGBShort(v)).toBe(false);
12
+ });
13
+ });
@@ -0,0 +1,23 @@
1
+ import { expect, it } from 'vitest';
2
+
3
+ import { toRGB } from '../toRGB';
4
+
5
+ it('should return same value in case, full version of RGB is passed', () => {
6
+ expect(toRGB('#ffffff')).toBe('#ffffff');
7
+ });
8
+
9
+ it('should return full RGB value in case, its short presentation is passed', () => {
10
+ expect(toRGB('#abc')).toBe('#aabbcc');
11
+ });
12
+
13
+ it('should return RGB representation of rgb(*,*,*) pattern', () => {
14
+ expect(toRGB('rgb(6,56,11)')).toBe('#06380b');
15
+ });
16
+
17
+ it('should return RGB representation of rgba(*,*,*) pattern', () => {
18
+ expect(toRGB('rgba(6,56,11,22)')).toBe('#06380b');
19
+ });
20
+
21
+ it('should throw an error in other cases', () => {
22
+ expect(() => toRGB('abc')).toThrow();
23
+ });
@@ -0,0 +1,145 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { EventEmitter } from '../EventEmitter';
4
+
5
+ interface EventsMap {
6
+ test: (a: number, b: boolean) => void;
7
+ hey: never;
8
+ }
9
+
10
+ let ee: EventEmitter<EventsMap>;
11
+
12
+ beforeEach(() => {
13
+ ee = new EventEmitter<EventsMap>();
14
+ });
15
+
16
+ describe('on', () => {
17
+ it('should emit bound listener with specified arguments', () => {
18
+ const listener = vi.fn();
19
+ ee.on('test', listener);
20
+ ee.emit('test', 1, true);
21
+ expect(listener).toHaveBeenCalledOnce();
22
+ expect(listener).toBeCalledWith(1, true);
23
+ });
24
+
25
+ it('should emit bound listener with specified arguments as many times as it was bound', () => {
26
+ const listener = vi.fn();
27
+ ee.on('test', listener);
28
+ ee.on('test', listener);
29
+ ee.emit('test', 1, true);
30
+ expect(listener).toHaveBeenCalledTimes(2);
31
+ expect(listener).toHaveBeenNthCalledWith(1, 1, true);
32
+ expect(listener).toHaveBeenNthCalledWith(2, 1, true);
33
+ });
34
+
35
+ it('should not emit bound listener in case, event name does not match', () => {
36
+ const listener = vi.fn();
37
+ ee.on('test', listener);
38
+ ee.emit('hey');
39
+ expect(listener).not.toBeCalled();
40
+ });
41
+
42
+ it('should remove listener if returned function was called', () => {
43
+ const listener = vi.fn();
44
+ const off = ee.on('test', listener);
45
+
46
+ off();
47
+ ee.emit('test', 1, true);
48
+ expect(listener).not.toBeCalled();
49
+ });
50
+ });
51
+
52
+ describe('once', () => {
53
+ it('should emit bound listener with specified arguments only once', () => {
54
+ const listener = vi.fn();
55
+ ee.once('test', listener);
56
+ ee.emit('test', 1, true);
57
+ ee.emit('test', 1, true);
58
+ ee.emit('test', 1, true);
59
+ expect(listener).toHaveBeenCalledOnce();
60
+ expect(listener).toBeCalledWith(1, true);
61
+ });
62
+
63
+ it('should emit bound listener with specified arguments as many times as it was bound', () => {
64
+ const listener = vi.fn();
65
+ ee.once('test', listener);
66
+ ee.once('test', listener);
67
+ ee.emit('test', 1, true);
68
+ ee.emit('test', 1, true);
69
+ expect(listener).toHaveBeenCalledTimes(2);
70
+ expect(listener).toHaveBeenNthCalledWith(1, 1, true);
71
+ expect(listener).toHaveBeenNthCalledWith(2, 1, true);
72
+ });
73
+
74
+ it('should not emit bound listener in case, event name does not match', () => {
75
+ const listener = vi.fn();
76
+ ee.once('test', listener);
77
+ ee.emit('hey');
78
+ expect(listener).not.toBeCalled();
79
+ });
80
+
81
+ it('should remove listener if returned function was called', () => {
82
+ const listener = vi.fn();
83
+ const off = ee.once('test', listener);
84
+
85
+ off();
86
+ ee.emit('test', 1, true);
87
+ expect(listener).not.toBeCalled();
88
+ });
89
+ });
90
+
91
+ describe('off', () => {
92
+ it('should not emit bound listener in case, it was unbound', () => {
93
+ const listener = vi.fn();
94
+ ee.on('test', listener);
95
+ ee.off('test', listener);
96
+ ee.emit('test', 1, true);
97
+ expect(listener).not.toBeCalled();
98
+ });
99
+
100
+ it('should not do anything in case, event has no listeners', () => {
101
+ expect(() => {
102
+ const listener = vi.fn();
103
+ ee.off('test', listener);
104
+ }).not.toThrow();
105
+ });
106
+
107
+ it('should remove event listener bound via "once" method', () => {
108
+ const listener = vi.fn();
109
+ ee.once('test', listener);
110
+ ee.off('test', listener);
111
+ ee.emit('test', 1, true);
112
+ expect(listener).not.toBeCalled();
113
+ });
114
+
115
+ it('should not do anything if received not bound listener', () => {
116
+ ee.on('test', vi.fn());
117
+ expect(() => ee.off('test', vi.fn())).not.toThrow();
118
+ });
119
+ });
120
+
121
+ describe('subscribe', () => {
122
+ it('should catch any emitted event', () => {
123
+ const listener = vi.fn();
124
+ ee.subscribe(listener);
125
+ ee.emit('test', 1, true);
126
+ ee.emit('hey');
127
+ expect(listener).toBeCalledTimes(2);
128
+ expect(listener).toHaveBeenNthCalledWith(1, 'test', 1, true);
129
+ expect(listener).toHaveBeenNthCalledWith(2, 'hey');
130
+ });
131
+ });
132
+
133
+ describe('unsubscribe', () => {
134
+ it('should not emit event if it was unbound', () => {
135
+ const listener = vi.fn();
136
+ ee.subscribe(listener);
137
+ ee.unsubscribe(listener);
138
+ ee.emit('test', 1, true);
139
+ expect(listener).not.toBeCalled();
140
+ });
141
+
142
+ it('should not do anything if received not bound listener', () => {
143
+ expect(() => ee.unsubscribe(vi.fn())).not.toThrow();
144
+ });
145
+ });
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { HapticFeedback } from '../HapticFeedback';
4
+
5
+ describe('impactOccurred', () => {
6
+ it('should call "web_app_trigger_haptic_feedback" method with { type: "impact", style: {{style}} }', () => {
7
+ const postEvent = vi.fn();
8
+ const haptic = new HapticFeedback('', postEvent);
9
+
10
+ expect(postEvent).toHaveBeenCalledTimes(0);
11
+ haptic.impactOccurred('heavy');
12
+ expect(postEvent).toHaveBeenCalledTimes(1);
13
+ expect(postEvent).toHaveBeenCalledWith('web_app_trigger_haptic_feedback', {
14
+ type: 'impact',
15
+ impact_style: 'heavy',
16
+ });
17
+ });
18
+ });
19
+
20
+ describe('notificationOccurred', () => {
21
+ it('should call "web_app_trigger_haptic_feedback" method with { type: "notification", notification_type: {{type}} }', () => {
22
+ const postEvent = vi.fn();
23
+ const haptic = new HapticFeedback('', postEvent);
24
+
25
+ expect(postEvent).toHaveBeenCalledTimes(0);
26
+ haptic.notificationOccurred('success');
27
+ expect(postEvent).toHaveBeenCalledTimes(1);
28
+ expect(postEvent).toHaveBeenCalledWith('web_app_trigger_haptic_feedback', {
29
+ type: 'notification',
30
+ notification_type: 'success',
31
+ });
32
+ });
33
+ });
34
+
35
+ describe('selectionChanged', () => {
36
+ it('should call "web_app_trigger_haptic_feedback" method with { type: "selection_change" }', () => {
37
+ const postEvent = vi.fn();
38
+ const haptic = new HapticFeedback('', postEvent);
39
+
40
+ expect(postEvent).toHaveBeenCalledTimes(0);
41
+ haptic.selectionChanged();
42
+ expect(postEvent).toHaveBeenCalledTimes(1);
43
+ expect(postEvent).toHaveBeenCalledWith('web_app_trigger_haptic_feedback', {
44
+ type: 'selection_change',
45
+ });
46
+ });
47
+ });
48
+
49
+ describe('supports', () => {
50
+ describe('impactOccurred / notificationOccurred / selectionChanged', () => {
51
+ it('should return true in case, HapticFeedback version is 6.1 or higher. False, otherwise', () => {
52
+ const haptic1 = new HapticFeedback('6.0');
53
+ expect(haptic1.supports('impactOccurred')).toBe(false);
54
+ expect(haptic1.supports('notificationOccurred')).toBe(false);
55
+ expect(haptic1.supports('selectionChanged')).toBe(false);
56
+
57
+ const haptic2 = new HapticFeedback('6.1');
58
+ expect(haptic2.supports('impactOccurred')).toBe(true);
59
+ expect(haptic2.supports('notificationOccurred')).toBe(true);
60
+ expect(haptic2.supports('selectionChanged')).toBe(true);
61
+
62
+ const haptic3 = new HapticFeedback('6.2');
63
+ expect(haptic3.supports('impactOccurred')).toBe(true);
64
+ expect(haptic3.supports('notificationOccurred')).toBe(true);
65
+ expect(haptic3.supports('selectionChanged')).toBe(true);
66
+ });
67
+ });
68
+ });
package/src/index.ts CHANGED
@@ -154,6 +154,7 @@ export type { RequestId, CreateRequestIdFunc } from './types/index.js';
154
154
  export { Utils } from './utils/index.js';
155
155
  export { compareVersions, type Version } from './version/index.js';
156
156
  export {
157
+ isStableViewportPlatform,
157
158
  requestViewport,
158
159
  Viewport,
159
160
  type RequestViewportResult,
@@ -0,0 +1,96 @@
1
+ import { mockSessionStorageGetItem } from 'test-utils';
2
+ import type { SpyInstance } from 'vitest';
3
+ import { afterEach, expect, it, vi } from 'vitest';
4
+
5
+ import { createWindow } from '../../../../test-utils/createWindow';
6
+ import { requestViewport } from '../../../viewport/requestViewport';
7
+ import { createViewport } from '../createViewport';
8
+
9
+ vi.mock('../../../viewport/requestViewport', () => {
10
+ return {
11
+ requestViewport: vi.fn(),
12
+ };
13
+ });
14
+
15
+ type RequestViewport = typeof requestViewport;
16
+
17
+ const requestViewportMock = requestViewport as unknown as SpyInstance<Parameters<RequestViewport>, ReturnType<RequestViewport>>;
18
+
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ requestViewportMock.mockReset();
22
+ });
23
+
24
+ it('should create Viewport from the data located in the storage if page was reloaded and storage has viewport data', () => {
25
+ mockSessionStorageGetItem('{"height":992,"isExpanded":false,"stableHeight":992,"width":320}');
26
+ expect(createViewport(true, 'macos', vi.fn(), false)).toMatchObject({
27
+ height: 992,
28
+ isExpanded: false,
29
+ stableHeight: 992,
30
+ width: 320,
31
+ });
32
+ });
33
+
34
+ it('should return viewport with window data if page was not reloaded or storage is missing required data, and platform has a stable viewport', () => {
35
+ createWindow({ innerHeight: 1000, innerWidth: 2000 });
36
+ expect(createViewport(false, 'macos', vi.fn(), false)).toMatchObject({
37
+ height: 1000,
38
+ isExpanded: true,
39
+ stableHeight: 1000,
40
+ width: 2000,
41
+ });
42
+
43
+ expect(createViewport(true, 'macos', vi.fn(), false)).toMatchObject({
44
+ height: 1000,
45
+ isExpanded: true,
46
+ stableHeight: 1000,
47
+ width: 2000,
48
+ });
49
+ });
50
+
51
+ it('should create Viewport instance from the result of calling requestViewport function if page was not reloaded, platform has no stable viewport and initialization is complete', () => {
52
+ requestViewportMock.mockImplementation(async () => ({
53
+ height: 922,
54
+ isStateStable: false,
55
+ width: 800,
56
+ isExpanded: false,
57
+ }));
58
+
59
+ expect(createViewport(false, 'android', vi.fn(), true)).resolves.toMatchObject({
60
+ height: 922,
61
+ isExpanded: false,
62
+ stableHeight: 0,
63
+ width: 800,
64
+ });
65
+
66
+ requestViewportMock.mockImplementation(async () => ({
67
+ height: 111,
68
+ isStateStable: true,
69
+ width: 222,
70
+ isExpanded: true,
71
+ }));
72
+
73
+ expect(createViewport(false, 'android', vi.fn(), true)).resolves.toMatchObject({
74
+ height: 111,
75
+ isExpanded: true,
76
+ stableHeight: 111,
77
+ width: 222,
78
+ });
79
+ });
80
+
81
+ it('should return viewport with zeroes if page was reloaded, storage doesnt contain data, platform has no stable viewport and initialization is not complete', () => {
82
+ requestViewportMock.mockImplementation(async () => ({
83
+ height: 922,
84
+ isStateStable: false,
85
+ width: 800,
86
+ isExpanded: false,
87
+ }));
88
+
89
+ mockSessionStorageGetItem(null);
90
+ expect(createViewport(true, 'android', vi.fn(), false)).toMatchObject({
91
+ height: 0,
92
+ isExpanded: false,
93
+ stableHeight: 0,
94
+ width: 0,
95
+ });
96
+ });
@@ -1,53 +1,19 @@
1
1
  import { getStorageValue, saveStorageValue } from '~/storage.js';
2
- import { requestViewport, Viewport } from '~/viewport/index.js';
2
+ import {
3
+ isStableViewportPlatform,
4
+ requestViewport,
5
+ Viewport,
6
+ type ViewportProps,
7
+ } from '~/viewport/index.js';
3
8
  import type { PostEvent } from '~/bridge/index.js';
4
9
  import type { Platform } from '~/types/index.js';
5
10
 
6
11
  /**
7
- * Returns true in case, specified platform supports calling Mini Apps
8
- * "web_app_request_viewport" method.
9
- * @param platform - platform identifier.
10
- */
11
- function isRequestSupportedPlatform(platform: Platform): boolean {
12
- return !['macos', 'web', 'weba'].includes(platform);
13
- }
14
-
15
- /**
16
- * Attempts to create Viewport instance using known parameters and local storage.
17
- * @param isPageReload - was page reloaded.
18
- * @param platform - platform identifier.
19
- * @param postEvent - Bridge postEvent function.
20
- */
21
- function tryCreate(
22
- isPageReload: boolean,
23
- platform: Platform,
24
- postEvent: PostEvent,
25
- ): Viewport | null {
26
- if (isPageReload || !isRequestSupportedPlatform(platform)) {
27
- return new Viewport({
28
- height: window.innerHeight,
29
- isExpanded: true,
30
- postEvent,
31
- stableHeight: window.innerHeight,
32
- width: window.innerWidth,
33
- });
34
- }
35
-
36
- const state = getStorageValue('viewport');
37
- if (state) {
38
- return new Viewport({ ...state, postEvent });
39
- }
40
-
41
- return null;
42
- }
43
-
44
- /**
45
- * Synchronizes specified Viewport instance with Telegram application and always saves its state
46
- * in local storage.
47
- * @param viewport - Viewport instance.
12
+ * Creates new bound instance of the Viewport component.
13
+ * @param props - properties to create new instance.
48
14
  */
49
- function bind(viewport: Viewport): Viewport {
50
- viewport.listen();
15
+ function instantiate(props: ViewportProps): Viewport {
16
+ const viewport = new Viewport(props);
51
17
 
52
18
  // TODO: Should probably use throttle for height.
53
19
  viewport.on('change', () => saveStorageValue('viewport', {
@@ -57,59 +23,72 @@ function bind(viewport: Viewport): Viewport {
57
23
  width: viewport.width,
58
24
  }));
59
25
 
26
+ viewport.listen();
27
+
60
28
  return viewport;
61
29
  }
62
30
 
63
31
  /**
64
- * Creates Viewport instance using its actual state from the storage. Otherwise, creates it
65
- * with default parameters.
32
+ * Creates Viewport instance using its actual state from the Telegram application.
66
33
  * @param isPageReload - was page reloaded.
67
34
  * @param platform - platform identifier.
68
35
  * @param postEvent - Bridge postEvent function.
36
+ * @param complete - is initialization complete.
69
37
  */
70
- export function createViewportSync(
38
+ export function createViewport(
71
39
  isPageReload: boolean,
72
40
  platform: Platform,
73
41
  postEvent: PostEvent,
74
- ): Viewport {
75
- const viewport = bind(
76
- tryCreate(isPageReload, platform, postEvent) || new Viewport({
77
- width: 0,
78
- height: 0,
79
- isExpanded: false,
80
- postEvent,
81
- stableHeight: 0,
82
- }),
83
- );
42
+ complete: boolean,
43
+ ): Viewport | Promise<Viewport> {
44
+ // If page was reloaded, we expect viewport to be restored from the storage.
45
+ const state = isPageReload ? getStorageValue('viewport') : null;
46
+ if (state) {
47
+ return instantiate({ ...state, postEvent });
48
+ }
84
49
 
85
- if (isRequestSupportedPlatform(platform)) {
86
- viewport.sync({ postEvent, timeout: 100 }).catch((e) => {
87
- // eslint-disable-next-line no-console
88
- console.error('Unable to actualize viewport state', e);
50
+ // If platform has a stable viewport, it means we could instantiate Viewport using
51
+ // the window global object properties.
52
+ if (isStableViewportPlatform(platform)) {
53
+ return instantiate({
54
+ height: window.innerHeight,
55
+ isExpanded: true,
56
+ postEvent,
57
+ stableHeight: window.innerHeight,
58
+ width: window.innerWidth,
89
59
  });
90
60
  }
91
61
 
92
- return viewport;
93
- }
94
-
95
- /**
96
- * Creates Viewport instance using its actual state from the Telegram application.
97
- * @param isPageReload - was page reloaded.
98
- * @param platform - platform identifier.
99
- * @param postEvent - Bridge postEvent function.
100
- */
101
- export async function createViewportAsync(
102
- isPageReload: boolean,
103
- platform: Platform,
104
- postEvent: PostEvent,
105
- ): Promise<Viewport> {
106
- return bind(
107
- tryCreate(isPageReload, platform, postEvent)
108
- || await requestViewport({ postEvent, timeout: 100 })
109
- .then(({ height, isStateStable, ...rest }) => new Viewport({
62
+ // If initialization is complete, we have to create Viewport instance using its actual
63
+ // state from the Telegram application.
64
+ if (complete) {
65
+ return requestViewport({
66
+ postEvent,
67
+ timeout: 5000,
68
+ })
69
+ .then(({ height, isStateStable, ...rest }) => instantiate({
110
70
  ...rest,
111
71
  height,
112
72
  stableHeight: isStateStable ? height : 0,
113
- })),
114
- );
73
+ }));
74
+ }
75
+
76
+ // Otherwise we have no sources to get viewport properties from. In this case we just
77
+ // return some "empty" Viewport instance and synchronize it in the background.
78
+ const viewport = instantiate({
79
+ width: 0,
80
+ height: 0,
81
+ isExpanded: false,
82
+ postEvent,
83
+ stableHeight: 0,
84
+ });
85
+
86
+ /* c8 ignore start */
87
+ viewport.sync({ postEvent, timeout: 5000 }).catch((e) => {
88
+ // eslint-disable-next-line no-console
89
+ console.error('Unable to actualize viewport state', e);
90
+ });
91
+ /* c8 ignore stop */
92
+
93
+ return viewport;
115
94
  }