diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index 1f0e74f..9e32f1c 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -127,4 +127,6 @@ enum AppStep { case seriesAll(creatorId: Int) case seriesContentAll(seriesId: Int, seriesTitle: String) + + case tempCanPayment(title: String, can: Int, onSuccess: () -> Void) } diff --git a/SodaLive/Sources/Content/Detail/ContentDetailCreatorProfileView.swift b/SodaLive/Sources/Content/Detail/ContentDetailCreatorProfileView.swift index 1b45d4b..60922af 100644 --- a/SodaLive/Sources/Content/Detail/ContentDetailCreatorProfileView.swift +++ b/SodaLive/Sources/Content/Detail/ContentDetailCreatorProfileView.swift @@ -23,7 +23,7 @@ struct ContentDetailCreatorProfileView: View { Text(creator.nickname) .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) .padding(.horizontal, 5.3) Spacer() diff --git a/SodaLive/Sources/Content/Detail/ContentDetailPurchaseButton.swift b/SodaLive/Sources/Content/Detail/ContentDetailPurchaseButton.swift index 143aba8..5e7e23a 100644 --- a/SodaLive/Sources/Content/Detail/ContentDetailPurchaseButton.swift +++ b/SodaLive/Sources/Content/Detail/ContentDetailPurchaseButton.swift @@ -14,16 +14,18 @@ struct ContentDetailPurchaseButton: View { var body: some View { HStack(spacing: 0) { - Image("ic_can") - .resizable() - .frame(width: 16.7, height: 16.7) + if UserDefaults.int(forKey: .userId) != 17958 { + Image("ic_can") + .resizable() + .frame(width: 16.7, height: 16.7) + } - Text("\(price)") + Text(UserDefaults.int(forKey: .userId) == 17958 ? "\(price * 110)" : "\(price)") .font(.custom(Font.bold.rawValue, size: 14.7)) .foregroundColor(.white) .padding(.leading, 5.3) - Text("캔으로") + Text(UserDefaults.int(forKey: .userId) == 17958 ? "원으로": "캔으로") .font(.custom(Font.light.rawValue, size: 12)) .foregroundColor(.white) diff --git a/SodaLive/Sources/Content/Detail/ContentDetailView.swift b/SodaLive/Sources/Content/Detail/ContentDetailView.swift index 0224433..b8204e1 100644 --- a/SodaLive/Sources/Content/Detail/ContentDetailView.swift +++ b/SodaLive/Sources/Content/Detail/ContentDetailView.swift @@ -38,7 +38,7 @@ struct ContentDetailView: View { Text("콘텐츠 상세") .font(.custom(Font.bold.rawValue, size: 18.3)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(Color.grayee) } Spacer() @@ -99,7 +99,7 @@ struct ContentDetailView: View { .foregroundColor(.white) .frame(maxWidth: .infinity) .frame(height: 48.7) - .background(Color(hex: "525252")) + .background(Color.gray52) .cornerRadius(5.3) .padding(.top, 18.3) .padding(.horizontal, 13.3) @@ -113,7 +113,7 @@ struct ContentDetailView: View { .foregroundColor(.white) .frame(maxWidth: .infinity) .frame(height: 48.7) - .background(Color(hex: "525252")) + .background(Color.gray52) .cornerRadius(5.3) .padding(.top, 18.3) .padding(.horizontal, 13.3) @@ -162,7 +162,7 @@ struct ContentDetailView: View { .padding(.horizontal, 13.3) Rectangle() - .foregroundColor(Color(hex: "232323")) + .foregroundColor(Color.gray23) .frame(height: 6.7) .padding(.top, 24) @@ -207,7 +207,7 @@ struct ContentDetailView: View { if proxy.safeAreaInsets.bottom > 0 { Rectangle() - .foregroundColor(Color(hex: "222222")) + .foregroundColor(Color.gray22) .frame(width: proxy.size.width, height: 15.3) } } @@ -226,7 +226,20 @@ struct ContentDetailView: View { orderType: orderType, isOnlyRental: audioContent.isOnlyRental, onClickConfirm: { - viewModel.order(orderType: orderType) + if UserDefaults.int(forKey: .userId) == 17958 { + AppState.shared + .setAppStep( + step: .tempCanPayment( + title: audioContent.title, + can: orderType == .RENTAL ? Int(ceil(Double(audioContent.price) * 0.6)) : audioContent.price, + onSuccess: { + viewModel.order(orderType: orderType) + } + ) + ) + } else { + viewModel.order(orderType: orderType) + } } ) } @@ -272,7 +285,7 @@ struct ContentDetailView: View { if proxy.safeAreaInsets.bottom > 0 { Rectangle() - .foregroundColor(Color(hex: "222222")) + .foregroundColor(Color.gray22) .frame(width: proxy.size.width, height: 15.3) } } diff --git a/SodaLive/Sources/Content/Detail/ContentOrderConfirmDialogView.swift b/SodaLive/Sources/Content/Detail/ContentOrderConfirmDialogView.swift index 2b53d54..2992803 100644 --- a/SodaLive/Sources/Content/Detail/ContentOrderConfirmDialogView.swift +++ b/SodaLive/Sources/Content/Detail/ContentOrderConfirmDialogView.swift @@ -27,7 +27,7 @@ struct ContentOrderConfirmDialogView: View { VStack(spacing: 0) { Text("구매확인") .font(.custom(Font.bold.rawValue, size: 18.3)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(Color.grayee) HStack(spacing: 11) { ZStack(alignment: .topLeading) { @@ -48,7 +48,7 @@ struct ContentOrderConfirmDialogView: View { Text(audioContent.title) .font(.custom(Font.bold.rawValue, size: 11.3)) - .foregroundColor(Color(hex: "d2d2d2")) + .foregroundColor(Color.grayd2) .padding(.top, 2) HStack(spacing: 4.3) { @@ -60,13 +60,13 @@ struct ContentOrderConfirmDialogView: View { Text(audioContent.creator.nickname) .font(.custom(Font.medium.rawValue, size: 10)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) } .padding(.top, 6.7) Text(audioContent.duration) .font(.custom(Font.medium.rawValue, size: 11)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) .padding(.top, 6.7) } @@ -77,52 +77,74 @@ struct ContentOrderConfirmDialogView: View { .cornerRadius(5.3) .padding(.top, 21.3) - Text("콘텐츠를 \(orderType == .RENTAL ? "대여" : "소장")하시겠습니까?\n아래 캔이 차감됩니다.") + Text("콘텐츠를 \(orderType == .RENTAL ? "대여" : "소장")하시겠습니까?") .font(.custom(Font.medium.rawValue, size: 13.3)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(Color.grayee) .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.center) .padding(.top, 13.3) + if UserDefaults.int(forKey: .userId) != 17958 { + Text("아래 캔이 차감됩니다.") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + .padding(.top, 13.3) + } + HStack(spacing: 2.7) { Spacer() - - Image("ic_can") - .resizable() - .frame(width: 16.7, height: 16.7) - - if orderType == .RENTAL { - Text("\(isOnlyRental ? audioContent.price : Int(ceil(Double(audioContent.price) * 0.6)))") - .font(.custom(Font.bold.rawValue, size: 13.3)) - .foregroundColor(Color(hex: "eeeeee")) + + if UserDefaults.int(forKey: .userId) != 17958 { + Image("ic_can") + .resizable() + .frame(width: 16.7, height: 16.7) + + if orderType == .RENTAL { + Text("\(isOnlyRental ? audioContent.price : Int(ceil(Double(audioContent.price) * 0.6)))") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + } else { + Text("\(audioContent.price)") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + } } else { - Text("\(audioContent.price)") - .font(.custom(Font.bold.rawValue, size: 13.3)) - .foregroundColor(Color(hex: "eeeeee")) + if orderType == .RENTAL { + Text("\(isOnlyRental ? audioContent.price * 110 : Int(ceil(Double(audioContent.price) * 0.6)) * 110)원") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + } else { + Text("\(audioContent.price * 110)원") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + } } Spacer() } .padding(.vertical, 13.3) - .background(Color(hex: "333333")) + .background(Color.gray33) .cornerRadius(6.7) .overlay( RoundedRectangle(cornerRadius: CGFloat(6.7)) .stroke(lineWidth: 1) - .foregroundColor(Color(hex: "979797")) + .foregroundColor(Color.gray97) ) .padding(.top, 13.3) HStack(spacing: 12) { Text("취소") .font(.custom(Font.bold.rawValue, size: 18.3)) - .foregroundColor(Color(hex: "9970ff")) + .foregroundColor(Color.button) .padding(.vertical, 15.7) .frame(maxWidth: .infinity) + .contentShape(Rectangle()) .overlay( RoundedRectangle(cornerRadius: CGFloat(10)) .stroke(lineWidth: 1) - .foregroundColor(Color(hex: "9970ff")) + .foregroundColor(Color.button) ) .onTapGesture { isShowing = false } @@ -131,7 +153,8 @@ struct ContentOrderConfirmDialogView: View { .foregroundColor(.white) .padding(.vertical, 15.7) .frame(maxWidth: .infinity) - .background(Color(hex: "9970ff")) + .contentShape(Rectangle()) + .background(Color.button) .cornerRadius(10) .onTapGesture { onClickConfirm() @@ -143,7 +166,7 @@ struct ContentOrderConfirmDialogView: View { .padding(.horizontal, 13.3) .padding(.top, 26.7) .padding(.bottom, 16.7) - .background(Color(hex: "222222")) + .background(Color.gray22) .cornerRadius(10) .padding(.horizontal, 20) } diff --git a/SodaLive/Sources/Content/Detail/ContentOrderDialogView.swift b/SodaLive/Sources/Content/Detail/ContentOrderDialogView.swift index 0b72b3c..91c1527 100644 --- a/SodaLive/Sources/Content/Detail/ContentOrderDialogView.swift +++ b/SodaLive/Sources/Content/Detail/ContentOrderDialogView.swift @@ -40,17 +40,25 @@ struct ContentOrderDialogView: View { Spacer() HStack(spacing: 8) { - Image("ic_can") - .resizable() - .frame(width: 16.7, height: 16.7) + if UserDefaults.int(forKey: .userId) != 17958 { + Image("ic_can") + .resizable() + .frame(width: 16.7, height: 16.7) + } - Text(isOnlyRental ? "\(price)" : "\(Int(ceil(Double(price) * 0.6)))") + Text(isOnlyRental ? "\(price * 110)" : "\(Int(ceil(Double(price) * 0.6)) * 110)") .font(.custom(Font.bold.rawValue, size: 13.3)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(Color.grayee) + + if UserDefaults.int(forKey: .userId) == 17958 { + Text("원") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + } } .padding(.vertical, 8) .padding(.horizontal, 13.3) - .background(Color(hex: "9970ff")) + .background(Color.button) .cornerRadius(5.3) .onTapGesture { onTapPurchase(.RENTAL) @@ -73,17 +81,25 @@ struct ContentOrderDialogView: View { Spacer() HStack(spacing: 8) { - Image("ic_can") - .resizable() - .frame(width: 16.7, height: 16.7) + if UserDefaults.int(forKey: .userId) != 17958 { + Image("ic_can") + .resizable() + .frame(width: 16.7, height: 16.7) + } - Text("\(price)") + Text("\(price * 110)") .font(.custom(Font.bold.rawValue, size: 13.3)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(Color.grayee) + + if UserDefaults.int(forKey: .userId) == 17958 { + Text("원") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + } } .padding(.vertical, 8) .padding(.horizontal, 13.3) - .background(Color(hex: "9970ff")) + .background(Color.button) .cornerRadius(5.3) .onTapGesture { onTapPurchase(.KEEP) @@ -93,7 +109,7 @@ struct ContentOrderDialogView: View { } } .padding(24) - .background(Color(hex: "222222")) + .background(Color.gray22) } } } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 7014189..98ec774 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -187,6 +187,10 @@ struct ContentView: View { case .seriesContentAll(let seriesId, let seriesTitle): SeriesContentAllView(seriesId: seriesId, seriesTitle: seriesTitle) + case .tempCanPayment(let title, let can, let onSuccess): + CanPaymentTempView(title: title, can: can, onSuccess: onSuccess) + + default: EmptyView() .frame(width: 0, height: 0, alignment: .topLeading) diff --git a/SodaLive/Sources/MyPage/Can/Payment/CanPgPaymentView.swift b/SodaLive/Sources/MyPage/Can/Payment/CanPgPaymentView.swift index af2bc35..bd6be03 100644 --- a/SodaLive/Sources/MyPage/Can/Payment/CanPgPaymentView.swift +++ b/SodaLive/Sources/MyPage/Can/Payment/CanPgPaymentView.swift @@ -86,11 +86,11 @@ struct CanPgPaymentView: View { Text("\(canResponse.price) 원") .font(.custom(Font.bold.rawValue, size: 15.3)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(Color.grayee) } .padding(.horizontal, 13.3) .padding(.vertical, 23.3) - .background(Color(hex: "222222")) + .background(Color.gray22) .cornerRadius(16.7) .padding(.horizontal, 13.3) .frame(width: screenSize().width) @@ -98,7 +98,7 @@ struct CanPgPaymentView: View { Text("결제 수단 선택") .font(.custom(Font.bold.rawValue, size: 16.7)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(Color.grayee) .frame(width: screenSize().width - 26.7, alignment: .leading) .padding(.top, 26.7) @@ -106,18 +106,19 @@ struct CanPgPaymentView: View { HStack(spacing: 13.3) { Text("카드") .font(.custom( viewModel.paymentMethod == .card ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7)) - .foregroundColor(Color(hex: viewModel.paymentMethod == .card ? "9970ff" : "eeeeee")) + .foregroundColor(viewModel.paymentMethod == .card ? Color.button : Color.grayee) .frame(width: (screenSize().width - 40) / 2) .padding(.vertical, 16.7) .background( - Color(hex: viewModel.paymentMethod == .card ? "9970ff" : "232323") - .opacity(viewModel.paymentMethod == .card ? 0.3 : 1) + viewModel.paymentMethod == .card ? + Color.button.opacity(0.3) : + Color.gray23 ) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(lineWidth: 1) - .foregroundColor(Color(hex: viewModel.paymentMethod == .card ? "9970ff" : "777777")) + .foregroundColor(viewModel.paymentMethod == .card ? Color.button : Color.gray77) ) .onTapGesture { if viewModel.paymentMethod != .card { @@ -128,18 +129,19 @@ struct CanPgPaymentView: View { Text("계좌이체") .font(.custom( viewModel.paymentMethod == .bank ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7)) - .foregroundColor(Color(hex: viewModel.paymentMethod == .bank ? "9970ff" : "eeeeee")) + .foregroundColor(viewModel.paymentMethod == .bank ? Color.button : Color.grayee) .frame(width: (screenSize().width - 40) / 2) .padding(.vertical, 16.7) .background( - Color(hex: viewModel.paymentMethod == .bank ? "9970ff" : "232323") - .opacity(viewModel.paymentMethod == .bank ? 0.3 : 1) + viewModel.paymentMethod == .bank ? + Color.button.opacity(0.3) : + Color.gray23 ) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(lineWidth: 1) - .foregroundColor(Color(hex: viewModel.paymentMethod == .bank ? "9970ff" : "777777")) + .foregroundColor(viewModel.paymentMethod == .bank ? Color.button : Color.gray77) ) .onTapGesture { if viewModel.paymentMethod != .bank { @@ -153,18 +155,19 @@ struct CanPgPaymentView: View { HStack(spacing: 13.3) { Text("휴대폰 결제") .font(.custom( viewModel.paymentMethod == .phone ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7)) - .foregroundColor(Color(hex: viewModel.paymentMethod == .phone ? "9970ff" : "eeeeee")) + .foregroundColor(viewModel.paymentMethod == .phone ? Color.button : Color.grayee) .frame(width: (screenSize().width - 40) / 2) .padding(.vertical, 16.7) .background( - Color(hex: viewModel.paymentMethod == .phone ? "9970ff" : "232323") - .opacity(viewModel.paymentMethod == .phone ? 0.3 : 1) + viewModel.paymentMethod == .phone ? + Color.button.opacity(0.3) : + Color.gray23 ) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(lineWidth: 1) - .foregroundColor(Color(hex: viewModel.paymentMethod == .phone ? "9970ff" : "777777")) + .foregroundColor(viewModel.paymentMethod == .phone ? Color.button : Color.gray77) ) .onTapGesture { if viewModel.paymentMethod != .phone { @@ -184,7 +187,7 @@ struct CanPgPaymentView: View { Text("구매조건 확인 및 결제 진행 동의") .font(.custom(Font.medium.rawValue, size: 14.7)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(Color.grayee) } .frame(width: screenSize().width - 53.4, alignment: .leading) .padding(.top, 16.7) @@ -196,11 +199,11 @@ struct CanPgPaymentView: View { HStack(alignment: .top, spacing: 0) { Text("- ") .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) Text("충전된 캔의 유효기간은 충전 후 5년 입니다.") .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) .fixedSize(horizontal: false, vertical: true) } .frame(width: screenSize().width - 53.4, alignment: .leading) @@ -209,11 +212,11 @@ struct CanPgPaymentView: View { HStack(alignment: .top, spacing: 0) { Text("- ") .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) Text("결제 취소는 결제 후 7일 이내에만 할 수 있습니다.\n단, 캔의 일부를 사용하면 결제 취소를 할 수 없습니다.") .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) .fixedSize(horizontal: false, vertical: true) } .frame(width: screenSize().width - 53.4, alignment: .leading) @@ -221,11 +224,11 @@ struct CanPgPaymentView: View { HStack(alignment: .top, spacing: 0) { Text("- ") .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) Text("광고성 이벤트 등 회사가 무료로 지급한 포인트는 환불되지 않습니다.") .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) .fixedSize(horizontal: false, vertical: true) } .frame(width: screenSize().width - 53.4, alignment: .leading) @@ -233,11 +236,11 @@ struct CanPgPaymentView: View { HStack(alignment: .top, spacing: 0) { Text("- ") .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) Text("자세한 내용은 소다라이브 이용약관에서 확인할 수 있습니다.") .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color(hex: "777777")) + .foregroundColor(Color.gray77) .fixedSize(horizontal: false, vertical: true) } .frame(width: screenSize().width - 53.4, alignment: .leading) @@ -251,12 +254,12 @@ struct CanPgPaymentView: View { VStack(alignment: .leading, spacing: 5) { Text("결제금액") .font(.custom(Font.medium.rawValue, size: 13.3)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(Color.grayee) HStack(spacing: 0) { Text("\(canResponse.price) 원") .font(.custom(Font.bold.rawValue, size: 23.3)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(Color.grayee) } } @@ -267,7 +270,7 @@ struct CanPgPaymentView: View { .foregroundColor(.white) .padding(.vertical, 16) .frame(minWidth: 200) - .background(Color(hex: "9970ff")) + .background(Color.button) .cornerRadius(10) .onTapGesture { if viewModel.paymentMethod == nil { @@ -290,12 +293,12 @@ struct CanPgPaymentView: View { .padding(.leading, 22) .padding(.trailing, 13.3) .padding(.vertical, 13.3) - .background(Color(hex: "222222")) + .background(Color.gray22) .cornerRadius(16.7, corners: [.topLeft, .topRight]) if proxy.safeAreaInsets.bottom > 0 { Rectangle() - .foregroundColor(Color(hex: "222222")) + .foregroundColor(Color.gray22) .frame(width: proxy.size.width, height: 15.3) } } @@ -308,7 +311,7 @@ struct CanPgPaymentView: View { .padding(.horizontal, 6.7) .frame(width: geo.size.width - 66.7, alignment: .center) .font(.custom(Font.medium.rawValue, size: 12)) - .background(Color(hex: "9970ff")) + .background(Color.button) .foregroundColor(Color.white) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) @@ -326,25 +329,6 @@ struct CanPgPaymentView: View { LoadingView() } } - .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(hex: "9970ff")) - .foregroundColor(Color.white) - .multilineTextAlignment(.leading) - .fixedSize(horizontal: false, vertical: true) - .cornerRadius(20) - .padding(.top, 66.7) - Spacer() - } - } - } } } diff --git a/SodaLive/Sources/MyPage/Can/Payment/Temp/CanChargeTempRequest.swift b/SodaLive/Sources/MyPage/Can/Payment/Temp/CanChargeTempRequest.swift new file mode 100644 index 0000000..a47a8b9 --- /dev/null +++ b/SodaLive/Sources/MyPage/Can/Payment/Temp/CanChargeTempRequest.swift @@ -0,0 +1,12 @@ +// +// CanChargeTempRequest.swift +// SodaLive +// +// Created by klaus on 5/20/24. +// + +struct CanChargeTempRequest: Encodable { + let can: Int + let price: Int + let paymentGateway: PaymentGateway +} diff --git a/SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempRepository.swift b/SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempRepository.swift new file mode 100644 index 0000000..1637044 --- /dev/null +++ b/SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempRepository.swift @@ -0,0 +1,23 @@ +// +// CanPaymentTempRepository.swift +// SodaLive +// +// Created by klaus on 5/20/24. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +class CanPaymentTempRepository { + private let api = MoyaProvider() + + func chargeCan(request: CanChargeTempRequest) -> AnyPublisher { + return api.requestPublisher(.chargeCan(request: request)) + } + + func pgVerify(receiptId: String, orderId: String) -> AnyPublisher { + return api.requestPublisher(.verify(request: PgVerifyRequest(receiptId: receiptId, orderId: orderId))) + } +} diff --git a/SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempView.swift b/SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempView.swift new file mode 100644 index 0000000..b27f7fa --- /dev/null +++ b/SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempView.swift @@ -0,0 +1,275 @@ +// +// CanPaymentTempView.swift +// SodaLive +// +// Created by klaus on 5/20/24. +// + +import SwiftUI +import Bootpay +import BootpayUI + +struct CanPaymentTempView: View { + + @StateObject var viewModel = CanPaymentTempViewModel() + + let title: String + let can: Int + let onSuccess: () -> Void + + init(title: String, can: Int, onSuccess: @escaping () -> Void) { + self.title = title + self.can = can + self.onSuccess = onSuccess + } + + 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 + self.can, forKey: .can) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + AppState.shared.back() + onSuccess() + } + } + } + .onClose { + DEBUG_LOG("onClose") + viewModel.isShowPaymentView = false + } + } else { + GeometryReader { proxy in + VStack(spacing: 0) { + DetailNavigationBar(title: "결제하기") + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + HStack(spacing: 0) { + Text(self.title) + .font(.custom(Font.bold.rawValue, size: 15.3)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.leading, 13.3) + + Spacer() + + Text("\(self.can * 110) 원") + .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: 13.3) { + Text("카드") + .font(.custom( viewModel.paymentMethod == .card ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7)) + .foregroundColor(viewModel.paymentMethod == .card ? Color.button : Color.grayee) + .frame(width: (screenSize().width - 40) / 2) + .padding(.vertical, 16.7) + .background( + viewModel.paymentMethod == .card ? + Color.button.opacity(0.3) : + Color.gray23 + ) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(lineWidth: 1) + .foregroundColor(viewModel.paymentMethod == .card ? Color.button : Color.gray77) + ) + .onTapGesture { + if viewModel.paymentMethod != .card { + viewModel.paymentMethod = .card + } + } + + + Text("계좌이체") + .font(.custom( viewModel.paymentMethod == .bank ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7)) + .foregroundColor(viewModel.paymentMethod == .bank ? Color.button : Color.grayee) + .frame(width: (screenSize().width - 40) / 2) + .padding(.vertical, 16.7) + .background( + viewModel.paymentMethod == .bank ? + Color.button.opacity(0.3) : + Color.gray23 + ) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(lineWidth: 1) + .foregroundColor(viewModel.paymentMethod == .bank ? Color.button : Color.gray77) + ) + .onTapGesture { + if viewModel.paymentMethod != .bank { + viewModel.paymentMethod = .bank + } + } + } + .frame(width: screenSize().width - 26.7) + .padding(.top, 16.7) + + HStack(spacing: 13.3) { + Text("휴대폰 결제") + .font(.custom( viewModel.paymentMethod == .phone ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7)) + .foregroundColor(viewModel.paymentMethod == .phone ? Color.button : Color.grayee) + .frame(width: (screenSize().width - 40) / 2) + .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 + } + } + + Spacer() + } + .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() + } + } + } + + 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("\(self.can * 110) 원") + .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 { + viewModel.chargeCan(can: can, paymentGateway: .PG){ + viewModel.payload.orderName = self.title + viewModel.payload.price = Double(self.can * 110) + 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() + } + } + } +} + +#Preview { + CanPaymentTempView(title: "콘텐츠 제목", can: 1000, onSuccess: {}) +} diff --git a/SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempViewModel.swift b/SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempViewModel.swift new file mode 100644 index 0000000..0e3b813 --- /dev/null +++ b/SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempViewModel.swift @@ -0,0 +1,129 @@ +// +// CanPaymentTempViewModel.swift +// SodaLive +// +// Created by klaus on 5/20/24. +// + +import Foundation +import Combine + +import Bootpay + +enum TempPaymentMethod: String { + case card = "카드" + case bank = "계좌이체" + case phone = "휴대폰" +} + +final class CanPaymentTempViewModel: ObservableObject { + + private let repository = CanPaymentTempRepository() + private var subscription = Set() + + @Published var isTermsAgree = false + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isLoading = false + + @Published var isShowPaymentView = false + @Published var paymentMethod: TempPaymentMethod? = nil + + let payload = Payload() + + func chargeCan(can: Int, paymentGateway: PaymentGateway, onSuccess: @escaping () -> Void) { + isLoading = true + repository.chargeCan(request: CanChargeTempRequest(can: can, price: can * 110, paymentGateway: paymentGateway)) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + let bootUser = BootUser() + bootUser.userId = "\(UserDefaults.int(forKey: .userId))" + bootUser.username = UserDefaults.string(forKey: .nickname) + + payload.applicationId = BOOTPAY_APP_ID + payload.pg = "세틀뱅크" + payload.orderId = "\(data.chargeId)" + payload.method = paymentMethod!.rawValue + payload.user = bootUser + + onSuccess() + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + + func verifyPayment(_ data: [String: Any], onSuccess: @escaping () -> Void) { + isLoading = true + + let _data = data["data"] as? [String: Any] + + if let data = _data { + let receiptId = data["receipt_id"] as! String + let orderId = data["order_id"] as! String + + repository.pgVerify(receiptId: receiptId, orderId: orderId) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) + + if decoded.success { + onSuccess() + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } else { + isLoading = false + errorMessage = "본인인증 중 오류가 발생했습니다." + isShowPopup = true + } + } +} diff --git a/SodaLive/Sources/MyPage/Can/Payment/Temp/CanTempApi.swift b/SodaLive/Sources/MyPage/Can/Payment/Temp/CanTempApi.swift new file mode 100644 index 0000000..068595f --- /dev/null +++ b/SodaLive/Sources/MyPage/Can/Payment/Temp/CanTempApi.swift @@ -0,0 +1,55 @@ +// +// CanTempApi.swift +// SodaLive +// +// Created by klaus on 5/20/24. +// + +import Foundation +import Moya + +enum CanTempApi { + case chargeCan(request: CanChargeTempRequest) + case verify(request: PgVerifyRequest) +} + +extension CanTempApi: TargetType { + var baseURL: URL { + return URL(string: BASE_URL)! + } + + var path: String { + switch self { + case .chargeCan: + return "/charge/temp" + + case .verify: + return "/charge/temp/verify" + } + } + + var method: Moya.Method { + switch self { + case .chargeCan, .verify: + return .post + } + } + + var task: Task { + switch self { + case .chargeCan(let request): + return .requestJSONEncodable(request) + + case .verify(let request): + return .requestJSONEncodable(request) + + } + } + + var headers: [String : String]? { + switch self { + default: + return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] + } + } +}