Subscriptions only: no question credits anywhere; Bazaar bridge is subscribeProduct only (bazaarBuy(sku, orderId, token), bazaarPrices(skusJson)), no consume

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 20:33:47 +03:30
parent e0a9605b8e
commit 854149bb5d
7 changed files with 98 additions and 180 deletions

View File

@@ -25,13 +25,13 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
const chat = useAskState();
const [input, setInput] = useState('');
const [busy, setBusy] = useState(false);
// «X سوالِ رایگانِ امروز + Y سوالِ خریده‌شده»؛ پیش از اولین سوال از سرور، بعد از هر جواب از خودِ جواب.
const [quota, setQuota] = useState<{ free: number; credits: number } | null>(null);
// «X سوالِ امروز»؛ پیش از اولین سوال از سرور، بعد از هر جواب از خودِ جواب.
const [quota, setQuota] = useState<number | null>(null);
const [quotaDone, setQuotaDone] = useState(false);
useEffect(() => {
api.ai
.quota()
.then((q) => setQuota({ free: q.remainingQuota, credits: q.credits }))
.then((q) => setQuota(q.remainingQuota))
.catch((err: unknown) => console.warn('ai quota lookup failed', err))
.finally(() => setQuotaDone(true));
}, []);
@@ -109,7 +109,7 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
diagram: done.diagram ?? null,
});
askStore.setConversation(done.conversationId);
setQuota((q) => ({ free: done.remainingQuota, credits: done.credits ?? q?.credits ?? 0 }));
setQuota(done.remainingQuota);
} catch (err) {
// سقفِ روزانه (نه throttleِ چندثانیه‌ای): «دوباره» تا فردا بی‌فایده است.
const quotaReached = err instanceof ApiError && err.isRateLimited && err.detail.includes('سوال');
@@ -117,7 +117,7 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
text: friendlyError(err, 'جوابی نرسید'),
...(quotaReached ? { quotaReached: true } : { failedQuestion: question }),
});
if (quotaReached) setQuota({ free: 0, credits: 0 });
if (quotaReached) setQuota(0);
} finally {
setBusy(false);
}
@@ -145,7 +145,7 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
mascot={{ mood: busy ? 'excited' : 'idle', size: 60 }}
>
<div className="mt-3 flex flex-wrap items-center gap-2">
{quota !== null && <QuotaPills free={quota.free} credits={quota.credits} />}
{quota !== null && <QuotaPills free={quota} />}
{!empty && (
<button
type="button"
@@ -355,23 +355,21 @@ function Message({
}
/**
* شمارنده در سربرگِ دستیار: سوال‌های رایگانِ باقیِ امروز، و اگر از بسته‌های قدیمی سوالی خریده و مانده، آن هم جدا.
* بسته از ۲۰۲۶-۱۰-۱۲ فروخته نمی‌شود؛ وقتی رایگانِ امروز تمام شد، دکمه‌ی دوم به فروشگاهِ اشتراک می‌برد.
* شمارنده در سربرگِ دستیار: سوال‌های باقیِ امروز. وقتی تمام شد، دکمه‌ی دوم به فروشگاهِ اشتراک می‌برد
* (سوال جدا فروخته نمی‌شود؛ مشترک فردا دوباره سهم دارد).
*/
function QuotaPills({ free, credits }: { free: number; credits: number }) {
function QuotaPills({ free }: { free: number }) {
const nav = useNav();
return (
<>
<span className={`rounded-full px-3 py-1.5 text-xs font-black ${free > 0 ? 'bg-white text-[#2E7D5B]' : 'bg-white/15 text-white/80'}`}>
{free > 0 ? <>{toFa(free)} سوالِ امروز</> : 'سوال‌های امروز تموم شد'}
</span>
{credits > 0 ? (
<span className="rounded-full bg-white px-3 py-1.5 text-xs font-black text-violet">{toFa(credits)} سوالِ خریده‌شده</span>
) : free === 0 ? (
{free === 0 && (
<button type="button" onClick={() => nav.switchTab('subscription')} className="rounded-full bg-white/15 px-3 py-1.5 text-xs font-black text-white">
سوالِ بیشتر با اشتراک
</button>
) : null}
)}
</>
);
}