@thinkingcat/subscription-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @thinkingcat/subscription-ui
2
+
3
+ ThinkingCat 서비스에서 공통으로 사용하는 구독 UI 컴포넌트 라이브러리입니다.
4
+
5
+ ## 설치
6
+
7
+ ```bash
8
+ npm install @thinkingcat/subscription-ui
9
+ ```
10
+
11
+ ## 사용법
12
+
13
+ ### 1. 프록시 API 라우트 생성
14
+
15
+ 서비스마다 CM_sso의 외부 청구 API를 호출하는 프록시 라우트가 필요합니다.
16
+
17
+ **`/api/subscribe/plans/route.ts`**
18
+ ```typescript
19
+ import { NextRequest, NextResponse } from 'next/server';
20
+ import { getToken } from 'next-auth/jwt';
21
+
22
+ const SSO_URL = process.env.SUBSCRIPTION_SERVER_URL!;
23
+ const SERVICE_KEY = process.env.SUBSCRIPTION_SERVICE_KEY!;
24
+ const SERVICE_ID = process.env.SUBSCRIPTION_SERVICE_ID || 'my-service';
25
+
26
+ export async function GET(req: NextRequest) {
27
+ const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET! });
28
+ if (!token?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
29
+
30
+ const res = await fetch(`${SSO_URL}/api/external/billing/plans?serviceId=${SERVICE_ID}`, {
31
+ headers: { 'x-auth-service-key': SERVICE_KEY },
32
+ cache: 'no-store',
33
+ });
34
+ return NextResponse.json(await res.json(), { status: res.status });
35
+ }
36
+ ```
37
+
38
+ **`/api/subscribe/cards/route.ts`** — GET(카드 목록), POST(카드 등록)
39
+
40
+ **`/api/subscribe/pay/route.ts`** — POST(구독 생성/결제)
41
+
42
+ ### 2. 구독 페이지 생성
43
+
44
+ **`/app/subscribe/page.tsx`**
45
+ ```tsx
46
+ import { Suspense } from "react";
47
+ import { SubscribePage } from "@thinkingcat/subscription-ui";
48
+
49
+ function SubscribeContent() {
50
+ return (
51
+ <SubscribePage
52
+ plansApiPath="/api/subscribe/plans"
53
+ cardsApiPath="/api/subscribe/cards"
54
+ payApiPath="/api/subscribe/pay"
55
+ serviceName="내 서비스"
56
+ primaryColor="#60A5FA"
57
+ />
58
+ );
59
+ }
60
+
61
+ export default function Page() {
62
+ return (
63
+ <Suspense fallback={<div>불러오는 중...</div>}>
64
+ <SubscribeContent />
65
+ </Suspense>
66
+ );
67
+ }
68
+ ```
69
+
70
+ ### 3. 미들웨어 설정
71
+
72
+ `middleware.ts`에서 `/subscribe`를 `subscriptionSkipPaths`에 추가하고, 미구독 시 `/subscribe?callbackUrl=...`로 리다이렉트 설정.
73
+
74
+ ## SubscribePage Props
75
+
76
+ | 속성 | 타입 | 기본값 | 설명 |
77
+ |---|---|---|---|
78
+ | `plansApiPath` | string | `/api/subscribe/plans` | 플랜 조회 API 경로 |
79
+ | `cardsApiPath` | string | `/api/subscribe/cards` | 카드 조회/등록 API 경로 |
80
+ | `payApiPath` | string | `/api/subscribe/pay` | 결제 API 경로 |
81
+ | `callbackUrl` | string | `/` | 구독 완료 후 이동 경로 |
82
+ | `serviceName` | string | `서비스` | 헤더에 표시할 서비스명 |
83
+ | `primaryColor` | string | `#60A5FA` | 브랜드 주 색상 |
84
+ | `showExpiredBanner` | boolean | `true` | 구독 만료 안내 배너 표시 여부 |
85
+
86
+ ## 빌드
87
+
88
+ ```bash
89
+ npm run build
90
+ ```
@@ -0,0 +1,43 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface Plan {
4
+ id: string;
5
+ name: string;
6
+ description: string;
7
+ price: number;
8
+ interval: number;
9
+ currency: string;
10
+ features: string[];
11
+ isPopular: boolean;
12
+ maxUser: number | null;
13
+ }
14
+ interface Card {
15
+ id: string;
16
+ maskedCardNumber: string;
17
+ cardBrand: string;
18
+ cardType?: string;
19
+ cardHolderName: string;
20
+ expiryMonth: string;
21
+ expiryYear: string;
22
+ isDefault: boolean;
23
+ }
24
+ interface SubscribePageConfig {
25
+ /** 플랜 목록 API 경로 (default: /api/subscribe/plans) */
26
+ plansApiPath?: string;
27
+ /** 카드 조회/등록 API 경로 (default: /api/subscribe/cards) */
28
+ cardsApiPath?: string;
29
+ /** 결제 API 경로 (default: /api/subscribe/pay) */
30
+ payApiPath?: string;
31
+ /** 결제 완료 후 이동할 경로 (default: /) */
32
+ callbackUrl?: string;
33
+ /** 서비스명 (헤더에 표시) */
34
+ serviceName?: string;
35
+ /** 브랜드 색상 (default: #60A5FA) */
36
+ primaryColor?: string;
37
+ /** 구독 만료 안내 메시지 표시 여부 */
38
+ showExpiredBanner?: boolean;
39
+ }
40
+
41
+ declare function SubscribePage({ plansApiPath, cardsApiPath, payApiPath, callbackUrl, serviceName, primaryColor, showExpiredBanner, }: SubscribePageConfig): react_jsx_runtime.JSX.Element;
42
+
43
+ export { type Card, type Plan, SubscribePage, type SubscribePageConfig };
@@ -0,0 +1,43 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface Plan {
4
+ id: string;
5
+ name: string;
6
+ description: string;
7
+ price: number;
8
+ interval: number;
9
+ currency: string;
10
+ features: string[];
11
+ isPopular: boolean;
12
+ maxUser: number | null;
13
+ }
14
+ interface Card {
15
+ id: string;
16
+ maskedCardNumber: string;
17
+ cardBrand: string;
18
+ cardType?: string;
19
+ cardHolderName: string;
20
+ expiryMonth: string;
21
+ expiryYear: string;
22
+ isDefault: boolean;
23
+ }
24
+ interface SubscribePageConfig {
25
+ /** 플랜 목록 API 경로 (default: /api/subscribe/plans) */
26
+ plansApiPath?: string;
27
+ /** 카드 조회/등록 API 경로 (default: /api/subscribe/cards) */
28
+ cardsApiPath?: string;
29
+ /** 결제 API 경로 (default: /api/subscribe/pay) */
30
+ payApiPath?: string;
31
+ /** 결제 완료 후 이동할 경로 (default: /) */
32
+ callbackUrl?: string;
33
+ /** 서비스명 (헤더에 표시) */
34
+ serviceName?: string;
35
+ /** 브랜드 색상 (default: #60A5FA) */
36
+ primaryColor?: string;
37
+ /** 구독 만료 안내 메시지 표시 여부 */
38
+ showExpiredBanner?: boolean;
39
+ }
40
+
41
+ declare function SubscribePage({ plansApiPath, cardsApiPath, payApiPath, callbackUrl, serviceName, primaryColor, showExpiredBanner, }: SubscribePageConfig): react_jsx_runtime.JSX.Element;
42
+
43
+ export { type Card, type Plan, SubscribePage, type SubscribePageConfig };
package/dist/index.js ADDED
@@ -0,0 +1,372 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ SubscribePage: () => SubscribePage
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/components/SubscribePage.tsx
28
+ var import_react = require("react");
29
+ var import_jsx_runtime = require("react/jsx-runtime");
30
+ var DEFAULT_PRIMARY = "#60A5FA";
31
+ function SubscribePage({
32
+ plansApiPath = "/api/subscribe/plans",
33
+ cardsApiPath = "/api/subscribe/cards",
34
+ payApiPath = "/api/subscribe/pay",
35
+ callbackUrl = "/",
36
+ serviceName = "\uC11C\uBE44\uC2A4",
37
+ primaryColor = DEFAULT_PRIMARY,
38
+ showExpiredBanner = true
39
+ }) {
40
+ const [step, setStep] = (0, import_react.useState)("plan");
41
+ const [plans, setPlans] = (0, import_react.useState)([]);
42
+ const [cards, setCards] = (0, import_react.useState)([]);
43
+ const [selectedPlan, setSelectedPlan] = (0, import_react.useState)(null);
44
+ const [selectedCard, setSelectedCard] = (0, import_react.useState)(null);
45
+ const [loading, setLoading] = (0, import_react.useState)(true);
46
+ const [submitting, setSubmitting] = (0, import_react.useState)(false);
47
+ const [error, setError] = (0, import_react.useState)("");
48
+ const [success, setSuccess] = (0, import_react.useState)(false);
49
+ const [showCardForm, setShowCardForm] = (0, import_react.useState)(false);
50
+ const [cardForm, setCardForm] = (0, import_react.useState)({
51
+ cardNumber: "",
52
+ expiryDate: "",
53
+ cvc: "",
54
+ cardPassword: "",
55
+ cardHolderName: "",
56
+ birthNumber: ""
57
+ });
58
+ const [cardError, setCardError] = (0, import_react.useState)("");
59
+ const [cardSubmitting, setCardSubmitting] = (0, import_react.useState)(false);
60
+ (0, import_react.useEffect)(() => {
61
+ fetchPlans();
62
+ fetchCards();
63
+ }, []);
64
+ async function fetchPlans() {
65
+ try {
66
+ const res = await fetch(plansApiPath);
67
+ const data = await res.json();
68
+ if (data.success) setPlans(data.plans);
69
+ else setError("\uD50C\uB79C \uC815\uBCF4\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
70
+ } catch {
71
+ setError("\uD50C\uB79C \uC815\uBCF4\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
72
+ } finally {
73
+ setLoading(false);
74
+ }
75
+ }
76
+ async function fetchCards() {
77
+ try {
78
+ const res = await fetch(cardsApiPath);
79
+ const data = await res.json();
80
+ if (data.success) {
81
+ setCards(data.cards);
82
+ const def = data.cards.find((c) => c.isDefault);
83
+ if (def) setSelectedCard(def);
84
+ }
85
+ } catch {
86
+ }
87
+ }
88
+ async function handleRegisterCard() {
89
+ setCardError("");
90
+ setCardSubmitting(true);
91
+ try {
92
+ const res = await fetch(cardsApiPath, {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify(cardForm)
96
+ });
97
+ const data = await res.json();
98
+ if (!data.success) {
99
+ setCardError(data.error || "\uCE74\uB4DC \uB4F1\uB85D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
100
+ return;
101
+ }
102
+ await fetchCards();
103
+ setSelectedCard(data.card);
104
+ setShowCardForm(false);
105
+ setCardForm({ cardNumber: "", expiryDate: "", cvc: "", cardPassword: "", cardHolderName: "", birthNumber: "" });
106
+ } catch {
107
+ setCardError("\uCE74\uB4DC \uB4F1\uB85D \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
108
+ } finally {
109
+ setCardSubmitting(false);
110
+ }
111
+ }
112
+ async function handleSubscribe() {
113
+ if (!selectedPlan || !selectedCard) return;
114
+ setError("");
115
+ setSubmitting(true);
116
+ try {
117
+ const res = await fetch(payApiPath, {
118
+ method: "POST",
119
+ headers: { "Content-Type": "application/json" },
120
+ body: JSON.stringify({ planId: selectedPlan.id, paymentMethodId: selectedCard.id })
121
+ });
122
+ const data = await res.json();
123
+ if (!data.success) {
124
+ setError(data.error || "\uAD6C\uB3C5 \uCC98\uB9AC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
125
+ return;
126
+ }
127
+ setSuccess(true);
128
+ setTimeout(() => {
129
+ window.location.href = callbackUrl;
130
+ }, 2e3);
131
+ } catch {
132
+ setError("\uAD6C\uB3C5 \uCC98\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
133
+ } finally {
134
+ setSubmitting(false);
135
+ }
136
+ }
137
+ function fmt(price) {
138
+ return price.toLocaleString("ko-KR") + "\uC6D0";
139
+ }
140
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PageLayout, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex items-center justify-center h-64 text-gray-400", children: "\uBD88\uB7EC\uC624\uB294 \uC911..." }) });
141
+ if (success) {
142
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PageLayout, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex items-center justify-center h-64", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-center", children: [
143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-16 h-16 rounded-full bg-green-500/20 flex items-center justify-center mx-auto mb-4", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CheckIcon, { className: "w-8 h-8 text-green-400" }) }),
144
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-xl font-bold text-white mb-2", children: "\uAD6C\uB3C5\uC774 \uC2DC\uC791\uB418\uC5C8\uC2B5\uB2C8\uB2E4!" }),
145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-gray-400 text-sm", children: "\uC7A0\uC2DC \uD6C4 \uC774\uB3D9\uD569\uB2C8\uB2E4..." })
146
+ ] }) }) });
147
+ }
148
+ const steps = [
149
+ { key: "plan", label: "\uD50C\uB79C" },
150
+ { key: "card", label: "\uACB0\uC81C\uC218\uB2E8" },
151
+ { key: "confirm", label: "\uD655\uC778" }
152
+ ];
153
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(PageLayout, { children: [
154
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "border-b border-white/10 px-4 py-4 mb-8", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "max-w-3xl mx-auto flex items-center justify-between", children: [
155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
156
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h1", { className: "text-lg font-bold text-white", children: [
157
+ serviceName,
158
+ " \uAD6C\uB3C5"
159
+ ] }),
160
+ showExpiredBanner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs text-amber-400 mt-0.5", children: "\uAD6C\uB3C5\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC11C\uBE44\uC2A4\uB97C \uACC4\uC18D \uC774\uC6A9\uD558\uB824\uBA74 \uAD6C\uB3C5\uC774 \uD544\uC694\uD569\uB2C8\uB2E4." })
161
+ ] }),
162
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex items-center gap-2", children: steps.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
163
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-4 h-px bg-white/20" }),
164
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
165
+ "div",
166
+ {
167
+ className: `w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${step === s.key ? "text-white" : "bg-white/10 text-gray-400"}`,
168
+ style: step === s.key ? { backgroundColor: primaryColor } : void 0,
169
+ children: i + 1
170
+ }
171
+ )
172
+ ] }, s.key)) })
173
+ ] }) }),
174
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "max-w-3xl mx-auto px-4 pb-12", children: [
175
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mb-6 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm", children: error }),
176
+ step === "plan" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
177
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-white font-semibold mb-4", children: "\uD50C\uB79C\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }),
178
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid gap-3", children: plans.map((plan) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
179
+ "button",
180
+ {
181
+ onClick: () => setSelectedPlan(plan),
182
+ className: `w-full text-left p-4 rounded-xl border transition-all ${selectedPlan?.id === plan.id ? "border-blue-500 bg-blue-500/10" : "border-white/10 bg-white/5 hover:border-white/20"}`,
183
+ children: [
184
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-white font-medium", children: plan.name }),
187
+ plan.isPopular && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded-full border border-blue-500/30", children: "\uC778\uAE30" })
188
+ ] }),
189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-right", children: [
190
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-white font-bold", children: fmt(plan.price) }),
191
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-gray-400 text-xs ml-1", children: [
192
+ "/ ",
193
+ plan.interval,
194
+ "\uAC1C\uC6D4"
195
+ ] })
196
+ ] })
197
+ ] }),
198
+ plan.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-gray-400 text-sm mb-2", children: plan.description }),
199
+ plan.features.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "space-y-1", children: plan.features.slice(0, 3).map((f, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: "flex items-center gap-1.5 text-xs text-gray-300", children: [
200
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CheckIcon, { className: "w-3 h-3 text-green-400 flex-shrink-0" }),
201
+ f
202
+ ] }, i)) })
203
+ ]
204
+ },
205
+ plan.id
206
+ )) }),
207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
208
+ "button",
209
+ {
210
+ disabled: !selectedPlan,
211
+ onClick: () => setStep("card"),
212
+ style: selectedPlan ? { backgroundColor: primaryColor } : void 0,
213
+ className: `mt-6 w-full py-3 rounded-xl font-medium transition-all ${selectedPlan ? "text-white hover:opacity-90" : "bg-white/10 text-gray-500 cursor-not-allowed"}`,
214
+ children: "\uB2E4\uC74C \u2014 \uACB0\uC81C\uC218\uB2E8 \uC120\uD0DD"
215
+ }
216
+ )
217
+ ] }),
218
+ step === "card" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
219
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-white font-semibold mb-4", children: "\uACB0\uC81C\uC218\uB2E8\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }),
220
+ cards.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid gap-3 mb-4", children: cards.map((card) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
221
+ "button",
222
+ {
223
+ onClick: () => setSelectedCard(card),
224
+ className: `w-full text-left p-4 rounded-xl border transition-all ${selectedCard?.id === card.id ? "border-blue-500 bg-blue-500/10" : "border-white/10 bg-white/5 hover:border-white/20"}`,
225
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between", children: [
226
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
227
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-white font-medium", children: card.cardBrand }),
228
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-gray-400 text-sm ml-2", children: card.maskedCardNumber })
229
+ ] }),
230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
231
+ card.isDefault && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded-full border border-blue-500/30", children: "\uAE30\uBCF8" }),
232
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-gray-400 text-xs", children: [
233
+ card.expiryMonth,
234
+ "/",
235
+ card.expiryYear
236
+ ] })
237
+ ] })
238
+ ] })
239
+ },
240
+ card.id
241
+ )) }),
242
+ !showCardForm ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
243
+ "button",
244
+ {
245
+ onClick: () => setShowCardForm(true),
246
+ className: "w-full p-4 rounded-xl border border-dashed border-white/20 text-gray-400 hover:border-white/40 hover:text-gray-300 transition-all text-sm",
247
+ children: "+ \uC0C8 \uCE74\uB4DC \uCD94\uAC00"
248
+ }
249
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-4 rounded-xl border border-white/10 bg-white/5 space-y-3", children: [
250
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-white text-sm font-medium mb-1", children: "\uCE74\uB4DC \uC815\uBCF4 \uC785\uB825" }),
251
+ cardError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-xs", children: cardError }),
252
+ [
253
+ { label: "\uCE74\uB4DC \uBC88\uD638", key: "cardNumber", placeholder: "0000-0000-0000-0000", type: "text" },
254
+ { label: "\uB9CC\uB8CC\uC77C", key: "expiryDate", placeholder: "MM/YY", type: "text" },
255
+ { label: "CVC", key: "cvc", placeholder: "3\uC790\uB9AC", type: "password" },
256
+ { label: "\uCE74\uB4DC \uBE44\uBC00\uBC88\uD638 \uC55E 2\uC790\uB9AC", key: "cardPassword", placeholder: "\u2022\u2022", type: "password" },
257
+ { label: "\uCE74\uB4DC \uC18C\uC720\uC790\uBA85", key: "cardHolderName", placeholder: "\uD64D\uAE38\uB3D9", type: "text" },
258
+ { label: "\uC0DD\uB144\uC6D4\uC77C 6\uC790\uB9AC / \uC0AC\uC5C5\uC790\uBC88\uD638 10\uC790\uB9AC", key: "birthNumber", placeholder: "990101", type: "text" }
259
+ ].map(({ label, key, placeholder, type }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
260
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "text-xs text-gray-400 block mb-1", children: label }),
261
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
262
+ "input",
263
+ {
264
+ type,
265
+ placeholder,
266
+ value: cardForm[key],
267
+ onChange: (e) => setCardForm((f) => ({ ...f, [key]: e.target.value })),
268
+ className: "w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm placeholder-gray-600 focus:outline-none focus:border-blue-500"
269
+ }
270
+ )
271
+ ] }, key)),
272
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex gap-2 pt-1", children: [
273
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
274
+ "button",
275
+ {
276
+ onClick: () => {
277
+ setShowCardForm(false);
278
+ setCardError("");
279
+ },
280
+ className: "flex-1 py-2 rounded-lg border border-white/10 text-gray-400 text-sm hover:border-white/20 transition-all",
281
+ children: "\uCDE8\uC18C"
282
+ }
283
+ ),
284
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
285
+ "button",
286
+ {
287
+ onClick: handleRegisterCard,
288
+ disabled: cardSubmitting,
289
+ style: { backgroundColor: primaryColor },
290
+ className: "flex-1 py-2 rounded-lg text-white text-sm font-medium hover:opacity-90 disabled:opacity-50 transition-all",
291
+ children: cardSubmitting ? "\uB4F1\uB85D \uC911..." : "\uCE74\uB4DC \uB4F1\uB85D"
292
+ }
293
+ )
294
+ ] })
295
+ ] }),
296
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex gap-3 mt-6", children: [
297
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { onClick: () => setStep("plan"), className: "flex-1 py-3 rounded-xl border border-white/10 text-gray-400 font-medium hover:border-white/20 transition-all", children: "\uC774\uC804" }),
298
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
299
+ "button",
300
+ {
301
+ disabled: !selectedCard,
302
+ onClick: () => setStep("confirm"),
303
+ style: selectedCard ? { backgroundColor: primaryColor } : void 0,
304
+ className: `flex-1 py-3 rounded-xl font-medium transition-all ${selectedCard ? "text-white hover:opacity-90" : "bg-white/10 text-gray-500 cursor-not-allowed"}`,
305
+ children: "\uB2E4\uC74C \u2014 \uCD5C\uC885 \uD655\uC778"
306
+ }
307
+ )
308
+ ] })
309
+ ] }),
310
+ step === "confirm" && selectedPlan && selectedCard && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
311
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-white font-semibold mb-6", children: "\uAD6C\uB3C5 \uB0B4\uC6A9\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694" }),
312
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-3 mb-6", children: [
313
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-4 rounded-xl border border-white/10 bg-white/5", children: [
314
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs text-gray-400 mb-1", children: "\uC120\uD0DD \uD50C\uB79C" }),
315
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between", children: [
316
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-white font-medium", children: selectedPlan.name }),
317
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-white font-bold", children: [
318
+ fmt(selectedPlan.price),
319
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-gray-400 text-xs font-normal ml-1", children: [
320
+ "/ ",
321
+ selectedPlan.interval,
322
+ "\uAC1C\uC6D4"
323
+ ] })
324
+ ] })
325
+ ] })
326
+ ] }),
327
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-4 rounded-xl border border-white/10 bg-white/5", children: [
328
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs text-gray-400 mb-1", children: "\uACB0\uC81C\uC218\uB2E8" }),
329
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
330
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-white font-medium", children: selectedCard.cardBrand }),
331
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-gray-400 text-sm", children: selectedCard.maskedCardNumber })
332
+ ] })
333
+ ] }),
334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-4 rounded-xl border border-white/10 bg-white/5", style: { borderColor: `${primaryColor}4D` }, children: [
335
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between", children: [
336
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-gray-300 text-sm", children: "\uACB0\uC81C \uAE08\uC561" }),
337
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-bold text-lg", style: { color: primaryColor }, children: fmt(selectedPlan.price) })
338
+ ] }),
339
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "text-gray-500 text-xs mt-1", children: [
340
+ "\uC624\uB298 \uACB0\uC81C \uD6C4 ",
341
+ selectedPlan.interval,
342
+ "\uAC1C\uC6D4 \uAC04 \uC774\uC6A9 \uAC00\uB2A5\uD569\uB2C8\uB2E4"
343
+ ] })
344
+ ] })
345
+ ] }),
346
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex gap-3", children: [
347
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { onClick: () => setStep("card"), className: "flex-1 py-3 rounded-xl border border-white/10 text-gray-400 font-medium hover:border-white/20 transition-all", children: "\uC774\uC804" }),
348
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
349
+ "button",
350
+ {
351
+ onClick: handleSubscribe,
352
+ disabled: submitting,
353
+ style: { backgroundColor: primaryColor },
354
+ className: "flex-1 py-3 rounded-xl text-white font-medium hover:opacity-90 disabled:opacity-50 transition-all",
355
+ children: submitting ? "\uCC98\uB9AC \uC911..." : `${fmt(selectedPlan.price)} \uACB0\uC81C\uD558\uAE30`
356
+ }
357
+ )
358
+ ] })
359
+ ] })
360
+ ] })
361
+ ] });
362
+ }
363
+ function PageLayout({ children }) {
364
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "min-h-screen bg-gray-950", children });
365
+ }
366
+ function CheckIcon({ className }) {
367
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) });
368
+ }
369
+ // Annotate the CommonJS export names for ESM import in node:
370
+ 0 && (module.exports = {
371
+ SubscribePage
372
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,345 @@
1
+ // src/components/SubscribePage.tsx
2
+ import { useState, useEffect } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ var DEFAULT_PRIMARY = "#60A5FA";
5
+ function SubscribePage({
6
+ plansApiPath = "/api/subscribe/plans",
7
+ cardsApiPath = "/api/subscribe/cards",
8
+ payApiPath = "/api/subscribe/pay",
9
+ callbackUrl = "/",
10
+ serviceName = "\uC11C\uBE44\uC2A4",
11
+ primaryColor = DEFAULT_PRIMARY,
12
+ showExpiredBanner = true
13
+ }) {
14
+ const [step, setStep] = useState("plan");
15
+ const [plans, setPlans] = useState([]);
16
+ const [cards, setCards] = useState([]);
17
+ const [selectedPlan, setSelectedPlan] = useState(null);
18
+ const [selectedCard, setSelectedCard] = useState(null);
19
+ const [loading, setLoading] = useState(true);
20
+ const [submitting, setSubmitting] = useState(false);
21
+ const [error, setError] = useState("");
22
+ const [success, setSuccess] = useState(false);
23
+ const [showCardForm, setShowCardForm] = useState(false);
24
+ const [cardForm, setCardForm] = useState({
25
+ cardNumber: "",
26
+ expiryDate: "",
27
+ cvc: "",
28
+ cardPassword: "",
29
+ cardHolderName: "",
30
+ birthNumber: ""
31
+ });
32
+ const [cardError, setCardError] = useState("");
33
+ const [cardSubmitting, setCardSubmitting] = useState(false);
34
+ useEffect(() => {
35
+ fetchPlans();
36
+ fetchCards();
37
+ }, []);
38
+ async function fetchPlans() {
39
+ try {
40
+ const res = await fetch(plansApiPath);
41
+ const data = await res.json();
42
+ if (data.success) setPlans(data.plans);
43
+ else setError("\uD50C\uB79C \uC815\uBCF4\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
44
+ } catch {
45
+ setError("\uD50C\uB79C \uC815\uBCF4\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
46
+ } finally {
47
+ setLoading(false);
48
+ }
49
+ }
50
+ async function fetchCards() {
51
+ try {
52
+ const res = await fetch(cardsApiPath);
53
+ const data = await res.json();
54
+ if (data.success) {
55
+ setCards(data.cards);
56
+ const def = data.cards.find((c) => c.isDefault);
57
+ if (def) setSelectedCard(def);
58
+ }
59
+ } catch {
60
+ }
61
+ }
62
+ async function handleRegisterCard() {
63
+ setCardError("");
64
+ setCardSubmitting(true);
65
+ try {
66
+ const res = await fetch(cardsApiPath, {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify(cardForm)
70
+ });
71
+ const data = await res.json();
72
+ if (!data.success) {
73
+ setCardError(data.error || "\uCE74\uB4DC \uB4F1\uB85D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
74
+ return;
75
+ }
76
+ await fetchCards();
77
+ setSelectedCard(data.card);
78
+ setShowCardForm(false);
79
+ setCardForm({ cardNumber: "", expiryDate: "", cvc: "", cardPassword: "", cardHolderName: "", birthNumber: "" });
80
+ } catch {
81
+ setCardError("\uCE74\uB4DC \uB4F1\uB85D \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
82
+ } finally {
83
+ setCardSubmitting(false);
84
+ }
85
+ }
86
+ async function handleSubscribe() {
87
+ if (!selectedPlan || !selectedCard) return;
88
+ setError("");
89
+ setSubmitting(true);
90
+ try {
91
+ const res = await fetch(payApiPath, {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify({ planId: selectedPlan.id, paymentMethodId: selectedCard.id })
95
+ });
96
+ const data = await res.json();
97
+ if (!data.success) {
98
+ setError(data.error || "\uAD6C\uB3C5 \uCC98\uB9AC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
99
+ return;
100
+ }
101
+ setSuccess(true);
102
+ setTimeout(() => {
103
+ window.location.href = callbackUrl;
104
+ }, 2e3);
105
+ } catch {
106
+ setError("\uAD6C\uB3C5 \uCC98\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
107
+ } finally {
108
+ setSubmitting(false);
109
+ }
110
+ }
111
+ function fmt(price) {
112
+ return price.toLocaleString("ko-KR") + "\uC6D0";
113
+ }
114
+ if (loading) return /* @__PURE__ */ jsx(PageLayout, { children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-64 text-gray-400", children: "\uBD88\uB7EC\uC624\uB294 \uC911..." }) });
115
+ if (success) {
116
+ return /* @__PURE__ */ jsx(PageLayout, { children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-64", children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
117
+ /* @__PURE__ */ jsx("div", { className: "w-16 h-16 rounded-full bg-green-500/20 flex items-center justify-center mx-auto mb-4", children: /* @__PURE__ */ jsx(CheckIcon, { className: "w-8 h-8 text-green-400" }) }),
118
+ /* @__PURE__ */ jsx("h2", { className: "text-xl font-bold text-white mb-2", children: "\uAD6C\uB3C5\uC774 \uC2DC\uC791\uB418\uC5C8\uC2B5\uB2C8\uB2E4!" }),
119
+ /* @__PURE__ */ jsx("p", { className: "text-gray-400 text-sm", children: "\uC7A0\uC2DC \uD6C4 \uC774\uB3D9\uD569\uB2C8\uB2E4..." })
120
+ ] }) }) });
121
+ }
122
+ const steps = [
123
+ { key: "plan", label: "\uD50C\uB79C" },
124
+ { key: "card", label: "\uACB0\uC81C\uC218\uB2E8" },
125
+ { key: "confirm", label: "\uD655\uC778" }
126
+ ];
127
+ return /* @__PURE__ */ jsxs(PageLayout, { children: [
128
+ /* @__PURE__ */ jsx("div", { className: "border-b border-white/10 px-4 py-4 mb-8", children: /* @__PURE__ */ jsxs("div", { className: "max-w-3xl mx-auto flex items-center justify-between", children: [
129
+ /* @__PURE__ */ jsxs("div", { children: [
130
+ /* @__PURE__ */ jsxs("h1", { className: "text-lg font-bold text-white", children: [
131
+ serviceName,
132
+ " \uAD6C\uB3C5"
133
+ ] }),
134
+ showExpiredBanner && /* @__PURE__ */ jsx("p", { className: "text-xs text-amber-400 mt-0.5", children: "\uAD6C\uB3C5\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC11C\uBE44\uC2A4\uB97C \uACC4\uC18D \uC774\uC6A9\uD558\uB824\uBA74 \uAD6C\uB3C5\uC774 \uD544\uC694\uD569\uB2C8\uB2E4." })
135
+ ] }),
136
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: steps.map((s, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
137
+ i > 0 && /* @__PURE__ */ jsx("div", { className: "w-4 h-px bg-white/20" }),
138
+ /* @__PURE__ */ jsx(
139
+ "div",
140
+ {
141
+ className: `w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${step === s.key ? "text-white" : "bg-white/10 text-gray-400"}`,
142
+ style: step === s.key ? { backgroundColor: primaryColor } : void 0,
143
+ children: i + 1
144
+ }
145
+ )
146
+ ] }, s.key)) })
147
+ ] }) }),
148
+ /* @__PURE__ */ jsxs("div", { className: "max-w-3xl mx-auto px-4 pb-12", children: [
149
+ error && /* @__PURE__ */ jsx("div", { className: "mb-6 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm", children: error }),
150
+ step === "plan" && /* @__PURE__ */ jsxs("div", { children: [
151
+ /* @__PURE__ */ jsx("h2", { className: "text-white font-semibold mb-4", children: "\uD50C\uB79C\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }),
152
+ /* @__PURE__ */ jsx("div", { className: "grid gap-3", children: plans.map((plan) => /* @__PURE__ */ jsxs(
153
+ "button",
154
+ {
155
+ onClick: () => setSelectedPlan(plan),
156
+ className: `w-full text-left p-4 rounded-xl border transition-all ${selectedPlan?.id === plan.id ? "border-blue-500 bg-blue-500/10" : "border-white/10 bg-white/5 hover:border-white/20"}`,
157
+ children: [
158
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
159
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
160
+ /* @__PURE__ */ jsx("span", { className: "text-white font-medium", children: plan.name }),
161
+ plan.isPopular && /* @__PURE__ */ jsx("span", { className: "px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded-full border border-blue-500/30", children: "\uC778\uAE30" })
162
+ ] }),
163
+ /* @__PURE__ */ jsxs("div", { className: "text-right", children: [
164
+ /* @__PURE__ */ jsx("span", { className: "text-white font-bold", children: fmt(plan.price) }),
165
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-400 text-xs ml-1", children: [
166
+ "/ ",
167
+ plan.interval,
168
+ "\uAC1C\uC6D4"
169
+ ] })
170
+ ] })
171
+ ] }),
172
+ plan.description && /* @__PURE__ */ jsx("p", { className: "text-gray-400 text-sm mb-2", children: plan.description }),
173
+ plan.features.length > 0 && /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: plan.features.slice(0, 3).map((f, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1.5 text-xs text-gray-300", children: [
174
+ /* @__PURE__ */ jsx(CheckIcon, { className: "w-3 h-3 text-green-400 flex-shrink-0" }),
175
+ f
176
+ ] }, i)) })
177
+ ]
178
+ },
179
+ plan.id
180
+ )) }),
181
+ /* @__PURE__ */ jsx(
182
+ "button",
183
+ {
184
+ disabled: !selectedPlan,
185
+ onClick: () => setStep("card"),
186
+ style: selectedPlan ? { backgroundColor: primaryColor } : void 0,
187
+ className: `mt-6 w-full py-3 rounded-xl font-medium transition-all ${selectedPlan ? "text-white hover:opacity-90" : "bg-white/10 text-gray-500 cursor-not-allowed"}`,
188
+ children: "\uB2E4\uC74C \u2014 \uACB0\uC81C\uC218\uB2E8 \uC120\uD0DD"
189
+ }
190
+ )
191
+ ] }),
192
+ step === "card" && /* @__PURE__ */ jsxs("div", { children: [
193
+ /* @__PURE__ */ jsx("h2", { className: "text-white font-semibold mb-4", children: "\uACB0\uC81C\uC218\uB2E8\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }),
194
+ cards.length > 0 && /* @__PURE__ */ jsx("div", { className: "grid gap-3 mb-4", children: cards.map((card) => /* @__PURE__ */ jsx(
195
+ "button",
196
+ {
197
+ onClick: () => setSelectedCard(card),
198
+ className: `w-full text-left p-4 rounded-xl border transition-all ${selectedCard?.id === card.id ? "border-blue-500 bg-blue-500/10" : "border-white/10 bg-white/5 hover:border-white/20"}`,
199
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
200
+ /* @__PURE__ */ jsxs("div", { children: [
201
+ /* @__PURE__ */ jsx("span", { className: "text-white font-medium", children: card.cardBrand }),
202
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400 text-sm ml-2", children: card.maskedCardNumber })
203
+ ] }),
204
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
205
+ card.isDefault && /* @__PURE__ */ jsx("span", { className: "px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded-full border border-blue-500/30", children: "\uAE30\uBCF8" }),
206
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-400 text-xs", children: [
207
+ card.expiryMonth,
208
+ "/",
209
+ card.expiryYear
210
+ ] })
211
+ ] })
212
+ ] })
213
+ },
214
+ card.id
215
+ )) }),
216
+ !showCardForm ? /* @__PURE__ */ jsx(
217
+ "button",
218
+ {
219
+ onClick: () => setShowCardForm(true),
220
+ className: "w-full p-4 rounded-xl border border-dashed border-white/20 text-gray-400 hover:border-white/40 hover:text-gray-300 transition-all text-sm",
221
+ children: "+ \uC0C8 \uCE74\uB4DC \uCD94\uAC00"
222
+ }
223
+ ) : /* @__PURE__ */ jsxs("div", { className: "p-4 rounded-xl border border-white/10 bg-white/5 space-y-3", children: [
224
+ /* @__PURE__ */ jsx("h3", { className: "text-white text-sm font-medium mb-1", children: "\uCE74\uB4DC \uC815\uBCF4 \uC785\uB825" }),
225
+ cardError && /* @__PURE__ */ jsx("div", { className: "p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-xs", children: cardError }),
226
+ [
227
+ { label: "\uCE74\uB4DC \uBC88\uD638", key: "cardNumber", placeholder: "0000-0000-0000-0000", type: "text" },
228
+ { label: "\uB9CC\uB8CC\uC77C", key: "expiryDate", placeholder: "MM/YY", type: "text" },
229
+ { label: "CVC", key: "cvc", placeholder: "3\uC790\uB9AC", type: "password" },
230
+ { label: "\uCE74\uB4DC \uBE44\uBC00\uBC88\uD638 \uC55E 2\uC790\uB9AC", key: "cardPassword", placeholder: "\u2022\u2022", type: "password" },
231
+ { label: "\uCE74\uB4DC \uC18C\uC720\uC790\uBA85", key: "cardHolderName", placeholder: "\uD64D\uAE38\uB3D9", type: "text" },
232
+ { label: "\uC0DD\uB144\uC6D4\uC77C 6\uC790\uB9AC / \uC0AC\uC5C5\uC790\uBC88\uD638 10\uC790\uB9AC", key: "birthNumber", placeholder: "990101", type: "text" }
233
+ ].map(({ label, key, placeholder, type }) => /* @__PURE__ */ jsxs("div", { children: [
234
+ /* @__PURE__ */ jsx("label", { className: "text-xs text-gray-400 block mb-1", children: label }),
235
+ /* @__PURE__ */ jsx(
236
+ "input",
237
+ {
238
+ type,
239
+ placeholder,
240
+ value: cardForm[key],
241
+ onChange: (e) => setCardForm((f) => ({ ...f, [key]: e.target.value })),
242
+ className: "w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm placeholder-gray-600 focus:outline-none focus:border-blue-500"
243
+ }
244
+ )
245
+ ] }, key)),
246
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
247
+ /* @__PURE__ */ jsx(
248
+ "button",
249
+ {
250
+ onClick: () => {
251
+ setShowCardForm(false);
252
+ setCardError("");
253
+ },
254
+ className: "flex-1 py-2 rounded-lg border border-white/10 text-gray-400 text-sm hover:border-white/20 transition-all",
255
+ children: "\uCDE8\uC18C"
256
+ }
257
+ ),
258
+ /* @__PURE__ */ jsx(
259
+ "button",
260
+ {
261
+ onClick: handleRegisterCard,
262
+ disabled: cardSubmitting,
263
+ style: { backgroundColor: primaryColor },
264
+ className: "flex-1 py-2 rounded-lg text-white text-sm font-medium hover:opacity-90 disabled:opacity-50 transition-all",
265
+ children: cardSubmitting ? "\uB4F1\uB85D \uC911..." : "\uCE74\uB4DC \uB4F1\uB85D"
266
+ }
267
+ )
268
+ ] })
269
+ ] }),
270
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-3 mt-6", children: [
271
+ /* @__PURE__ */ jsx("button", { onClick: () => setStep("plan"), className: "flex-1 py-3 rounded-xl border border-white/10 text-gray-400 font-medium hover:border-white/20 transition-all", children: "\uC774\uC804" }),
272
+ /* @__PURE__ */ jsx(
273
+ "button",
274
+ {
275
+ disabled: !selectedCard,
276
+ onClick: () => setStep("confirm"),
277
+ style: selectedCard ? { backgroundColor: primaryColor } : void 0,
278
+ className: `flex-1 py-3 rounded-xl font-medium transition-all ${selectedCard ? "text-white hover:opacity-90" : "bg-white/10 text-gray-500 cursor-not-allowed"}`,
279
+ children: "\uB2E4\uC74C \u2014 \uCD5C\uC885 \uD655\uC778"
280
+ }
281
+ )
282
+ ] })
283
+ ] }),
284
+ step === "confirm" && selectedPlan && selectedCard && /* @__PURE__ */ jsxs("div", { children: [
285
+ /* @__PURE__ */ jsx("h2", { className: "text-white font-semibold mb-6", children: "\uAD6C\uB3C5 \uB0B4\uC6A9\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694" }),
286
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3 mb-6", children: [
287
+ /* @__PURE__ */ jsxs("div", { className: "p-4 rounded-xl border border-white/10 bg-white/5", children: [
288
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400 mb-1", children: "\uC120\uD0DD \uD50C\uB79C" }),
289
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
290
+ /* @__PURE__ */ jsx("span", { className: "text-white font-medium", children: selectedPlan.name }),
291
+ /* @__PURE__ */ jsxs("span", { className: "text-white font-bold", children: [
292
+ fmt(selectedPlan.price),
293
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-400 text-xs font-normal ml-1", children: [
294
+ "/ ",
295
+ selectedPlan.interval,
296
+ "\uAC1C\uC6D4"
297
+ ] })
298
+ ] })
299
+ ] })
300
+ ] }),
301
+ /* @__PURE__ */ jsxs("div", { className: "p-4 rounded-xl border border-white/10 bg-white/5", children: [
302
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400 mb-1", children: "\uACB0\uC81C\uC218\uB2E8" }),
303
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
304
+ /* @__PURE__ */ jsx("span", { className: "text-white font-medium", children: selectedCard.cardBrand }),
305
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400 text-sm", children: selectedCard.maskedCardNumber })
306
+ ] })
307
+ ] }),
308
+ /* @__PURE__ */ jsxs("div", { className: "p-4 rounded-xl border border-white/10 bg-white/5", style: { borderColor: `${primaryColor}4D` }, children: [
309
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
310
+ /* @__PURE__ */ jsx("span", { className: "text-gray-300 text-sm", children: "\uACB0\uC81C \uAE08\uC561" }),
311
+ /* @__PURE__ */ jsx("span", { className: "font-bold text-lg", style: { color: primaryColor }, children: fmt(selectedPlan.price) })
312
+ ] }),
313
+ /* @__PURE__ */ jsxs("p", { className: "text-gray-500 text-xs mt-1", children: [
314
+ "\uC624\uB298 \uACB0\uC81C \uD6C4 ",
315
+ selectedPlan.interval,
316
+ "\uAC1C\uC6D4 \uAC04 \uC774\uC6A9 \uAC00\uB2A5\uD569\uB2C8\uB2E4"
317
+ ] })
318
+ ] })
319
+ ] }),
320
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
321
+ /* @__PURE__ */ jsx("button", { onClick: () => setStep("card"), className: "flex-1 py-3 rounded-xl border border-white/10 text-gray-400 font-medium hover:border-white/20 transition-all", children: "\uC774\uC804" }),
322
+ /* @__PURE__ */ jsx(
323
+ "button",
324
+ {
325
+ onClick: handleSubscribe,
326
+ disabled: submitting,
327
+ style: { backgroundColor: primaryColor },
328
+ className: "flex-1 py-3 rounded-xl text-white font-medium hover:opacity-90 disabled:opacity-50 transition-all",
329
+ children: submitting ? "\uCC98\uB9AC \uC911..." : `${fmt(selectedPlan.price)} \uACB0\uC81C\uD558\uAE30`
330
+ }
331
+ )
332
+ ] })
333
+ ] })
334
+ ] })
335
+ ] });
336
+ }
337
+ function PageLayout({ children }) {
338
+ return /* @__PURE__ */ jsx("div", { className: "min-h-screen bg-gray-950", children });
339
+ }
340
+ function CheckIcon({ className }) {
341
+ return /* @__PURE__ */ jsx("svg", { className, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) });
342
+ }
343
+ export {
344
+ SubscribePage
345
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@thinkingcat/subscription-ui",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "1.0.0",
7
+ "description": "Subscription UI components for ThinkingCat services",
8
+ "main": "dist/index.js",
9
+ "module": "dist/index.mjs",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --external react --external react-dom --external next",
20
+ "dev": "tsup src/index.ts --format cjs,esm --dts --external react --external react-dom --external next --watch",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": ["subscription", "ui", "react", "thinkingcat"],
24
+ "author": "ThinkingCat",
25
+ "license": "MIT",
26
+ "peerDependencies": {
27
+ "next": ">=13.0.0",
28
+ "react": ">=18.0.0",
29
+ "react-dom": ">=18.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20",
33
+ "@types/react": "^19",
34
+ "@types/react-dom": "^19",
35
+ "react": "^19.2.1",
36
+ "react-dom": "^19.2.1",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "README.md"
43
+ ]
44
+ }