shop stipe
This commit is contained in:
@@ -92,6 +92,8 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ orderId }),
|
||||
}),
|
||||
shopOsuOrder: (orderId) =>
|
||||
request(`/api/shop/osu/orders/${encodeURIComponent(orderId)}`),
|
||||
shopAd: () => request('/api/shop/ad', { method: 'POST' }),
|
||||
feed: () => request('/api/feed'),
|
||||
feedAnnounce: (inventoryItemIds, caseName, opts = {}) => {
|
||||
|
||||
@@ -3103,6 +3103,22 @@ a.achievement-badge:hover .achievement-badge-name {
|
||||
}
|
||||
|
||||
|
||||
.inv-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.inv-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.inv-grid .item-tile {
|
||||
padding: 0.65rem;
|
||||
min-height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.inv-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -69,9 +69,11 @@ export default function CasePage() {
|
||||
const finishingRef = useRef(false);
|
||||
const pendingAnnounceRef = useRef({ ids: [], caseName: '', respin: false });
|
||||
const pendingAutoSellRef = useRef([]);
|
||||
/** Auto-sell ids from the discarded first roll — applied when that reel finishes. */
|
||||
/** Auto-sell ids from the first roll — applied when that reel finishes. */
|
||||
const pendingPreRespinAutoSellRef = useRef([]);
|
||||
/** When set, first reel pass is a discarded roll; then we restart with these final items. */
|
||||
/** Keepers from first roll (auto-sell on, value >= threshold) — merged into final loot UI. */
|
||||
const pendingKeptPreRespinRef = useRef([]);
|
||||
/** When set, first reel pass is the pre-respin roll; then we restart with these final items. */
|
||||
const pendingRespinFinalRef = useRef(null);
|
||||
/** Snapshot of first-roll drops for feed (best item announced when first reels finish). */
|
||||
const pendingPreRespinAnnounceRef = useRef(null);
|
||||
@@ -417,8 +419,8 @@ export default function CasePage() {
|
||||
|
||||
const heroHidden = opening || (spins.length > 0 && !allDone);
|
||||
|
||||
const beginReelSpins = (items) => {
|
||||
setAllDrops(items);
|
||||
const beginReelSpins = (items, extraAllDrops = []) => {
|
||||
setAllDrops(extraAllDrops.length ? [...extraAllDrops, ...items] : items);
|
||||
const showcase = pickShowcaseDrops(items, MAX_VISIBLE_REELS);
|
||||
const next = showcase.map((item, idx) => ({
|
||||
key: `${item.inventoryId || item.id || 'x'}-${idx}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
@@ -447,6 +449,7 @@ export default function CasePage() {
|
||||
pendingRespinFinalRef.current = null;
|
||||
pendingPreRespinAnnounceRef.current = null;
|
||||
pendingPreRespinAutoSellRef.current = [];
|
||||
pendingKeptPreRespinRef.current = [];
|
||||
respinPhaseRef.current = null;
|
||||
|
||||
try {
|
||||
@@ -508,6 +511,10 @@ export default function CasePage() {
|
||||
pendingPreRespinAutoSellRef.current = pre
|
||||
.filter((item) => item?.willAutoSell && item?.inventoryId)
|
||||
.map((item) => item.inventoryId);
|
||||
pendingKeptPreRespinRef.current =
|
||||
result.keptPreRespinItems?.length
|
||||
? result.keptPreRespinItems
|
||||
: pre.filter((item) => item?.kept || (!item?.willAutoSell && item?.inventoryId));
|
||||
pendingPreRespinAnnounceRef.current = {
|
||||
items: pre,
|
||||
caseName: resolvedCaseName || '',
|
||||
@@ -517,6 +524,7 @@ export default function CasePage() {
|
||||
beginReelSpins(pre);
|
||||
} else {
|
||||
pendingPreRespinAutoSellRef.current = [];
|
||||
pendingKeptPreRespinRef.current = [];
|
||||
respinPhaseRef.current = null;
|
||||
beginReelSpins(items);
|
||||
}
|
||||
@@ -572,15 +580,21 @@ export default function CasePage() {
|
||||
|
||||
const t = setTimeout(() => {
|
||||
pendingRespinFinalRef.current = null;
|
||||
const kept = pendingKeptPreRespinRef.current || [];
|
||||
pendingKeptPreRespinRef.current = [];
|
||||
respinPhaseRef.current = 'final';
|
||||
const keptNote =
|
||||
kept.length > 0
|
||||
? ` · Kept ${kept.length} item${kept.length === 1 ? '' : 's'} from first spin`
|
||||
: '';
|
||||
setOpenBonusMessage((msg) =>
|
||||
msg?.includes('Respin')
|
||||
? msg
|
||||
: msg
|
||||
? `${msg} · Respin! Re-rolling…`
|
||||
: 'Respin! Re-rolling all drops…'
|
||||
? `${msg} · Respin! Re-rolling…${keptNote}`
|
||||
: `Respin! Re-rolling all drops…${keptNote}`
|
||||
);
|
||||
beginReelSpins(finalItems);
|
||||
beginReelSpins(finalItems, kept);
|
||||
setRespinRestarting(false);
|
||||
}, 550);
|
||||
|
||||
|
||||
@@ -587,7 +587,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid">
|
||||
<div className="grid inv-grid">
|
||||
{pageItems.map((inv) => (
|
||||
<ItemTile
|
||||
key={inv.id}
|
||||
|
||||
@@ -255,6 +255,7 @@ export default function PrestigePage() {
|
||||
<li>Skill-tree bonuses</li>
|
||||
<li>Account & prestige count</li>
|
||||
<li>Catalog progress</li>
|
||||
<li>Opens-by-case stats</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -73,6 +73,67 @@ export default function ShopPage() {
|
||||
};
|
||||
}, [user?.id, user?.role, load]);
|
||||
|
||||
// After Stripe Checkout redirect (?osu=success|cancel)
|
||||
useEffect(() => {
|
||||
if (!user || user.role === 'admin') return undefined;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const osu = params.get('osu');
|
||||
if (!osu) return undefined;
|
||||
|
||||
const orderId = params.get('order');
|
||||
const cleanUrl = () => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('osu');
|
||||
url.searchParams.delete('order');
|
||||
window.history.replaceState({}, '', url.pathname + url.search);
|
||||
};
|
||||
|
||||
if (osu === 'cancel') {
|
||||
setMessage('Payment cancelled — no osu charged');
|
||||
cleanUrl();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (osu !== 'success') return undefined;
|
||||
|
||||
let cancelled = false;
|
||||
setMessage('Payment received — confirming osu credit…');
|
||||
cleanUrl();
|
||||
|
||||
(async () => {
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
if (cancelled) return;
|
||||
try {
|
||||
if (orderId) {
|
||||
const data = await api.shopOsuOrder(orderId);
|
||||
if (data.order?.status === 'paid') {
|
||||
await load();
|
||||
if (!cancelled) {
|
||||
setMessage(`+${data.order.osuAmount} osu added to your wallet`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await load();
|
||||
}
|
||||
} catch {
|
||||
/* retry */
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
}
|
||||
if (!cancelled) {
|
||||
await load().catch(() => {});
|
||||
setMessage(
|
||||
'Payment submitted — if osu is missing, refresh in a few seconds (webhook pending)'
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [user?.id, user?.role, load]);
|
||||
|
||||
const buyCredits = async (packageId) => {
|
||||
setBusy(true);
|
||||
setError('');
|
||||
@@ -111,6 +172,9 @@ export default function ShopPage() {
|
||||
: `+${data.osuGranted} osu added to your wallet`
|
||||
);
|
||||
await load();
|
||||
} else if (checkout.mode === 'stripe' && checkout.url) {
|
||||
window.location.assign(checkout.url);
|
||||
return;
|
||||
} else {
|
||||
setError('Payment provider is not ready for live checkout yet');
|
||||
}
|
||||
@@ -179,11 +243,13 @@ export default function ShopPage() {
|
||||
<span className="muted">Credits</span>
|
||||
<strong className="balance-pill">{formatCredits(user.balance)} cr</strong>
|
||||
</div>
|
||||
{payments?.mock && (
|
||||
{payments?.mock ? (
|
||||
<p className="shop-mock-note muted">
|
||||
Dev payments: mock checkout credits osu instantly (Stripe-ready backend).
|
||||
Dev payments: mock checkout credits osu instantly.
|
||||
</p>
|
||||
)}
|
||||
) : payments?.provider === 'stripe' ? (
|
||||
<p className="shop-mock-note muted">Secure checkout via Stripe.</p>
|
||||
) : null}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user