forked from Tadrisino/MathApp_frontend
Users never see error text: offline/slow wording in app and web; technical details are never shown
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 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<!-- فقط برای فرقِ «اینترنت قطع است» و «اینترنت کند است» در اسپلشِ بیاتصال؛ مجوزِ عادی، بیپرسش. -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<!-- برای لرزشِ کوتاهِ جوابِ درست/غلط (navigator.vibrate در WebView). -->
|
<!-- برای لرزشِ کوتاهِ جوابِ درست/غلط (navigator.vibrate در WebView). -->
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package com.tadrisino.app
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -63,6 +66,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
private lateinit var offlineView: View
|
private lateinit var offlineView: View
|
||||||
|
private lateinit var offlineText: TextView
|
||||||
private var offlineShown = false
|
private var offlineShown = false
|
||||||
private val retryLoad = object : Runnable {
|
private val retryLoad = object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
@@ -82,7 +86,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
addView(ImageView(context).apply { setImageResource(R.drawable.splash_icon) },
|
addView(ImageView(context).apply { setImageResource(R.drawable.splash_icon) },
|
||||||
FrameLayout.LayoutParams(dp(288), dp(288), Gravity.CENTER))
|
FrameLayout.LayoutParams(dp(288), dp(288), Gravity.CENTER))
|
||||||
addView(TextView(context).apply {
|
addView(TextView(context).apply {
|
||||||
text = "در حال اتصال به اینترنت…"
|
offlineText = this
|
||||||
|
text = "اینترنت قطع است"
|
||||||
setTextColor(Color.parseColor("#1a1a2e"))
|
setTextColor(Color.parseColor("#1a1a2e"))
|
||||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
||||||
layoutDirection = View.LAYOUT_DIRECTION_RTL
|
layoutDirection = View.LAYOUT_DIRECTION_RTL
|
||||||
@@ -91,7 +96,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
.apply { topMargin = dp(150) })
|
.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() {
|
private fun showOffline() {
|
||||||
|
offlineText.text = if (hasInternet()) "اینترنت کند است؛ در حال اتصال…" else "اینترنت قطع است؛ در حال اتصال…"
|
||||||
if (!offlineShown) {
|
if (!offlineShown) {
|
||||||
offlineShown = true
|
offlineShown = true
|
||||||
webView.visibility = View.INVISIBLE // صفحهی خطای پیشفرضِ WebView دیده نشود
|
webView.visibility = View.INVISIBLE // صفحهی خطای پیشفرضِ WebView دیده نشود
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export class ApiError extends Error {
|
|||||||
readonly status: number,
|
readonly status: number,
|
||||||
readonly detail: string,
|
readonly detail: string,
|
||||||
readonly body: Record<string, unknown> = {},
|
readonly body: Record<string, unknown> = {},
|
||||||
|
/** فقط برای status 0: درخواست بهخاطرِ timeout قطع شد (اینترنت کند)، نه رسیدننداشتن (اینترنت قطع). */
|
||||||
|
readonly timedOut = false,
|
||||||
) {
|
) {
|
||||||
super(detail);
|
super(detail);
|
||||||
this.name = 'ApiError';
|
this.name = 'ApiError';
|
||||||
@@ -248,7 +250,7 @@ export function createHttpClient(options: HttpClientOptions) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (opts.signal?.aborted) throw err;
|
if (opts.signal?.aborted) throw err;
|
||||||
const timedOut = err instanceof DOMException && (err.name === 'TimeoutError' || err.name === 'AbortError');
|
const timedOut = err instanceof DOMException && (err.name === 'TimeoutError' || err.name === 'AbortError');
|
||||||
throw new ApiError(0, timedOut ? 'زمانِ درخواست تمام شد' : 'ارتباط با سرور برقرار نشد');
|
throw new ApiError(0, timedOut ? 'زمانِ درخواست تمام شد' : 'ارتباط با سرور برقرار نشد', {}, timedOut);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { ApiError } from '@/api';
|
import { ApiError } from '@/api';
|
||||||
|
|
||||||
/** پیامِ قابلِ فهم برای دانشآموز، از هر خطایی. */
|
const PERSIAN = /[\u0600-\u06FF]/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* پیامِ قابلِ فهم برای دانشآموز، از هر خطایی. کاربر هرگز متنِ فنیِ خطا را نمیبیند: «خطای ۴۰۴»،
|
||||||
|
* انگلیسیِ پیشفرضِ DRF یا پیامِ مرورگر نشان داده نمیشود. فقط جملهی فارسیِ خودِ سرور (که برای
|
||||||
|
* همین نوشته شده، مثل «کد درست نبود») یا fallback.
|
||||||
|
*/
|
||||||
export function friendlyError(err: unknown, fallback = 'یه مشکلی پیش اومد، دوباره امتحان کن'): string {
|
export function friendlyError(err: unknown, fallback = 'یه مشکلی پیش اومد، دوباره امتحان کن'): string {
|
||||||
if (!(err instanceof ApiError)) return fallback;
|
if (!(err instanceof ApiError)) return fallback;
|
||||||
if (err.isNetwork) return 'ارتباط با سرور برقرار نشد؛ اینترنتت رو چک کن';
|
if (err.isNetwork) {
|
||||||
if (err.isRateLimited) return err.detail && !err.detail.startsWith('خطای') ? err.detail : 'زیادی تلاش کردی؛ چند لحظه صبر کن';
|
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.isProviderDown) return 'این سرویس الان در دسترس نیست؛ کمی بعد دوباره امتحان کن';
|
||||||
if (err.status >= 500) return 'سرور موقتاً مشکل داره؛ کمی بعد دوباره امتحان کن';
|
if (err.status >= 500) return 'سرور موقتاً مشکل داره؛ کمی بعد دوباره امتحان کن';
|
||||||
return err.detail || fallback;
|
return err.detail && PERSIAN.test(err.detail) && !err.detail.startsWith('خطای ') ? err.detail : fallback;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user