// // CanPgPaymentView.swift // SodaLive // // Created by klaus on 2023/08/11. // import SwiftUI import Bootpay import BootpayUI struct CanPgPaymentView: View { @EnvironmentObject var viewModel: CanPgPaymentViewModel let canResponse: GetCanResponse let refresh: () -> Void let afterCompletionToGoBack: Bool @State private var showExitConfirm: Bool = false init(canResponse: GetCanResponse, refresh: @escaping () -> Void, afterCompletionToGoBack: Bool) { self.canResponse = canResponse self.refresh = refresh self.afterCompletionToGoBack = afterCompletionToGoBack } var body: some View { ZStack { Color.black.ignoresSafeArea() if viewModel.isShowPaymentView { BootpayUI(payload: viewModel.payload, requestType: BootpayRequest.TYPE_PAYMENT) .onConfirm { DEBUG_LOG("onConfirm: \($0)") return true } .onCancel { DEBUG_LOG("onCancel: \($0)") } .onError { DEBUG_LOG("onError: \($0)") viewModel.isShowPaymentView = false viewModel.errorMessage = "결제 중 오류가 발생했습니다." viewModel.isShowPopup = true } .onDone { DEBUG_LOG("onDone: \($0)") viewModel.verifyPayment($0) { let can = UserDefaults.int(forKey: .can) UserDefaults.set(can + canResponse.can + canResponse.rewardCan, forKey: .can) self.refresh() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { if afterCompletionToGoBack { AppState.shared.back() AppState.shared.back() } else { AppState.shared.setAppStep(step: .canStatus(refresh: refresh)) } } } } .onClose { DEBUG_LOG("onClose") viewModel.isShowPaymentView = false } } else if viewModel.isShowPayversePaymentView { ZStack(alignment: .topLeading) { PayverseWebView(startPayloadJson: viewModel.payversePayloadJson) .ignoresSafeArea(edges: .bottom) HStack(spacing: 8) { Button(action: { showExitConfirm = true }) { Text("닫기") .font(.custom(Font.bold.rawValue, size: 14)) .foregroundColor(.white) .padding(.horizontal, 12) .padding(.vertical, 8) .background(Color.black.opacity(0.6)) .clipShape(Capsule()) } .padding(.leading, 16) .padding(.top, 12) Spacer() } } .background(Color.black.ignoresSafeArea()) .alert("결제를 종료할까요?", isPresented: $showExitConfirm) { Button("계속", role: .cancel) { } Button("종료", role: .destructive) { DEBUG_LOG("Payverse: user requested to exit") viewModel.isShowPayversePaymentView = false // 필요 시 상위로 복귀 AppState.shared.back() } } message: { Text("진행 중인 결제를 중단하고 이전 화면으로 돌아갑니다.") } } else { GeometryReader { proxy in VStack(spacing: 0) { DetailNavigationBar(title: "결제하기") ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { HStack(spacing: 0) { Image("ic_can") .resizable() .scaledToFill() .frame(width: 26.7, height: 26.7, alignment: .top) Text(canResponse.title) .font(.custom(Font.bold.rawValue, size: 15.3)) .foregroundColor(Color(hex: "eeeeee")) .padding(.leading, 13.3) Spacer() Text("\(canResponse.price) 원") .font(.custom(Font.bold.rawValue, size: 15.3)) .foregroundColor(Color.grayee) } .padding(.horizontal, 13.3) .padding(.vertical, 23.3) .background(Color.gray22) .cornerRadius(16.7) .padding(.horizontal, 13.3) .frame(width: screenSize().width) .padding(.top, 13.3) Text("결제 수단 선택") .font(.custom(Font.bold.rawValue, size: 16.7)) .foregroundColor(Color.grayee) .frame(width: screenSize().width - 26.7, alignment: .leading) .padding(.top, 26.7) HStack(spacing: 16.7) { Text("통합 결제") .font(.custom( viewModel.paymentMethod == .unified ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7)) .foregroundColor(viewModel.paymentMethod == .unified ? Color.button : Color.grayee) .frame(maxWidth: .infinity) .padding(.vertical, 16.7) .background( viewModel.paymentMethod == .unified ? Color.button.opacity(0.3) : Color.gray23 ) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(lineWidth: 1) .foregroundColor(viewModel.paymentMethod == .unified ? Color.button : Color.gray77) ) .onTapGesture { if viewModel.paymentMethod != .unified { viewModel.paymentMethod = .unified } } Image("ic_kakaopay") .foregroundColor(viewModel.paymentMethod == .kakaopay ? Color.button : Color.grayee) .frame(maxWidth: .infinity) .padding(.vertical, 8.3) .background( viewModel.paymentMethod == .kakaopay ? Color.button.opacity(0.3) : Color.gray23 ) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(lineWidth: 1) .foregroundColor(viewModel.paymentMethod == .kakaopay ? Color.button : Color.gray77) ) .onTapGesture { if viewModel.paymentMethod != .kakaopay { viewModel.paymentMethod = .kakaopay } } Text("휴대폰 결제") .font(.custom( viewModel.paymentMethod == .phone ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7)) .foregroundColor(viewModel.paymentMethod == .phone ? Color.button : Color.grayee) .frame(maxWidth: .infinity) .padding(.vertical, 16.7) .background( viewModel.paymentMethod == .phone ? Color.button.opacity(0.3) : Color.gray23 ) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(lineWidth: 1) .foregroundColor(viewModel.paymentMethod == .phone ? Color.button : Color.gray77) ) .onTapGesture { if viewModel.paymentMethod != .phone { viewModel.paymentMethod = .phone } } } .frame(width: screenSize().width - 26.7) .padding(.top, 16.7) HStack(spacing: 6.7) { Image(viewModel.isTermsAgree ? "btn_select_checked" : "btn_select_normal") .resizable() .frame(width: 20, height: 20) Text("구매조건 확인 및 결제 진행 동의") .font(.custom(Font.medium.rawValue, size: 14.7)) .foregroundColor(Color.grayee) } .frame(width: screenSize().width - 53.4, alignment: .leading) .padding(.top, 16.7) .onTapGesture { viewModel.isTermsAgree.toggle() } VStack(spacing: 6.7) { HStack(alignment: .top, spacing: 0) { Text("- ") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.gray77) Text("충전된 캔의 유효기간은 충전 후 5년 입니다.") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.gray77) .fixedSize(horizontal: false, vertical: true) } .frame(width: screenSize().width - 53.4, alignment: .leading) .padding(.top, 26.7) HStack(alignment: .top, spacing: 0) { Text("- ") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.gray77) Text("결제 취소는 결제 후 7일 이내에만 할 수 있습니다.\n단, 캔의 일부를 사용하면 결제 취소를 할 수 없습니다.") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.gray77) .fixedSize(horizontal: false, vertical: true) } .frame(width: screenSize().width - 53.4, alignment: .leading) HStack(alignment: .top, spacing: 0) { Text("- ") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.gray77) Text("광고성 이벤트 등 회사가 무료로 지급한 포인트는 환불되지 않습니다.") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.gray77) .fixedSize(horizontal: false, vertical: true) } .frame(width: screenSize().width - 53.4, alignment: .leading) HStack(alignment: .top, spacing: 0) { Text("- ") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.gray77) Text("자세한 내용은 보이스온 이용약관에서 확인할 수 있습니다.") .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color.gray77) .fixedSize(horizontal: false, vertical: true) } .frame(width: screenSize().width - 53.4, alignment: .leading) } } } Spacer() HStack(spacing: 0) { VStack(alignment: .leading, spacing: 5) { Text("결제금액") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color.grayee) HStack(spacing: 0) { Text("\(canResponse.price) 원") .font(.custom(Font.bold.rawValue, size: 23.3)) .foregroundColor(Color.grayee) } } Spacer() Text("결제하기") .font(.custom(Font.bold.rawValue, size: 18.3)) .foregroundColor(.white) .padding(.vertical, 16) .frame(minWidth: 200) .background(Color.button) .cornerRadius(10) .onTapGesture { if viewModel.paymentMethod == nil { viewModel.errorMessage = "결제수단을 선택해 주세요." viewModel.isShowPopup = true } else if !viewModel.isTermsAgree { viewModel.errorMessage = "결제진행에 동의하셔야 결제가 가능합니다." viewModel.isShowPopup = true } else { if viewModel.paymentMethod == .unified { viewModel.payverseChargeCan(canId: canResponse.id) } else { viewModel.chargeCan(canId: canResponse.id) { viewModel.payload.orderName = canResponse.title viewModel.payload.price = Double(canResponse.price) viewModel.payload.taxFree = 0 viewModel.isShowPaymentView = true } } } } } .padding(.leading, 22) .padding(.trailing, 13.3) .padding(.vertical, 13.3) .background(Color.gray22) .cornerRadius(16.7, corners: [.topLeft, .topRight]) if proxy.safeAreaInsets.bottom > 0 { Rectangle() .foregroundColor(Color.gray22) .frame(width: proxy.size.width, height: 15.3) } } .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { GeometryReader { geo in HStack { Spacer() Text(viewModel.errorMessage) .padding(.vertical, 13.3) .padding(.horizontal, 6.7) .frame(width: geo.size.width - 66.7, alignment: .center) .font(.custom(Font.medium.rawValue, size: 12)) .background(Color.button) .foregroundColor(Color.white) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) .cornerRadius(20) .padding(.top, 66.7) Spacer() } } } .edgesIgnoringSafeArea(.bottom) } } if viewModel.isLoading { LoadingView() } } .onAppear { viewModel.canResponse = canResponse viewModel.refresh = refresh viewModel.afterCompletionToGoBack = afterCompletionToGoBack } .onDisappear { viewModel.canResponse = nil viewModel.refresh = nil viewModel.afterCompletionToGoBack = nil viewModel.paymentMethod = nil viewModel.isTermsAgree = false viewModel.isShowPaymentView = false viewModel.isShowPayversePaymentView = false } } } struct CanPgPaymentView_Previews: PreviewProvider { static var previews: some View { CanPgPaymentView( canResponse: GetCanResponse(id: 1, title: "300 캔", can: 300, rewardCan: 0, price: 7500), refresh: {}, afterCompletionToGoBack: false ) } }