248 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			248 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
//
 | 
						|
//  PayverseWebView.swift
 | 
						|
//  SodaLive
 | 
						|
//
 | 
						|
//  Created by klaus on 9/29/25.
 | 
						|
//
 | 
						|
 | 
						|
import SwiftUI
 | 
						|
import WebKit
 | 
						|
 | 
						|
struct PayverseWebView: UIViewRepresentable {
 | 
						|
    let startPayloadJson: String
 | 
						|
    
 | 
						|
    func makeUIView(context: Context) -> WKWebView {
 | 
						|
        // WKWebView 구성: JavaScript 허용 및 팝업 허용
 | 
						|
        let config = WKWebViewConfiguration()
 | 
						|
        config.defaultWebpagePreferences.allowsContentJavaScript = true
 | 
						|
        config.preferences.javaScriptCanOpenWindowsAutomatically = true
 | 
						|
        config.websiteDataStore = .default()
 | 
						|
        
 | 
						|
        let webView = WKWebView(frame: .zero, configuration: config)
 | 
						|
        webView.navigationDelegate = context.coordinator
 | 
						|
        
 | 
						|
        // 빈 화면을 검은 화면으로 보기 위해 다크 모드 강제 적용
 | 
						|
        if #available(iOS 13.0, *) {
 | 
						|
            webView.overrideUserInterfaceStyle = .dark
 | 
						|
        }
 | 
						|
        webView.isOpaque = false
 | 
						|
        webView.backgroundColor = .black
 | 
						|
        webView.scrollView.backgroundColor = .black
 | 
						|
        webView.allowsBackForwardNavigationGestures = true
 | 
						|
        webView.scrollView.contentInsetAdjustmentBehavior = .never
 | 
						|
        
 | 
						|
        // 번들 리소스 로딩 (PAYVERSE_HTML_RESOURCE.html)
 | 
						|
        if let url = Bundle.main.url(forResource: PAYVERSE_HTML_RESOURCE,
 | 
						|
                                     withExtension: "html") {
 | 
						|
            // 로컬 파일 접근 권한을 위해 디렉터리 권한 부여
 | 
						|
            webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
 | 
						|
            DEBUG_LOG("[DEBUG_LOG] Loading local Payverse HTML: \(url.lastPathComponent)")
 | 
						|
        } else {
 | 
						|
            DEBUG_LOG("[ERROR_LOG] Payverse HTML resource not found: \(PAYVERSE_HTML_RESOURCE).html")
 | 
						|
        }
 | 
						|
        
 | 
						|
        return webView
 | 
						|
    }
 | 
						|
    
 | 
						|
    func updateUIView(_ uiView: WKWebView, context: Context) {}
 | 
						|
    
 | 
						|
    func makeCoordinator() -> Coordinator {
 | 
						|
        Coordinator(startPayloadJson: startPayloadJson)
 | 
						|
    }
 | 
						|
    
 | 
						|
    final class Coordinator: NSObject, WKNavigationDelegate {
 | 
						|
        private let startPayloadJson: String
 | 
						|
        init(startPayloadJson: String) {
 | 
						|
            self.startPayloadJson = startPayloadJson
 | 
						|
        }
 | 
						|
        func webView(_ webView: WKWebView,
 | 
						|
                     decidePolicyFor navigationAction: WKNavigationAction,
 | 
						|
                     decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
 | 
						|
            guard let url = navigationAction.request.url else {
 | 
						|
                decisionHandler(.allow); return
 | 
						|
            }
 | 
						|
            
 | 
						|
            updateBlindViewIfNaverLogin(webView, url.absoluteString)
 | 
						|
            
 | 
						|
            // 커스텀 스킴: myapp://payverse/result?...  → 앱으로 전환
 | 
						|
            if url.scheme?.lowercased() == APPSCHEME.lowercased() {
 | 
						|
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
 | 
						|
                decisionHandler(.cancel)
 | 
						|
            } else if(url.scheme?.lowercased() == "file") {
 | 
						|
                // 로컬 리소스(file://)는 WebView에서 로드 허용
 | 
						|
                decisionHandler(.allow)
 | 
						|
            } else if(url.absoluteString.starts(with: "about:blank")) {
 | 
						|
                decisionHandler(.allow)
 | 
						|
            } else if(isItunesURL(url.absoluteString)) {
 | 
						|
                startAppToApp(url)
 | 
						|
                decisionHandler(.cancel)
 | 
						|
            } else if(!url.absoluteString.starts(with: "http")) {
 | 
						|
                // http/https 이외의 스킴은 외부 앱/App Store 처리
 | 
						|
                startAppToApp(url)
 | 
						|
                decisionHandler(.cancel)
 | 
						|
            } else {
 | 
						|
                decisionHandler(.allow)
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        // 리소스 로드 성공 여부 확인 + 결제 시작 호출
 | 
						|
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
 | 
						|
            webView.evaluateJavaScript("document.readyState") { value, error in
 | 
						|
                if let state = value as? String {
 | 
						|
                    DEBUG_LOG("[DEBUG_LOG] WebView readyState: \(state)")
 | 
						|
                }
 | 
						|
                if let error = error {
 | 
						|
                    DEBUG_LOG("[ERROR_LOG] readyState eval error: \(error.localizedDescription)")
 | 
						|
                }
 | 
						|
            }
 | 
						|
            // startPay 가 노출되어 있는지 확인 로그
 | 
						|
            webView.evaluateJavaScript("typeof startPay === 'function'") { value, _ in
 | 
						|
                if let ok = value as? Bool {
 | 
						|
                    DEBUG_LOG("[DEBUG_LOG] startPay function available: \(ok)")
 | 
						|
                }
 | 
						|
            }
 | 
						|
            // JSON 문자열을 JS 문자열 리터럴에 안전하게 담기 위해 이스케이프 처리
 | 
						|
            func esc(_ s: String) -> String {
 | 
						|
                var r = s.replacingOccurrences(of: "\\", with: "\\\\")
 | 
						|
                r = r.replacingOccurrences(of: "'", with: "\\'")
 | 
						|
                r = r.replacingOccurrences(of: "\n", with: "\\n")
 | 
						|
                r = r.replacingOccurrences(of: "\r", with: "")
 | 
						|
                r = r.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
 | 
						|
                r = r.replacingOccurrences(of: "\u{2029}", with: "\\u2029")
 | 
						|
                r = r.replacingOccurrences(of: "</", with: "<\\/")
 | 
						|
                return r
 | 
						|
            }
 | 
						|
            let escaped = esc(self.startPayloadJson)
 | 
						|
            let js = """
 | 
						|
            (function(){
 | 
						|
                try {
 | 
						|
                    if (typeof startPay === 'function') {
 | 
						|
                        startPay('\(escaped)');
 | 
						|
                        console.log('iOS: startPay invoked');
 | 
						|
                    } else {
 | 
						|
                        setTimeout(function(){
 | 
						|
                            try {
 | 
						|
                                if (typeof startPay === 'function') {
 | 
						|
                                    startPay('\(escaped)');
 | 
						|
                                    console.log('iOS: startPay invoked (retry)');
 | 
						|
                                }
 | 
						|
                            } catch(e) {
 | 
						|
                                console.log('iOS: startPay retry error: ' + (e && e.message ? e.message : e));
 | 
						|
                            }
 | 
						|
                        }, 300);
 | 
						|
                    }
 | 
						|
                } catch(e) {
 | 
						|
                    console.log('iOS: startPay call error: ' + (e && e.message ? e.message : e));
 | 
						|
                }
 | 
						|
            })();
 | 
						|
            """
 | 
						|
            webView.evaluateJavaScript(js) { _, error in
 | 
						|
                if let error = error {
 | 
						|
                    DEBUG_LOG("[ERROR_LOG] startPay invoke error: \(error.localizedDescription)")
 | 
						|
                } else {
 | 
						|
                    DEBUG_LOG("[DEBUG_LOG] startPay invoked from iOS")
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
 | 
						|
            DEBUG_LOG("[ERROR_LOG] WebView didFail: \(error.localizedDescription)")
 | 
						|
        }
 | 
						|
        
 | 
						|
        func updateBlindViewIfNaverLogin(_ webView: WKWebView, _ url: String) {
 | 
						|
            if(url.starts(with: "https://nid.naver.com")) { //show
 | 
						|
                webView.evaluateJavaScript("document.getElementById('back').remove();")
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        func isItunesURL(_ urlString: String) -> Bool {
 | 
						|
            return isMatch(urlString, "\\/\\/itunes\\.apple\\.com\\/")
 | 
						|
        }
 | 
						|
        
 | 
						|
        func isMatch(_ urlString: String, _ pattern: String) -> Bool {
 | 
						|
            let regex = try! NSRegularExpression(pattern: pattern, options: [])
 | 
						|
            let result = regex.matches(in: urlString, options: [], range: NSRange(location: 0, length: urlString.count))
 | 
						|
            return result.count > 0
 | 
						|
        }
 | 
						|
        
 | 
						|
        func startAppToApp(_ url: URL) {
 | 
						|
            UIApplication.shared.open(url, options: [:], completionHandler: { result in
 | 
						|
                if(result == false) {
 | 
						|
                    self.startItunesToInstall(url)
 | 
						|
                }
 | 
						|
            })
 | 
						|
        }
 | 
						|
        
 | 
						|
        func startItunesToInstall(_ url: URL) {
 | 
						|
            let sUrl = url.absoluteString
 | 
						|
            var itunesUrl = ""
 | 
						|
            
 | 
						|
            if(sUrl.starts(with: "kfc-bankpay")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EB%B1%85%ED%81%AC%ED%8E%98%EC%9D%B4-%EA%B8%88%EC%9C%B5%EA%B8%B0%EA%B4%80-%EA%B3%B5%EB%8F%99-%EA%B3%84%EC%A2%8C%EC%9D%B4%EC%B2%B4-%EA%B2%B0%EC%A0%9C-%EC%A0%9C%EB%A1%9C%ED%8E%98%EC%9D%B4/id398456030"
 | 
						|
            } else if(sUrl.starts(with: "ispmobile")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/isp/id369125087"
 | 
						|
            } else if(sUrl.starts(with: "hdcardappcardansimclick") || sUrl.starts(with: "smhyundaiansimclick")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%ED%98%84%EB%8C%80%EC%B9%B4%EB%93%9C/id702653088"
 | 
						|
            } else if(sUrl.starts(with: "shinhan-sr-ansimclick") || sUrl.starts(with: "smshinhanansimclick")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EC%8B%A0%ED%95%9C%ED%8E%98%EC%9D%B4%ED%8C%90/id572462317"
 | 
						|
            } else if(sUrl.starts(with: "kb-acp")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/kb-pay/id695436326"
 | 
						|
            } else if(sUrl.starts(with: "liivbank")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EB%A6%AC%EB%B8%8C/id1126232922"
 | 
						|
            } else if(sUrl.starts(with: "mpocket.online.ansimclick") || sUrl.starts(with: "ansimclickscard") || sUrl.starts(with: "ansimclickipcollect") || sUrl.starts(with: "samsungpay") || sUrl.starts(with: "scardcertiapp")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EC%82%BC%EC%84%B1%EC%B9%B4%EB%93%9C/id535125356"
 | 
						|
            } else if(sUrl.starts(with: "lottesmartpay")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/us/app/%EB%A1%AF%EB%8D%B0%EC%B9%B4%EB%93%9C-%EC%95%B1%EC%B9%B4%EB%93%9C/id688047200"
 | 
						|
            } else if(sUrl.starts(with: "lotteappcard")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EB%94%94%EC%A7%80%EB%A1%9C%EC%B9%B4-%EB%A1%AF%EB%8D%B0%EC%B9%B4%EB%93%9C/id688047200"
 | 
						|
            } else if(sUrl.starts(with: "newsmartpib")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EC%9A%B0%EB%A6%AC-won-%EB%B1%85%ED%82%B9/id1470181651"
 | 
						|
            } else if(sUrl.starts(with: "com.wooricard.wcard")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EC%9A%B0%EB%A6%ACwon%EC%B9%B4%EB%93%9C/id1499598869"
 | 
						|
            } else if(sUrl.starts(with: "citispay") || sUrl.starts(with: "citicardappkr") || sUrl.starts(with: "citimobileapp")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EC%94%A8%ED%8B%B0%EB%AA%A8%EB%B0%94%EC%9D%BC/id1179759666"
 | 
						|
            } else if(sUrl.starts(with: "shinsegaeeasypayment")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/ssgpay/id666237916"
 | 
						|
            } else if(sUrl.starts(with: "cloudpay")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%ED%95%98%EB%82%98%EC%B9%B4%EB%93%9C-%EC%9B%90%ED%81%90%ED%8E%98%EC%9D%B4/id847268987"
 | 
						|
            } else if(sUrl.starts(with: "hanawalletmembers")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/n-wallet/id492190784"
 | 
						|
            } else if(sUrl.starts(with: "nhappvardansimclick")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EC%98%AC%EC%9B%90%ED%8E%98%EC%9D%B4-nh%EC%95%B1%EC%B9%B4%EB%93%9C/id1177889176"
 | 
						|
            } else if(sUrl.starts(with: "nhallonepayansimclick") || sUrl.starts(with: "nhappcardansimclick") || sUrl.starts(with: "nhallonepayansimclick") || sUrl.starts(with: "nonghyupcardansimclick")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EC%98%AC%EC%9B%90%ED%8E%98%EC%9D%B4-nh%EC%95%B1%EC%B9%B4%EB%93%9C/id1177889176"
 | 
						|
            } else if(sUrl.starts(with: "payco")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/payco/id924292102"
 | 
						|
            } else if(sUrl.starts(with: "lpayapp") || sUrl.starts(with: "lmslpay")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/l-point-with-l-pay/id473250588"
 | 
						|
            } else if(sUrl.starts(with: "naversearchapp")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EB%84%A4%EC%9D%B4%EB%B2%84-naver/id393499958"
 | 
						|
            } else if(sUrl.starts(with: "tauthlink")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/pass-by-skt/id1141258007"
 | 
						|
            } else if(sUrl.starts(with: "uplusauth") || sUrl.starts(with: "upluscorporation")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/pass-by-u/id1147394645"
 | 
						|
            } else if(sUrl.starts(with: "ktauthexternalcall")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/pass-by-kt/id1134371550"
 | 
						|
            } else if(sUrl.starts(with: "supertoss")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%ED%86%A0%EC%8A%A4/id839333328"
 | 
						|
            } else if(sUrl.starts(with: "kakaotalk")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/kakaotalk/id362057947"
 | 
						|
            } else if(sUrl.starts(with: "chaipayment")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/%EC%B0%A8%EC%9D%B4/id1459979272"
 | 
						|
            } else if(sUrl.starts(with: "ukbanksmartbanknonloginpay")) {
 | 
						|
                itunesUrl = "https://itunes.apple.com/kr/developer/%EC%BC%80%EC%9D%B4%EB%B1%85%ED%81%AC/id1178872626?mt=8"
 | 
						|
            } else if(sUrl.starts(with: "newliiv")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/us/app/%EB%A6%AC%EB%B8%8C-next/id1573528126"
 | 
						|
            } else if(sUrl.starts(with: "kbbank")) {
 | 
						|
                itunesUrl = "https://apps.apple.com/kr/app/kb%EC%8A%A4%ED%83%80%EB%B1%85%ED%82%B9/id373742138"
 | 
						|
            }
 | 
						|
            
 | 
						|
            if(itunesUrl.count > 0) {
 | 
						|
                if let appstore = URL(string: itunesUrl) {
 | 
						|
                    startAppToApp(appstore)
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |