From 0d6396e0de865da9ed3e2fbeeafef77fa54e2cdc Mon Sep 17 00:00:00 2001 From: AliReza Date: Sun, 11 Oct 2026 22:50:58 +0330 Subject: [PATCH] Users never see error text: offline/slow wording in app and web; technical details are never shown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit friendlyError now shows a server detail only when it is Persian prose (no 'خطای 404', no DRF English defaults); network errors say 'اینترنت قطع است' (navigator offline) or 'اینترنت کند است' (timeout). The native connecting screen picks the same two wordings from ConnectivityManager (ACCESS_NETWORK_STATE). Debug build compiles; not run on a device. Co-Authored-By: Claude Sonnet 5.5 --- android/app/src/main/AndroidManifest.xml | 2 ++ .../main/java/com/tadrisino/app/MainActivity.kt | 15 ++++++++++++++- src/api/http.ts | 4 +++- src/lib/errors.ts | 17 +++++++++++++---- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8596419..52596a3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + diff --git a/android/app/src/main/java/com/tadrisino/app/MainActivity.kt b/android/app/src/main/java/com/tadrisino/app/MainActivity.kt index 32322da..2c5ac5e 100644 --- a/android/app/src/main/java/com/tadrisino/app/MainActivity.kt +++ b/android/app/src/main/java/com/tadrisino/app/MainActivity.kt @@ -2,7 +2,10 @@ package com.tadrisino.app import android.annotation.SuppressLint import android.content.Intent +import android.content.Context import android.graphics.Color +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.net.Uri import android.os.Build import android.os.Bundle @@ -63,6 +66,7 @@ class MainActivity : AppCompatActivity() { private val handler = Handler(Looper.getMainLooper()) private lateinit var offlineView: View + private lateinit var offlineText: TextView private var offlineShown = false private val retryLoad = object : Runnable { override fun run() { @@ -82,7 +86,8 @@ class MainActivity : AppCompatActivity() { addView(ImageView(context).apply { setImageResource(R.drawable.splash_icon) }, FrameLayout.LayoutParams(dp(288), dp(288), Gravity.CENTER)) addView(TextView(context).apply { - text = "در حال اتصال به اینترنت…" + offlineText = this + text = "اینترنت قطع است" setTextColor(Color.parseColor("#1a1a2e")) setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) layoutDirection = View.LAYOUT_DIRECTION_RTL @@ -91,7 +96,15 @@ class MainActivity : AppCompatActivity() { .apply { topMargin = dp(150) }) } + /** هیچ متنِ فنیِ خطا نشان داده نمی‌شود؛ فقط یکی از دو جمله: «قطع است» (شبکه‌ای نیست) یا «کند است». */ + private fun hasInternet(): Boolean { + val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val caps = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false + return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } + private fun showOffline() { + offlineText.text = if (hasInternet()) "اینترنت کند است؛ در حال اتصال…" else "اینترنت قطع است؛ در حال اتصال…" if (!offlineShown) { offlineShown = true webView.visibility = View.INVISIBLE // صفحه‌ی خطای پیش‌فرضِ WebView دیده نشود diff --git a/src/api/http.ts b/src/api/http.ts index 3d504ce..8f9addb 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -21,6 +21,8 @@ export class ApiError extends Error { readonly status: number, readonly detail: string, readonly body: Record = {}, + /** فقط برای status 0: درخواست به‌خاطرِ timeout قطع شد (اینترنت کند)، نه رسیدن‌نداشتن (اینترنت قطع). */ + readonly timedOut = false, ) { super(detail); this.name = 'ApiError'; @@ -248,7 +250,7 @@ export function createHttpClient(options: HttpClientOptions) { } catch (err) { if (opts.signal?.aborted) throw err; const timedOut = err instanceof DOMException && (err.name === 'TimeoutError' || err.name === 'AbortError'); - throw new ApiError(0, timedOut ? 'زمانِ درخواست تمام شد' : 'ارتباط با سرور برقرار نشد'); + throw new ApiError(0, timedOut ? 'زمانِ درخواست تمام شد' : 'ارتباط با سرور برقرار نشد', {}, timedOut); } } diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 92b669b..4c46b9e 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -1,11 +1,20 @@ import { ApiError } from '@/api'; -/** پیامِ قابلِ فهم برای دانش‌آموز، از هر خطایی. */ +const PERSIAN = /[\u0600-\u06FF]/; + +/** + * پیامِ قابلِ فهم برای دانش‌آموز، از هر خطایی. کاربر هرگز متنِ فنیِ خطا را نمی‌بیند: «خطای ۴۰۴»، + * انگلیسیِ پیش‌فرضِ DRF یا پیامِ مرورگر نشان داده نمی‌شود. فقط جمله‌ی فارسیِ خودِ سرور (که برای + * همین نوشته شده، مثل «کد درست نبود») یا fallback. + */ export function friendlyError(err: unknown, fallback = 'یه مشکلی پیش اومد، دوباره امتحان کن'): string { if (!(err instanceof ApiError)) return fallback; - if (err.isNetwork) return 'ارتباط با سرور برقرار نشد؛ اینترنتت رو چک کن'; - if (err.isRateLimited) return err.detail && !err.detail.startsWith('خطای') ? err.detail : 'زیادی تلاش کردی؛ چند لحظه صبر کن'; + if (err.isNetwork) { + if (typeof navigator !== 'undefined' && navigator.onLine === false) return 'اینترنت قطع است؛ اتصالت رو چک کن و دوباره امتحان کن'; + return err.timedOut ? 'اینترنت کند است؛ کمی صبر کن و دوباره امتحان کن' : 'اینترنت قطع یا ناپایدار است؛ دوباره امتحان کن'; + } + if (err.isRateLimited) return err.detail && PERSIAN.test(err.detail) && !err.detail.startsWith('خطا') ? err.detail : 'زیادی تلاش کردی؛ چند لحظه صبر کن'; if (err.isProviderDown) return 'این سرویس الان در دسترس نیست؛ کمی بعد دوباره امتحان کن'; if (err.status >= 500) return 'سرور موقتاً مشکل داره؛ کمی بعد دوباره امتحان کن'; - return err.detail || fallback; + return err.detail && PERSIAN.test(err.detail) && !err.detail.startsWith('خطای ') ? err.detail : fallback; }