From 6e85dc9c6514fee2c38c2dc1a04e0cb456ef0362 Mon Sep 17 00:00:00 2001 From: Vladimir Ilyin Date: Wed, 1 Apr 2026 22:47:38 +0300 Subject: [PATCH 1/2] feat: add v9.5 bottom buttons icons --- apps/docs/platform/methods.md | 19 ++++++++++ .../bridge/src/methods/createPostEvent.ts | 7 ++++ .../src/methods/getReleaseVersion.test.ts | 12 ++++++ .../bridge/src/methods/getReleaseVersion.ts | 4 ++ packages/bridge/src/methods/types/methods.ts | 9 ++++- .../features/MainButton/MainButton.test.ts | 10 +++++ .../sdk/src/features/MainButton/MainButton.ts | 37 +++++++++++++++++-- .../sdk/src/features/MainButton/instance.ts | 1 + .../SecondaryButton/SecondaryButton.test.ts | 10 +++++ .../SecondaryButton/SecondaryButton.ts | 37 +++++++++++++++++-- .../src/features/SecondaryButton/instance.ts | 1 + 11 files changed, 139 insertions(+), 8 deletions(-) diff --git a/apps/docs/platform/methods.md b/apps/docs/platform/methods.md index 4f833810b..f5aec12f6 100644 --- a/apps/docs/platform/methods.md +++ b/apps/docs/platform/methods.md @@ -653,6 +653,7 @@ Updates the [Main Button](main-button.md) settings. | color | `string` | _Optional_. The button background color in `#RRGGBB` format. | | text_color | `string` | _Optional_. The button text color in `#RRGGBB` format. | | has_shine_effect | `boolean` | _Optional_. Should the button have a shining effect. | `v7.8` | +| icon_custom_emoji_id | `string` | _Optional_. The ID of custom emoji icon displayed alongside button text. | `v9.5` | ### `web_app_setup_settings_button` @@ -750,6 +751,7 @@ The method that updates the Secondary Button settings. Field Type Description + Available since @@ -761,6 +763,7 @@ The method that updates the Secondary Button settings. boolean Optional. Should the button be displayed. + v7.10 @@ -769,6 +772,7 @@ The method that updates the Secondary Button settings. boolean Optional. Should the button be enabled. + v7.10 @@ -780,6 +784,7 @@ The method that updates the Secondary Button settings. Optional. Should loader inside the button be displayed. Use this property in case, some operation takes time. This loader will make user notified about it. + v7.10 @@ -788,6 +793,7 @@ The method that updates the Secondary Button settings. string Optional. The button background color in #RRGGBB format. + v7.10 @@ -796,6 +802,7 @@ The method that updates the Secondary Button settings. string Optional. The button text color in #RRGGBB format. + v7.10 @@ -804,6 +811,7 @@ The method that updates the Secondary Button settings. boolean Optional. Should the button have a shining effect. + v7.10 @@ -829,7 +837,18 @@ The method that updates the Secondary Button settings. + v7.10 + + + icon_custom_emoji_id + + string + + Optional. The ID of custom emoji icon displayed alongside button text. + v9.5 + + diff --git a/packages/bridge/src/methods/createPostEvent.ts b/packages/bridge/src/methods/createPostEvent.ts index 1f7d6c95c..cf3890c5c 100644 --- a/packages/bridge/src/methods/createPostEvent.ts +++ b/packages/bridge/src/methods/createPostEvent.ts @@ -72,6 +72,13 @@ export function createPostEvent( ) { return onUnsupported({ version, method, param: 'color' }); } + if ( + (method === 'web_app_setup_main_button' || method === 'web_app_setup_secondary_button') + && is(looseObject({ icon_custom_emoji_id: any() }), params) + && !supports(method, 'icon_custom_emoji_id', version) + ) { + return onUnsupported({ version, method, param: 'icon_custom_emoji_id' }); + } return postEvent(method, params); }) as PostEventFn; diff --git a/packages/bridge/src/methods/getReleaseVersion.test.ts b/packages/bridge/src/methods/getReleaseVersion.test.ts index 555e478b1..e9fb1ea15 100644 --- a/packages/bridge/src/methods/getReleaseVersion.test.ts +++ b/packages/bridge/src/methods/getReleaseVersion.test.ts @@ -124,6 +124,18 @@ describe.each<[ 'web_app_secure_storage_save_key', ]], ['9.1', ['web_app_hide_keyboard']], + ['9.5', [ + { + title: 'web_app_setup_main_button.icon_custom_emoji_id', + method: 'web_app_setup_main_button', + param: 'icon_custom_emoji_id', + }, + { + title: 'web_app_setup_secondary_button.icon_custom_emoji_id', + method: 'web_app_setup_secondary_button', + param: 'icon_custom_emoji_id', + }, + ]], ])('%s', (version, methods) => { const methodsOnly = methods.filter((m): m is MethodName => { return typeof m === 'string'; diff --git a/packages/bridge/src/methods/getReleaseVersion.ts b/packages/bridge/src/methods/getReleaseVersion.ts index d5a0bb247..23220827b 100644 --- a/packages/bridge/src/methods/getReleaseVersion.ts +++ b/packages/bridge/src/methods/getReleaseVersion.ts @@ -93,6 +93,10 @@ const releases = { 'web_app_secure_storage_save_key', ], 9.1: ['web_app_hide_keyboard'], + 9.5: [ + { method: 'web_app_setup_main_button', param: 'icon_custom_emoji_id' }, + { method: 'web_app_setup_secondary_button', param: 'icon_custom_emoji_id' }, + ], }; /** diff --git a/packages/bridge/src/methods/types/methods.ts b/packages/bridge/src/methods/types/methods.ts index 96166e0fe..121db2555 100644 --- a/packages/bridge/src/methods/types/methods.ts +++ b/packages/bridge/src/methods/types/methods.ts @@ -28,6 +28,11 @@ interface ButtonParams { * @since 7.10 */ has_shine_effect?: boolean; + /** + * The ID of custom emoji icon displayed alongside button text. + * @since 9.5 + */ + icon_custom_emoji_id?: string; /** * Should the button be displayed. */ @@ -562,7 +567,7 @@ export interface Methods { * Updates the Main Button settings. * @see https://docs.telegram-mini-apps.com/platform/methods#web-app-setup-main-button */ - web_app_setup_main_button: CreateMethodParams; + web_app_setup_main_button: CreateMethodParams; /** * Updates the secondary button settings. @@ -581,7 +586,7 @@ export interface Methods { * - `bottom`, displayed below the main button. */ position?: SecondaryButtonPosition; - }>; + }, 'icon_custom_emoji_id'>; /** * Updates the current state of the Settings Button. diff --git a/packages/sdk/src/features/MainButton/MainButton.test.ts b/packages/sdk/src/features/MainButton/MainButton.test.ts index 64ea238c2..a0585b642 100644 --- a/packages/sdk/src/features/MainButton/MainButton.test.ts +++ b/packages/sdk/src/features/MainButton/MainButton.test.ts @@ -53,6 +53,7 @@ describe.each([ ['hideLoader', (component: MainButton) => component.hideLoader(), true], ['setText', (component: MainButton) => component.setText('a'), true], ['setTextColor', (component: MainButton) => component.setTextColor('#aaa'), true], + ['setIconCustomEmojiId', (component: MainButton) => component.setIconCustomEmojiId('123'), true], ['setBgColor', (component: MainButton) => component.setBgColor('#ddd'), true], ] as const)('%s', (method, tryCall, requireMount) => { describe('safety', () => { @@ -182,6 +183,13 @@ describe.each([ usedValue: '#cba', use: (component: MainButton) => component.setTextColor('#cba'), }, + { + method: 'setIconCustomEmojiId', + property: 'iconCustomEmojiId', + payloadProperty: 'icon_custom_emoji_id', + usedValue: '123', + use: (component: MainButton) => component.setIconCustomEmojiId('123'), + }, // { // method: 'setText', // property: 'text', @@ -265,6 +273,7 @@ describe('mount', () => { isLoaderVisible: false, text: 'Text', textColor: '#112', + iconCustomEmojiId: '123', } as const)); const component = instantiate({ storage: { get, set: vi.fn() } }); component.mount(); @@ -291,6 +300,7 @@ describe('mount', () => { isLoaderVisible: false, text: 'Text', textColor: '#112', + iconCustomEmojiId: '123', } as const)); const component2 = instantiate({ storage: { get: get2, set: vi.fn() }, diff --git a/packages/sdk/src/features/MainButton/MainButton.ts b/packages/sdk/src/features/MainButton/MainButton.ts index fe457cc5c..8bfa6d570 100644 --- a/packages/sdk/src/features/MainButton/MainButton.ts +++ b/packages/sdk/src/features/MainButton/MainButton.ts @@ -18,6 +18,7 @@ export interface MainButtonState { isLoaderVisible: boolean; text: string; textColor?: RGB; + iconCustomEmojiId?: string; } export interface MainButtonOptions extends Omit< @@ -30,6 +31,7 @@ export interface MainButtonOptions extends Omit< defaults: { bgColor: MaybeAccessor; textColor: MaybeAccessor; + iconCustomEmojiId: MaybeAccessor; }; } @@ -44,6 +46,7 @@ export class MainButton { isLoaderVisible: false, isVisible: false, text: 'Continue', + iconCustomEmojiId: '', }, method: 'web_app_setup_main_button', payload: state => ({ @@ -54,10 +57,11 @@ export class MainButton { text: state.text, color: state.bgColor, text_color: state.textColor, + icon_custom_emoji_id: state.iconCustomEmojiId, }), }); - const withDefault = ( + const withDefaultColor = ( field: 'bgColor' | 'textColor', getDefault: MaybeAccessor, ) => { @@ -65,12 +69,21 @@ export class MainButton { return computed(() => fromState() || access(getDefault)); }; - this.bgColor = withDefault('bgColor', defaults.bgColor); - this.textColor = withDefault('textColor', defaults.textColor); + const withDefault = ( + field: 'iconCustomEmojiId', + getDefault: MaybeAccessor, + ) => { + const fromState = button.stateGetter(field); + return computed(() => fromState() || access(getDefault)); + }; + + this.bgColor = withDefaultColor('bgColor', defaults.bgColor); + this.textColor = withDefaultColor('textColor', defaults.textColor); this.hasShineEffect = button.stateGetter('hasShineEffect'); this.isEnabled = button.stateGetter('isEnabled'); this.isLoaderVisible = button.stateGetter('isLoaderVisible'); this.text = button.stateGetter('text'); + this.iconCustomEmojiId = withDefault('iconCustomEmojiId', ''); this.isVisible = button.stateGetter('isVisible'); this.isMounted = button.isMounted; this.state = button.state; @@ -91,6 +104,7 @@ export class MainButton { ] = button.stateBoolSetters('isLoaderVisible'); [this.setText, this.setTextFp] = button.stateSetters('text'); + [this.setIconCustomEmojiId, this.setIconCustomEmojiIdFp] = button.stateSetters('iconCustomEmojiId'); [[this.hide, this.hideFp], [this.show, this.showFp]] = button.stateBoolSetters('isVisible'); this.setParams = button.setState; this.setParamsFp = button.setStateFp; @@ -152,6 +166,12 @@ export class MainButton { * params colors. */ readonly textColor: Computed; + + /** + * The ID of custom emoji icon displayed alongside button text. + * @since Mini Apps v9.5 + */ + readonly iconCustomEmojiId: Computed; //#endregion //#region Methods. @@ -245,6 +265,17 @@ export class MainButton { */ readonly setText: WithChecks<(value: string) => void, false>; + /** + * Updates the button custom emoji ID. + * @since Mini Apps v9.5 + */ + readonly setIconCustomEmojiIdFp: WithChecksFp<(value: string) => MainButtonEither, false>; + + /** + * @see setIconCustomEmojiIdFp + */ + readonly setIconCustomEmojiId: WithChecks<(value: string) => void, false>; + /** * Shows the button loader. */ diff --git a/packages/sdk/src/features/MainButton/instance.ts b/packages/sdk/src/features/MainButton/instance.ts index c8a9b0fd3..bf83d5951 100644 --- a/packages/sdk/src/features/MainButton/instance.ts +++ b/packages/sdk/src/features/MainButton/instance.ts @@ -8,5 +8,6 @@ export const mainButton = /* @__PURE__*/ new MainButton( bottomButtonOptions('mainButton', 'main_button_pressed', { bgColor: computed(() => themeParams.buttonColor() || '#2481cc'), textColor: computed(() => themeParams.buttonTextColor() || '#ffffff'), + iconCustomEmojiId: computed(() => ''), }), ); diff --git a/packages/sdk/src/features/SecondaryButton/SecondaryButton.test.ts b/packages/sdk/src/features/SecondaryButton/SecondaryButton.test.ts index 9e6f8647b..3fae82520 100644 --- a/packages/sdk/src/features/SecondaryButton/SecondaryButton.test.ts +++ b/packages/sdk/src/features/SecondaryButton/SecondaryButton.test.ts @@ -58,6 +58,7 @@ describe.each([ ['hideLoader', (component: SecondaryButton) => component.hideLoader(), true], ['setText', (component: SecondaryButton) => component.setText('a'), true], ['setTextColor', (component: SecondaryButton) => component.setTextColor('#aaa'), true], + ['setIconCustomEmojiId', (component: SecondaryButton) => component.setIconCustomEmojiId('123'), true], ['setBgColor', (component: SecondaryButton) => component.setBgColor('#ddd'), true], ['setPosition', (component: SecondaryButton) => component.setPosition('right'), true], ] as const)('%s', (method, tryCall, requireMount) => { @@ -196,6 +197,13 @@ describe.each([ usedValue: 'Some text', use: (component: SecondaryButton) => component.setText('Some text'), }, + { + method: 'setIconCustomEmojiId', + property: 'iconCustomEmojiId', + payloadProperty: 'icon_custom_emoji_id', + usedValue: '123', + use: (component: SecondaryButton) => component.setIconCustomEmojiId('123'), + }, { method: 'setPosition', property: 'position', @@ -279,6 +287,7 @@ describe('mount', () => { isLoaderVisible: false, text: 'Text', textColor: '#112', + iconCustomEmojiId: '123', } as const)); const component = instantiate({ storage: { get, set: vi.fn() } }); component.mount(); @@ -305,6 +314,7 @@ describe('mount', () => { isLoaderVisible: false, text: 'Text', textColor: '#112', + iconCustomEmojiId: '123', } as const)); const component2 = instantiate({ storage: { get: get2, set: vi.fn() }, diff --git a/packages/sdk/src/features/SecondaryButton/SecondaryButton.ts b/packages/sdk/src/features/SecondaryButton/SecondaryButton.ts index 6749962f6..27eaa80fc 100644 --- a/packages/sdk/src/features/SecondaryButton/SecondaryButton.ts +++ b/packages/sdk/src/features/SecondaryButton/SecondaryButton.ts @@ -20,6 +20,7 @@ export interface SecondaryButtonState { text: string; textColor?: RGB; position: SecondaryButtonPosition; + iconCustomEmojiId?: string; } export interface SecondaryButtonOptions extends Omit< @@ -32,6 +33,7 @@ export interface SecondaryButtonOptions extends Omit< defaults: { bgColor: MaybeAccessor; textColor: MaybeAccessor; + iconCustomEmojiId: MaybeAccessor; }; } @@ -49,6 +51,7 @@ export class SecondaryButton { isVisible: false, text: 'Cancel', position: 'left', + iconCustomEmojiId: '', }, method: 'web_app_setup_secondary_button', payload: state => ({ @@ -60,10 +63,11 @@ export class SecondaryButton { color: state.bgColor, text_color: state.textColor, position: state.position, + icon_custom_emoji_id: state.iconCustomEmojiId, }), }); - const withDefault = ( + const withDefaultColor = ( field: 'bgColor' | 'textColor', getDefault: MaybeAccessor, ) => { @@ -71,14 +75,23 @@ export class SecondaryButton { return computed(() => fromState() || access(getDefault)); }; + const withDefault = ( + field: 'iconCustomEmojiId', + getDefault: MaybeAccessor, + ) => { + const fromState = button.stateGetter(field); + return computed(() => fromState() || access(getDefault)); + }; + this.isSupported = createIsSupportedSignal('web_app_setup_secondary_button', options.version); - this.bgColor = withDefault('bgColor', defaults.bgColor); - this.textColor = withDefault('textColor', defaults.textColor); + this.bgColor = withDefaultColor('bgColor', defaults.bgColor); + this.textColor = withDefaultColor('textColor', defaults.textColor); this.position = button.stateGetter('position'); this.hasShineEffect = button.stateGetter('hasShineEffect'); this.isEnabled = button.stateGetter('isEnabled'); this.isLoaderVisible = button.stateGetter('isLoaderVisible'); this.text = button.stateGetter('text'); + this.iconCustomEmojiId = withDefault('iconCustomEmojiId', ''); this.isVisible = button.stateGetter('isVisible'); this.isMounted = button.isMounted; this.state = button.state; @@ -100,6 +113,7 @@ export class SecondaryButton { ] = button.stateBoolSetters('isLoaderVisible'); [this.setText, this.setTextFp] = button.stateSetters('text'); + [this.setIconCustomEmojiId, this.setIconCustomEmojiIdFp] = button.stateSetters('iconCustomEmojiId'); [[this.hide, this.hideFp], [this.show, this.showFp]] = button.stateBoolSetters('isVisible'); this.setParams = button.setState; this.setParamsFp = button.setStateFp; @@ -171,6 +185,12 @@ export class SecondaryButton { * params colors. */ readonly textColor: Computed; + + /** + * The ID of custom emoji icon displayed alongside button text. + * @since Mini Apps v9.5 + */ + readonly iconCustomEmojiId: Computed; //#endregion //#region Methods. @@ -287,6 +307,17 @@ export class SecondaryButton { */ readonly setPosition: WithChecks<(position: SecondaryButtonPosition) => void, true>; + /** + * Updates the button custom emoji Id. + * @since Mini Apps v9.5 + */ + readonly setIconCustomEmojiIdFp: WithChecksFp<(value: string) => SecondaryButtonEither, false>; + + /** + * @see setIconCustomEmojiIdFp + */ + readonly setIconCustomEmojiId: WithChecks<(value: string) => void, false>; + /** * Shows the button loader. * @since Mini Apps v7.10 diff --git a/packages/sdk/src/features/SecondaryButton/instance.ts b/packages/sdk/src/features/SecondaryButton/instance.ts index 62f6744b8..9254aab4b 100644 --- a/packages/sdk/src/features/SecondaryButton/instance.ts +++ b/packages/sdk/src/features/SecondaryButton/instance.ts @@ -10,6 +10,7 @@ function instantiate() { bottomButtonOptions('secondaryButton', 'secondary_button_pressed', { bgColor: computed(() => miniApp.bottomBarColorRgb() || '#000000'), textColor: computed(() => themeParams.buttonColor() || '#2481cc'), + iconCustomEmojiId: computed(() => ''), }), ); } From 2d24b83660b93e96e5ca41ea8a55cd52a54dd7c8 Mon Sep 17 00:00:00 2001 From: Vladimir Ilyin Date: Wed, 1 Apr 2026 22:50:59 +0300 Subject: [PATCH 2/2] docs(changeset): added support for icon_custom_emoji_id in bottom buttons --- .changeset/happy-boxes-brake.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/happy-boxes-brake.md diff --git a/.changeset/happy-boxes-brake.md b/.changeset/happy-boxes-brake.md new file mode 100644 index 000000000..5e4d16381 --- /dev/null +++ b/.changeset/happy-boxes-brake.md @@ -0,0 +1,6 @@ +--- +"@tma.js/bridge": minor +"@tma.js/sdk": minor +--- + +added support for icon_custom_emoji_id in bottom buttons