package com.peachpasswords.feedback import com.peachpasswords.BuildConfig import kotlinx.coroutines.Dispatchers 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 javax.inject.Inject import javax.inject.Singleton /** * Submits user feedback through Web3Forms — the same inbox and the same access * key the browser extension posts to (`peach-browser/src/shared/config.ts`). * Android had no endpoint at all and said so on every attempt; there is no * reason for the two clients to reach different places. * * The access key is a client-side, write-only form identifier, not a secret. * It cannot read prior submissions or reach anything else, and the only abuse * it enables is spamming Peach's own feedback inbox — the same trade already * accepted for the extension bundle. * * The payload carries the category and the user-typed message only. No vault * data, keys, credentials, or device identifiers are included; the app version * rides in the subject line so a report can be placed without asking the user * who they are. */ class FeedbackUnavailableException : Exception("feedback_endpoint_not_configured") @Singleton class FeedbackRepository @Inject constructor() { private val client: OkHttpClient by lazy { OkHttpClient() } suspend fun submit(type: String, message: String) { if (message.isBlank()) throw IllegalArgumentException("message_empty") if (WEB3FORMS_KEY.isBlank()) throw FeedbackUnavailableException() withContext(Dispatchers.IO) { val body = buildSubmission(type, message).toRequestBody(JSON) val request = Request.Builder().url(ENDPOINT).post(body).build() client.newCall(request).execute().use { res -> val text = res.body?.string().orEmpty() if (!res.isSuccessful) throw java.io.IOException("HTTP ${res.code}") // Web3Forms answers 200 with {"success": false} when it rejects // a submission, so status alone would report a send that never // happened. Never tell the user it went if it did not. val accepted = runCatching { JSONObject(text).optBoolean("success") } .getOrDefault(false) if (!accepted) throw java.io.IOException("feedback_rejected") } } } companion object { const val ENDPOINT = "https://api.web3forms.com/submit" /** Mirrors `WEB3FORMS_KEY` in the browser's `src/shared/config.ts`. */ const val WEB3FORMS_KEY = "af0b7072-e3fa-4cb3-99b5-4ad3b87fac66" private val JSON = "application/json; charset=utf-8".toMediaType() internal fun buildSubmission( type: String, message: String, version: String = BuildConfig.VERSION_NAME, ): String = JSONObject().apply { put("access_key", WEB3FORMS_KEY) put("subject", "Peach Feedback - $type (Android $version)") put("message", message) put("from_name", "Peach User") // Honeypot — Web3Forms drops the submission if this arrives filled. put("botcheck", "") }.toString() } }