A signed-in user with a snapshot goes straight to home; the same boot requests run in the
background and replace it. Snapshot is cleared whenever tokens are cleared and on a new login,
ignored from another build or when older than 7 days, and dropped when the fresh profile is a
different user (reboot remounts MainApp via a generation key; AnimatePresence otherwise kept the
exiting one). Notifications/characters wait for the profile answer so an expired access token
costs the same requests as before. refreshHome applies only the latest answer. Home shows a
"saved data" strip when the map could not be refreshed.
Measured locally (old vs new build, same backend, Chrome throttling, median of 3, time to map):
Fast 3G 2.46s -> 1.80s (expired token 3.63s -> 1.80s); Slow 3G 8.52s -> 6.14s (expired 12.6s ->
6.13s); API requests per open unchanged (6 valid, 11 expired). 12/12 behaviour checks passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Text: kasra/fatha/damma/shadda/sukun/hamza-above and «هی» ezafe removed from every user-visible string (comments untouched); tanwin kept. Stale copy fixed (question packs are no longer sold), awkward onboarding sentences rewritten, سؤال → سوال. plainPersian() in MathText cleans the live tutor stream.
- Home: the mascot + speech bubble header is gone; the selected companion hops beside the current island and sits at every third bend (CSS transform animations only).
- Checkout: order summary, payment-method rows with Zarinpal/Bazaar icons and radio, amount on the pay button, lock trust line, darker overlay. paymentMethods in config.js per channel (web: zarinpal, app: zarinpal+bazaar by default).
- Profile: «ورود با حساب دیگر» removed (it was logout); new «حساب کاربری» sheet shows phone/ID and holds «حذف حساب کاربری»; logout is a red row. Delete sheet tells Bazaar subscribers to cancel renewal in Bazaar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Android: buy() checks the Poolakey connection state and reconnects; BazaarNotFound / not supported / dynamic price
not supported become error "unavailable" (not_installed | old_bazaar | not_connected) instead of a generic failure.
assembleDebug green: com.tadrisino.math.app, versionCode 2, queries com.farsitel.bazaar. Not run on a phone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Checkout sheet always shows both methods as two tiles; Bazaar tile disabled with the reason
(not in Bazaar / Android app only / update the app) instead of hidden; default is Bazaar inside an app that can buy.
- Bridge v2: TadrisinoAndroid.bazaarBuy(sku, orderId, productType, dynamicPriceToken); inAndroidApp() split
from canBuyFromBazaar() so the pay.html workaround still applies to the installed APK.
- Bazaar flow: order -> optional discount code -> POST /payment/bazaar/orders/{id}/start -> Poolakey -> verify;
consume only packs.
- Android: RSA key, subscribeProduct/getSubscribedProducts for subscriptions, dynamicPriceToken, versionCode 2.
compileDebugKotlin green; no purchase tested on a phone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Writes keep 15s so an early cut does not make the user submit twice. Worst-case boot
wait with an unreachable API goes from ~50s to ~27s (3 attempts + backoff). Not tested
on a real slow network.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
Inside the app the web splash is invisible with the new APK (the system splash is still on
top) and icon-only for old APKs, so there is no second picture to switch to. Offline or a
5xx on the main frame: system splash released onto the same icon (288dp drawable) with
'در حال اتصال به اینترنت…', retried every 5s. A slow-but-online load shows nothing extra.
Debug build compiles; not run on a device.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Installed APK sequence was system splash -> white WebView -> web splash -> home. The
activity now holds the system splash (core-splashscreen) until the page calls
TadrisinoAndroid.appReady(), after its first non-boot screen; released after 10s or on a
main-frame load error so a dead site shows the WebView error, not an endless splash.
Older APKs have no appReady and are unaffected. Debug build compiles; not run on a device.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The first second was a blank page, then a splash with the selected companion, then the
home screen. Now index.html carries the splash (icon from the Android launcher, name,
white to match the native window background) and SplashScreen draws the same markup, so
the hand-over from HTML to React shows no change. Not run on a device.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The splash used to stay a fixed 700ms plus a fade, so a fast response still showed it
briefly and jumped away. Now only the matching background shows for the first 350ms;
the splash content appears only if the response has not arrived by then, and once shown
it stays at least 700ms. Not run on a device.
- state/companion.ts: one store for the chosen companion (also in localStorage so splash/login show it).
BrandMark (the orange «π» tile) now shows the companion's symbol (Σ Δ π ω) in its colour; screen-header
mascots, loaders and empty/error states default to it; the assistant is «از {name} بپرس» with its face,
colour and wording; the shop, lesson and quiz «ask» buttons name it; the active tab takes its tint.
- Traits, all client-side and light: Σ sigma keeps a running tally of correct answers in the quiz header;
Δ delta shows a flame «×N» chip and an extra buzz for answers in a row; π pi gives a short solving tip
after a wrong answer; ω omega throws gold confetti at the end of every island whatever the score.
Each companion's «correct» sound is pitched differently; confetti uses its colours. Characters screen
lists each trait.
- android/README.md: what MainActivity.kt has that the installed APK lacks (vibrate bridge, media gesture,
in-app gateway + paysInApp), what to check on a phone after building, signing owner, Bazaar policy note.
Checked in headless Chrome with Sigma, Delta and Omega selected (home, assistant, shop, characters, quiz).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app hands every outside URL to the phone browser, which sends no Referer, and Zarinpal's StartPay
refuses a request without our domain as Referer («دسترسی از این دامنه مجاز نمیباشد»). Checked against
production on a pending order: no Referer → refused; Referer https://math.tadrisino2.ir/ → forwards to
the bank (sep.shaparak.ir).
- pay.html + src/pay.ts: opened in the phone browser, forwards only to our own
/api/payment/orders/<id>/pay link (anything else is refused), so the Referer is math.tadrisino2.ir.
The app opens it as http:// (the WebView keeps only https on its own host); ArvanCloud moves it to
https, and the link rides in the fragment, which never reaches a server.
- MainActivity.kt (next APK): payment pages (our subdomains, zarinpal, shaparak) stay inside the WebView
and `paysInApp()` tells the frontend to navigate directly. Not built.
Not checked: a full payment on a real phone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Shop: subscription perks shown once (each card repeated them, «۳ سوال در روز» twice), plans as selectable
rows with one big buy button, prices as «۷۹۸ هزار تومان», a single clear status card for subscribers.
Checkout inside the Bazaar app offers «کارتِ بانکی» (default) or «کافهبازار» for packs with a SKU;
«بهزودی در بازار» is gone. The gateway opens in the phone browser; status refreshes on return.
- Back button: every back-able level (open sheet, pushed screen, non-home tab, shop list) owns exactly one
history entry created right after the tap; back closes the sheet, then the screen, then returns to Home,
then leaves the app. (A re-pushed trap is skipped by Chrome's history intervention.)
- Sound: AudioContext unlocked on the first touch (it was created after a network wait, which WebView/Safari
keep muted); softer notes, option tick, finish fanfare, streak whoosh. Haptics through the new
TadrisinoAndroid.vibrate bridge when the APK has it, navigator.vibrate otherwise.
- Streak: animated flame with lit / at-risk (grey) / frozen (ice) states on Home and the streak screen,
a companion reminder when the streak is at risk or frozen, and a full-screen «روزِ پیاپی» celebration
after the first lesson of the day (needs backend 9024c6b: streak.activeToday / lost).
- Lessons: intro/example text with inline «۱. … ۲. …» renders as a numbered list (2,250 of 4,469 texts).
- Quiz: duplicate React key on the answer bubble fixed.
- android/MainActivity.kt: vibrate bridge and mediaPlaybackRequiresUserGesture=false. Not built; needs the
next APK from the signing owner.
Checked in headless Chrome against the local backend (shop as a subscriber with the Bazaar bridge stubbed,
back sequences, quiz → streak celebration, frozen state). Not checked on a real phone or in the APK.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Navigator stage: the next screen mounts hidden under the current one and is revealed only when its
first data has arrived (ScreenGate held by useAsync/useGateHold), at most 450 ms; a thin top bar shows
after 120 ms of waiting. Push slides from the left, back from the right, tabs only fade.
- Session cache (stale-while-revalidate) for screen GETs: revisited screens draw instantly.
- Home map and stats are fetched during the splash, so the first screen arrives complete.
- Entrance pop-ins (mascot, islands, lesson cards) run on first view only, not on every revisit.
- Shop status card shows placeholders instead of a wrong «نداری» while loading.
- Re-tapping the open tab scrolls to top instead of reloading; sheets close with a swipe down.
- Quiz: «۱ از ۴» instead of «۱/۴» (read as a fraction), varied praise, «N تا پشتِ هم درست»,
growth-mindset copy after a weak round, zero-reward tiles hidden.
- Streak: a concrete next step when the streak is 0 or today is still empty.
Checked in headless Chrome against the local backend (frame captures at 150 ms and 700 ms latency,
rapid tab tapping, full quiz run). Not checked on a real phone or iOS Safari.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Store opens with two choices (subscription / question packs); 3- and 6-month cards
show per-month price and saving.
- Units that need a subscription show a crown badge and open the store; a 402 from
the quiz goes there too. «ادامه بده» skips locked units.
- Profile: subscription card with an animated days-left bar (planDurationDays).
- BrandMark (SVG) on splash, home header and footer; copyright footer; floating math
glyphs on splash. CSS transform/opacity only; honours reduced motion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>