diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index bb0c184..438dba3 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -105,5 +105,7 @@ android { dependencies { implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.activity:activity-ktx:1.9.2") + // اسپلشِ سیستم تا آماده‌شدنِ اولین صفحه‌ی وب می‌ماند (MainActivity.keepSplash). + implementation("androidx.core:core-splashscreen:1.0.1") implementation("com.github.cafebazaar.Poolakey:poolakey:2.2.0") } \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b524dd4..8596419 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ android:name=".MainActivity" android:configChanges="orientation|screenSize|keyboardHidden" android:exported="true" + android:theme="@style/Theme.Tadrisino.Starting" android:screenOrientation="portrait"> 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 86d3cc6..d0a756e 100644 --- a/android/app/src/main/java/com/tadrisino/app/MainActivity.kt +++ b/android/app/src/main/java/com/tadrisino/app/MainActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager @@ -15,6 +17,7 @@ import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import ir.cafebazaar.poolakey.Connection import ir.cafebazaar.poolakey.Payment import ir.cafebazaar.poolakey.config.PaymentConfiguration @@ -41,6 +44,13 @@ import org.json.JSONObject class MainActivity : AppCompatActivity() { private lateinit var webView: WebView + + /** + * اسپلشِ سیستم تا وقتی true نشده روی صفحه می‌ماند. فرانت وقتی اولین صفحه‌ی واقعی (ورود/خانه) کشیده شد + * `TadrisinoAndroid.appReady()` را صدا می‌زند. اگر هرگز نرسید (سایت بالا نیامد، JS خطا داد) بعد از + * SPLASH_MAX_MS رها می‌شود تا کاربر به‌جای اسپلشِ بی‌انتها صفحه‌ی خطای WebView را ببیند، نه چیزِ دیگر. + */ + @Volatile private var webReady = false private var payment: Payment? = null private var connection: Connection? = null private val webHost: String = Uri.parse(BuildConfig.WEB_URL).host ?: "" @@ -59,6 +69,11 @@ class MainActivity : AppCompatActivity() { @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen().setKeepOnScreenCondition { !webReady } + Handler(Looper.getMainLooper()).postDelayed({ + if (!webReady) Log.w(TAG, "web app never reported ready; releasing the splash after ${SPLASH_MAX_MS}ms") + webReady = true + }, SPLASH_MAX_MS) super.onCreate(savedInstanceState) webView = WebView(this) setContentView(webView) @@ -71,6 +86,11 @@ class MainActivity : AppCompatActivity() { // صداهای کوتاهِ رابط (Web Audio) بعد از پاسخِ سرور پخش می‌شوند، نه درست در لحظه‌ی لمس. webView.settings.mediaPlaybackRequiresUserGesture = false webView.webViewClient = object : WebViewClient() { + // صفحه‌ی اصلی بالا نیامد (نبودِ شبکه): اسپلش را رها کن تا خطای WebView دیده شود، نه ۱۰ ثانیه انتظار. + override fun onReceivedError(view: WebView, request: WebResourceRequest, error: android.webkit.WebResourceError) { + if (request.isForMainFrame) webReady = true + } + // هر آدرسی بیرونِ دامنه‌ی خودمان در مرورگرِ گوشی باز می‌شود، نه داخلِ اپ؛ پس پل فقط // در صفحه‌های خودمان در دسترس است. override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { @@ -205,6 +225,12 @@ class MainActivity : AppCompatActivity() { else @Suppress("DEPRECATION") vibrator.vibrate(timings, -1) } + /** فرانت اولین صفحه‌ی واقعی را کشید؛ اسپلشِ سیستم می‌تواند برود. */ + @JavascriptInterface + fun appReady() { + webReady = true + } + /** فرانت با این می‌فهمد درگاه داخلِ اپ باز می‌شود (isPaymentPage)، نه در مرورگرِ بیرونی. */ @JavascriptInterface fun paysInApp(): Boolean = true @@ -225,5 +251,6 @@ class MainActivity : AppCompatActivity() { companion object { private const val TAG = "Tadrisino" + private const val SPLASH_MAX_MS = 10_000L } } diff --git a/android/app/src/main/res/drawable-nodpi/splash_icon_art.png b/android/app/src/main/res/drawable-nodpi/splash_icon_art.png new file mode 100644 index 0000000..a4ad595 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/splash_icon_art.png differ diff --git a/android/app/src/main/res/drawable/splash_icon.xml b/android/app/src/main/res/drawable/splash_icon.xml new file mode 100644 index 0000000..f56f94f --- /dev/null +++ b/android/app/src/main/res/drawable/splash_icon.xml @@ -0,0 +1,8 @@ + + + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index 449d7ba..e0d2e95 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -4,4 +4,15 @@ #ED6956 @android:color/white + + + diff --git a/src/app/App.tsx b/src/app/App.tsx index 2900199..31cf398 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -62,6 +62,14 @@ export function App() { void boot(); }, [boot]); + // داخلِ اپِ اندروید اسپلشِ سیستم تا این لحظه روی صفحه می‌ماند؛ پس از اولین صفحه‌ی واقعی (نه boot) می‌رود + // و کاربر مستقیم همان صفحه را می‌بیند. ۳۰۰ms: تا محوشدنِ ورودِ صفحه تمام شود. APKِ قدیمی‌تر appReady ندارد. + useEffect(() => { + if (flow.kind === 'boot') return; + const t = setTimeout(() => window.TadrisinoAndroid?.appReady?.(), 300); + return () => clearTimeout(t); + }, [flow.kind]); + // هر جا نشست باطل شد (refresh هم شکست خورد)، برگرد به صفحه‌ی ورود. useEffect( () => diff --git a/src/lib/bazaar.ts b/src/lib/bazaar.ts index d72ce04..7577263 100644 --- a/src/lib/bazaar.ts +++ b/src/lib/bazaar.ts @@ -23,6 +23,8 @@ import { store } from './storage'; interface AndroidBridge { bazaarPurchase?: (sku: string, orderId: string) => void; bazaarConsume?: (purchaseToken: string) => void; + /** اولین صفحه‌ی واقعی کشیده شد؛ اسپلشِ سیستم می‌تواند برود (APKهای قدیمی‌تر این را ندارند). */ + appReady?: () => void; } declare global {