react-native-bootpay-api 13.13.42 → 13.13.46
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 +37 -0
- package/lib/Bootpay.js +4 -4
- package/lib/BootpayCommerce.js +14 -0
- package/lib/BootpayWidget.js +103 -0
- package/lib/CommerceTypes.js +1 -0
- package/lib/UserInfo.js +1 -1
- package/lib/WidgetTypes.js +1 -0
- package/lib/bootpayAnalytics.js +1 -1
- package/lib/index.js +1 -1
- package/package.json +40 -41
- package/react-native.config.js +1 -59
- package/src/Bootpay.tsx +20 -6
- package/src/BootpayCommerce.tsx +429 -0
- package/src/BootpayTypes.ts +19 -7
- package/src/BootpayWidget.tsx +1095 -0
- package/src/CommerceTypes.ts +123 -0
- package/src/UserInfo.ts +4 -1
- package/src/WidgetTypes.ts +110 -0
- package/src/__tests__/index.test.d.ts +1 -1
- package/src/__tests__/index.test.js +1 -1
- package/src/index.tsx +132 -2
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import React, { Component } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
SafeAreaView,
|
|
4
|
+
Modal,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
Image,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import WebView, {
|
|
10
|
+
WebViewMessageEvent,
|
|
11
|
+
WebViewNavigation,
|
|
12
|
+
ShouldStartLoadRequest,
|
|
13
|
+
} from 'react-native-webview-bootpay';
|
|
14
|
+
import {
|
|
15
|
+
BootpayCommerceProps,
|
|
16
|
+
CommerceEventData,
|
|
17
|
+
CommercePayload,
|
|
18
|
+
} from './CommerceTypes';
|
|
19
|
+
import { debounce } from 'lodash';
|
|
20
|
+
|
|
21
|
+
const COMMERCE_URL = 'https://webview.bootpay.co.kr/commerce/1.0.5/index.html';
|
|
22
|
+
const DEBUG_MODE = false;
|
|
23
|
+
|
|
24
|
+
export class BootpayCommerce extends Component<BootpayCommerceProps> {
|
|
25
|
+
webView: React.RefObject<WebView>;
|
|
26
|
+
payload?: CommercePayload;
|
|
27
|
+
isScriptInjected: boolean = false;
|
|
28
|
+
|
|
29
|
+
constructor(props: BootpayCommerceProps) {
|
|
30
|
+
super(props);
|
|
31
|
+
this.webView = React.createRef();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
state = {
|
|
35
|
+
visibility: false,
|
|
36
|
+
script: '',
|
|
37
|
+
firstLoad: false,
|
|
38
|
+
showCloseButton: true,
|
|
39
|
+
isShowProgress: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
dismiss = () => {
|
|
43
|
+
this.setState({ visibility: false });
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
showProgressBar = (isShow: boolean) => {
|
|
47
|
+
this.setState({ isShowProgress: isShow });
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
closeDismiss = () => {
|
|
51
|
+
// iOS SDK와 동일하게 destroy 호출
|
|
52
|
+
this.callJavaScript('BootpayCommerce.destroy();');
|
|
53
|
+
if (this.props.onClose) this.props.onClose();
|
|
54
|
+
this.dismiss();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
callJavaScript = (script: string) => {
|
|
58
|
+
this.webView.current?.injectJavaScript(
|
|
59
|
+
`setTimeout(function() { ${script} }, 30);`
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
getEnvironmentMode = () => {
|
|
64
|
+
// iOS SDK와 동일: development 모드에서만 setEnvironmentMode 호출
|
|
65
|
+
return DEBUG_MODE ? "BootpayCommerce.setEnvironmentMode('development');" : '';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
generateCommerceScript = (payload: CommercePayload) => {
|
|
69
|
+
const scripts: string[] = [];
|
|
70
|
+
|
|
71
|
+
// 개발 환경에서만 로그 레벨 설정
|
|
72
|
+
if (DEBUG_MODE) {
|
|
73
|
+
scripts.push('BootpayCommerce.setLogLevel(1);');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// close 이벤트 리스너
|
|
77
|
+
scripts.push(
|
|
78
|
+
"document.addEventListener('bootpayclose', function(e) { " +
|
|
79
|
+
"window.BootpayRNWebView.postMessage('close'); " +
|
|
80
|
+
'});'
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// 결제 요청 호출
|
|
84
|
+
scripts.push('BootpayCommerce.requestCheckout(');
|
|
85
|
+
scripts.push(payload.toJSONString());
|
|
86
|
+
scripts.push(')');
|
|
87
|
+
scripts.push('.then(function(res) {');
|
|
88
|
+
scripts.push(' window.BootpayRNWebView.postMessage(JSON.stringify(res));');
|
|
89
|
+
scripts.push('}).catch(function(err) {');
|
|
90
|
+
scripts.push(
|
|
91
|
+
" window.BootpayRNWebView.postMessage(JSON.stringify({event: 'error', data: err}));"
|
|
92
|
+
);
|
|
93
|
+
scripts.push('});');
|
|
94
|
+
|
|
95
|
+
return scripts.join('');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
getMountJavascript = () => {
|
|
99
|
+
// iOS SDK와 동일: Commerce에서는 setDevice 호출하지 않음
|
|
100
|
+
return `
|
|
101
|
+
${this.getEnvironmentMode()}
|
|
102
|
+
`;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
componentWillUnmount() {
|
|
106
|
+
this.setState({
|
|
107
|
+
visibility: false,
|
|
108
|
+
firstLoad: false,
|
|
109
|
+
showCloseButton: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
componentDidMount() {
|
|
114
|
+
this.closeDismiss = debounce(this.closeDismiss, 30);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
removePaymentWindow = () => {
|
|
118
|
+
this.dismiss();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
requestCheckout = (payload: CommercePayload) => {
|
|
122
|
+
this.payload = payload;
|
|
123
|
+
this.isScriptInjected = false;
|
|
124
|
+
|
|
125
|
+
this.setState({
|
|
126
|
+
visibility: true,
|
|
127
|
+
script: `
|
|
128
|
+
${this.getMountJavascript()}
|
|
129
|
+
${this.generateCommerceScript(payload)}
|
|
130
|
+
`,
|
|
131
|
+
firstLoad: false,
|
|
132
|
+
showCloseButton: true,
|
|
133
|
+
spinner: false,
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// iOS SDK와 동일하게 페이지 로드 완료 후 JavaScript 주입
|
|
138
|
+
onLoadEnd = (event: { nativeEvent: WebViewNavigation }) => {
|
|
139
|
+
const url = event.nativeEvent.url;
|
|
140
|
+
if (DEBUG_MODE) {
|
|
141
|
+
console.log('[BootpayCommerce] onLoadEnd URL:', url);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// payload가 없으면 스크립트 주입하지 않음
|
|
145
|
+
if (!this.payload) {
|
|
146
|
+
if (DEBUG_MODE) {
|
|
147
|
+
console.log('[BootpayCommerce] No payload, skipping script injection');
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Commerce 페이지 로드 완료 시에만 JavaScript 주입 (iOS SDK didFinish와 동일)
|
|
153
|
+
if (url.includes('webview.bootpay.co.kr/commerce') && !this.isScriptInjected) {
|
|
154
|
+
this.isScriptInjected = true;
|
|
155
|
+
|
|
156
|
+
if (DEBUG_MODE) {
|
|
157
|
+
console.log('[BootpayCommerce] Injecting JavaScript...');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// iOS SDK와 동일한 순서로 JavaScript 주입
|
|
161
|
+
// 1. 환경 설정
|
|
162
|
+
// 2. 디바이스 설정
|
|
163
|
+
// 3. 결제 요청
|
|
164
|
+
const fullScript = `
|
|
165
|
+
try {
|
|
166
|
+
${this.getMountJavascript()}
|
|
167
|
+
${this.generateCommerceScript(this.payload)}
|
|
168
|
+
} catch(e) {
|
|
169
|
+
console.error('[BootpayCommerce] Script error:', e);
|
|
170
|
+
window.BootpayRNWebView.postMessage(JSON.stringify({event: 'error', message: e.message}));
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
|
|
174
|
+
this.callJavaScript(fullScript);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// iOS SDK decidePolicyFor와 동일: redirect URL 처리
|
|
179
|
+
onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest): boolean => {
|
|
180
|
+
const url = request.url;
|
|
181
|
+
|
|
182
|
+
if (DEBUG_MODE) {
|
|
183
|
+
console.log('[BootpayCommerce] onShouldStartLoadWithRequest:', url);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// redirect_url 콜백 처리 (api.bootpay.co.kr/v2/callback) - iOS SDK와 동일
|
|
187
|
+
if (url.includes('api.bootpay.co.kr/v2/callback')) {
|
|
188
|
+
if (DEBUG_MODE) {
|
|
189
|
+
console.log('[BootpayCommerce] Callback URL detected, handling...');
|
|
190
|
+
}
|
|
191
|
+
this.handleCallbackURL(url);
|
|
192
|
+
return false; // 네비게이션 취소
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// about:blank 허용
|
|
196
|
+
if (url.startsWith('about:blank')) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// http/https가 아닌 스키마는 앱 실행 시도 (외부 앱 호출)
|
|
201
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
202
|
+
if (DEBUG_MODE) {
|
|
203
|
+
console.log('[BootpayCommerce] Non-http scheme, should open external app:', url);
|
|
204
|
+
}
|
|
205
|
+
// React Native에서는 Linking을 사용하여 외부 앱 열기
|
|
206
|
+
// 여기서는 일단 네비게이션만 취소
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return true;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// URL query string 파싱 헬퍼 (React Native에서 URL.searchParams 미지원)
|
|
214
|
+
parseQueryString = (urlString: string): Record<string, string> => {
|
|
215
|
+
const result: Record<string, string> = {};
|
|
216
|
+
const queryIndex = urlString.indexOf('?');
|
|
217
|
+
if (queryIndex === -1) return result;
|
|
218
|
+
|
|
219
|
+
const queryString = urlString.substring(queryIndex + 1);
|
|
220
|
+
const pairs = queryString.split('&');
|
|
221
|
+
|
|
222
|
+
for (const pair of pairs) {
|
|
223
|
+
const eqIndex = pair.indexOf('=');
|
|
224
|
+
if (eqIndex > 0) {
|
|
225
|
+
const key = pair.substring(0, eqIndex);
|
|
226
|
+
const value = pair.substring(eqIndex + 1);
|
|
227
|
+
try {
|
|
228
|
+
result[key] = decodeURIComponent(value);
|
|
229
|
+
} catch {
|
|
230
|
+
result[key] = value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return result;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// iOS SDK handleCallbackURL과 동일: URL query parameters 파싱 및 콜백 호출
|
|
239
|
+
handleCallbackURL = (urlString: string) => {
|
|
240
|
+
try {
|
|
241
|
+
const params = this.parseQueryString(urlString);
|
|
242
|
+
const data: Record<string, unknown> = {};
|
|
243
|
+
|
|
244
|
+
// Query parameters 변환
|
|
245
|
+
for (const [key, value] of Object.entries(params)) {
|
|
246
|
+
// metadata는 JSON 문자열이므로 파싱 시도
|
|
247
|
+
if (key === 'metadata') {
|
|
248
|
+
try {
|
|
249
|
+
data[key] = JSON.parse(value);
|
|
250
|
+
} catch {
|
|
251
|
+
data[key] = value;
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
data[key] = value;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (DEBUG_MODE) {
|
|
259
|
+
console.log('[BootpayCommerce] Callback data:', data);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// event에 따라 콜백 호출 - iOS SDK와 동일
|
|
263
|
+
const event = (data.event as string) || '';
|
|
264
|
+
|
|
265
|
+
this.showProgressBar(false);
|
|
266
|
+
|
|
267
|
+
switch (event) {
|
|
268
|
+
case 'done':
|
|
269
|
+
if (this.props.onDone) {
|
|
270
|
+
this.props.onDone(data as CommerceEventData);
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
case 'issued':
|
|
274
|
+
// 가상계좌 발급 완료 - iOS SDK와 동일
|
|
275
|
+
if (this.props.onIssued) {
|
|
276
|
+
this.props.onIssued(data as CommerceEventData);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
case 'cancel':
|
|
280
|
+
if (this.props.onCancel) {
|
|
281
|
+
this.props.onCancel(data as CommerceEventData);
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
case 'error':
|
|
285
|
+
if (this.props.onError) {
|
|
286
|
+
this.props.onError(data as CommerceEventData);
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
default:
|
|
290
|
+
// event가 없으면 receipt_id로 판단 - iOS SDK와 동일
|
|
291
|
+
if (data.receipt_id) {
|
|
292
|
+
if (this.props.onDone) {
|
|
293
|
+
this.props.onDone(data as CommerceEventData);
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
if (this.props.onCancel) {
|
|
297
|
+
this.props.onCancel(data as CommerceEventData);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.closeDismiss();
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('[BootpayCommerce] Error parsing callback URL:', error);
|
|
306
|
+
if (this.props.onError) {
|
|
307
|
+
this.props.onError({
|
|
308
|
+
event: 'error',
|
|
309
|
+
message: 'Failed to parse callback URL',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
this.closeDismiss();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
onMessage = async (event: WebViewMessageEvent) => {
|
|
317
|
+
if (!event) return;
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const messageData = event.nativeEvent.data;
|
|
321
|
+
|
|
322
|
+
// close 이벤트 처리
|
|
323
|
+
if (messageData === 'close') {
|
|
324
|
+
this.showProgressBar(false);
|
|
325
|
+
this.closeDismiss();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const data =
|
|
330
|
+
typeof messageData === 'string' ? JSON.parse(messageData) : messageData;
|
|
331
|
+
|
|
332
|
+
const handleEvent = (
|
|
333
|
+
_eventName: string,
|
|
334
|
+
callback: ((data: CommerceEventData) => void) | undefined
|
|
335
|
+
) => {
|
|
336
|
+
this.showProgressBar(false);
|
|
337
|
+
if (callback) callback(data as CommerceEventData);
|
|
338
|
+
this.closeDismiss();
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// 이벤트별 처리
|
|
342
|
+
switch (data.event) {
|
|
343
|
+
case 'cancel':
|
|
344
|
+
handleEvent('cancel', this.props.onCancel);
|
|
345
|
+
break;
|
|
346
|
+
case 'error':
|
|
347
|
+
handleEvent('error', this.props.onError);
|
|
348
|
+
break;
|
|
349
|
+
case 'done':
|
|
350
|
+
handleEvent('done', this.props.onDone);
|
|
351
|
+
break;
|
|
352
|
+
case 'issued':
|
|
353
|
+
// 가상계좌 발급 완료 - iOS SDK와 동일
|
|
354
|
+
handleEvent('issued', this.props.onIssued);
|
|
355
|
+
break;
|
|
356
|
+
case 'close':
|
|
357
|
+
this.showProgressBar(false);
|
|
358
|
+
this.closeDismiss();
|
|
359
|
+
break;
|
|
360
|
+
default:
|
|
361
|
+
// receipt_id가 있으면 done 이벤트로 처리
|
|
362
|
+
if (data.receipt_id) {
|
|
363
|
+
handleEvent('done', this.props.onDone);
|
|
364
|
+
} else {
|
|
365
|
+
console.warn(`Unknown commerce event type: ${data.event}`);
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error('Error processing commerce message:', error);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
render() {
|
|
375
|
+
return (
|
|
376
|
+
<Modal
|
|
377
|
+
animationType="slide"
|
|
378
|
+
transparent={false}
|
|
379
|
+
visible={this.state.visibility}
|
|
380
|
+
onRequestClose={this.closeDismiss}
|
|
381
|
+
>
|
|
382
|
+
<SafeAreaView style={{ flex: 1 }}>
|
|
383
|
+
{this.state.showCloseButton && (
|
|
384
|
+
<TouchableOpacity onPress={this.closeDismiss}>
|
|
385
|
+
<Image
|
|
386
|
+
style={styles.overlay}
|
|
387
|
+
source={require('../images/close.png')}
|
|
388
|
+
/>
|
|
389
|
+
</TouchableOpacity>
|
|
390
|
+
)}
|
|
391
|
+
<WebView
|
|
392
|
+
ref={this.webView}
|
|
393
|
+
originWhitelist={['*']}
|
|
394
|
+
source={{ uri: COMMERCE_URL }}
|
|
395
|
+
javaScriptEnabled
|
|
396
|
+
javaScriptCanOpenWindowsAutomatically
|
|
397
|
+
useSharedProcessPool={true}
|
|
398
|
+
sharedCookiesEnabled={true}
|
|
399
|
+
onMessage={this.onMessage}
|
|
400
|
+
onLoadEnd={this.onLoadEnd}
|
|
401
|
+
onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
|
|
402
|
+
onError={(syntheticEvent) => {
|
|
403
|
+
const { nativeEvent } = syntheticEvent;
|
|
404
|
+
if (nativeEvent.code === 3) {
|
|
405
|
+
this.showProgressBar(false);
|
|
406
|
+
if (this.props.onError)
|
|
407
|
+
this.props.onError({
|
|
408
|
+
event: 'error',
|
|
409
|
+
code: nativeEvent.code,
|
|
410
|
+
message: nativeEvent.description,
|
|
411
|
+
});
|
|
412
|
+
this.closeDismiss();
|
|
413
|
+
}
|
|
414
|
+
}}
|
|
415
|
+
/>
|
|
416
|
+
</SafeAreaView>
|
|
417
|
+
</Modal>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const styles = StyleSheet.create({
|
|
423
|
+
overlay: {
|
|
424
|
+
width: 25,
|
|
425
|
+
height: 25,
|
|
426
|
+
right: 5,
|
|
427
|
+
alignSelf: 'flex-end',
|
|
428
|
+
},
|
|
429
|
+
});
|
package/src/BootpayTypes.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { ViewProps } from 'react-native';
|
|
2
2
|
|
|
3
|
+
export interface BootpayEventData {
|
|
4
|
+
event: string;
|
|
5
|
+
receipt_id?: string;
|
|
6
|
+
order_id?: string;
|
|
7
|
+
price?: number;
|
|
8
|
+
method?: string;
|
|
9
|
+
pg?: string;
|
|
10
|
+
message?: string;
|
|
11
|
+
code?: number | string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export interface BootpayTypesProps extends ViewProps {
|
|
4
|
-
ref?:
|
|
16
|
+
ref?: React.RefObject<unknown>;
|
|
5
17
|
ios_application_id?: string;
|
|
6
18
|
android_application_id?: string;
|
|
7
|
-
onCancel?: (data:
|
|
8
|
-
onError?: (data:
|
|
9
|
-
onIssued?: (data:
|
|
10
|
-
onConfirm?: (data:
|
|
11
|
-
onDone?: (data:
|
|
19
|
+
onCancel?: (data: BootpayEventData) => void;
|
|
20
|
+
onError?: (data: BootpayEventData) => void;
|
|
21
|
+
onIssued?: (data: BootpayEventData) => void;
|
|
22
|
+
onConfirm?: (data: BootpayEventData) => boolean;
|
|
23
|
+
onDone?: (data: BootpayEventData) => void;
|
|
12
24
|
onClose?: () => void;
|
|
13
25
|
}
|
|
14
26
|
|
|
@@ -110,7 +122,7 @@ export class Payload {
|
|
|
110
122
|
order_id?: string;
|
|
111
123
|
subscription_id?: string;
|
|
112
124
|
authentication_id?: string;
|
|
113
|
-
metadata?:
|
|
125
|
+
metadata?: Record<string, unknown>;
|
|
114
126
|
user_token?: string;
|
|
115
127
|
extra?: Extra;
|
|
116
128
|
user?: User;
|