Skip to content

Commit

Permalink
Merge pull request #230 from Telegram-Mini-Apps/feature/some-fixes
Browse files Browse the repository at this point in the history
Feature/some fixes
  • Loading branch information
heyqbnk authored Feb 22, 2024
2 parents b439e45 + b22fa8a commit 9c76339
Show file tree
Hide file tree
Showing 7 changed files with 38 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-wombats-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tma.js/sdk": patch
---

Add source = window.parent in emitEvent function. Fix types in merge class names utility
12 changes: 5 additions & 7 deletions packages/sdk/src/bridge/events/__tests__/onTelegramEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,12 @@ import { createWindow, type WindowSpy } from '../../../../test-utils/createWindo
import { dispatchWindowMessageEvent } from '../../../../test-utils/dispatchWindowMessageEvent';
import { onTelegramEvent } from '../onTelegramEvent';

let windowSpy: WindowSpy;

beforeEach(() => {
windowSpy = createWindow();
});

afterEach(() => {
windowSpy.mockRestore();
vi.restoreAllMocks()
});

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.', () => {
createWindow({ env: 'iframe' });
const callback = vi.fn();
onTelegramEvent(callback);

Expand All @@ -25,13 +20,15 @@ it('should call passed callback with event type and data in case, window generat
});

it('should not define event handlers twice in case, window object contains "TelegramGameProxy_receiveEvent" property.', () => {
createWindow();
(window as any).TelegramGameProxy_receiveEvent = true;

onTelegramEvent(vi.fn());
expect(window).not.toHaveProperty('Telegram');
});

it('should call passed callback with event type and data in case, external environment generated event.', () => {
createWindow();
const callback = vi.fn();
onTelegramEvent(callback);

Expand All @@ -42,6 +39,7 @@ it('should call passed callback with event type and data in case, external envir
});

it('should ignore a message event with unexpected data', () => {
createWindow();
const callback = vi.fn();
onTelegramEvent(callback);

Expand Down
8 changes: 7 additions & 1 deletion packages/sdk/src/bridge/events/onTelegramEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { parseMessage } from '~/bridge/parseMessage.js';
* Emits event sent from Telegram native application like it was sent in
* default web environment between 2 iframes. It dispatches new MessageEvent
* and expects it to be handled via `window.addEventListener('message', ...)`
* as developer would do it to handle messages sent from parent iframe.
* as developer would do it to handle messages sent from the parent iframe.
* @param eventType - event name.
* @param eventData - event payload.
*/
function emitEvent(eventType: string, eventData: unknown): void {
window.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({ eventType, eventData }),
// We specify window.parent to imitate the case, it sent us this event.
source: window.parent,
}));
}

Expand Down Expand Up @@ -65,6 +67,10 @@ export function onTelegramEvent(cb: (eventType: string, eventData: unknown) => v

// We expect Telegram to send us new event through "message" event.
window.addEventListener('message', (event) => {
if (event.source !== window.parent) {
return;
}

try {
const { eventType, eventData } = parseMessage(event.data);
cb(eventType, eventData);
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/classnames/__tests__/mergeClassNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect, it } from 'vitest';
import { mergeClassNames } from '../mergeClassNames';

it('should ignore non-object values', () => {
expect(mergeClassNames({}, null, undefined, false, true, {}));
expect(mergeClassNames({}, null, undefined, false, true, { tma: 'good' })).toStrictEqual({ tma: 'good' });
});

it('should merge objects keys by values applying classNames function', () => {
Expand Down
40 changes: 16 additions & 24 deletions packages/sdk/src/classnames/mergeClassNames.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { classNames } from './classNames.js';
import { isRecord } from '~/misc/index.js';

type FilterUnion<U> = Exclude<U, number | string | null | undefined | any[] | boolean>;
import { classNames } from './classNames.js';

/**
* Returns union keys removing those, which values are not strings.
*/
type UnionFilteredKeys<U> = U extends U
type UnionStringKeys<U> = U extends U
? {
[K in keyof U]: U[K] extends string ? K : never
[K in keyof U]-?: U[K] extends string | undefined ? K : never;
}[keyof U]
: never;

Expand All @@ -16,32 +16,24 @@ type UnionFilteredKeys<U> = U extends U
*/
type UnionRequiredKeys<U> = U extends U
? {
[K in UnionFilteredKeys<U>]-?: ({} extends { [P in K]: U[K] } ? never : K)
}[UnionFilteredKeys<U>]
[K in UnionStringKeys<U>]: ({} extends Pick<U, K> ? never : K)
}[UnionStringKeys<U>]
: never;

/**
* Returns union optional keys.
*/
type UnionOptionalKeys<U> = Exclude<UnionFilteredKeys<U>, UnionRequiredKeys<U>>;
type UnionOptionalKeys<U> = Exclude<UnionStringKeys<U>, UnionRequiredKeys<U>>;

type MergeClassNames<Tuple extends any[]> = Tuple[number] extends infer Union
? FilterUnion<Union> extends infer UnionFiltered
type MergeClassNames<Tuple extends any[]> =
// Removes all types from union which will be ignored by the mergeClassNames function.
Exclude<Tuple[number], number | string | null | undefined | any[] | boolean> extends infer Union
? {
[K in UnionRequiredKeys<UnionFiltered>]: string;
} & {
[K in UnionOptionalKeys<UnionFiltered>]?: string;
}
: never
: never;

/**
* Returns true in case, passed value is Record.
* @param value
*/
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(null);
}
[K in UnionRequiredKeys<Union>]: string;
} & {
[K in UnionOptionalKeys<Union>]?: string;
}
: never;

/**
* Merges 2 sets of parameters. Function expects passing an array of objects with values, which
Expand All @@ -51,7 +43,7 @@ function isObject(value: unknown): value is Record<string, unknown> {
*/
export function mergeClassNames<T extends any[]>(...partials: T): MergeClassNames<T> {
return partials.reduce<MergeClassNames<T>>((acc, partial) => {
if (!isObject(partial)) {
if (!isRecord(partial)) {
return acc;
}

Expand Down
5 changes: 3 additions & 2 deletions packages/sdk/test-utils/createWindow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { vi, type SpyInstance } from 'vitest';
import { type SpyInstance, vi } from 'vitest';

import { createDomEmitter } from './createDomEmitter.js';

Expand All @@ -20,11 +20,12 @@ export function createWindow(options: CreateWindowOptions = {}): WindowSpy {
const wnd = {
innerHeight,
innerWidth,
// We need this property to correctly re-emit received event from Telegram.
parent: { postMessage: postMessageSpy },
...createDomEmitter(),
...(env === 'iframe' ? {
top: 1,
self: 2,
parent: { postMessage: postMessageSpy },
} : {}),
};

Expand Down
1 change: 1 addition & 0 deletions packages/sdk/test-utils/dispatchWindowMessageEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
export function dispatchWindowMessageEvent(eventType: string, eventData?: unknown): void {
window.dispatchEvent(new MessageEvent('message', {
data: JSON.stringify({ eventType, eventData }),
source: window.parent,
}));
}

0 comments on commit 9c76339

Please sign in to comment.