Files
MathApp_frontend/android/README.md

12 KiB
Raw Permalink Blame History

اپِ اندرویدِ تدریسینو (پوسته‌ی WebView + پرداختِ بازار)

همان سایتِ math.tadrisino2.ir داخلِ یک WebView. تنها کدِ بومی پرداختِ درون‌برنامه‌ایِ کافه‌بازار است (کتابخانه‌ی Poolakey)، چون بازار برای اپی که در آن منتشر می‌شود پرداختِ محصولِ دیجیتال را فقط از راهِ خودش می‌پذیرد. باقیِ اپ همان فرانتِ وب است و با دیپلویِ فرانت به‌روز می‌شود، بی‌آن‌که نسخه‌ی تازه‌ی اپ لازم باشد.

ساخت و امضا با هم‌تیمی است (MH-W90، m.hassanabadi) با روشِ امضای خودش (my-release-key.keystore + propertyهای Gradle)؛ تصمیمِ مالک این است که همان روش بماند. پیش از دست زدن به امضا از مالک بپرس و هرگز keystore را commit نکن. applicationId واقعی com.tadrisino.math.app است (از APKِ ساخته‌شده با aapt2). هیچ خریدِ واقعیِ بازار هنوز آزموده نشده.

نسخه‌ی بعدیِ APK — چه چیزی در کد هست و در اپِ نصب‌شده نیست

این‌ها در MainActivity.kt نوشته شده‌اند (۱۴۰۵-۰۷، 468f5b2 و 737414f) ولی هیچ‌کدام build نشده‌اند؛ اپی که الان روی گوشی‌هاست بدونِ آن‌هاست. فرانت با هر دو نسخه کار می‌کند (اگر پل نبود، راهِ قدیم را می‌رود).

تغییر چرا بعد از build روی گوشی بسنج
TadrisinoAndroid.vibrate(patternJson) با Vibratorِ گوشی navigator.vibrate در WebView همیشه کار نمی‌کند؛ مالک گفت اپ لرزش ندارد انتخابِ گزینه‌ی آزمون، جوابِ درست/غلط، پایانِ جزیره و صفحه‌ی «روزِ پیاپی» باید بلرزند؛ با خاموش کردنِ «صداها» در پروفایل نه
mediaPlaybackRequiresUserGesture = false صداهای رابط بعد از پاسخِ سرور پخش می‌شوند، نه در لحظه‌ی لمس صدای جوابِ درست/غلط و پایانِ تمرین
isPaymentPage + paysInApp() درگاهِ زرین‌پال داخلِ خودِ اپ باز می‌شود. اپِ فعلی آن را به مرورگرِ گوشی می‌دهد که Referer نمی‌فرستد و زرین‌پال بی‌Referer «دسترسی از این دامنه مجاز نمی‌باشد» می‌دهد؛ فعلاً فرانت از راهِ pay.html (با http://) دورش می‌زند «پرداخت» با کارتِ بانکی: صفحه‌ی زرین‌پال و بانک داخلِ اپ، و بعد از پرداخت برگشت به اپ با پیامِ «پرداخت موفق بود» (PaymentResult). دکمه‌ی برگشت وسطِ صفحه‌ی بانک چه می‌کند را هم ببین
اسپلشِ یک‌مرحله‌ای: installSplashScreen() + setKeepOnScreenCondition + TadrisinoAndroid.appReady() (446cf36، ab02bb4) اپِ فعلی سه تکه می‌شود: اسپلشِ اندروید ← WebViewِ سفیدِ خالی ← اسپلشِ وب (آیکون + نام) ← خانه. با APKِ تازه اسپلشِ بومی تا اولین صفحه‌ی واقعیِ وب می‌ماند و مستقیم می‌رود روی همان صفحه؛ اسپلشِ وب داخلِ اپ نامرئی است (در APKِ قدیمی فقط آیکونِ بی‌نام). فرانت appReady را بعد از اولین صفحه‌ی غیر‌boot صدا می‌زند و APKِ قدیمی آن را ندارد، پس بی‌اثر است روی گوشی: باز کردنِ اپ با اینترنت باید آیکون ← خانه باشد، بی‌سفیدی و بی‌تصویرِ دوم؛ اندازه‌ی آیکون روی اندروید ۱۲+ (drawable/splash_icon.xml: ۲۸۸dp، حاشیه ۲۲٪)
خریدِ اشتراک از کافه‌بازار: کلیدِ RSA، bazaarBuy (subscribeProduct)، قیمتِ پیشخان (bazaarPrices)، «تخفیف پویا»؛ versionCode ۲ بی‌این‌ها از بازار هیچ خریدی ممکن نیست (APKهای قبلی کلید نداشتند) داخلِ اپ ← فروشگاه ← اشتراکِ سه‌ماهه: «کارتِ بانکی» پیش‌فرض، کاشیِ «کافه‌بازار» فعال با قیمتِ بازار زیرِ آن؛ خرید با حسابِ تست، برگشت با «اشتراکت فعاله»؛ در /admin/payment/order/ سفارشِ بازار «پرداخت‌شده». یک بار هم «انصراف» در صفحه‌ی بازار
صفحه‌ی بی‌اتصال: onReceivedError/onReceivedHttpError ← همان تصویر + «اینترنت قطع است / کند است؛ در حال اتصال…»، تلاشِ دوباره هر ۵ ثانیه، و مجوزِ ACCESS_NETWORK_STATE کاربر هرگز صفحه‌ی خطای WebView یا متنِ فنی نمی‌بیند. اتصالِ کند (بی‌خطا) عمداً هیچ متنِ اضافه نمی‌آورد و روی اسپلش می‌ماند هواپیما روشن ← باز کردنِ اپ ← خاموش کردن: باید خودش وصل شود. سرورِ ۵۰۲ (مثلاً nginx خاموش) هم همین صفحه. اگر صفحه لود شد ولی ۱۵ ثانیه appReady نیامد، لاگِ ERROR و رهاشدنِ اسپلش (باگِ JS)

اسپلش و بی‌اینترنتی همه کارِ APK است (مالک، ۲۰۲۶-۱۰-۱۱، با اپِ نصب‌شده امتحان شد). چیزی که روی گوشی دیده می‌شود از وب قابل رفع نیست، چون وب هنوز بار نشده یا اصلاً نمی‌رسد:

  • اپِ نصب‌شده: اسپلشِ اندروید ← صفحه‌ی سفید ← اسپلشِ وب ← خانه. با فیلترشکن (یا هر وقت math.tadrisino2.ir نرسد) فقط سفید می‌ماند و هیچ پیامی نیست، چون HTML هرگز نرسیده و هیچ کدِ ما اجرا نمی‌شود. علتِ دقیقِ آن سفیدی (HTML نرسیده یا API نرسیده) روی فیلترشکن بازتولید نشد؛ APKِ تازه هر دو را با همان صفحه‌ی بی‌اتصال می‌پوشاند.
  • APKِ تازه (دو ردیفِ بالا) باید: ۱) با اینترنت، اسپلشِ بومی مستقیم به خانه برود و وسطش هیچ‌چیز نیفتد؛ ۲) بی‌اینترنت یا کند، زیرِ همان تصویرِ ثابت بگوید «اینترنت قطع است / کند است»، نه سفید و نه متنِ خطا.
  • آزمونِ پذیرش روی گوشی، پیش از انتشار: اینترنتِ عادی؛ هواپیما؛ فیلترشکنِ روشن (همان که سفید می‌داد)؛ اینترنتِ خیلی کند. در هیچ‌کدام نباید سفیدِ خالی یا صفحه‌ی خطای WebView دیده شود.

پیشنهادهایی که هنوز نوشته نشده‌اند:

  • «برای خروج دوباره بزن»: دکمه‌ی برگشتِ گوشی حالا قدم‌به‌قدم در خودِ وب عقب می‌رود (src/nav/Navigator.tsx) و از خانه یک‌راست بیرون می‌رود. اگر تأییدِ خروج خواستید، در handleOnBackPressed وقتی !webView.canGoBack() یک Toast بدهید و فقط برگشتِ دوم در دو ثانیه finish() کند. در وب شدنی نیست (کروم ورودیِ history بی‌لمس را رد می‌کند).
  • versionCode/versionName را برای هر نسخه‌ای که منتشر می‌شود بالا ببرید؛ الان ۲ و 1.1.0 است (بازار فقط versionCodeِ بزرگ‌تر را می‌پذیرد).

قانونِ بازار: داخلِ اپِ بازار حالا پرداخت با درگاه هم پیشنهاد می‌شود (خواستِ مالک). بازار پرداختِ محتوای دیجیتال بیرون از پرداختِ درون‌برنامه‌ای را محدود می‌کند؛ پیش از انتشار در بازار این را با بازار یا مالک روشن کنید.

ساخت روی مکِ مالک: SDK در ~/Library/Android/sdk، JDK 17 (/opt/homebrew/opt/openjdk@17)، و چون Google Maven از ایران بی‌VPN ۴۰۴ می‌دهد، ~/.gradle/init.d/google-mirror.gradle آینه‌ی Aliyun را اضافه می‌کند؛ ساختِ اول ~۹ دقیقه.

پیش از build — آنچه از پیشخانِ بازار لازم است

چه کجا گذاشته می‌شود وضعیت (۲۰۲۶-۱۰-۱۲)
اسمِ پکیج applicationId در app/build.gradle.kts com.tadrisino.math.app؛ بازار محصول‌های ۱۰۳۴۲–۱۰۳۴۴ را زیرِ همین نام شناخت (API، توکنِ ساختگی)
کلیدِ RSAِ عمومیِ اپ BAZAAR_RSA_KEY در app/build.gradle.kts گذاشته شد
SKUِ هر پلن پنلِ ادمینِ بک‌اند: /admin/subscription/plan/ ← «Bazaar sku» یک‌ماهه 10342، سه‌ماهه 10343، شش‌ماهه 10344؛ بسته‌ی سوال فروخته نمی‌شود
کلیدِ «تخفیف پویا» فقط .envِ سرور (BAZAAR_DYNAMIC_PRICE_KEY)، هرگز در اپ روی سرور

فقط اشتراک فروخته می‌شود. قیمت: فرانت TadrisinoAndroid.bazaarPrices(skusJson) را صدا می‌زند و قیمتِ پیشخان را روی کاشیِ بازار نشان می‌دهد. خرید: TadrisinoAndroid.bazaarBuy(sku, orderId, dynamicPriceToken) با subscribeProduct (مصرف نمی‌شود، بازار خودش تمدید می‌کند). قیمتِ پیشخان باید با قیمتِ همان پلن در ادمین یکی باشد؛ فقط وقتی کدِ تخفیف خورده، سرور قیمتِ کم‌شده را امضا می‌کند. compileDebugKotlin سبز است؛ هیچ خریدی روی گوشی آزموده نشده.

build

  1. Android Studio ← Open ← همین پوشه‌ی android/. Studio خودش Gradle wrapper را می‌سازد.
  2. Build ← Generate Signed App Bundle / APK، با کلیدِ امضای شرکت. کلیدِ امضا را گم نکنید: بدونِ همان کلید هیچ نسخه‌ی بعدی روی بازار به‌روزرسانی نمی‌شود.
  3. نصب روی گوشی‌ای که کافه‌بازار دارد و با حسابِ تست وارد شده.

جریانِ خرید (برای عیب‌یابی)

فرانت  POST /api/subscription/purchase  {planId, paymentMethod:"bazaar"}  → orderId, bazaarSku
فرانت  (اختیاری) POST /api/discount/redeem  {code, orderId}
فرانت  POST /api/payment/bazaar/orders/{orderId}/start  → sku، dynamicPriceToken (اگر تخفیف خورده)
فرانت  TadrisinoAndroid.bazaarBuy(sku, orderId, dynamicPriceToken)
اپ     Poolakey.subscribeProduct(sku, payload=orderId)  → purchaseToken
اپ     رویدادِ «tadrisino:bazaar» {orderId, purchaseToken}
فرانت  POST /api/payment/bazaar/verify  {orderId, purchaseToken}   ← سرور از بازار می‌پرسد و اشتراک را فعال می‌کند

اگر پول رفت ولی تأییدِ سرور نرسید، کاربر دوباره «پرداخت» می‌زند: فرانت همان سفارش را می‌فرستد و اپ توکنِ همان سفارش را از getSubscribedProducts پیدا می‌کند.

قرارداد سمتِ وب: src/lib/bazaar.ts. سمتِ سرور: payment/bazaar.py و docs/operations.mdِ بک‌اند.