// ──────────────────────────────────────────────────────────── // 会員認証・課金ゲート // 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 (
確認メールを送信しました
{email} 宛のメールにあるリンクを開くと、ログインが完了します。 (迷惑メールフォルダもご確認ください)
); } return (
メールアドレスにログイン用リンクをお送りします。パスワードは不要です。
setEmail(e.target.value)} onKeyDown={e => { if (e.key === "Enter") send(); }} placeholder="メールアドレス" style={{ flex: 1, minWidth: 0, fontSize: 13, padding: "9px 10px", border: dark ? "1px solid rgba(255,255,255,0.2)" : "1px solid var(--hairline-strong)", background: dark ? "rgba(255,255,255,0.06)" : "var(--bg)", color: fg, outline: "none", }} />
{err &&
{err}
}
); } // ---- 未課金ユーザー向け:決済への誘導 ---- function CheckoutBox({ dark }) { const auth = useAuth(); const url = _checkoutUrl(auth.user); const sub = dark ? "#9aa3b8" : "var(--muted)"; return (
{auth.user && auth.user.email ? `${auth.user.email} でログイン中。` : ""} 月額500円(税込)で全コンテンツをご覧いただけます。いつでも解約できます。
{url ? ( 会員登録にすすむ(月額500円) ) : (
決済の準備中です。もうしばらくお待ちください。
)}
); } // ---- 課金ゲート:有料コンテンツを包む ---- function PayGate({ children, dark, label, navigate }) { const auth = useAuth(); if (!auth.enabled) return children; if (auth.paid) return children; const boxStyle = dark ? { background: "#0E1628", color: "#E8EBF2", minHeight: "100%", padding: "0 0 24px" } : { padding: "0 0 24px" }; const cardStyle = dark ? { margin: "16px 16px 0", padding: "18px 16px", background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.1)" } : { margin: "16px 14px 0", padding: "18px 16px", background: "var(--surface)", border: "1px solid var(--hairline)" }; return (
{dark ? (

{label || "有料会員限定"}

) : ( window.PageHeader && ( navigate("home") : undefined}/> ) )}
有料会員限定
このコンテンツは会員の方のみご覧いただけます
{auth.loading || auth.paidLoading ? (
確認中…
) : auth.user ? ( ) : ( )}
無料でご覧いただける「今日の急騰理由メモ」「急騰理由カテゴリ」「アーカイブ(直近1週間)」はこれまで通りご利用いただけます。
); } // ---- プランページ用:ログイン/会員状態ブロック ---- function PaywallAccount({ navigate }) { const auth = useAuth(); if (!auth.enabled) return null; return (
会員ステータス
{auth.loading ? (
確認中…
) : !auth.user ? ( ) : auth.paid ? (
有料会員としてログイン中
{auth.user.email}
{_paywallCfg().STRIPE_PORTAL_LINK && ( お支払い・解約の管理 )}
) : ( )}
); } // ---- アーカイブページ上部の注意書き(無料は直近30日) ---- function ArchivePayNotice({ navigate }) { const auth = useAuth(); if (!auth.enabled || auth.paid || auth.loading) return null; return (
無料プランでは直近1週間分を表示しています。それ以前の閲覧は有料会員限定です。
); } Object.assign(window, { useAuth, getAuthClient, PayGate, PaywallAccount, ArchivePayNotice, LoginBox, });