Skip to content

Commit 335ee64

Browse files
authored
Merge pull request #866 from wishyoudie/feature/button-custom-icon
Add support for custom icon emoji in bottom buttons (Mini Apps v9.5)
2 parents 5eb7119 + 2d24b83 commit 335ee64

12 files changed

Lines changed: 145 additions & 8 deletions

File tree

.changeset/happy-boxes-brake.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@tma.js/bridge": minor
3+
"@tma.js/sdk": minor
4+
---
5+
6+
added support for icon_custom_emoji_id in bottom buttons

apps/docs/platform/methods.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,7 @@ Updates the [Main Button](main-button.md) settings.
653653
| color | `string` | _Optional_. The button background color in `#RRGGBB` format. |
654654
| text_color | `string` | _Optional_. The button text color in `#RRGGBB` format. |
655655
| has_shine_effect | `boolean` | _Optional_. Should the button have a shining effect. | `v7.8` |
656+
| icon_custom_emoji_id | `string` | _Optional_. The ID of custom emoji icon displayed alongside button text. | `v9.5` |
656657

657658
### `web_app_setup_settings_button`
658659

@@ -750,6 +751,7 @@ The method that updates the Secondary Button settings.
750751
<th>Field</th>
751752
<th>Type</th>
752753
<th>Description</th>
754+
<th>Available since</th>
753755
</tr>
754756

755757
</thead>
@@ -761,6 +763,7 @@ The method that updates the Secondary Button settings.
761763
<code>boolean</code>
762764
</td>
763765
<td><i>Optional</i>. Should the button be displayed.</td>
766+
<td><code>v7.10</code></td>
764767
</tr>
765768

766769
<tr>
@@ -769,6 +772,7 @@ The method that updates the Secondary Button settings.
769772
<code>boolean</code>
770773
</td>
771774
<td><i>Optional</i>. Should the button be enabled.</td>
775+
<td><code>v7.10</code></td>
772776
</tr>
773777

774778
<tr>
@@ -780,6 +784,7 @@ The method that updates the Secondary Button settings.
780784
<i>Optional</i>. Should loader inside the button be displayed. Use this property in case,
781785
some operation takes time. This loader will make user notified about it.
782786
</td>
787+
<td><code>v7.10</code></td>
783788
</tr>
784789

785790
<tr>
@@ -788,6 +793,7 @@ The method that updates the Secondary Button settings.
788793
<code>string</code>
789794
</td>
790795
<td><i>Optional</i>. The button background color in <code>#RRGGBB</code> format.</td>
796+
<td><code>v7.10</code></td>
791797
</tr>
792798

793799
<tr>
@@ -796,6 +802,7 @@ The method that updates the Secondary Button settings.
796802
<code>string</code>
797803
</td>
798804
<td><i>Optional</i>. The button text color in <code>#RRGGBB</code> format.</td>
805+
<td><code>v7.10</code></td>
799806
</tr>
800807

801808
<tr>
@@ -804,6 +811,7 @@ The method that updates the Secondary Button settings.
804811
<code>boolean</code>
805812
</td>
806813
<td><i>Optional</i>. Should the button have a shining effect.</td>
814+
<td><code>v7.10</code></td>
807815
</tr>
808816

809817
<tr>
@@ -829,7 +837,18 @@ The method that updates the Secondary Button settings.
829837
</li>
830838
</ul>
831839
</td>
840+
<td><code>v7.10</code></td>
832841
</tr>
842+
843+
<tr>
844+
<td>icon_custom_emoji_id</td>
845+
<td>
846+
<code>string</code>
847+
</td>
848+
<td><i>Optional</i>. The ID of custom emoji icon displayed alongside button text.</td>
849+
<td><code>v9.5</code></td>
850+
</tr>
851+
833852
</tbody>
834853
</table>
835854

packages/bridge/src/methods/createPostEvent.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ export function createPostEvent(
7272
) {
7373
return onUnsupported({ version, method, param: 'color' });
7474
}
75+
if (
76+
(method === 'web_app_setup_main_button' || method === 'web_app_setup_secondary_button')
77+
&& is(looseObject({ icon_custom_emoji_id: any() }), params)
78+
&& !supports(method, 'icon_custom_emoji_id', version)
79+
) {
80+
return onUnsupported({ version, method, param: 'icon_custom_emoji_id' });
81+
}
7582

7683
return postEvent(method, params);
7784
}) as PostEventFn;

packages/bridge/src/methods/getReleaseVersion.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,18 @@ describe.each<[
124124
'web_app_secure_storage_save_key',
125125
]],
126126
['9.1', ['web_app_hide_keyboard']],
127+
['9.5', [
128+
{
129+
title: 'web_app_setup_main_button.icon_custom_emoji_id',
130+
method: 'web_app_setup_main_button',
131+
param: 'icon_custom_emoji_id',
132+
},
133+
{
134+
title: 'web_app_setup_secondary_button.icon_custom_emoji_id',
135+
method: 'web_app_setup_secondary_button',
136+
param: 'icon_custom_emoji_id',
137+
},
138+
]],
127139
])('%s', (version, methods) => {
128140
const methodsOnly = methods.filter((m): m is MethodName => {
129141
return typeof m === 'string';

packages/bridge/src/methods/getReleaseVersion.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ const releases = {
9393
'web_app_secure_storage_save_key',
9494
],
9595
9.1: ['web_app_hide_keyboard'],
96+
9.5: [
97+
{ method: 'web_app_setup_main_button', param: 'icon_custom_emoji_id' },
98+
{ method: 'web_app_setup_secondary_button', param: 'icon_custom_emoji_id' },
99+
],
96100
};
97101

98102
/**

packages/bridge/src/methods/types/methods.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ interface ButtonParams {
2828
* @since 7.10
2929
*/
3030
has_shine_effect?: boolean;
31+
/**
32+
* The ID of custom emoji icon displayed alongside button text.
33+
* @since 9.5
34+
*/
35+
icon_custom_emoji_id?: string;
3136
/**
3237
* Should the button be displayed.
3338
*/
@@ -562,7 +567,7 @@ export interface Methods {
562567
* Updates the Main Button settings.
563568
* @see https://docs.telegram-mini-apps.com/platform/methods#web-app-setup-main-button
564569
*/
565-
web_app_setup_main_button: CreateMethodParams<ButtonParams, 'has_shine_effect'>;
570+
web_app_setup_main_button: CreateMethodParams<ButtonParams, 'has_shine_effect' | 'icon_custom_emoji_id'>;
566571

567572
/**
568573
* Updates the secondary button settings.
@@ -581,7 +586,7 @@ export interface Methods {
581586
* - `bottom`, displayed below the main button.
582587
*/
583588
position?: SecondaryButtonPosition;
584-
}>;
589+
}, 'icon_custom_emoji_id'>;
585590

586591
/**
587592
* Updates the current state of the Settings Button.

packages/sdk/src/features/MainButton/MainButton.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ describe.each([
5353
['hideLoader', (component: MainButton) => component.hideLoader(), true],
5454
['setText', (component: MainButton) => component.setText('a'), true],
5555
['setTextColor', (component: MainButton) => component.setTextColor('#aaa'), true],
56+
['setIconCustomEmojiId', (component: MainButton) => component.setIconCustomEmojiId('123'), true],
5657
['setBgColor', (component: MainButton) => component.setBgColor('#ddd'), true],
5758
] as const)('%s', (method, tryCall, requireMount) => {
5859
describe('safety', () => {
@@ -182,6 +183,13 @@ describe.each([
182183
usedValue: '#cba',
183184
use: (component: MainButton) => component.setTextColor('#cba'),
184185
},
186+
{
187+
method: 'setIconCustomEmojiId',
188+
property: 'iconCustomEmojiId',
189+
payloadProperty: 'icon_custom_emoji_id',
190+
usedValue: '123',
191+
use: (component: MainButton) => component.setIconCustomEmojiId('123'),
192+
},
185193
// {
186194
// method: 'setText',
187195
// property: 'text',
@@ -265,6 +273,7 @@ describe('mount', () => {
265273
isLoaderVisible: false,
266274
text: 'Text',
267275
textColor: '#112',
276+
iconCustomEmojiId: '123',
268277
} as const));
269278
const component = instantiate({ storage: { get, set: vi.fn() } });
270279
component.mount();
@@ -291,6 +300,7 @@ describe('mount', () => {
291300
isLoaderVisible: false,
292301
text: 'Text',
293302
textColor: '#112',
303+
iconCustomEmojiId: '123',
294304
} as const));
295305
const component2 = instantiate({
296306
storage: { get: get2, set: vi.fn() },

packages/sdk/src/features/MainButton/MainButton.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface MainButtonState {
1818
isLoaderVisible: boolean;
1919
text: string;
2020
textColor?: RGB;
21+
iconCustomEmojiId?: string;
2122
}
2223

2324
export interface MainButtonOptions extends Omit<
@@ -30,6 +31,7 @@ export interface MainButtonOptions extends Omit<
3031
defaults: {
3132
bgColor: MaybeAccessor<RGB>;
3233
textColor: MaybeAccessor<RGB>;
34+
iconCustomEmojiId: MaybeAccessor<string>;
3335
};
3436
}
3537

@@ -44,6 +46,7 @@ export class MainButton {
4446
isLoaderVisible: false,
4547
isVisible: false,
4648
text: 'Continue',
49+
iconCustomEmojiId: '',
4750
},
4851
method: 'web_app_setup_main_button',
4952
payload: state => ({
@@ -54,23 +57,33 @@ export class MainButton {
5457
text: state.text,
5558
color: state.bgColor,
5659
text_color: state.textColor,
60+
icon_custom_emoji_id: state.iconCustomEmojiId,
5761
}),
5862
});
5963

60-
const withDefault = (
64+
const withDefaultColor = (
6165
field: 'bgColor' | 'textColor',
6266
getDefault: MaybeAccessor<RGB>,
6367
) => {
6468
const fromState = button.stateGetter(field);
6569
return computed(() => fromState() || access(getDefault));
6670
};
6771

68-
this.bgColor = withDefault('bgColor', defaults.bgColor);
69-
this.textColor = withDefault('textColor', defaults.textColor);
72+
const withDefault = (
73+
field: 'iconCustomEmojiId',
74+
getDefault: MaybeAccessor<string>,
75+
) => {
76+
const fromState = button.stateGetter(field);
77+
return computed(() => fromState() || access(getDefault));
78+
};
79+
80+
this.bgColor = withDefaultColor('bgColor', defaults.bgColor);
81+
this.textColor = withDefaultColor('textColor', defaults.textColor);
7082
this.hasShineEffect = button.stateGetter('hasShineEffect');
7183
this.isEnabled = button.stateGetter('isEnabled');
7284
this.isLoaderVisible = button.stateGetter('isLoaderVisible');
7385
this.text = button.stateGetter('text');
86+
this.iconCustomEmojiId = withDefault('iconCustomEmojiId', '');
7487
this.isVisible = button.stateGetter('isVisible');
7588
this.isMounted = button.isMounted;
7689
this.state = button.state;
@@ -91,6 +104,7 @@ export class MainButton {
91104
] = button.stateBoolSetters('isLoaderVisible');
92105

93106
[this.setText, this.setTextFp] = button.stateSetters('text');
107+
[this.setIconCustomEmojiId, this.setIconCustomEmojiIdFp] = button.stateSetters('iconCustomEmojiId');
94108
[[this.hide, this.hideFp], [this.show, this.showFp]] = button.stateBoolSetters('isVisible');
95109
this.setParams = button.setState;
96110
this.setParamsFp = button.setStateFp;
@@ -152,6 +166,12 @@ export class MainButton {
152166
* params colors.
153167
*/
154168
readonly textColor: Computed<RGB>;
169+
170+
/**
171+
* The ID of custom emoji icon displayed alongside button text.
172+
* @since Mini Apps v9.5
173+
*/
174+
readonly iconCustomEmojiId: Computed<string>;
155175
//#endregion
156176

157177
//#region Methods.
@@ -245,6 +265,17 @@ export class MainButton {
245265
*/
246266
readonly setText: WithChecks<(value: string) => void, false>;
247267

268+
/**
269+
* Updates the button custom emoji ID.
270+
* @since Mini Apps v9.5
271+
*/
272+
readonly setIconCustomEmojiIdFp: WithChecksFp<(value: string) => MainButtonEither, false>;
273+
274+
/**
275+
* @see setIconCustomEmojiIdFp
276+
*/
277+
readonly setIconCustomEmojiId: WithChecks<(value: string) => void, false>;
278+
248279
/**
249280
* Shows the button loader.
250281
*/

packages/sdk/src/features/MainButton/instance.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ export const mainButton = /* @__PURE__*/ new MainButton(
88
bottomButtonOptions('mainButton', 'main_button_pressed', {
99
bgColor: computed(() => themeParams.buttonColor() || '#2481cc'),
1010
textColor: computed(() => themeParams.buttonTextColor() || '#ffffff'),
11+
iconCustomEmojiId: computed(() => ''),
1112
}),
1213
);

packages/sdk/src/features/SecondaryButton/SecondaryButton.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ describe.each([
5858
['hideLoader', (component: SecondaryButton) => component.hideLoader(), true],
5959
['setText', (component: SecondaryButton) => component.setText('a'), true],
6060
['setTextColor', (component: SecondaryButton) => component.setTextColor('#aaa'), true],
61+
['setIconCustomEmojiId', (component: SecondaryButton) => component.setIconCustomEmojiId('123'), true],
6162
['setBgColor', (component: SecondaryButton) => component.setBgColor('#ddd'), true],
6263
['setPosition', (component: SecondaryButton) => component.setPosition('right'), true],
6364
] as const)('%s', (method, tryCall, requireMount) => {
@@ -196,6 +197,13 @@ describe.each([
196197
usedValue: 'Some text',
197198
use: (component: SecondaryButton) => component.setText('Some text'),
198199
},
200+
{
201+
method: 'setIconCustomEmojiId',
202+
property: 'iconCustomEmojiId',
203+
payloadProperty: 'icon_custom_emoji_id',
204+
usedValue: '123',
205+
use: (component: SecondaryButton) => component.setIconCustomEmojiId('123'),
206+
},
199207
{
200208
method: 'setPosition',
201209
property: 'position',
@@ -279,6 +287,7 @@ describe('mount', () => {
279287
isLoaderVisible: false,
280288
text: 'Text',
281289
textColor: '#112',
290+
iconCustomEmojiId: '123',
282291
} as const));
283292
const component = instantiate({ storage: { get, set: vi.fn() } });
284293
component.mount();
@@ -305,6 +314,7 @@ describe('mount', () => {
305314
isLoaderVisible: false,
306315
text: 'Text',
307316
textColor: '#112',
317+
iconCustomEmojiId: '123',
308318
} as const));
309319
const component2 = instantiate({
310320
storage: { get: get2, set: vi.fn() },

0 commit comments

Comments
 (0)