pay-rehearsal 0.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/LICENSE +21 -0
- package/README.ko.md +308 -0
- package/README.md +307 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +896 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/styles.css +702 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pay Rehearsal contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Pay Rehearsal React
|
|
2
|
+
|
|
3
|
+
PG사 계약과 심사를 기다리는 동안에도 주문부터 결제 완료까지의 화면과 로직을 개발할 수 있는 React/Next.js용 TypeScript 결제 UI SDK입니다.
|
|
4
|
+
|
|
5
|
+
> 이 패키지의 Mock 어댑터는 실제 승인, 청구, 카드정보 수집을 수행하지 않습니다. 운영 환경에서는 반드시 실제 PG 어댑터와 서버 측 결제 검증을 연결해야 합니다.
|
|
6
|
+
|
|
7
|
+
## 제공 기능
|
|
8
|
+
|
|
9
|
+
- 반응형 결제 모달과 접근성 기본 지원
|
|
10
|
+
- 카드, 계좌이체, 가상계좌, 휴대폰 결제 UI
|
|
11
|
+
- KB국민, 신한, 삼성, 현대, 롯데, 하나, 우리, NH농협, BC 카드사 선택
|
|
12
|
+
- 일시불·할부 선택과 카드사 인증 단계 시뮬레이션
|
|
13
|
+
- 주요 은행 선택과 계좌이체 인증 단계 시뮬레이션
|
|
14
|
+
- 가상계좌 발급 정보와 입금 대기(`pending`) 결과
|
|
15
|
+
- 성공, 실패, 취소, 무작위 시나리오
|
|
16
|
+
- Promise 기반 `requestPayment()` API
|
|
17
|
+
- 미리 구성된 `PaymentButton`
|
|
18
|
+
- 실제 PG 구현으로 교체 가능한 `PaymentAdapter` 인터페이스
|
|
19
|
+
- 색상, 모서리, 글꼴 테마 설정
|
|
20
|
+
- React 18/19 및 Next.js App Router 호환
|
|
21
|
+
|
|
22
|
+
## 설치
|
|
23
|
+
|
|
24
|
+
로컬에서 패키지를 빌드한 뒤 애플리케이션에 연결합니다.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install
|
|
28
|
+
npm run build
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
다른 프로젝트에서 로컬 패키지를 설치하는 경우:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install /absolute/path/to/pay-rehearsal
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Next.js App Router에서 사용하기
|
|
38
|
+
|
|
39
|
+
루트 레이아웃에서 스타일을 한 번 가져옵니다.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// app/layout.tsx
|
|
43
|
+
import "pay-rehearsal/styles.css";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
클라이언트 Provider를 만듭니다.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// app/payment-provider.tsx
|
|
50
|
+
"use client";
|
|
51
|
+
|
|
52
|
+
import {
|
|
53
|
+
MockPaymentProvider,
|
|
54
|
+
} from "pay-rehearsal";
|
|
55
|
+
import type { ReactNode } from "react";
|
|
56
|
+
|
|
57
|
+
export function AppPaymentProvider({ children }: { children: ReactNode }) {
|
|
58
|
+
return (
|
|
59
|
+
<MockPaymentProvider
|
|
60
|
+
result="success"
|
|
61
|
+
delayMs={900}
|
|
62
|
+
paymentMethods={["card", "bank-transfer", "virtual-account"]}
|
|
63
|
+
defaultPaymentMethod="card"
|
|
64
|
+
theme={{ accentColor: "#4f46e5", borderRadius: 24 }}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</MockPaymentProvider>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
루트 레이아웃에서 Provider를 연결합니다.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// app/layout.tsx
|
|
76
|
+
import "pay-rehearsal/styles.css";
|
|
77
|
+
import { AppPaymentProvider } from "./payment-provider";
|
|
78
|
+
|
|
79
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
80
|
+
return (
|
|
81
|
+
<html lang="ko">
|
|
82
|
+
<body>
|
|
83
|
+
<AppPaymentProvider>{children}</AppPaymentProvider>
|
|
84
|
+
</body>
|
|
85
|
+
</html>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
결제가 필요한 클라이언트 컴포넌트에서 호출합니다.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
"use client";
|
|
94
|
+
|
|
95
|
+
import { usePayment } from "pay-rehearsal";
|
|
96
|
+
|
|
97
|
+
export function CheckoutButton() {
|
|
98
|
+
const { requestPayment, isOpen } = usePayment();
|
|
99
|
+
|
|
100
|
+
const checkout = async () => {
|
|
101
|
+
const result = await requestPayment({
|
|
102
|
+
orderId: `ORDER-${Date.now()}`,
|
|
103
|
+
orderName: "프로 플랜 1개월",
|
|
104
|
+
amount: 29_000,
|
|
105
|
+
currency: "KRW",
|
|
106
|
+
customer: { email: "developer@example.com" },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (result.status === "success") {
|
|
110
|
+
console.log(result.paymentId, result.testMode);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (result.status === "pending") {
|
|
114
|
+
// 가상계좌는 발급 시점에 결제가 완료되지 않습니다.
|
|
115
|
+
console.log(result.virtualAccount);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<button type="button" disabled={isOpen} onClick={checkout}>
|
|
121
|
+
결제하기
|
|
122
|
+
</button>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
간단한 경우에는 `PaymentButton`을 사용할 수도 있습니다.
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<PaymentButton
|
|
131
|
+
request={{
|
|
132
|
+
orderId: "ORDER-1001",
|
|
133
|
+
orderName: "프로 플랜",
|
|
134
|
+
amount: 29_000,
|
|
135
|
+
}}
|
|
136
|
+
onResult={(result) => console.log(result)}
|
|
137
|
+
>
|
|
138
|
+
29,000원 결제하기
|
|
139
|
+
</PaymentButton>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## 결제수단 노출과 순서 설정
|
|
143
|
+
|
|
144
|
+
Provider의 `paymentMethods`로 결제창에 표시할 수단과 순서를 정할 수 있습니다. 생략하면 카드, 계좌이체, 가상계좌, 휴대폰을 모두 표시합니다.
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<MockPaymentProvider
|
|
148
|
+
paymentMethods={["virtual-account", "card"]}
|
|
149
|
+
defaultPaymentMethod="virtual-account"
|
|
150
|
+
>
|
|
151
|
+
{children}
|
|
152
|
+
</MockPaymentProvider>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
특정 결제에서만 목록을 바꾸려면 `requestPayment`의 두 번째 매개변수로 덮어씁니다.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
requestPayment(order, {
|
|
159
|
+
paymentMethods: ["card", "bank-transfer"],
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
결제수단을 하나로 고정하면 결제수단 선택 UI를 생략하고 해당 수단의 상세 단계부터 시작합니다.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
requestPayment(order, {
|
|
167
|
+
paymentMethod: "card",
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
설정 우선순위는 요청별 `paymentMethod` → 요청별 `paymentMethods` → Provider의 `paymentMethods`이며, 배열의 순서가 화면 표시 순서가 됩니다. 실제 운영에서는 UI 노출 여부와 별개로 서버에서도 계약된 결제수단인지 검증해야 합니다.
|
|
172
|
+
|
|
173
|
+
## 테스트 시나리오
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
<MockPaymentProvider result="success">...</MockPaymentProvider>
|
|
177
|
+
<MockPaymentProvider result="failure">...</MockPaymentProvider>
|
|
178
|
+
<MockPaymentProvider result="cancelled">...</MockPaymentProvider>
|
|
179
|
+
<MockPaymentProvider result="random" randomSuccessRate={0.7}>...</MockPaymentProvider>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
실패 코드와 지연 시간도 지정할 수 있습니다.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
createMockPaymentAdapter({
|
|
186
|
+
result: "failure",
|
|
187
|
+
delayMs: 1_500,
|
|
188
|
+
failureCode: "CARD_DECLINED",
|
|
189
|
+
failureMessage: "카드 승인이 거절되었습니다.",
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
가상계좌의 예금주와 입금기한도 설정할 수 있습니다.
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
<MockPaymentProvider
|
|
197
|
+
result="success"
|
|
198
|
+
virtualAccountHolder="테스트상점"
|
|
199
|
+
virtualAccountDueHours={24}
|
|
200
|
+
>
|
|
201
|
+
{children}
|
|
202
|
+
</MockPaymentProvider>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
한 번의 결제만 다른 결과로 테스트하려면 `requestPayment`의 두 번째 매개변수를 사용합니다. 이 값이 Provider의 기본 `result`보다 우선합니다.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const result = await requestPayment(
|
|
209
|
+
{
|
|
210
|
+
orderId: "ORDER-FAILURE-TEST",
|
|
211
|
+
orderName: "실패 화면 테스트",
|
|
212
|
+
amount: 29_000,
|
|
213
|
+
},
|
|
214
|
+
{ mockResult: "failure" },
|
|
215
|
+
);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## 결제수단별 테스트 흐름
|
|
219
|
+
|
|
220
|
+
- 신용·체크카드: 카드사 선택 → 일시불·할부 선택 → 카드사 인증 → 승인 확인 → `success`
|
|
221
|
+
- 계좌이체: 출금 은행 선택 → 은행 앱·뱅크페이 인증 화면 → 이체 결과 확인 → `success`
|
|
222
|
+
- 가상계좌: 입금 은행 선택 → 계좌 발급 → 계좌번호와 입금기한 안내 → `pending`
|
|
223
|
+
- 휴대폰: 테스트 승인 → `success`
|
|
224
|
+
|
|
225
|
+
실제 가상계좌 결제는 계좌 발급만으로 완료되지 않습니다. 입금 이후 PG사의 웹훅을 서버가 검증한 시점에 주문을 결제 완료로 변경해야 합니다. `pay-rehearsal`은 이 차이를 재현하기 위해 가상계좌 발급 결과를 `pending`으로 반환합니다.
|
|
226
|
+
|
|
227
|
+
## 실제 PG로 교체하기
|
|
228
|
+
|
|
229
|
+
UI와 사용하는 쪽의 코드는 유지하고 `PaymentAdapter` 구현만 교체합니다.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import type { PaymentAdapter } from "pay-rehearsal";
|
|
233
|
+
|
|
234
|
+
export const realPgAdapter: PaymentAdapter = {
|
|
235
|
+
name: "My PG",
|
|
236
|
+
testMode: false,
|
|
237
|
+
|
|
238
|
+
async pay(
|
|
239
|
+
{ request, method, cardIssuer, installmentMonths, bank },
|
|
240
|
+
signal,
|
|
241
|
+
) {
|
|
242
|
+
// 1. PG SDK 결제창 호출
|
|
243
|
+
// 2. 사용자 서비스 서버에 paymentKey/orderId/amount 전달
|
|
244
|
+
// 3. 서버가 PG 승인 API 호출 및 금액 검증
|
|
245
|
+
// 4. 서버에서 검증된 결과만 반환
|
|
246
|
+
const response = await fetch("/api/payments/confirm", {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: { "content-type": "application/json" },
|
|
249
|
+
body: JSON.stringify({
|
|
250
|
+
request,
|
|
251
|
+
method,
|
|
252
|
+
cardIssuer,
|
|
253
|
+
installmentMonths,
|
|
254
|
+
bank,
|
|
255
|
+
}),
|
|
256
|
+
signal,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
return {
|
|
261
|
+
status: "failed",
|
|
262
|
+
orderId: request.orderId,
|
|
263
|
+
amount: request.amount,
|
|
264
|
+
method,
|
|
265
|
+
cardIssuer,
|
|
266
|
+
installmentMonths,
|
|
267
|
+
bank,
|
|
268
|
+
testMode: false,
|
|
269
|
+
code: "PG_CONFIRM_FAILED",
|
|
270
|
+
message: "결제 승인에 실패했습니다.",
|
|
271
|
+
retryable: false,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const data = await response.json();
|
|
276
|
+
return {
|
|
277
|
+
status: "success",
|
|
278
|
+
orderId: request.orderId,
|
|
279
|
+
amount: request.amount,
|
|
280
|
+
method,
|
|
281
|
+
cardIssuer,
|
|
282
|
+
installmentMonths,
|
|
283
|
+
bank,
|
|
284
|
+
testMode: false,
|
|
285
|
+
paymentId: data.paymentId,
|
|
286
|
+
approvedAt: data.approvedAt,
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
운영 전환 시에는 다음 원칙을 지켜야 합니다.
|
|
293
|
+
|
|
294
|
+
- 카드번호, 비밀번호, 주민번호를 이 UI에서 직접 수집하거나 저장하지 않습니다.
|
|
295
|
+
- 주문 금액과 승인 상태는 브라우저 결과가 아니라 서비스 서버에서 검증합니다.
|
|
296
|
+
- `orderId`는 서버가 생성한 유일한 값을 사용합니다.
|
|
297
|
+
- 중복 승인 방지를 위해 서버 승인 API에 멱등성을 적용합니다.
|
|
298
|
+
- 운영 빌드에서 Mock 어댑터가 연결되지 않았는지 확인합니다.
|
|
299
|
+
|
|
300
|
+
## 명령어
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
npm run typecheck
|
|
304
|
+
npm test
|
|
305
|
+
npm run build
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
동작하는 Next.js 예제는 `examples/nextjs`에 있습니다.
|
package/README.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Pay Rehearsal React
|
|
2
|
+
|
|
3
|
+
[한국어](./README.ko.md)
|
|
4
|
+
|
|
5
|
+
A TypeScript payment UI SDK for React and Next.js that lets you build the complete flow from checkout to payment completion while waiting for your payment gateway contract and approval.
|
|
6
|
+
|
|
7
|
+
> The Mock adapter does not perform real authorizations, charges, or card-data collection. In production, always connect a real payment gateway adapter and verify payments on your server.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Responsive payment modal with baseline accessibility support
|
|
12
|
+
- UI flows for cards, bank transfers, virtual accounts, and mobile payments
|
|
13
|
+
- Card issuer selection for KB Kookmin, Shinhan, Samsung, Hyundai, Lotte, Hana, Woori, NH Nonghyup, and BC
|
|
14
|
+
- One-time and installment payment selection with card issuer authentication simulation
|
|
15
|
+
- Major bank selection with bank transfer authentication simulation
|
|
16
|
+
- Virtual account details with a deposit-pending (`pending`) result
|
|
17
|
+
- Success, failure, cancellation, and random test scenarios
|
|
18
|
+
- Promise-based `requestPayment()` API
|
|
19
|
+
- Prebuilt `PaymentButton`
|
|
20
|
+
- A replaceable `PaymentAdapter` interface for real payment gateway integrations
|
|
21
|
+
- Theme configuration for color, border radius, and font family
|
|
22
|
+
- Compatible with React 18/19 and the Next.js App Router
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Install the package from npm:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install pay-rehearsal
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The package injects its styles automatically when imported. You do not need to import a separate CSS file.
|
|
33
|
+
|
|
34
|
+
To install a local checkout in another project, build it first and install its directory:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install
|
|
38
|
+
npm run build
|
|
39
|
+
npm install /absolute/path/to/pay-rehearsal
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Using It with the Next.js App Router
|
|
43
|
+
|
|
44
|
+
Create a client-side provider.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// app/payment-provider.tsx
|
|
48
|
+
"use client";
|
|
49
|
+
|
|
50
|
+
import { MockPaymentProvider } from "pay-rehearsal";
|
|
51
|
+
import type { ReactNode } from "react";
|
|
52
|
+
|
|
53
|
+
export function AppPaymentProvider({ children }: { children: ReactNode }) {
|
|
54
|
+
return (
|
|
55
|
+
<MockPaymentProvider
|
|
56
|
+
result="success"
|
|
57
|
+
delayMs={900}
|
|
58
|
+
paymentMethods={["card", "bank-transfer", "virtual-account"]}
|
|
59
|
+
defaultPaymentMethod="card"
|
|
60
|
+
theme={{ accentColor: "#4f46e5", borderRadius: 24 }}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</MockPaymentProvider>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Add the provider to your root layout.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// app/layout.tsx
|
|
72
|
+
import { AppPaymentProvider } from "./payment-provider";
|
|
73
|
+
|
|
74
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
75
|
+
return (
|
|
76
|
+
<html lang="en">
|
|
77
|
+
<body>
|
|
78
|
+
<AppPaymentProvider>{children}</AppPaymentProvider>
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Call the API from any client component that needs to initiate a payment.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
"use client";
|
|
89
|
+
|
|
90
|
+
import { usePayment } from "pay-rehearsal";
|
|
91
|
+
|
|
92
|
+
export function CheckoutButton() {
|
|
93
|
+
const { requestPayment, isOpen } = usePayment();
|
|
94
|
+
|
|
95
|
+
const checkout = async () => {
|
|
96
|
+
const result = await requestPayment({
|
|
97
|
+
orderId: `ORDER-${Date.now()}`,
|
|
98
|
+
orderName: "Pro Plan — 1 Month",
|
|
99
|
+
amount: 29_000,
|
|
100
|
+
currency: "KRW",
|
|
101
|
+
customer: { email: "developer@example.com" },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (result.status === "success") {
|
|
105
|
+
console.log(result.paymentId, result.testMode);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (result.status === "pending") {
|
|
109
|
+
// Issuing a virtual account does not complete the payment.
|
|
110
|
+
console.log(result.virtualAccount);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<button type="button" disabled={isOpen} onClick={checkout}>
|
|
116
|
+
Pay now
|
|
117
|
+
</button>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For simpler use cases, you can use `PaymentButton`.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<PaymentButton
|
|
126
|
+
request={{
|
|
127
|
+
orderId: "ORDER-1001",
|
|
128
|
+
orderName: "Pro Plan",
|
|
129
|
+
amount: 29_000,
|
|
130
|
+
}}
|
|
131
|
+
onResult={(result) => console.log(result)}
|
|
132
|
+
>
|
|
133
|
+
Pay KRW 29,000
|
|
134
|
+
</PaymentButton>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Configuring Payment Methods and Their Order
|
|
138
|
+
|
|
139
|
+
Use the provider's `paymentMethods` property to choose which methods appear in the modal and in what order. When omitted, cards, bank transfers, virtual accounts, and mobile payments are all displayed.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<MockPaymentProvider
|
|
143
|
+
paymentMethods={["virtual-account", "card"]}
|
|
144
|
+
defaultPaymentMethod="virtual-account"
|
|
145
|
+
>
|
|
146
|
+
{children}
|
|
147
|
+
</MockPaymentProvider>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
To override the list for a single payment, pass options as the second argument to `requestPayment`.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
requestPayment(order, {
|
|
154
|
+
paymentMethods: ["card", "bank-transfer"],
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
To lock a payment to one method, use `paymentMethod`. The method selection UI is skipped and the modal starts at that method's detail step.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
requestPayment(order, {
|
|
162
|
+
paymentMethod: "card",
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Configuration precedence is request-level `paymentMethod` → request-level `paymentMethods` → provider-level `paymentMethods`. Array order determines display order. In production, your server must verify that a selected payment method is enabled under your gateway contract regardless of whether the method appears in the UI.
|
|
167
|
+
|
|
168
|
+
## Test Scenarios
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
<MockPaymentProvider result="success">...</MockPaymentProvider>
|
|
172
|
+
<MockPaymentProvider result="failure">...</MockPaymentProvider>
|
|
173
|
+
<MockPaymentProvider result="cancelled">...</MockPaymentProvider>
|
|
174
|
+
<MockPaymentProvider result="random" randomSuccessRate={0.7}>...</MockPaymentProvider>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
You can also configure the failure code, failure message, and simulated delay.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
createMockPaymentAdapter({
|
|
181
|
+
result: "failure",
|
|
182
|
+
delayMs: 1_500,
|
|
183
|
+
failureCode: "CARD_DECLINED",
|
|
184
|
+
failureMessage: "The card authorization was declined.",
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The account holder and deposit deadline for virtual accounts are configurable as well.
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
<MockPaymentProvider
|
|
192
|
+
result="success"
|
|
193
|
+
virtualAccountHolder="Test Merchant"
|
|
194
|
+
virtualAccountDueHours={24}
|
|
195
|
+
>
|
|
196
|
+
{children}
|
|
197
|
+
</MockPaymentProvider>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
To test a different outcome for a single payment, pass options as the second argument to `requestPayment`. This value takes precedence over the provider's default `result`.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const result = await requestPayment(
|
|
204
|
+
{
|
|
205
|
+
orderId: "ORDER-FAILURE-TEST",
|
|
206
|
+
orderName: "Failure Screen Test",
|
|
207
|
+
amount: 29_000,
|
|
208
|
+
},
|
|
209
|
+
{ mockResult: "failure" },
|
|
210
|
+
);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Test Flow by Payment Method
|
|
214
|
+
|
|
215
|
+
- Credit or debit card: select issuer → select one-time or installment payment → simulate issuer authentication → confirm authorization → `success`
|
|
216
|
+
- Bank transfer: select withdrawal bank → simulate bank app or BankPay authentication → confirm transfer → `success`
|
|
217
|
+
- Virtual account: select deposit bank → issue account → display account number and deposit deadline → `pending`
|
|
218
|
+
- Mobile payment: simulate authorization → `success`
|
|
219
|
+
|
|
220
|
+
A real virtual account payment is not complete when the account is issued. The order should be marked as paid only after your server verifies the payment gateway's webhook following the deposit. `pay-rehearsal` reproduces this distinction by returning `pending` when a virtual account is issued.
|
|
221
|
+
|
|
222
|
+
## Replacing the Mock Adapter with a Real Payment Gateway
|
|
223
|
+
|
|
224
|
+
Keep the UI and consumer code unchanged and replace only the `PaymentAdapter` implementation.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import type { PaymentAdapter } from "pay-rehearsal";
|
|
228
|
+
|
|
229
|
+
export const realPgAdapter: PaymentAdapter = {
|
|
230
|
+
name: "My Payment Gateway",
|
|
231
|
+
testMode: false,
|
|
232
|
+
|
|
233
|
+
async pay(
|
|
234
|
+
{ request, method, cardIssuer, installmentMonths, bank },
|
|
235
|
+
signal,
|
|
236
|
+
) {
|
|
237
|
+
// 1. Open the payment gateway SDK checkout UI.
|
|
238
|
+
// 2. Send paymentKey, orderId, and amount to your application server.
|
|
239
|
+
// 3. Have the server call the gateway's confirmation API and verify the amount.
|
|
240
|
+
// 4. Return only the server-verified result.
|
|
241
|
+
const response = await fetch("/api/payments/confirm", {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers: { "content-type": "application/json" },
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
request,
|
|
246
|
+
method,
|
|
247
|
+
cardIssuer,
|
|
248
|
+
installmentMonths,
|
|
249
|
+
bank,
|
|
250
|
+
}),
|
|
251
|
+
signal,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (!response.ok) {
|
|
255
|
+
return {
|
|
256
|
+
status: "failed",
|
|
257
|
+
orderId: request.orderId,
|
|
258
|
+
amount: request.amount,
|
|
259
|
+
method,
|
|
260
|
+
cardIssuer,
|
|
261
|
+
installmentMonths,
|
|
262
|
+
bank,
|
|
263
|
+
testMode: false,
|
|
264
|
+
code: "PG_CONFIRM_FAILED",
|
|
265
|
+
message: "Payment authorization failed.",
|
|
266
|
+
retryable: false,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const data = await response.json();
|
|
271
|
+
return {
|
|
272
|
+
status: "success",
|
|
273
|
+
orderId: request.orderId,
|
|
274
|
+
amount: request.amount,
|
|
275
|
+
method,
|
|
276
|
+
cardIssuer,
|
|
277
|
+
installmentMonths,
|
|
278
|
+
bank,
|
|
279
|
+
testMode: false,
|
|
280
|
+
paymentId: data.paymentId,
|
|
281
|
+
approvedAt: data.approvedAt,
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Follow these rules before switching to production:
|
|
288
|
+
|
|
289
|
+
- Never collect or store card numbers, passwords, or government-issued identification numbers in this UI.
|
|
290
|
+
- Verify the order amount and authorization status on your application server, not from browser results.
|
|
291
|
+
- Use a unique, server-generated `orderId`.
|
|
292
|
+
- Apply idempotency to your server-side authorization endpoint to prevent duplicate charges.
|
|
293
|
+
- Confirm that the Mock adapter is not connected in production builds.
|
|
294
|
+
|
|
295
|
+
## Commands
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
npm run typecheck
|
|
299
|
+
npm test
|
|
300
|
+
npm run build
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
A working Next.js example is available in `examples/nextjs`.
|
|
304
|
+
|
|
305
|
+
## Disclaimer
|
|
306
|
+
|
|
307
|
+
Pay Rehearsal is not provided, sponsored, or endorsed by any payment gateway, card issuer, or financial institution. Financial institution names are used only to describe simulated payment flows.
|