// ──────────────────────────────────────────────────────────── // 会員認証・課金ゲート // Supabase Auth(メールのマジックリンク)+ Stripe Payment Link // // SITE_CONFIG.PAYWALL.ENABLED が false の間は何も表示・ブロックしない // (=Stripe審査中も今まで通りのサイトとして動く)。 // 有料公開時に site-config.js の ENABLED を true にし、 // paywall_setup/PAYWALL-SETUP.md の手順で Stripe / Supabase を設定する。 // ──────────────────────────────────────────────────────────── function _paywallCfg() { return (window.SITE_CONFIG && window.SITE_CONFIG.PAYWALL) || {}; } function _paywallEnabled() { return !!_paywallCfg().ENABLED; } // ---- Supabase クライアント(authセッション永続化つき・1個だけ) ---- let _authClient = null; function getAuthClient() { if (_authClient) return _authClient; const c = window.SITE_CONFIG || {}; if (!(c.SUPABASE_URL && c.SUPABASE_ANON_KEY && window.supabase && window.supabase.createClient)) return null; _authClient = window.supabase.createClient(c.SUPABASE_URL, c.SUPABASE_ANON_KEY, { auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: true, storageKey: "kyuutou-auth", }, }); return _authClient; } // ---- グローバル auth ストア(Provider不要でどこからでも useAuth() できる) ---- const _authStore = { state: { loading: true, user: null, paid: false, paidLoading: false }, subs: new Set(), set(patch) { this.state = { ...this.state, ...patch }; this.subs.forEach(f => f(this.state)); }, }; async function _refreshMembership(user) { if (!user) { _authStore.set({ paid: false, paidLoading: false }); return; } const sb = getAuthClient(); if (!sb) return; const table = _paywallCfg().MEMBERS_TABLE || "members"; _authStore.set({ paidLoading: true }); try { const { data, error } = await sb .from(table) .select("status, current_period_end") .eq("user_id", user.id) .maybeSingle(); if (error) throw error; const paid = !!( data && data.status === "active" && (!data.current_period_end || new Date(data.current_period_end).getTime() > Date.now() - 24 * 3600 * 1000) ); _authStore.set({ paid, paidLoading: false }); } catch (e) { console.warn("[auth] 会員状態の取得に失敗", e); _authStore.set({ paid: false, paidLoading: false }); } } let _authInited = false; function _initAuth() { if (_authInited) return; _authInited = true; const sb = getAuthClient(); if (!sb) { _authStore.set({ loading: false }); return; } sb.auth.getSession().then(({ data }) => { const user = data && data.session ? data.session.user : null; _authStore.set({ loading: false, user }); _refreshMembership(user); }); sb.auth.onAuthStateChange((_ev, session) => { const user = session ? session.user : null; _authStore.set({ loading: false, user }); _refreshMembership(user); }); } function useAuth() { const [s, setS] = React.useState(_authStore.state); React.useEffect(() => { _initAuth(); const f = st => setS(st); _authStore.subs.add(f); setS(_authStore.state); return () => { _authStore.subs.delete(f); }; }, []); return { ...s, enabled: _paywallEnabled(), signOut: async () => { const sb = getAuthClient(); if (sb) await sb.auth.signOut(); }, refresh: () => _refreshMembership(_authStore.state.user), }; } // Stripe Payment Link に「誰の支払いか」を載せる function _checkoutUrl(user) { const link = _paywallCfg().STRIPE_PAYMENT_LINK || ""; if (!link || !user) return null; const sep = link.includes("?") ? "&" : "?"; return link + sep + "client_reference_id=" + encodeURIComponent(user.id) + "&prefilled_email=" + encodeURIComponent(user.email || ""); } // ---- ログインボックス(マジックリンク送信) ---- function LoginBox({ dark }) { const [email, setEmail] = React.useState(""); const [busy, setBusy] = React.useState(false); const [sent, setSent] = React.useState(false); const [err, setErr] = React.useState(null); const send = async () => { const sb = getAuthClient(); if (!sb || !email.trim()) return; setBusy(true); setErr(null); try { const { error } = await sb.auth.signInWithOtp({ email: email.trim(), options: { emailRedirectTo: window.location.origin + window.location.pathname }, }); if (error) throw error; setSent(true); } catch (e) { setErr(e.message || "送信に失敗しました。時間をおいてお試しください。"); } finally { setBusy(false); } }; const fg = dark ? "#E8EBF2" : "var(--ink)"; const sub = dark ? "#9aa3b8" : "var(--muted)"; if (sent) { return (