Skip to content

Commit

Permalink
Merge pull request #204 from Telegram-Mini-Apps/feature/settings-button
Browse files Browse the repository at this point in the history
Feature/settings button
  • Loading branch information
heyqbnk authored Dec 14, 2023
2 parents 41882d6 + 0c7646c commit 53c5e28
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/red-moons-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tma.js/sdk": minor
---

Implement SettingsButton component
8 changes: 4 additions & 4 deletions apps/local-playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
* You can import any code from other packages here. There are currently 2 shortcuts:
*
* 1. "@packages/*". Provides access to "packages" directory:
* import { postEvent } from '@packages/bridge/src/index.js';
* import { postEvent } from '@packages/sdk/src/index.js';
*
* 2. "@/*". Provides easy access to packages' index files:
* import { postEvent } from '@/bridge';
* import { postEvent } from '@/sdk';
*/

import { postEvent } from '@/bridge';
import { postEvent } from '@/sdk';

postEvent('web_app_expand');
postEvent('web_app_expand');
6 changes: 6 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ export {
type QRScannerEventName,
type QRScannerEvents,
} from './qr-scanner/index.js';
export {
SettingsButton,
type SettingsButtonEventName,
type SettingsButtonEventListener,
type SettingsButtonEvents,
} from './settings-button/index.js';
export { supports } from './supports/index.js';
export {
parseThemeParams,
Expand Down
25 changes: 25 additions & 0 deletions packages/sdk/src/init/creators/createSettingsButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SettingsButton } from '~/settings-button/index.js';
import { getStorageValue, saveStorageValue } from '~/storage.js';
import type { PostEvent } from '~/bridge/index.js';

/**
* Creates SettingsButton instance using last locally saved data also saving each state in
* the storage.
* @param isPageReload - was current page reloaded.
* @param version - platform version.
* @param postEvent - Bridge postEvent function
*/
export function createSettingsButton(
isPageReload: boolean,
version: string,
postEvent: PostEvent,
): SettingsButton {
const { isVisible = false } = isPageReload ? getStorageValue('settings-button') || {} : {};
const component = new SettingsButton(isVisible, version, postEvent);

component.on('change', () => {
saveStorageValue('settings-button', { isVisible: component.isVisible });
});

return component;
}
21 changes: 16 additions & 5 deletions packages/sdk/src/init/creators/createViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import { requestViewport, Viewport } from '~/viewport/index.js';
import type { PostEvent } from '~/bridge/index.js';
import type { Platform } from '~/types/index.js';

/**
* Returns true in case, specified platform supports calling Mini Apps
* "web_app_request_viewport" method.
* @param platform - platform identifier.
*/
function isRequestSupportedPlatform(platform: Platform): boolean {
return !['macos', 'web', 'weba'].includes(platform);
}

/**
* Attempts to create Viewport instance using known parameters and local storage.
* @param isPageReload - was page reloaded.
Expand All @@ -14,7 +23,7 @@ function tryCreate(
platform: Platform,
postEvent: PostEvent,
): Viewport | null {
if (isPageReload || platform === 'macos' || platform === 'web' || platform === 'weba') {
if (isPageReload || !isRequestSupportedPlatform(platform)) {
return new Viewport({
height: window.innerHeight,
isExpanded: true,
Expand Down Expand Up @@ -73,10 +82,12 @@ export function createViewportSync(
}),
);

viewport.sync({ postEvent, timeout: 100 }).catch((e) => {
// eslint-disable-next-line no-console
console.error('Unable to actualize viewport state', e);
});
if (isRequestSupportedPlatform(platform)) {
viewport.sync({ postEvent, timeout: 100 }).catch((e) => {
// eslint-disable-next-line no-console
console.error('Unable to actualize viewport state', e);
});
}

return viewport;
}
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/init/creators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from './createClosingBehavior.js';
export * from './createMainButton.js';
export * from './createMiniApp.js';
export * from './createRequestIdGenerator.js';
export * from './createSettingsButton.js';
export * from './createThemeParams.js';
export * from './createViewport.js';
13 changes: 8 additions & 5 deletions packages/sdk/src/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
createClosingBehavior,
createMainButton,
createMiniApp,
createRequestIdGenerator,
createRequestIdGenerator, createSettingsButton,
createThemeParams, createViewportAsync,
createViewportSync,
} from '~/init/creators/index.js';
Expand All @@ -23,7 +23,9 @@ import type { InitOptions, InitResult } from './types.js';

type ComputedInitResult<O> = O extends { async: true } ? Promise<InitResult> : InitResult;

export function init<O extends InitOptions>(options: O): ComputedInitResult<O> {
export function init(): InitResult;
export function init<O extends InitOptions>(options: O): ComputedInitResult<O>;
export function init(options: InitOptions = {}): InitResult | Promise<InitResult> {
const {
async = false,
cssVars = false,
Expand Down Expand Up @@ -84,6 +86,7 @@ export function init<O extends InitOptions>(options: O): ComputedInitResult<O> {
popup: new Popup(version, postEvent),
postEvent,
qrScanner: new QRScanner(version, postEvent),
settingsButton: createSettingsButton(isPageReload, version, postEvent),
themeParams: createThemeParams(themeParams),
utils: new Utils(version, createRequestId, postEvent),
...(initData
Expand Down Expand Up @@ -112,7 +115,7 @@ export function init<O extends InitOptions>(options: O): ComputedInitResult<O> {
...result,
viewport: vp,
};
}) as ComputedInitResult<O>;
});
}

processCSSVars(
Expand All @@ -122,10 +125,10 @@ export function init<O extends InitOptions>(options: O): ComputedInitResult<O> {
viewport,
);

return { ...result, viewport } as ComputedInitResult<O>;
return { ...result, viewport };
} catch (e) {
if (async) {
return Promise.reject(e) as unknown as ComputedInitResult<O>;
return Promise.reject(e);
}
throw e;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/init/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { MainButton } from '~/main-button/index.js';
import type { MiniApp } from '~/mini-app/index.js';
import type { Popup } from '~/popup/index.js';
import type { QRScanner } from '~/qr-scanner/index.js';
import type { SettingsButton } from '~/settings-button/index.js';
import type { ThemeParams } from '~/theme-params/index.js';
import type { CreateRequestIdFunc } from '~/types/index.js';
import type { Utils } from '~/utils/index.js';
Expand All @@ -28,6 +29,7 @@ export interface InitResult {
popup: Popup;
postEvent: PostEvent;
qrScanner: QRScanner;
settingsButton: SettingsButton;
themeParams: ThemeParams;
utils: Utils;
viewport: Viewport;
Expand Down
85 changes: 85 additions & 0 deletions packages/sdk/src/settings-button/SettingsButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
off,
on,
type PostEvent,
postEvent as defaultPostEvent,
} from '~/bridge/index.js';
import { EventEmitter } from '~/event-emitter/index.js';
import { State } from '~/state/index.js';
import { createSupportsFunc, type SupportsFunc } from '~/supports/index.js';
import type { Version } from '~/version/index.js';

import type { SettingsButtonEvents, SettingsButtonState } from './types.js';

type Emitter = EventEmitter<SettingsButtonEvents>;

export class SettingsButton {
private readonly ee: Emitter = new EventEmitter();

private readonly state: State<SettingsButtonState>;

constructor(
isVisible: boolean,
version: Version,
private readonly postEvent: PostEvent = defaultPostEvent,
) {
this.state = new State({ isVisible }, this.ee);
this.supports = createSupportsFunc(version, {
show: 'web_app_setup_settings_button',
hide: 'web_app_setup_settings_button',
});
}

private set isVisible(visible: boolean) {
this.state.set('isVisible', visible);
this.postEvent('web_app_setup_settings_button', { is_visible: visible });
}

/**
* True if SettingsButton is currently visible.
*/
get isVisible(): boolean {
return this.state.get('isVisible');
}

/**
* Hides the SettingsButton.
*/
hide(): void {
this.isVisible = false;
}

/**
* Adds event listener.
* @param event - event name.
* @param listener - event listener.
*/
on: Emitter['on'] = (event, listener) => (
event === 'click'
? on('settings_button_pressed', listener)
: this.ee.on(event, listener)
);

/**
* Removes event listener.
* @param event - event name.
* @param listener - event listener.
*/
off: Emitter['off'] = (event, listener) => (
event === 'click'
? off('settings_button_pressed', listener)
: this.ee.off(event, listener)
);

/**
* Shows the SettingsButton.
*/
show(): void {
this.isVisible = true;
}

/**
* Checks if specified method is supported by current component.
*/
supports: SupportsFunc<'show' | 'hide'>;
}
2 changes: 2 additions & 0 deletions packages/sdk/src/settings-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './SettingsButton.js';
export * from './types.js';
15 changes: 15 additions & 0 deletions packages/sdk/src/settings-button/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { MiniAppsEventListener } from '~/bridge/index.js';
import type { StateEvents } from '~/state/index.js';

export interface SettingsButtonState {
isVisible: boolean;
}

export interface SettingsButtonEvents extends StateEvents<SettingsButtonState> {
click: MiniAppsEventListener<'settings_button_pressed'>;
}

export type SettingsButtonEventName = keyof SettingsButtonEvents;

export type SettingsButtonEventListener<E extends SettingsButtonEventName> =
SettingsButtonEvents[E];
3 changes: 3 additions & 0 deletions packages/sdk/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ interface StorageParams {
text: string;
textColor: RGB;
};
'settings-button': {
isVisible: boolean
};
viewport: {
height: number;
isExpanded: boolean;
Expand Down

0 comments on commit 53c5e28

Please sign in to comment.