Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
@file:SuppressLint("SetJavaScriptEnabled")

package io.ton.walletkit.demo.presentation.ui.screen

import android.annotation.SuppressLint
import android.view.ViewGroup
import android.webkit.WebView
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
Expand All @@ -40,34 +45,51 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import io.ton.walletkit.ITONWalletKit
import io.ton.walletkit.demo.R
import io.ton.walletkit.demo.designsystem.components.text.TonText
import io.ton.walletkit.demo.designsystem.theme.SmoothCornerShape
import io.ton.walletkit.demo.designsystem.theme.TonTheme
import io.ton.walletkit.demo.presentation.ui.dialog.UrlPromptDialog
import io.ton.walletkit.demo.presentation.ui.screen.iframesec.RealBridgeIframeCase
import io.ton.walletkit.demo.presentation.ui.screen.iframesec.WalletKitRealBridgeIframeScreen
import io.ton.walletkit.demo.presentation.util.QrScanner
import io.ton.walletkit.extensions.injectTonConnect

private enum class InvestigationPage { Tonconnect, DappWebView, RealBridge }

/**
* Developer "Wallet Kit Investigation" screen: a list of debug tools reached from the wallet-home
* gear icon. Currently one entry — Tonconnect, which opens a page with "Connect to dApp" (paste a
* link) and "Scan QR code" actions to connect the active wallet.
* gear icon. Hosts the TonConnect helper plus the iframe-security investigation screens (synthetic
* diagnostic matrix and the real-bridge matrix) and a plain dApp WebView.
*/
@Composable
fun WalletKitInvestigationScreen(
onBack: () -> Unit,
onConnect: (String) -> Unit,
walletKit: ITONWalletKit,
modifier: Modifier = Modifier,
) {
var showTonconnect by remember { mutableStateOf(false) }
var page by remember { mutableStateOf<InvestigationPage?>(null) }

if (showTonconnect) {
BackHandler { showTonconnect = false }
WalletKitTonconnectScreen(
onBack = { showTonconnect = false },
onConnect = onConnect,
modifier = modifier,
)
return
when (page) {
InvestigationPage.Tonconnect -> {
BackHandler { page = null }
WalletKitTonconnectScreen(onBack = { page = null }, onConnect = onConnect, modifier = modifier)
return
}
InvestigationPage.DappWebView -> {
BackHandler { page = null }
DappWebViewScreen(onBack = { page = null }, walletKit = walletKit, modifier = modifier)
return
}
InvestigationPage.RealBridge -> {
BackHandler { page = null }
WalletKitRealBridgeIframeScreen(walletKit = walletKit, onBack = { page = null }, modifier = modifier)
return
}
null -> Unit
}

Column(
Expand All @@ -84,7 +106,12 @@ fun WalletKitInvestigationScreen(
) {
InvestigationRow(
title = stringResource(R.string.investigation_tonconnect),
onClick = { showTonconnect = true },
onClick = { page = InvestigationPage.Tonconnect },
)
InvestigationRow(title = "dApp WebView", onClick = { page = InvestigationPage.DappWebView })
InvestigationRow(
title = "Iframe Security — Real dApp Bridge",
onClick = { page = InvestigationPage.RealBridge },
)
}
}
Expand Down Expand Up @@ -142,6 +169,37 @@ private fun WalletKitTonconnectScreen(
}
}

/** Minimal dApp WebView with the real WalletKit injection — mirrors the iOS investigation entry. */
@Composable
private fun DappWebViewScreen(
onBack: () -> Unit,
walletKit: ITONWalletKit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.background(TonTheme.colors.bgSecondary),
) {
SubScreenTopBar(title = "dApp WebView", onBack = onBack)
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
WebView(ctx).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
injectTonConnect(walletKit)
loadUrl(RealBridgeIframeCase.DAPP_URL)
}
},
)
}
}

@Composable
private fun InvestigationRow(title: String, onClick: () -> Unit) {
val shape = SmoothCornerShape(12.dp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ fun WalletScreen(
WalletKitInvestigationScreen(
onBack = { subScreen = HomeSubScreen.None },
onConnect = actions::onHandleUrl,
walletKit = walletKit,
)
}
HomeSubScreen.None -> Unit
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright (c) 2025 TonTech
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.ton.walletkit.demo.presentation.ui.screen.iframesec

import androidx.compose.runtime.mutableStateListOf
import java.util.UUID

/**
* One diagnostic log row. [actualOrigin] is the platform-reported origin of the frame that posted
* (the ground truth — JS cannot forge it). [claimedOrigin] is whatever the frame's JS claimed.
* [isNative] marks rows that come from the native SDK event stream rather than a JS bridge message.
*/
data class IframeSecLogEntry(
val id: String = UUID.randomUUID().toString(),
val timestamp: Long = System.currentTimeMillis(),
val frameLabel: String,
val action: String,
val claimedOrigin: String,
val actualOrigin: String,
val isMainFrame: Boolean,
val payload: String = "",
val isNative: Boolean = false,
)

/** Compose-observable log shared by the synthetic and real-bridge iframe-security screens. */
class IframeSecLog {
val entries = mutableStateListOf<IframeSecLogEntry>()

fun add(entry: IframeSecLogEntry) {
entries.add(entry)
}

/**
* Append an entry describing an event the native SDK actually received and surfaced — proof
* the bridge accepted the request, plus the [domain] the SDK attributed to it.
*/
fun addNative(action: String, domain: String, payload: String = "") {
entries.add(
IframeSecLogEntry(
frameLabel = "SDK EVENT",
action = action,
claimedOrigin = domain,
actualOrigin = domain,
isMainFrame = true,
payload = payload,
isNative = true,
),
)
}

fun clear() {
entries.clear()
}
}
Loading
Loading