Part 2. TMA on KMP. Writing a clicker for Telegram

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 2

  • Share 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, using ngrok. 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

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 webAppwhich 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
    )
    // ...
}
image.png

Decide on an icon for tap and take a screenshot again

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 ThemeParamsprovided 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 renderComposablepassing the theme parameters webApp.themeParams

renderComposable(rootElementId = "app") {
    Style(AppStyles(webApp.themeParams))
    // ...
}

We launch and see that the colors now match Telegram

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

image.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 /newappwe 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.startParamThis 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 MainButtonclicking 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *