@file:Suppress("SetJavaScriptEnabled")
package com.example.learninkwebview
import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.view.*
import android.webkit.*
import android.widget.FrameLayout
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.util.concurrent.TimeUnit
class LearnInkWebViewDialogFragment : DialogFragment() {
companion object {
private const val ARG_PATH = "arg_path"
private const val ARG_USER_ID = "arg_user_id"
// Configuration constants — replace these
private const val YOUR_BACKEND_AUTH_URL = "https://your-api.com/auth/learnink"
private const val WEBVIEW_BASE_URL = "https://m.learn.ink"
private const val ORG_ID = "acme"
private const val REQUEST_TIMEOUT_MS = 5000L
private const val MSG_CLOSE = "CLOSE"
private const val MSG_SESSION_EXPIRED = "SESSION_EXPIRED"
fun newInstance(path: String, userId: String) =
LearnInkWebViewDialogFragment().apply {
arguments = Bundle().apply {
putString(ARG_PATH, path)
putString(ARG_USER_ID, userId)
}
}
}
private lateinit var webView: WebView
private lateinit var loadingOverlay: View
private val path: String by lazy { requireArguments().getString(ARG_PATH).orEmpty() }
private val userId: String by lazy { requireArguments().getString(ARG_USER_ID).orEmpty() }
private val httpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.callTimeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.connectTimeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.readTimeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.writeTimeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.build()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val root = FrameLayout(requireContext())
webView = WebView(requireContext())
loadingOverlay = createLoadingOverlay()
root.addView(webView, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
root.addView(loadingOverlay, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
initWebView()
authenticateAndLoad()
return Dialog(requireContext()).apply {
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(root)
window?.setLayout(MATCH_PARENT, MATCH_PARENT)
}
}
private fun createLoadingOverlay(): View {
val overlay = FrameLayout(requireContext()).apply {
setBackgroundColor(0xFFFFFFFF.toInt())
isClickable = true
visibility = View.VISIBLE
}
val spinner = ProgressBar(requireContext()).apply { isIndeterminate = true }
overlay.addView(spinner, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.CENTER))
return overlay
}
@SuppressLint("AddJavascriptInterface")
private fun initWebView() {
CookieManager.getInstance().apply {
setAcceptCookie(true)
setAcceptThirdPartyCookies(webView, true)
}
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false
loadsImagesAutomatically = true
useWideViewPort = true
loadWithOverviewMode = true
}
webView.addJavascriptInterface(WebAppBridge(), "LearnInkBridge")
webView.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: android.graphics.Bitmap?) =
setLoading(true)
override fun onPageFinished(view: WebView, url: String) =
setLoading(false)
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
if (request.isForMainFrame) {
setLoading(false)
Toast.makeText(requireContext(), "Failed to load page", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun setLoading(isLoading: Boolean) {
loadingOverlay.visibility = if (isLoading) View.VISIBLE else View.GONE
}
private fun buildWebViewUrl(token: String?): String {
val params = mutableMapOf("webview" to "true")
if (!token.isNullOrBlank()) params["token"] = token
val query = params.entries.joinToString("&") { (k, v) ->
"${k.encodeUrlComponent()}=${v.encodeUrlComponent()}"
}
return "$WEBVIEW_BASE_URL/$ORG_ID/$path?$query"
}
private fun authenticateAndLoad() {
setLoading(true)
lifecycleScope.launch {
val url = try {
buildWebViewUrl(fetchAuthToken(userId))
} catch (e: Exception) {
// Fall back to loading without a token if the user has an existing session
buildWebViewUrl(null)
}
webView.loadUrl(url)
}
}
private fun handleSessionExpired() {
setLoading(true)
lifecycleScope.launch {
try {
webView.loadUrl(buildWebViewUrl(fetchAuthToken(userId)))
} catch (e: Exception) {
setLoading(false)
AlertDialog.Builder(requireContext())
.setTitle("Session Refresh Failed")
.setMessage("Unable to refresh your session. Please close and try again.")
.setPositiveButton("Close") { _, _ -> dismissAllowingStateLoss() }
.setNegativeButton("Retry") { _, _ -> handleSessionExpired() }
.show()
}
}
}
private suspend fun fetchAuthToken(userId: String): String = withContext(Dispatchers.IO) {
val body = JSONObject().put("id", userId).toString()
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url(YOUR_BACKEND_AUTH_URL)
.post(body)
.addHeader("Content-Type", "application/json")
// Add your own auth headers here if needed:
// .addHeader("Authorization", "Bearer ${yourUserSessionToken}")
.build()
httpClient.newCall(request).execute().use { resp ->
val respBody = resp.body?.string().orEmpty()
if (!resp.isSuccessful) {
val message = runCatching { JSONObject(respBody).optString("message") }.getOrNull()
throw RuntimeException(message ?: "Failed to authenticate")
}
val token = JSONObject(respBody).optString("token")
if (token.isBlank()) throw RuntimeException("No token returned from backend")
token
}
}
inner class WebAppBridge {
@JavascriptInterface
fun postMessage(raw: String) {
lifecycleScope.launch(Dispatchers.Main) {
try {
when (JSONObject(raw).optString("type")) {
MSG_CLOSE -> dismissAllowingStateLoss()
MSG_SESSION_EXPIRED -> handleSessionExpired()
}
} catch (_: Exception) {}
}
}
}
private fun String.encodeUrlComponent(): String =
java.net.URLEncoder.encode(this, Charsets.UTF_8.name())
override fun onDestroyView() {
super.onDestroyView()
try {
webView.apply {
loadUrl("about:blank")
stopLoading()
webChromeClient = null
webViewClient = null
removeJavascriptInterface("LearnInkBridge")
destroy()
}
} catch (_: Exception) {}
}
}