@tma.js/sdk 1.4.2 → 1.4.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tma.js/sdk",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "TypeScript Source Development Kit for Telegram Mini Apps client application.",
5
5
  "author": "Vladislav Kibenko <wolfram.deus@gmail.com>",
6
6
  "homepage": "https://github.com/Telegram-Mini-Apps/tma.js#readme",
@@ -4,17 +4,12 @@ import { createWindow, type WindowSpy } from '../../../../test-utils/createWindo
4
4
  import { dispatchWindowMessageEvent } from '../../../../test-utils/dispatchWindowMessageEvent';
5
5
  import { onTelegramEvent } from '../onTelegramEvent';
6
6
 
7
- let windowSpy: WindowSpy;
8
-
9
- beforeEach(() => {
10
- windowSpy = createWindow();
11
- });
12
-
13
7
  afterEach(() => {
14
- windowSpy.mockRestore();
8
+ vi.restoreAllMocks()
15
9
  });
16
10
 
17
11
  it('should call passed callback with event type and data in case, window generated "message" event with data, presented as object with properties "eventType" (string) and "eventData" (unknown). Object is converted to string.', () => {
12
+ createWindow({ env: 'iframe' });
18
13
  const callback = vi.fn();
19
14
  onTelegramEvent(callback);
20
15
 
@@ -25,6 +20,7 @@ it('should call passed callback with event type and data in case, window generat
25
20
  });
26
21
 
27
22
  it('should not define event handlers twice in case, window object contains "TelegramGameProxy_receiveEvent" property.', () => {
23
+ createWindow();
28
24
  (window as any).TelegramGameProxy_receiveEvent = true;
29
25
 
30
26
  onTelegramEvent(vi.fn());
@@ -32,6 +28,7 @@ it('should not define event handlers twice in case, window object contains "Tele
32
28
  });
33
29
 
34
30
  it('should call passed callback with event type and data in case, external environment generated event.', () => {
31
+ createWindow();
35
32
  const callback = vi.fn();
36
33
  onTelegramEvent(callback);
37
34
 
@@ -42,6 +39,7 @@ it('should call passed callback with event type and data in case, external envir
42
39
  });
43
40
 
44
41
  it('should ignore a message event with unexpected data', () => {
42
+ createWindow();
45
43
  const callback = vi.fn();
46
44
  onTelegramEvent(callback);
47
45
 
@@ -4,13 +4,15 @@ import { parseMessage } from '~/bridge/parseMessage.js';
4
4
  * Emits event sent from Telegram native application like it was sent in
5
5
  * default web environment between 2 iframes. It dispatches new MessageEvent
6
6
  * and expects it to be handled via `window.addEventListener('message', ...)`
7
- * as developer would do it to handle messages sent from parent iframe.
7
+ * as developer would do it to handle messages sent from the parent iframe.
8
8
  * @param eventType - event name.
9
9
  * @param eventData - event payload.
10
10
  */
11
11
  function emitEvent(eventType: string, eventData: unknown): void {
12
12
  window.dispatchEvent(new MessageEvent('message', {
13
13
  data: JSON.stringify({ eventType, eventData }),
14
+ // We specify window.parent to imitate the case, it sent us this event.
15
+ source: window.parent,
14
16
  }));
15
17
  }
16
18
 
@@ -65,6 +67,10 @@ export function onTelegramEvent(cb: (eventType: string, eventData: unknown) => v
65
67
 
66
68
  // We expect Telegram to send us new event through "message" event.
67
69
  window.addEventListener('message', (event) => {
70
+ if (event.source !== window.parent) {
71
+ return;
72
+ }
73
+
68
74
  try {
69
75
  const { eventType, eventData } = parseMessage(event.data);
70
76
  cb(eventType, eventData);
@@ -3,7 +3,7 @@ import { expect, it } from 'vitest';
3
3
  import { mergeClassNames } from '../mergeClassNames';
4
4
 
5
5
  it('should ignore non-object values', () => {
6
- expect(mergeClassNames({}, null, undefined, false, true, {}));
6
+ expect(mergeClassNames({}, null, undefined, false, true, { tma: 'good' })).toStrictEqual({ tma: 'good' });
7
7
  });
8
8
 
9
9
  it('should merge objects keys by values applying classNames function', () => {
@@ -1,13 +1,13 @@
1
- import { classNames } from './classNames.js';
1
+ import { isRecord } from '~/misc/index.js';
2
2
 
3
- type FilterUnion<U> = Exclude<U, number | string | null | undefined | any[] | boolean>;
3
+ import { classNames } from './classNames.js';
4
4
 
5
5
  /**
6
6
  * Returns union keys removing those, which values are not strings.
7
7
  */
8
- type UnionFilteredKeys<U> = U extends U
8
+ type UnionStringKeys<U> = U extends U
9
9
  ? {
10
- [K in keyof U]: U[K] extends string ? K : never
10
+ [K in keyof U]-?: U[K] extends string | undefined ? K : never;
11
11
  }[keyof U]
12
12
  : never;
13
13
 
@@ -16,32 +16,24 @@ type UnionFilteredKeys<U> = U extends U
16
16
  */
17
17
  type UnionRequiredKeys<U> = U extends U
18
18
  ? {
19
- [K in UnionFilteredKeys<U>]-?: ({} extends { [P in K]: U[K] } ? never : K)
20
- }[UnionFilteredKeys<U>]
19
+ [K in UnionStringKeys<U>]: ({} extends Pick<U, K> ? never : K)
20
+ }[UnionStringKeys<U>]
21
21
  : never;
22
22
 
23
23
  /**
24
24
  * Returns union optional keys.
25
25
  */
26
- type UnionOptionalKeys<U> = Exclude<UnionFilteredKeys<U>, UnionRequiredKeys<U>>;
26
+ type UnionOptionalKeys<U> = Exclude<UnionStringKeys<U>, UnionRequiredKeys<U>>;
27
27
 
28
- type MergeClassNames<Tuple extends any[]> = Tuple[number] extends infer Union
29
- ? FilterUnion<Union> extends infer UnionFiltered
28
+ type MergeClassNames<Tuple extends any[]> =
29
+ // Removes all types from union which will be ignored by the mergeClassNames function.
30
+ Exclude<Tuple[number], number | string | null | undefined | any[] | boolean> extends infer Union
30
31
  ? {
31
- [K in UnionRequiredKeys<UnionFiltered>]: string;
32
- } & {
33
- [K in UnionOptionalKeys<UnionFiltered>]?: string;
34
- }
35
- : never
36
- : never;
37
-
38
- /**
39
- * Returns true in case, passed value is Record.
40
- * @param value
41
- */
42
- function isObject(value: unknown): value is Record<string, unknown> {
43
- return typeof value === 'object' && value !== null && !Array.isArray(null);
44
- }
32
+ [K in UnionRequiredKeys<Union>]: string;
33
+ } & {
34
+ [K in UnionOptionalKeys<Union>]?: string;
35
+ }
36
+ : never;
45
37
 
46
38
  /**
47
39
  * Merges 2 sets of parameters. Function expects passing an array of objects with values, which
@@ -51,7 +43,7 @@ function isObject(value: unknown): value is Record<string, unknown> {
51
43
  */
52
44
  export function mergeClassNames<T extends any[]>(...partials: T): MergeClassNames<T> {
53
45
  return partials.reduce<MergeClassNames<T>>((acc, partial) => {
54
- if (!isObject(partial)) {
46
+ if (!isRecord(partial)) {
55
47
  return acc;
56
48
  }
57
49
 
@@ -0,0 +1,175 @@
1
+ import { expect, vi, it, SpyInstance, afterEach, beforeAll, describe } from 'vitest';
2
+ import { ThemeParams } from '../../theme-params';
3
+ import { dispatchWindowMessageEvent } from '../../../test-utils/dispatchWindowMessageEvent';
4
+ import { bindMiniAppCSSVars } from '../bindMiniAppCSSVars';
5
+ import { MiniApp } from '../../mini-app';
6
+
7
+ let setCSSPropertySpy: SpyInstance<[string, string, string?], void>;
8
+
9
+ beforeAll(() => {
10
+ setCSSPropertySpy = vi
11
+ .spyOn(document.documentElement.style, 'setProperty')
12
+ .mockImplementation(() => {
13
+ });
14
+ });
15
+
16
+ afterEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ describe('background', () => {
21
+ it('should set --tg-background-color equal to miniApp.backgroundColor', () => {
22
+ bindMiniAppCSSVars(
23
+ new MiniApp({
24
+ backgroundColor: '#111111',
25
+ headerColor: '#222222',
26
+ botInline: false,
27
+ version: '7.0',
28
+ createRequestId() {
29
+ return 'abc';
30
+ },
31
+ }),
32
+ new ThemeParams({}),
33
+ );
34
+
35
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-background-color', '#111111');
36
+ });
37
+
38
+ it('should update --tg-background-color according to the changes applied to mini app background color', () => {
39
+ const ma = new MiniApp({
40
+ backgroundColor: '#111111',
41
+ headerColor: '#222222',
42
+ botInline: false,
43
+ version: '7.0',
44
+ postEvent() {
45
+ },
46
+ createRequestId() {
47
+ return 'abc';
48
+ },
49
+ });
50
+
51
+ bindMiniAppCSSVars(ma, new ThemeParams({}));
52
+ setCSSPropertySpy.mockClear();
53
+
54
+ ma.setBackgroundColor('#999999');
55
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-background-color', '#999999');
56
+ });
57
+ });
58
+
59
+ describe('header', () => {
60
+ it('should set --tg-header-color equal to miniApp.headerColor if this value is RGB', () => {
61
+ bindMiniAppCSSVars(
62
+ new MiniApp({
63
+ backgroundColor: '#111111',
64
+ headerColor: '#222222',
65
+ botInline: false,
66
+ version: '7.0',
67
+ createRequestId() {
68
+ return 'abc';
69
+ },
70
+ }),
71
+ new ThemeParams({}),
72
+ );
73
+
74
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-header-color', '#222222');
75
+ });
76
+
77
+ it('should set --tg-header-color equal to themeParams.backgroundColor if miniApp.headerColor = "bg_color" and backgroundColor property is presented in theme params', () => {
78
+ const tp = new ThemeParams({});
79
+ tp.listen();
80
+
81
+ bindMiniAppCSSVars(
82
+ new MiniApp({
83
+ backgroundColor: '#111111',
84
+ headerColor: 'bg_color',
85
+ botInline: false,
86
+ version: '7.0',
87
+ createRequestId() {
88
+ return 'abc';
89
+ },
90
+ }),
91
+ tp,
92
+ );
93
+
94
+ // Background only.
95
+ expect(setCSSPropertySpy).toHaveBeenCalledTimes(1);
96
+ setCSSPropertySpy.mockClear();
97
+
98
+ dispatchWindowMessageEvent('theme_changed', {
99
+ theme_params: {
100
+ bg_color: '#ffffff',
101
+ },
102
+ });
103
+
104
+ expect(setCSSPropertySpy).toHaveBeenCalledTimes(1);
105
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-header-color', '#ffffff');
106
+ });
107
+
108
+ it('should set --tg-header-color equal to themeParams.secondaryBackgroundColor if miniApp.headerColor = "secondary_bg_color" and secondaryBackgroundColor property is presented in theme params', () => {
109
+ const tp = new ThemeParams({});
110
+ tp.listen();
111
+
112
+ bindMiniAppCSSVars(
113
+ new MiniApp({
114
+ backgroundColor: '#111111',
115
+ headerColor: 'secondary_bg_color',
116
+ botInline: false,
117
+ version: '7.0',
118
+ createRequestId() {
119
+ return 'abc';
120
+ },
121
+ }),
122
+ tp,
123
+ );
124
+
125
+ // Background only.
126
+ expect(setCSSPropertySpy).toHaveBeenCalledTimes(1);
127
+ setCSSPropertySpy.mockClear();
128
+
129
+ dispatchWindowMessageEvent('theme_changed', {
130
+ theme_params: {
131
+ secondary_bg_color: '#000000',
132
+ },
133
+ });
134
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-header-color', '#000000');
135
+ });
136
+
137
+ it('should actualize --tg-header-color if either mini app or theme params changed', () => {
138
+ const ma = new MiniApp({
139
+ backgroundColor: '#111111',
140
+ headerColor: '#aabbcc',
141
+ botInline: false,
142
+ version: '7.0',
143
+ postEvent() {},
144
+ createRequestId() {
145
+ return 'abc';
146
+ },
147
+ });
148
+ const tp = new ThemeParams({
149
+ backgroundColor: '#ffffff',
150
+ secondaryBackgroundColor: '#000000',
151
+ });
152
+ tp.listen();
153
+
154
+ bindMiniAppCSSVars(ma, tp);
155
+ setCSSPropertySpy.mockClear();
156
+
157
+ ma.setHeaderColor('bg_color');
158
+ expect(setCSSPropertySpy).toHaveBeenCalledTimes(1);
159
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-header-color', '#ffffff');
160
+ setCSSPropertySpy.mockClear();
161
+
162
+ ma.setHeaderColor('secondary_bg_color');
163
+ expect(setCSSPropertySpy).toHaveBeenCalledTimes(1);
164
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-header-color', '#000000');
165
+ setCSSPropertySpy.mockClear();
166
+
167
+ dispatchWindowMessageEvent('theme_changed', {
168
+ theme_params: {
169
+ secondary_bg_color: '#abcdef',
170
+ },
171
+ });
172
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-header-color', '#abcdef');
173
+ setCSSPropertySpy.mockClear();
174
+ });
175
+ });
@@ -0,0 +1,52 @@
1
+ import { expect, vi, it, SpyInstance, afterEach, beforeAll } from 'vitest';
2
+ import { bindThemeCSSVars } from '../bindThemeCSSVars';
3
+ import { ThemeParams } from '../../theme-params';
4
+ import { dispatchWindowMessageEvent } from '../../../test-utils/dispatchWindowMessageEvent';
5
+
6
+ let setCSSPropertySpy: SpyInstance<[string, string, string?], void>;
7
+
8
+ beforeAll(() => {
9
+ setCSSPropertySpy = vi
10
+ .spyOn(document.documentElement.style, 'setProperty')
11
+ .mockImplementation(() => {
12
+ });
13
+ });
14
+
15
+ afterEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ it('should set --tg-theme-{key} CSS vars, where key is a kebab-cased theme keys', () => {
20
+ bindThemeCSSVars(new ThemeParams({
21
+ backgroundColor: '#abcdef',
22
+ accentTextColor: '#000011',
23
+ }));
24
+
25
+ expect(setCSSPropertySpy).toHaveBeenCalledTimes(2);
26
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-theme-background-color', '#abcdef');
27
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-theme-accent-text-color', '#000011');
28
+ });
29
+
30
+ it('should update --tg-theme-{key}variables to the values, received in the Theme change events', async () => {
31
+ const tp = new ThemeParams({
32
+ backgroundColor: '#abcdef',
33
+ accentTextColor: '#000011',
34
+ });
35
+ bindThemeCSSVars(tp);
36
+ setCSSPropertySpy.mockClear();
37
+
38
+ await tp.listen();
39
+
40
+ dispatchWindowMessageEvent('theme_changed', {
41
+ theme_params: {
42
+ bg_color: '#111111',
43
+ accent_text_color: '#222222',
44
+ text_color: '#333333',
45
+ },
46
+ });
47
+
48
+ expect(setCSSPropertySpy).toHaveBeenCalledTimes(3);
49
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-theme-background-color', '#111111');
50
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-theme-accent-text-color', '#222222');
51
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-theme-text-color', '#333333');
52
+ });
@@ -0,0 +1,55 @@
1
+ import { expect, vi, it, SpyInstance, afterEach, beforeAll } from 'vitest';
2
+ import { bindViewportCSSVars } from '../bindViewportCSSVars';
3
+ import { Viewport } from '../../viewport';
4
+ import { dispatchWindowMessageEvent } from '../../../test-utils/dispatchWindowMessageEvent';
5
+
6
+ let setCSSPropertySpy: SpyInstance<[string, string, string?], void>;
7
+
8
+ beforeAll(() => {
9
+ setCSSPropertySpy = vi
10
+ .spyOn(document.documentElement.style, 'setProperty')
11
+ .mockImplementation(() => {
12
+ });
13
+ });
14
+
15
+ afterEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ it('should set --tg-viewport-height = viewport.height, --tg-viewport-width = viewport.width, --tg-viewport-stable-height = viewport.stableHeight', () => {
20
+ bindViewportCSSVars(new Viewport({
21
+ height: 192,
22
+ width: 1000,
23
+ isExpanded: false,
24
+ stableHeight: 200,
25
+ }));
26
+
27
+ expect(setCSSPropertySpy).toHaveBeenCalledTimes(3);
28
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-viewport-height', '192px');
29
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-viewport-width', '1000px');
30
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-viewport-stable-height', '200px');
31
+ });
32
+
33
+ it('should update --tg-viewport-height, --tg-viewport-width, --tg-viewport-stable-height according to the values, received in the Viewport change events', () => {
34
+ const viewport = new Viewport({
35
+ height: 192,
36
+ width: 1000,
37
+ isExpanded: false,
38
+ stableHeight: 200,
39
+ });
40
+ bindViewportCSSVars(viewport);
41
+ setCSSPropertySpy.mockClear();
42
+
43
+ viewport.listen();
44
+ dispatchWindowMessageEvent('viewport_changed', {
45
+ height: 900,
46
+ is_state_stable: true,
47
+ is_expanded: false,
48
+ width: 1800,
49
+ })
50
+
51
+ expect(setCSSPropertySpy).toHaveBeenCalledTimes(3);
52
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-viewport-height', '900px');
53
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-viewport-width', '1800px');
54
+ expect(setCSSPropertySpy).toHaveBeenCalledWith('--tg-viewport-stable-height', '900px');
55
+ });
@@ -0,0 +1,14 @@
1
+ import { afterEach, expect, it, vi } from 'vitest';
2
+ import { setCSSVar } from '../setCSSVar';
3
+
4
+ afterEach(() => {
5
+ vi.restoreAllMocks();
6
+ })
7
+
8
+ it('should call document.documentElement.style.setProperty with passed "name" and "value" arguments', () => {
9
+ const spy = vi.spyOn(document.documentElement.style, 'setProperty').mockImplementation(() => {});
10
+
11
+ setCSSVar('tma', 'is cool')
12
+ expect(spy).toHaveBeenCalledOnce();
13
+ expect(spy).toHaveBeenCalledWith('tma', 'is cool');
14
+ })
@@ -1,4 +1,5 @@
1
- import { setCSSVar } from '~/init/css/setCSSVar.js';
1
+ import { isRGB } from '~/colors/index.js';
2
+ import { setCSSVar } from '~/css/setCSSVar.js';
2
3
  import type { MiniApp } from '~/mini-app/index.js';
3
4
  import type { ThemeParams } from '~/theme-params/index.js';
4
5
 
@@ -26,16 +27,18 @@ export function bindMiniAppCSSVars(miniApp: MiniApp, themeParams: ThemeParams):
26
27
  secondaryBackgroundColor,
27
28
  } = themeParams;
28
29
 
29
- if (miniApp.headerColor === 'bg_color') {
30
- if (backgroundColor) {
31
- setCSSVar('--tg-header-color', backgroundColor);
32
- }
33
- } else if (miniApp.headerColor === 'secondary_bg_color') {
34
- if (secondaryBackgroundColor) {
35
- setCSSVar('--tg-header-color', secondaryBackgroundColor);
36
- }
37
- } else {
30
+ if (isRGB(miniApp.headerColor)) {
38
31
  setCSSVar('--tg-header-color', miniApp.headerColor);
32
+ return;
33
+ }
34
+
35
+ if (miniApp.headerColor === 'bg_color' && backgroundColor) {
36
+ setCSSVar('--tg-header-color', backgroundColor);
37
+ return;
38
+ }
39
+
40
+ if (miniApp.headerColor === 'secondary_bg_color' && secondaryBackgroundColor) {
41
+ setCSSVar('--tg-header-color', secondaryBackgroundColor);
39
42
  }
40
43
  };
41
44
 
@@ -23,7 +23,7 @@ import { setCSSVar } from './setCSSVar.js';
23
23
  export function bindViewportCSSVars(viewport: Viewport): void {
24
24
  const setHeight = () => setCSSVar('--tg-viewport-height', `${viewport.height}px`);
25
25
  const setWidth = () => setCSSVar('--tg-viewport-width', `${viewport.width}px`);
26
- const setStableHeight = () => setCSSVar('--tg-viewport-height', `${viewport.stableHeight}px`);
26
+ const setStableHeight = () => setCSSVar('--tg-viewport-stable-height', `${viewport.stableHeight}px`);
27
27
 
28
28
  // TODO: Should probably add debounce or throttle.
29
29
  viewport.on('change:height', setHeight);
@@ -0,0 +1,4 @@
1
+ export * from './bindMiniAppCSSVars.js';
2
+ export * from './bindThemeCSSVars.js';
3
+ export * from './bindViewportCSSVars.js';
4
+ export * from './setCSSVar.js';
package/src/index.ts CHANGED
@@ -57,6 +57,12 @@ export {
57
57
  type RGB,
58
58
  type RGBShort,
59
59
  } from './colors/index.js';
60
+ export {
61
+ setCSSVar,
62
+ bindMiniAppCSSVars,
63
+ bindThemeCSSVars,
64
+ bindViewportCSSVars,
65
+ } from './css/index.js';
60
66
  export { HapticFeedback } from './haptic-feedback/index.js';
61
67
  export {
62
68
  init,
@@ -1,10 +1,8 @@
1
+ import { bindMiniAppCSSVars, bindThemeCSSVars, bindViewportCSSVars } from '~/css/index.js';
1
2
  import type { MiniApp } from '~/mini-app/index.js';
2
3
  import type { ThemeParams } from '~/theme-params/index.js';
3
4
  import type { Viewport } from '~/viewport/index.js';
4
5
 
5
- import { bindMiniAppCSSVars } from './bindMiniAppCSSVars.js';
6
- import { bindThemeCSSVars } from './bindThemeCSSVars.js';
7
- import { bindViewportCSSVars } from './bindViewportCSSVars.js';
8
6
  import type { InitCSSVarsOption, InitCSSVarsSpecificOption } from '../types.js';
9
7
 
10
8
  /**
File without changes
File without changes
File without changes