react-native-bootpay-api 13.13.42 → 13.13.44

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.
@@ -0,0 +1,427 @@
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
+ onMessage={this.onMessage}
398
+ onLoadEnd={this.onLoadEnd}
399
+ onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
400
+ onError={(syntheticEvent) => {
401
+ const { nativeEvent } = syntheticEvent;
402
+ if (nativeEvent.code === 3) {
403
+ this.showProgressBar(false);
404
+ if (this.props.onError)
405
+ this.props.onError({
406
+ event: 'error',
407
+ code: nativeEvent.code,
408
+ message: nativeEvent.description,
409
+ });
410
+ this.closeDismiss();
411
+ }
412
+ }}
413
+ />
414
+ </SafeAreaView>
415
+ </Modal>
416
+ );
417
+ }
418
+ }
419
+
420
+ const styles = StyleSheet.create({
421
+ overlay: {
422
+ width: 25,
423
+ height: 25,
424
+ right: 5,
425
+ alignSelf: 'flex-end',
426
+ },
427
+ });
@@ -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?: any;
16
+ ref?: React.RefObject<unknown>;
5
17
  ios_application_id?: string;
6
18
  android_application_id?: string;
7
- onCancel?: (data: Object) => void;
8
- onError?: (data: Object) => void;
9
- onIssued?: (data: Object) => void;
10
- onConfirm?: (data: Object) => boolean;
11
- onDone?: (data: Object) => void;
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?: Object;
125
+ metadata?: Record<string, unknown>;
114
126
  user_token?: string;
115
127
  extra?: Extra;
116
128
  user?: User;