Part 2. TMA on KMP. Writing a clicker for Telegram
![](https://habrastorage.org/getpro/habr/upload_files/26f/b1b/a12/26fb1ba12fe756bd5340b24220aecd12.png)
Navigate through the article series:
Part 1. Writing a clicker web application in Kotlin
Part 2. Writing a clicker for Telegram in Kotlin – current article
Part 2 and a half. User authentication with rest-framework. TMA on KMP – in development
Part 3. Adding Payment via Telegram Mini Apps on Kotlin – in development
Topics covered in the series
Web Application in Kotlin – Part 1
Integrating the App with Telegram Mini Apps – Part 2
Working with the interface elements of the TMA application. Theme,
MainButton
,BackButton
– part 2Share a link to an application via Telegram. Transferring data via a link – part 2
Authentication via TMA application – part 2 and 2.5
Telegram Payments API – Part 3
Launching the application in Telegram
Telegram Mini Apps (hereinafter referred to as TMA), if simply, are regular web applications that have some limited access to the API defined by Telegram. From which follows the ability to use any stack used in the development of web applications. In this article, we will launch the web application developed in the previous article in Telegram
To add a web application to Telegram, simply create a bot via BotFather documentation. The token itself is not needed yet, now we need to configure the bot to open our application. In the bot settings (in the chat with BotFather), go to the menu Bot Settings
>> Menu Button
>> Customize menu button
. Now we are asked to enter the link where our bot is located.
localhost
won't work, since most likely you don't have it configured to accept requests via https. So we'll take the simplest route, usingngrok
. To set up, register on the site ngrokinstall it and go through the initial setup. The launch itself, the final step, is simple:
ngrok http http://localhost:8080 --host-header="localhost:8080"
After this we get a host that works with the https scheme
![Figure 2](https://habrastorage.org/getpro/habr/upload_files/0ee/437/1f4/0ee4371f465cec7d6d491664f874f7b3.png)
Next, we send it to BotFather and set the inscription on the button. Done, now our newly created bot has a button. Click and our web application opens.
Navigation using TMA (MainButton, BackButton)
Let's add a second screen, we won't connect any libraries for navigation, we'll use enum, state and when.
// Don't use in real code
enum class Screen {
CLICKER,
FRIENDS_LIST,
}
// Don't use in real code
var currentScreen by mutableStateOf(Screen.CLICKER)
@Composable
fun App() {
when (currentScreen) {
Screen.CLICKER -> ClickerScreen(
onFriendsList = {
currentScreen = Screen.FRIENDS_LIST
}
)
Screen.FRIENDS_LIST -> FriendsListScreen(
onBack = {
currentScreen = Screen.CLICKER
},
)
}
}
The interface, which was previously located in App()
we take it out as a separate screen with a callback for transition to another screen
val score = mutableStateOf(0L)
@Composable
fun ClickerScreen(
onFriendsList: () -> Unit,
) {
Div(
attrs = {
classes(ClickerStyles.MainContainer)
}
) {
Div(
attrs = {
classes(ClickerStyles.ClickerBlock)
}
) {
H2 {
Text("Score: ${score.value}")
}
Img(
src = Res.images.click_item.fileUrl,
attrs = {
classes(ClickerStyles.MainImage)
onClick {
score.value += 1
}
}
)
}
}
}
The second screen will be the most important function of such clickers – a list of friends. We will invite them to our game via a link.
@Composable
fun FriendsListScreen(
onBack: () -> Unit,
) {
Div(
attrs = {
classes(FriendsListStyle.Container)
}
) {
H4 {
Text("У тебя пока нет друзей...")
}
}
}
Great! Now we have screens, but we can't navigate through them, let's use the standard tools that Telegram provides: this MainButton
And BackButton
.
The first thing we need to do is connect the TMA API itself to our web application. We will use a third-party, ready-made wrapper over the TMA JS library.
jsMain.dependencies {
//…
implementation("dev.inmo:tgbotapi.webapps:15.0.0")
}
However, this is not enough, this dependency is only a wrapper for the Telegram API, we need to connect the js version of the TMA API to our project. Let's add it as script
V index.html
document.
<head>
...
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
Now we have access to the global object webApp
which already has fields backButton
And mainButton
.
However, let's be resourceful and adapt them for use from the composable function – let's make components out of them.
@Composable
fun WebAppMainButton(
text: String,
onClick: () -> Unit,
visible: Boolean = true,
active: Boolean? = undefined,
loading: Boolean = false,
loadingLeaveActive: Boolean = false,
hideOnDispose: Boolean = true, // use if next screen has MainButton too
color: String? = undefined,
textColor: String? = undefined,
) {
DisposableEffect(visible) {
if (visible) {
webApp.mainButton.show()
} else {
webApp.mainButton.hide()
}
onDispose {
if (hideOnDispose)
webApp.mainButton.hide()
}
}
DisposableEffect(
keys = arrayOf(
onClick, text, color, textColor, visible, active,
loading, loadingLeaveActive
),
) {
webApp.mainButton.onClick(onClick)
webApp.mainButton.setParams(
MainButtonParams(
text = text,
color = color,
textColor = textColor,
isActive = active,
isVisible = visible,
)
)
if (loading)
webApp.mainButton.showProgress(leaveActive = loadingLeaveActive)
else
webApp.mainButton.hideProgress()
onDispose {
webApp.mainButton.offClick(onClick)
}
}
}
@Composable
fun WebAppBackButton(
onClick: () -> Unit,
) {
DisposableEffect(Unit) {
webApp.backButton.onClick(onClick)
webApp.backButton.show()
onDispose {
webApp.backButton.hide()
webApp.backButton.offClick(onClick)
}
}
}
Now it will be easier to work with TMA API. Let's add the main button for going to the friends list to the clicker screen, and the back button to the friends list screen.
@Composable
fun FriendsListScreen(
onBack: () -> Unit,
) {
WebAppBackButton(onClick = onBack)
// ...
}
@Composable
fun ClickerScreen(
onFriendsList: () -> Unit,
) {
WebAppMainButton(
text = "Список друзей",
hideOnDispose = false,
onClick = onFriendsList
)
// ...
}
![Decide on an icon for tap and take a screenshot again image.png](https://habrastorage.org/getpro/habr/upload_files/d8e/7c2/c16/d8e7c2c1618648843b674cf0dd07ceed.png)
Theme like Telegram
The colors of this application stand out a lot, but the TMA API provides its own colors for use in its CSS. Let's add these styles to ourselves via CSS in Kotlin. Let's create AppStyles, where these values will be applied as css variables
object TMAVariables {
val BackgroundColorValue = Color("#ffffff")
val BackgroundColor by variable<CSSColorValue>()
val SecondaryBackgroundColorValue = Color("#ffffff")
val SecondaryBackgroundColor by variable<CSSColorValue>()
val TextColorValue = Color("#000000")
val TextColor by variable<CSSColorValue>()
val HintColorValue = Color.gray
val HintColor by variable<CSSColorValue>()
// И т. д., полный код на github
}
For easy use ThemeParams
provided by the library, we create an extension function for StyleScope
. Since fields can be null, you need to define default values for each variable.
fun StyleScope.applyThemeVariables(theme: ThemeParams) {
BackgroundColor(theme.backgroundColor?.toCSSColorValue() ?: BackgroundColorValue)
SecondaryBackgroundColor(theme.secondaryBackgroundColor?.toCSSColorValue() ?: SecondaryBackgroundColorValue)
TextColor(theme.textColor?.toCSSColorValue() ?: TextColorValue)
HintColor(theme.hintColor?.toCSSColorValue() ?: HintColorValue)
// И т. д., полный код на github
}
Let's create a new, main stylesheet, where we will apply the created CSS variables for the entire application. Additionally, you can specify parameters for various tags.
class AppStyles(val themeParams: ThemeParams) : StyleSheet() {
init {
"body" style {
applyThemeVariables(themeParams)
backgroundColor(TMAVariables.BackgroundColor.value())
color(TMAVariables.TextColor.value())
}
"a" style {
color(TMAVariables.LinkColor.value())
}
"button" style {
color(TMAVariables.ButtonTextColor.value())
backgroundColor(TMAVariables.ButtonColor.value())
}
}
}
Next you need to apply this style to renderComposable
passing the theme parameters webApp.themeParams
renderComposable(rootElementId = "app") {
Style(AppStyles(webApp.themeParams))
// ...
}
We launch and see that the colors now match Telegram
![](https://habrastorage.org/getpro/habr/upload_files/684/1b7/0e4/6841b70e4d5f2c5ed3af87ca7bb8ebad.png)
User authorization and data storage
Quick reference: each TMA starts with query params that can be used to get the id and short information about the user, as well as authenticate. These are stored in the fields webApp.initData
And webApp.initDataUnsafe
. The reason for the separation is that initData is raw data that can be sent to the server for validations, initDataUnsafe
– this is a type-safe object where this data is stored in a transformed form, but on the client itself it is impossible to safely check the data for correctness, but it can be used to display some information about the user, for example, username or id.
![image.png](https://habrastorage.org/getpro/habr/upload_files/a5b/7ba/ae7/a5b7baae754a615166c5064baf2ee753.png)
For authorization, we will send a header with each request webApp.initData
. TapClient
is created in commonMain and we will set in advance the queries that we will need in the future.
object TapClient {
private val client: HttpClient = HttpClient(TapClientEngine) {
// …
authConfig()
}
suspend fun sendCurrentScore(score: Long) {
client.post("/score") {
setBody(SendCurrentScoreRequest(score))
contentType(ContentType.Application.Json)
}
}
suspend fun fetchCurrentScore(): Long {
return client.get("/score").body<GetCurrentScoreResponse>().score
}
suspend fun fetchFriendsList(): List<FriendInfo> {
return client.get("/friends").body<GetFriendsListResponse>().friends
}
}
Where authConfig()
defined as expect
and is implemented in the source code jsMain
actual fun HttpClientConfig<HttpClientEngineConfig>.authConfig() {
defaultRequest {
header("tma-data", webApp.initData)
}
}
Now each request will be checked to make sure it was launched from Telegram with the correct data. More details in another article, about the implementation of the server part (article in development).
Direct links, invite a friend
Direct link – the ability to open a TMA application via a link, including with predefined parameters.
To connect direct links, go back to BotFather and write the command /newapp
we select our bot and then
we set the name of our TMA application
Short description
Picture for the application 640×360.
Adding a GIF
The link that will open (the same one that we set when connecting the button in the application)
appname where the application will be located
Now we can open the application by link and pass the parameter as a query parameter with a key startapp. Parameters passed in a direct link will appear in webApp.initDataUnsafe.startParam
This is how we will add friends.
First, let's add a button to send a link to the screen with friends. This is just an example of using the TMA API and it is better to implement link generation on the server side. Call buildInviteLink()
creates a link that, when clicked, opens our TMA application, and buildShareLink()
uses Telegram's capabilities to send a message containing a link to the transition.
// Use build config instead
private const val AppLink = "https://t.me/botusername/appname"
private const val ShareMessage = "%D0%9F%D0%BE%D0%BF%D1%80%D0%BE%D0%B1%D1%83%D0%B9+%D1%8D%D1%82%D0%BE%D1%82+%D0%BD%D0%BE%D0%B2%D1%8B%D0%B9+%D0%BA%D0%BB%D0%B8%D0%BA%D0%B5%D1%80%21"
private fun buildInviteLink(telegramId: Long): String {
return "$AppLink?startapp=ref_$telegramId"
}
private fun buildShareLink(telegramId: Long): String {
return "https://t.me/share/url?url=${buildInviteLink(telegramId)}&text=${ShareMessage}"
}
There's more to add MainButton
clicking on which generates a link to send a message, and opens it through webApp.openTelegramLink()
. Telegram will process such a link by opening the “forward message” screen.
@Composable
fun FriendsListScreen(
onBack: () -> Unit,
) {
WebAppMainButton(
text = "Пригласить друзей",
onClick = {
val id = webApp.initDataUnsafe.user?.id ?: return@WebAppMainButton
webApp.openTelegramLink(buildShareLink(id.long))
}
)
//...
}
Now you can send a message with an invitation link to any user using this button. There is no need to process invitations separately on the client side (just to inform that he was invited), since the server already receives all initData from the client with each request.
Results
In this article, we learned how to use the TMA API in our Kotlin application. The most important things needed for the application are styles, authentication, and some kind of interface management Telegram provides us with, then you can do with such applications whatever the customer wants. The source code can be viewed in the branch clicker
of our project with template on GitHub
We not only share our experience in articles, but also use it for the benefit of business. Perhaps, yours! Contact us for turnkey mobile app development. We work on contract, subcontract, provide outstaffing.