2.5.4
Upgraded pro/extra access
This commit is contained in:
@@ -90,18 +90,91 @@ export function useAppState() {
|
||||
|
||||
// Pro tier flag is read early so useForecast can pick its refresh cadence
|
||||
// (Pro: 5 min, free: 15 min). Full setup notes in the PRO TIER section below.
|
||||
const [isPro, setIsPro] = useState(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('pro') === '1') {
|
||||
localStorage.setItem('sunscope_pro', '1');
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
return true;
|
||||
}
|
||||
return localStorage.getItem('sunscope_pro') === '1';
|
||||
});
|
||||
// Initial state trusts only what's already in localStorage - a bare
|
||||
// ?session_id=... in the URL is verified against Stripe (see the effect
|
||||
// below) before it's ever allowed to flip this on, so pasting/guessing a
|
||||
// URL param can't grant free access.
|
||||
const [isPro, setIsPro] = useState(() => localStorage.getItem('sunscope_pro') === '1');
|
||||
|
||||
const { forecast, airQuality, loading, error, now, fetchedAt, liveElev, normals } = useForecast(location, isPro);
|
||||
|
||||
// Just returned from a Stripe Payment Link: verify the checkout session
|
||||
// server-side (verify-session.php) before granting Pro. Also records
|
||||
// which kind of purchase it was (subscription vs one-off) and the Stripe
|
||||
// customer id, so the re-check effect below knows whether/how to follow up.
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const sessionId = params.get('session_id');
|
||||
if (!sessionId) return;
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
fetch(`verify-session.php?session_id=${encodeURIComponent(sessionId)}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (!data.paid) return;
|
||||
try {
|
||||
localStorage.setItem('sunscope_pro', '1');
|
||||
if (data.mode) localStorage.setItem('sunscope_pro_mode', data.mode);
|
||||
if (data.customer) localStorage.setItem('sunscope_pro_customer', data.customer);
|
||||
localStorage.setItem('sunscope_pro_checked_at', String(Date.now()));
|
||||
} catch (e) { /* ignore */ }
|
||||
setIsPro(true);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Dev-only testing unlock: ?dev=<token>, verified server-side against
|
||||
// DEV_UNLOCK_TOKEN in secrets.local.php (dev-unlock.php). Replaces the old
|
||||
// bare ?pro=1 trick - a guessed/copied URL with the wrong token does nothing.
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const devToken = params.get('dev');
|
||||
if (!devToken) return;
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
fetch(`dev-unlock.php?token=${encodeURIComponent(devToken)}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (!data.ok) return;
|
||||
try {
|
||||
localStorage.setItem('sunscope_pro', '1');
|
||||
localStorage.setItem('sunscope_pro_mode', 'dev');
|
||||
localStorage.removeItem('sunscope_pro_customer');
|
||||
localStorage.setItem('sunscope_pro_checked_at', String(Date.now()));
|
||||
} catch (e) { /* ignore */ }
|
||||
setIsPro(true);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Subscribers (not one-off payers) can cancel in Stripe at any time, so
|
||||
// Pro access shouldn't stay granted forever once localStorage is set.
|
||||
// Re-check roughly once a day per visitor - one-off payments are skipped
|
||||
// entirely since that access is permanent by design.
|
||||
useEffect(() => {
|
||||
if (!isPro) return;
|
||||
const mode = (() => { try { return localStorage.getItem('sunscope_pro_mode'); } catch (e) { return null; } })();
|
||||
if (mode !== 'subscription') return;
|
||||
const customer = (() => { try { return localStorage.getItem('sunscope_pro_customer'); } catch (e) { return null; } })();
|
||||
if (!customer) return;
|
||||
const lastChecked = (() => { try { return Number(localStorage.getItem('sunscope_pro_checked_at')) || 0; } catch (e) { return 0; } })();
|
||||
const RECHECK_MS = 24 * 60 * 60 * 1000;
|
||||
if (Date.now() - lastChecked < RECHECK_MS) return;
|
||||
|
||||
fetch(`check-subscription.php?customer=${encodeURIComponent(customer)}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
try { localStorage.setItem('sunscope_pro_checked_at', String(Date.now())); } catch (e) { /* ignore */ }
|
||||
if (!data.active) {
|
||||
try {
|
||||
localStorage.removeItem('sunscope_pro');
|
||||
localStorage.removeItem('sunscope_pro_mode');
|
||||
localStorage.removeItem('sunscope_pro_customer');
|
||||
} catch (e) { /* ignore */ }
|
||||
setIsPro(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [isPro]);
|
||||
|
||||
useEffect(() => { track('visit'); }, []);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
Reference in New Issue
Block a user