'use client' // apps/web/src/components/billing/portal-button.tsx // Stripe Customer Portal — 활성 구독 사용자가 결제 수단/취소 등을 관리 import { useState } from 'react' import { Button, CircularProgress, Alert, Box } from '@mui/material' import SettingsIcon from '@mui/icons-material/Settings' import { getSupabaseBrowserClient } from '@/lib/supabase-browser' import { browserBillingUrl } from '@/lib/web-app-url' export function PortalButton(): React.ReactElement { const [busy, setBusy] = useState(false) const [error, setError] = useState(null) async function handleOpenPortal(): Promise { setError(null) setBusy(true) try { const supabase = getSupabaseBrowserClient() const { data: { session }, error: sessionError } = await supabase.auth.getSession() if (sessionError || !session) { setError('로그인이 필요합니다') return } const baseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.trim() if (!baseUrl) throw new Error('결제 서버 설정이 완료되지 않았습니다') const response = await fetch( new URL('/functions/v1/stripe-portal', baseUrl).toString(), { method: 'POST', headers: { Authorization: `Bearer ${session.access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ return_url: browserBillingUrl() }) } ) const data = await response.json().catch(() => null) as { url?: unknown } | null if (!response.ok) throw new Error('Stripe 구독 관리 페이지를 열지 못했습니다') if (typeof data?.url !== 'string') throw new Error('구독 관리 URL을 받지 못했습니다') const portalUrl = new URL(data.url) if (portalUrl.protocol !== 'https:' || portalUrl.hostname !== 'billing.stripe.com') { throw new Error('구독 관리 URL을 신뢰할 수 없습니다') } window.location.assign(portalUrl.toString()) } catch (e) { setError(e instanceof Error ? e.message : 'Unknown error') } finally { setBusy(false) } } return ( {error && ( {error} )} ) }