@thinkingcat/subscription-ui 1.0.7 → 1.1.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/dist/index.d.mts +16 -9
- package/dist/index.d.ts +16 -9
- package/dist/index.js +56 -67
- package/dist/index.mjs +46 -67
- package/package.json +11 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react from 'react';
|
|
2
2
|
|
|
3
3
|
interface Plan {
|
|
4
4
|
id: string;
|
|
@@ -13,12 +13,12 @@ interface Plan {
|
|
|
13
13
|
}
|
|
14
14
|
interface Card {
|
|
15
15
|
id: string;
|
|
16
|
-
maskedCardNumber: string;
|
|
17
|
-
cardBrand: string;
|
|
18
|
-
cardType?: string;
|
|
19
|
-
cardHolderName
|
|
20
|
-
expiryMonth
|
|
21
|
-
expiryYear
|
|
16
|
+
maskedCardNumber: string | null;
|
|
17
|
+
cardBrand: string | null;
|
|
18
|
+
cardType?: string | null;
|
|
19
|
+
cardHolderName?: string | null;
|
|
20
|
+
expiryMonth?: string | null;
|
|
21
|
+
expiryYear?: string | null;
|
|
22
22
|
isDefault: boolean;
|
|
23
23
|
}
|
|
24
24
|
interface SubscribePageConfig {
|
|
@@ -43,6 +43,13 @@ interface SubscribePageConfig {
|
|
|
43
43
|
* - "system": OS/HTML 다크모드 설정 자동 적용
|
|
44
44
|
*/
|
|
45
45
|
theme?: "light" | "dark" | "system";
|
|
46
|
+
/**
|
|
47
|
+
* 포트원 상점 ID (공개값, 제품의 NEXT_PUBLIC_PORTONE_STORE_ID).
|
|
48
|
+
* 카드 등록을 포트원 결제창으로 진행하는 데 필요하다 — 없으면 카드 추가가 막힌다.
|
|
49
|
+
*/
|
|
50
|
+
portOneStoreId?: string;
|
|
51
|
+
/** 포트원 채널 키 (공개값, NEXT_PUBLIC_PORTONE_CHANNEL_KEY). */
|
|
52
|
+
portOneChannelKey?: string;
|
|
46
53
|
}
|
|
47
54
|
interface Subscription {
|
|
48
55
|
id: string;
|
|
@@ -93,8 +100,8 @@ interface SubscriptionManagementConfig {
|
|
|
93
100
|
theme?: "light" | "dark" | "system";
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
declare function SubscribePage({ plansApiPath, cardsApiPath, payApiPath, callbackUrl, serviceName, primaryColor, showExpiredBanner, theme, }: SubscribePageConfig):
|
|
103
|
+
declare function SubscribePage({ plansApiPath, cardsApiPath, payApiPath, callbackUrl, serviceName, primaryColor, showExpiredBanner, theme, portOneStoreId, portOneChannelKey, }: SubscribePageConfig): react.JSX.Element;
|
|
97
104
|
|
|
98
|
-
declare function SubscriptionManagement({ subscriptionsApiPath, paymentsApiPath, cardsApiPath, primaryColor, theme, }: SubscriptionManagementConfig):
|
|
105
|
+
declare function SubscriptionManagement({ subscriptionsApiPath, paymentsApiPath, cardsApiPath, primaryColor, theme, }: SubscriptionManagementConfig): react.JSX.Element;
|
|
99
106
|
|
|
100
107
|
export { type Card, type Payment, type Plan, SubscribePage, type SubscribePageConfig, type Subscription, SubscriptionManagement, type SubscriptionManagementConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react from 'react';
|
|
2
2
|
|
|
3
3
|
interface Plan {
|
|
4
4
|
id: string;
|
|
@@ -13,12 +13,12 @@ interface Plan {
|
|
|
13
13
|
}
|
|
14
14
|
interface Card {
|
|
15
15
|
id: string;
|
|
16
|
-
maskedCardNumber: string;
|
|
17
|
-
cardBrand: string;
|
|
18
|
-
cardType?: string;
|
|
19
|
-
cardHolderName
|
|
20
|
-
expiryMonth
|
|
21
|
-
expiryYear
|
|
16
|
+
maskedCardNumber: string | null;
|
|
17
|
+
cardBrand: string | null;
|
|
18
|
+
cardType?: string | null;
|
|
19
|
+
cardHolderName?: string | null;
|
|
20
|
+
expiryMonth?: string | null;
|
|
21
|
+
expiryYear?: string | null;
|
|
22
22
|
isDefault: boolean;
|
|
23
23
|
}
|
|
24
24
|
interface SubscribePageConfig {
|
|
@@ -43,6 +43,13 @@ interface SubscribePageConfig {
|
|
|
43
43
|
* - "system": OS/HTML 다크모드 설정 자동 적용
|
|
44
44
|
*/
|
|
45
45
|
theme?: "light" | "dark" | "system";
|
|
46
|
+
/**
|
|
47
|
+
* 포트원 상점 ID (공개값, 제품의 NEXT_PUBLIC_PORTONE_STORE_ID).
|
|
48
|
+
* 카드 등록을 포트원 결제창으로 진행하는 데 필요하다 — 없으면 카드 추가가 막힌다.
|
|
49
|
+
*/
|
|
50
|
+
portOneStoreId?: string;
|
|
51
|
+
/** 포트원 채널 키 (공개값, NEXT_PUBLIC_PORTONE_CHANNEL_KEY). */
|
|
52
|
+
portOneChannelKey?: string;
|
|
46
53
|
}
|
|
47
54
|
interface Subscription {
|
|
48
55
|
id: string;
|
|
@@ -93,8 +100,8 @@ interface SubscriptionManagementConfig {
|
|
|
93
100
|
theme?: "light" | "dark" | "system";
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
declare function SubscribePage({ plansApiPath, cardsApiPath, payApiPath, callbackUrl, serviceName, primaryColor, showExpiredBanner, theme, }: SubscribePageConfig):
|
|
103
|
+
declare function SubscribePage({ plansApiPath, cardsApiPath, payApiPath, callbackUrl, serviceName, primaryColor, showExpiredBanner, theme, portOneStoreId, portOneChannelKey, }: SubscribePageConfig): react.JSX.Element;
|
|
97
104
|
|
|
98
|
-
declare function SubscriptionManagement({ subscriptionsApiPath, paymentsApiPath, cardsApiPath, primaryColor, theme, }: SubscriptionManagementConfig):
|
|
105
|
+
declare function SubscriptionManagement({ subscriptionsApiPath, paymentsApiPath, cardsApiPath, primaryColor, theme, }: SubscriptionManagementConfig): react.JSX.Element;
|
|
99
106
|
|
|
100
107
|
export { type Card, type Payment, type Plan, SubscribePage, type SubscribePageConfig, type Subscription, SubscriptionManagement, type SubscriptionManagementConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
"use client";
|
|
3
|
+
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
9
|
var __export = (target, all) => {
|
|
8
10
|
for (var name in all)
|
|
@@ -16,6 +18,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
18
|
}
|
|
17
19
|
return to;
|
|
18
20
|
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
19
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
30
|
|
|
21
31
|
// src/index.ts
|
|
@@ -28,6 +38,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
38
|
|
|
29
39
|
// src/components/SubscribePage.tsx
|
|
30
40
|
var import_react = require("react");
|
|
41
|
+
var import_v2 = __toESM(require("@portone/browser-sdk/v2"));
|
|
31
42
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
32
43
|
var DEFAULT_PRIMARY = "#60A5FA";
|
|
33
44
|
function SubscribePage({
|
|
@@ -38,7 +49,9 @@ function SubscribePage({
|
|
|
38
49
|
serviceName = "\uC11C\uBE44\uC2A4",
|
|
39
50
|
primaryColor = DEFAULT_PRIMARY,
|
|
40
51
|
showExpiredBanner = true,
|
|
41
|
-
theme = "system"
|
|
52
|
+
theme = "system",
|
|
53
|
+
portOneStoreId,
|
|
54
|
+
portOneChannelKey
|
|
42
55
|
}) {
|
|
43
56
|
const [step, setStep] = (0, import_react.useState)("plan");
|
|
44
57
|
const [plans, setPlans] = (0, import_react.useState)([]);
|
|
@@ -49,15 +62,6 @@ function SubscribePage({
|
|
|
49
62
|
const [submitting, setSubmitting] = (0, import_react.useState)(false);
|
|
50
63
|
const [error, setError] = (0, import_react.useState)("");
|
|
51
64
|
const [success, setSuccess] = (0, import_react.useState)(false);
|
|
52
|
-
const [showCardForm, setShowCardForm] = (0, import_react.useState)(false);
|
|
53
|
-
const [cardForm, setCardForm] = (0, import_react.useState)({
|
|
54
|
-
cardNumber: "",
|
|
55
|
-
expiryDate: "",
|
|
56
|
-
cvc: "",
|
|
57
|
-
cardPassword: "",
|
|
58
|
-
cardHolderName: "",
|
|
59
|
-
birthNumber: ""
|
|
60
|
-
});
|
|
61
65
|
const [cardError, setCardError] = (0, import_react.useState)("");
|
|
62
66
|
const [cardSubmitting, setCardSubmitting] = (0, import_react.useState)(false);
|
|
63
67
|
(0, import_react.useEffect)(() => {
|
|
@@ -90,12 +94,25 @@ function SubscribePage({
|
|
|
90
94
|
}
|
|
91
95
|
async function handleRegisterCard() {
|
|
92
96
|
setCardError("");
|
|
97
|
+
if (!portOneStoreId || !portOneChannelKey) {
|
|
98
|
+
setCardError("\uACB0\uC81C \uC124\uC815\uC774 \uB204\uB77D\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
93
101
|
setCardSubmitting(true);
|
|
94
102
|
try {
|
|
103
|
+
const issue = await import_v2.default.requestIssueBillingKey({
|
|
104
|
+
storeId: portOneStoreId,
|
|
105
|
+
channelKey: portOneChannelKey,
|
|
106
|
+
billingKeyMethod: "CARD"
|
|
107
|
+
});
|
|
108
|
+
if (!issue || issue.code !== void 0) {
|
|
109
|
+
setCardError(issue?.message || "\uCE74\uB4DC \uB4F1\uB85D\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
95
112
|
const res = await fetch(cardsApiPath, {
|
|
96
113
|
method: "POST",
|
|
97
114
|
headers: { "Content-Type": "application/json" },
|
|
98
|
-
body: JSON.stringify(
|
|
115
|
+
body: JSON.stringify({ billingKey: issue.billingKey })
|
|
99
116
|
});
|
|
100
117
|
const data = await res.json();
|
|
101
118
|
if (!data.success) {
|
|
@@ -104,8 +121,6 @@ function SubscribePage({
|
|
|
104
121
|
}
|
|
105
122
|
await fetchCards();
|
|
106
123
|
setSelectedCard(data.card);
|
|
107
|
-
setShowCardForm(false);
|
|
108
|
-
setCardForm({ cardNumber: "", expiryDate: "", cvc: "", cardPassword: "", cardHolderName: "", birthNumber: "" });
|
|
109
124
|
} catch {
|
|
110
125
|
setCardError("\uCE74\uB4DC \uB4F1\uB85D \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
|
|
111
126
|
} finally {
|
|
@@ -252,7 +267,7 @@ function SubscribePage({
|
|
|
252
267
|
] }),
|
|
253
268
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
|
|
254
269
|
card.isDefault && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "px-2 py-0.5 bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400 text-xs rounded-full border border-blue-200 dark:border-blue-500/30", children: "\uAE30\uBCF8" }),
|
|
255
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-gray-400 dark:text-gray-500 text-xs", children: [
|
|
270
|
+
card.expiryMonth && card.expiryYear && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-gray-400 dark:text-gray-500 text-xs", children: [
|
|
256
271
|
card.expiryMonth,
|
|
257
272
|
"/",
|
|
258
273
|
card.expiryYear
|
|
@@ -262,60 +277,16 @@ function SubscribePage({
|
|
|
262
277
|
},
|
|
263
278
|
card.id
|
|
264
279
|
)) }),
|
|
265
|
-
|
|
280
|
+
cardError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mb-3 p-3 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 rounded-lg text-red-600 dark:text-red-400 text-xs", children: cardError }),
|
|
281
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
266
282
|
"button",
|
|
267
283
|
{
|
|
268
|
-
onClick:
|
|
269
|
-
|
|
270
|
-
|
|
284
|
+
onClick: handleRegisterCard,
|
|
285
|
+
disabled: cardSubmitting,
|
|
286
|
+
className: "w-full p-4 rounded-xl border border-dashed border-gray-300 dark:border-white/20 text-gray-400 dark:text-gray-500 hover:border-gray-400 dark:hover:border-white/40 hover:text-gray-500 dark:hover:text-gray-300 disabled:opacity-50 transition-all text-sm",
|
|
287
|
+
children: cardSubmitting ? "\uCE74\uB4DC \uB4F1\uB85D \uCC3D\uC744 \uC5EC\uB294 \uC911..." : "+ \uC0C8 \uCE74\uB4DC \uCD94\uAC00"
|
|
271
288
|
}
|
|
272
|
-
)
|
|
273
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-gray-900 dark:text-white text-sm font-medium", children: "\uCE74\uB4DC \uC815\uBCF4 \uC785\uB825" }),
|
|
274
|
-
cardError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "p-3 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 rounded-lg text-red-600 dark:text-red-400 text-xs", children: cardError }),
|
|
275
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: [
|
|
276
|
-
{ label: "\uCE74\uB4DC \uBC88\uD638", key: "cardNumber", placeholder: "0000-0000-0000-0000", type: "text", colSpan: true },
|
|
277
|
-
{ label: "\uB9CC\uB8CC\uC77C", key: "expiryDate", placeholder: "MM/YY", type: "text", colSpan: false },
|
|
278
|
-
{ label: "CVC", key: "cvc", placeholder: "3\uC790\uB9AC", type: "password", colSpan: false },
|
|
279
|
-
{ label: "\uCE74\uB4DC \uBE44\uBC00\uBC88\uD638 \uC55E 2\uC790\uB9AC", key: "cardPassword", placeholder: "\u2022\u2022", type: "password", colSpan: false },
|
|
280
|
-
{ label: "\uCE74\uB4DC \uC18C\uC720\uC790\uBA85", key: "cardHolderName", placeholder: "\uD64D\uAE38\uB3D9", type: "text", colSpan: false },
|
|
281
|
-
{ label: "\uC0DD\uB144\uC6D4\uC77C 6\uC790\uB9AC / \uC0AC\uC5C5\uC790\uBC88\uD638 10\uC790\uB9AC", key: "birthNumber", placeholder: "990101", type: "text", colSpan: true }
|
|
282
|
-
].map(({ label, key, placeholder, type, colSpan }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: colSpan ? "sm:col-span-2" : "", children: [
|
|
283
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "text-xs text-gray-500 dark:text-gray-400 block mb-1", children: label }),
|
|
284
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
285
|
-
"input",
|
|
286
|
-
{
|
|
287
|
-
type,
|
|
288
|
-
placeholder,
|
|
289
|
-
value: cardForm[key],
|
|
290
|
-
onChange: (e) => setCardForm((f) => ({ ...f, [key]: e.target.value })),
|
|
291
|
-
className: "w-full bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-3 py-2 text-gray-900 dark:text-white text-sm placeholder-gray-300 dark:placeholder-gray-600 focus:outline-none focus:border-blue-500 dark:focus:border-blue-500 transition-colors"
|
|
292
|
-
}
|
|
293
|
-
)
|
|
294
|
-
] }, key)) }),
|
|
295
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex gap-2 pt-1", children: [
|
|
296
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
297
|
-
"button",
|
|
298
|
-
{
|
|
299
|
-
onClick: () => {
|
|
300
|
-
setShowCardForm(false);
|
|
301
|
-
setCardError("");
|
|
302
|
-
},
|
|
303
|
-
className: "flex-1 py-2.5 rounded-lg border border-gray-200 dark:border-white/10 text-gray-500 dark:text-gray-400 text-sm hover:border-gray-300 dark:hover:border-white/20 transition-all",
|
|
304
|
-
children: "\uCDE8\uC18C"
|
|
305
|
-
}
|
|
306
|
-
),
|
|
307
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
308
|
-
"button",
|
|
309
|
-
{
|
|
310
|
-
onClick: handleRegisterCard,
|
|
311
|
-
disabled: cardSubmitting,
|
|
312
|
-
style: { backgroundColor: primaryColor },
|
|
313
|
-
className: "flex-1 py-2.5 rounded-lg text-white text-sm font-medium hover:opacity-90 disabled:opacity-50 transition-all",
|
|
314
|
-
children: cardSubmitting ? "\uB4F1\uB85D \uC911..." : "\uCE74\uB4DC \uB4F1\uB85D"
|
|
315
|
-
}
|
|
316
|
-
)
|
|
317
|
-
] })
|
|
318
|
-
] }),
|
|
289
|
+
),
|
|
319
290
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex gap-3 mt-6", children: [
|
|
320
291
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
321
292
|
"button",
|
|
@@ -545,7 +516,10 @@ function SubscriptionManagement({
|
|
|
545
516
|
const res = await fetch(cardsApiPath, {
|
|
546
517
|
method: "POST",
|
|
547
518
|
headers: { "Content-Type": "application/json" },
|
|
548
|
-
body: JSON.stringify(
|
|
519
|
+
body: JSON.stringify({
|
|
520
|
+
...cardForm,
|
|
521
|
+
cardNumber: cardForm.cardNumber.replace(/\s/g, "")
|
|
522
|
+
})
|
|
549
523
|
});
|
|
550
524
|
const data = await res.json();
|
|
551
525
|
if (!data.success) {
|
|
@@ -688,7 +662,22 @@ function SubscriptionManagement({
|
|
|
688
662
|
type,
|
|
689
663
|
placeholder,
|
|
690
664
|
value: cardForm[key],
|
|
691
|
-
onChange: (e) =>
|
|
665
|
+
onChange: (e) => {
|
|
666
|
+
const val = e.target.value;
|
|
667
|
+
if (key === "cardNumber") {
|
|
668
|
+
const formatted = val.replace(/\D/g, "").replace(/(\d{4})(?=\d)/g, "$1 ").slice(0, 19);
|
|
669
|
+
setCardForm((f) => ({ ...f, [key]: formatted }));
|
|
670
|
+
} else if (key === "expiryDate") {
|
|
671
|
+
const digits = val.replace(/\D/g, "");
|
|
672
|
+
let formatted = digits;
|
|
673
|
+
if (digits.length > 2) {
|
|
674
|
+
formatted = `${digits.slice(0, 2)}/${digits.slice(2, 4)}`;
|
|
675
|
+
}
|
|
676
|
+
setCardForm((f) => ({ ...f, [key]: formatted.slice(0, 5) }));
|
|
677
|
+
} else {
|
|
678
|
+
setCardForm((f) => ({ ...f, [key]: val }));
|
|
679
|
+
}
|
|
680
|
+
},
|
|
692
681
|
className: "w-full bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-3 py-2 text-gray-900 dark:text-white text-sm placeholder-gray-300 dark:placeholder-gray-600 focus:outline-none focus:border-blue-500 transition-colors"
|
|
693
682
|
}
|
|
694
683
|
)
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/components/SubscribePage.tsx
|
|
4
4
|
import { useState, useEffect } from "react";
|
|
5
|
+
import PortOne from "@portone/browser-sdk/v2";
|
|
5
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
7
|
var DEFAULT_PRIMARY = "#60A5FA";
|
|
7
8
|
function SubscribePage({
|
|
@@ -12,7 +13,9 @@ function SubscribePage({
|
|
|
12
13
|
serviceName = "\uC11C\uBE44\uC2A4",
|
|
13
14
|
primaryColor = DEFAULT_PRIMARY,
|
|
14
15
|
showExpiredBanner = true,
|
|
15
|
-
theme = "system"
|
|
16
|
+
theme = "system",
|
|
17
|
+
portOneStoreId,
|
|
18
|
+
portOneChannelKey
|
|
16
19
|
}) {
|
|
17
20
|
const [step, setStep] = useState("plan");
|
|
18
21
|
const [plans, setPlans] = useState([]);
|
|
@@ -23,15 +26,6 @@ function SubscribePage({
|
|
|
23
26
|
const [submitting, setSubmitting] = useState(false);
|
|
24
27
|
const [error, setError] = useState("");
|
|
25
28
|
const [success, setSuccess] = useState(false);
|
|
26
|
-
const [showCardForm, setShowCardForm] = useState(false);
|
|
27
|
-
const [cardForm, setCardForm] = useState({
|
|
28
|
-
cardNumber: "",
|
|
29
|
-
expiryDate: "",
|
|
30
|
-
cvc: "",
|
|
31
|
-
cardPassword: "",
|
|
32
|
-
cardHolderName: "",
|
|
33
|
-
birthNumber: ""
|
|
34
|
-
});
|
|
35
29
|
const [cardError, setCardError] = useState("");
|
|
36
30
|
const [cardSubmitting, setCardSubmitting] = useState(false);
|
|
37
31
|
useEffect(() => {
|
|
@@ -64,12 +58,25 @@ function SubscribePage({
|
|
|
64
58
|
}
|
|
65
59
|
async function handleRegisterCard() {
|
|
66
60
|
setCardError("");
|
|
61
|
+
if (!portOneStoreId || !portOneChannelKey) {
|
|
62
|
+
setCardError("\uACB0\uC81C \uC124\uC815\uC774 \uB204\uB77D\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
67
65
|
setCardSubmitting(true);
|
|
68
66
|
try {
|
|
67
|
+
const issue = await PortOne.requestIssueBillingKey({
|
|
68
|
+
storeId: portOneStoreId,
|
|
69
|
+
channelKey: portOneChannelKey,
|
|
70
|
+
billingKeyMethod: "CARD"
|
|
71
|
+
});
|
|
72
|
+
if (!issue || issue.code !== void 0) {
|
|
73
|
+
setCardError(issue?.message || "\uCE74\uB4DC \uB4F1\uB85D\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
69
76
|
const res = await fetch(cardsApiPath, {
|
|
70
77
|
method: "POST",
|
|
71
78
|
headers: { "Content-Type": "application/json" },
|
|
72
|
-
body: JSON.stringify(
|
|
79
|
+
body: JSON.stringify({ billingKey: issue.billingKey })
|
|
73
80
|
});
|
|
74
81
|
const data = await res.json();
|
|
75
82
|
if (!data.success) {
|
|
@@ -78,8 +85,6 @@ function SubscribePage({
|
|
|
78
85
|
}
|
|
79
86
|
await fetchCards();
|
|
80
87
|
setSelectedCard(data.card);
|
|
81
|
-
setShowCardForm(false);
|
|
82
|
-
setCardForm({ cardNumber: "", expiryDate: "", cvc: "", cardPassword: "", cardHolderName: "", birthNumber: "" });
|
|
83
88
|
} catch {
|
|
84
89
|
setCardError("\uCE74\uB4DC \uB4F1\uB85D \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
|
|
85
90
|
} finally {
|
|
@@ -226,7 +231,7 @@ function SubscribePage({
|
|
|
226
231
|
] }),
|
|
227
232
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
|
|
228
233
|
card.isDefault && /* @__PURE__ */ jsx("span", { className: "px-2 py-0.5 bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400 text-xs rounded-full border border-blue-200 dark:border-blue-500/30", children: "\uAE30\uBCF8" }),
|
|
229
|
-
/* @__PURE__ */ jsxs("span", { className: "text-gray-400 dark:text-gray-500 text-xs", children: [
|
|
234
|
+
card.expiryMonth && card.expiryYear && /* @__PURE__ */ jsxs("span", { className: "text-gray-400 dark:text-gray-500 text-xs", children: [
|
|
230
235
|
card.expiryMonth,
|
|
231
236
|
"/",
|
|
232
237
|
card.expiryYear
|
|
@@ -236,60 +241,16 @@ function SubscribePage({
|
|
|
236
241
|
},
|
|
237
242
|
card.id
|
|
238
243
|
)) }),
|
|
239
|
-
|
|
244
|
+
cardError && /* @__PURE__ */ jsx("div", { className: "mb-3 p-3 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 rounded-lg text-red-600 dark:text-red-400 text-xs", children: cardError }),
|
|
245
|
+
/* @__PURE__ */ jsx(
|
|
240
246
|
"button",
|
|
241
247
|
{
|
|
242
|
-
onClick:
|
|
243
|
-
|
|
244
|
-
|
|
248
|
+
onClick: handleRegisterCard,
|
|
249
|
+
disabled: cardSubmitting,
|
|
250
|
+
className: "w-full p-4 rounded-xl border border-dashed border-gray-300 dark:border-white/20 text-gray-400 dark:text-gray-500 hover:border-gray-400 dark:hover:border-white/40 hover:text-gray-500 dark:hover:text-gray-300 disabled:opacity-50 transition-all text-sm",
|
|
251
|
+
children: cardSubmitting ? "\uCE74\uB4DC \uB4F1\uB85D \uCC3D\uC744 \uC5EC\uB294 \uC911..." : "+ \uC0C8 \uCE74\uB4DC \uCD94\uAC00"
|
|
245
252
|
}
|
|
246
|
-
)
|
|
247
|
-
/* @__PURE__ */ jsx("h3", { className: "text-gray-900 dark:text-white text-sm font-medium", children: "\uCE74\uB4DC \uC815\uBCF4 \uC785\uB825" }),
|
|
248
|
-
cardError && /* @__PURE__ */ jsx("div", { className: "p-3 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 rounded-lg text-red-600 dark:text-red-400 text-xs", children: cardError }),
|
|
249
|
-
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: [
|
|
250
|
-
{ label: "\uCE74\uB4DC \uBC88\uD638", key: "cardNumber", placeholder: "0000-0000-0000-0000", type: "text", colSpan: true },
|
|
251
|
-
{ label: "\uB9CC\uB8CC\uC77C", key: "expiryDate", placeholder: "MM/YY", type: "text", colSpan: false },
|
|
252
|
-
{ label: "CVC", key: "cvc", placeholder: "3\uC790\uB9AC", type: "password", colSpan: false },
|
|
253
|
-
{ label: "\uCE74\uB4DC \uBE44\uBC00\uBC88\uD638 \uC55E 2\uC790\uB9AC", key: "cardPassword", placeholder: "\u2022\u2022", type: "password", colSpan: false },
|
|
254
|
-
{ label: "\uCE74\uB4DC \uC18C\uC720\uC790\uBA85", key: "cardHolderName", placeholder: "\uD64D\uAE38\uB3D9", type: "text", colSpan: false },
|
|
255
|
-
{ label: "\uC0DD\uB144\uC6D4\uC77C 6\uC790\uB9AC / \uC0AC\uC5C5\uC790\uBC88\uD638 10\uC790\uB9AC", key: "birthNumber", placeholder: "990101", type: "text", colSpan: true }
|
|
256
|
-
].map(({ label, key, placeholder, type, colSpan }) => /* @__PURE__ */ jsxs("div", { className: colSpan ? "sm:col-span-2" : "", children: [
|
|
257
|
-
/* @__PURE__ */ jsx("label", { className: "text-xs text-gray-500 dark:text-gray-400 block mb-1", children: label }),
|
|
258
|
-
/* @__PURE__ */ jsx(
|
|
259
|
-
"input",
|
|
260
|
-
{
|
|
261
|
-
type,
|
|
262
|
-
placeholder,
|
|
263
|
-
value: cardForm[key],
|
|
264
|
-
onChange: (e) => setCardForm((f) => ({ ...f, [key]: e.target.value })),
|
|
265
|
-
className: "w-full bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-3 py-2 text-gray-900 dark:text-white text-sm placeholder-gray-300 dark:placeholder-gray-600 focus:outline-none focus:border-blue-500 dark:focus:border-blue-500 transition-colors"
|
|
266
|
-
}
|
|
267
|
-
)
|
|
268
|
-
] }, key)) }),
|
|
269
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
|
|
270
|
-
/* @__PURE__ */ jsx(
|
|
271
|
-
"button",
|
|
272
|
-
{
|
|
273
|
-
onClick: () => {
|
|
274
|
-
setShowCardForm(false);
|
|
275
|
-
setCardError("");
|
|
276
|
-
},
|
|
277
|
-
className: "flex-1 py-2.5 rounded-lg border border-gray-200 dark:border-white/10 text-gray-500 dark:text-gray-400 text-sm hover:border-gray-300 dark:hover:border-white/20 transition-all",
|
|
278
|
-
children: "\uCDE8\uC18C"
|
|
279
|
-
}
|
|
280
|
-
),
|
|
281
|
-
/* @__PURE__ */ jsx(
|
|
282
|
-
"button",
|
|
283
|
-
{
|
|
284
|
-
onClick: handleRegisterCard,
|
|
285
|
-
disabled: cardSubmitting,
|
|
286
|
-
style: { backgroundColor: primaryColor },
|
|
287
|
-
className: "flex-1 py-2.5 rounded-lg text-white text-sm font-medium hover:opacity-90 disabled:opacity-50 transition-all",
|
|
288
|
-
children: cardSubmitting ? "\uB4F1\uB85D \uC911..." : "\uCE74\uB4DC \uB4F1\uB85D"
|
|
289
|
-
}
|
|
290
|
-
)
|
|
291
|
-
] })
|
|
292
|
-
] }),
|
|
253
|
+
),
|
|
293
254
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-3 mt-6", children: [
|
|
294
255
|
/* @__PURE__ */ jsx(
|
|
295
256
|
"button",
|
|
@@ -519,7 +480,10 @@ function SubscriptionManagement({
|
|
|
519
480
|
const res = await fetch(cardsApiPath, {
|
|
520
481
|
method: "POST",
|
|
521
482
|
headers: { "Content-Type": "application/json" },
|
|
522
|
-
body: JSON.stringify(
|
|
483
|
+
body: JSON.stringify({
|
|
484
|
+
...cardForm,
|
|
485
|
+
cardNumber: cardForm.cardNumber.replace(/\s/g, "")
|
|
486
|
+
})
|
|
523
487
|
});
|
|
524
488
|
const data = await res.json();
|
|
525
489
|
if (!data.success) {
|
|
@@ -662,7 +626,22 @@ function SubscriptionManagement({
|
|
|
662
626
|
type,
|
|
663
627
|
placeholder,
|
|
664
628
|
value: cardForm[key],
|
|
665
|
-
onChange: (e) =>
|
|
629
|
+
onChange: (e) => {
|
|
630
|
+
const val = e.target.value;
|
|
631
|
+
if (key === "cardNumber") {
|
|
632
|
+
const formatted = val.replace(/\D/g, "").replace(/(\d{4})(?=\d)/g, "$1 ").slice(0, 19);
|
|
633
|
+
setCardForm((f) => ({ ...f, [key]: formatted }));
|
|
634
|
+
} else if (key === "expiryDate") {
|
|
635
|
+
const digits = val.replace(/\D/g, "");
|
|
636
|
+
let formatted = digits;
|
|
637
|
+
if (digits.length > 2) {
|
|
638
|
+
formatted = `${digits.slice(0, 2)}/${digits.slice(2, 4)}`;
|
|
639
|
+
}
|
|
640
|
+
setCardForm((f) => ({ ...f, [key]: formatted.slice(0, 5) }));
|
|
641
|
+
} else {
|
|
642
|
+
setCardForm((f) => ({ ...f, [key]: val }));
|
|
643
|
+
}
|
|
644
|
+
},
|
|
666
645
|
className: "w-full bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg px-3 py-2 text-gray-900 dark:text-white text-sm placeholder-gray-300 dark:placeholder-gray-600 focus:outline-none focus:border-blue-500 transition-colors"
|
|
667
646
|
}
|
|
668
647
|
)
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.0
|
|
6
|
+
"version": "1.1.0",
|
|
7
7
|
"description": "Subscription UI components for ThinkingCat services",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"module": "dist/index.mjs",
|
|
@@ -20,7 +20,12 @@
|
|
|
20
20
|
"dev": "tsup src/index.ts --format cjs,esm --dts --external react --external react-dom --external next --watch",
|
|
21
21
|
"prepublishOnly": "npm run build"
|
|
22
22
|
},
|
|
23
|
-
"keywords": [
|
|
23
|
+
"keywords": [
|
|
24
|
+
"subscription",
|
|
25
|
+
"ui",
|
|
26
|
+
"react",
|
|
27
|
+
"thinkingcat"
|
|
28
|
+
],
|
|
24
29
|
"author": "ThinkingCat",
|
|
25
30
|
"license": "MIT",
|
|
26
31
|
"peerDependencies": {
|
|
@@ -40,5 +45,8 @@
|
|
|
40
45
|
"files": [
|
|
41
46
|
"dist",
|
|
42
47
|
"README.md"
|
|
43
|
-
]
|
|
48
|
+
],
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@portone/browser-sdk": "^0.0.19"
|
|
51
|
+
}
|
|
44
52
|
}
|