182 lines
7.6 KiB
Swift
182 lines
7.6 KiB
Swift
import SwiftUI
|
|
|
|
struct GiftDetailView: View {
|
|
@Environment(\.openURL) private var openURL
|
|
@StateObject private var viewModel: GiftDetailViewModel
|
|
@StateObject private var recipientAddressViewModel: GiftRecipientAddressViewModel
|
|
@State private var hasAppeared = false
|
|
@State private var isActive = false
|
|
|
|
init(applicationNo: String) {
|
|
let detailViewModel = GiftDetailViewModel(applicationNo: applicationNo) { canceledApplicationNo, completion in
|
|
AppState.shared.replaceCurrentAppStep(
|
|
step: .giftDetail(applicationNo: canceledApplicationNo),
|
|
ifCurrentStep: { step in
|
|
guard case .giftDetail(let currentApplicationNo) = step else { return false }
|
|
return currentApplicationNo == canceledApplicationNo
|
|
},
|
|
completion: completion
|
|
)
|
|
}
|
|
_viewModel = StateObject(wrappedValue: detailViewModel)
|
|
_recipientAddressViewModel = StateObject(wrappedValue: GiftRecipientAddressViewModel(applicationNo: applicationNo, onSubmitted: {
|
|
detailViewModel.fetch()
|
|
}))
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
DetailNavigationBar(title: navigationTitle)
|
|
.disabled(viewModel.isCancelSubmitting || isRecipientAddressBusy)
|
|
BaseView(isLoading: .constant(viewModel.isLoading || recipientAddressViewModel.isSubmitting)) {
|
|
ZStack {
|
|
switch viewModel.renderState {
|
|
case .sent(let model):
|
|
detailContent {
|
|
SentGiftDetailContentView(model: model)
|
|
}
|
|
.safeAreaInset(edge: .bottom, spacing: 0) {
|
|
GiftDetailActionBar(
|
|
model: model,
|
|
onCancel: viewModel.presentCancelDialog,
|
|
onPolicy: openGiftTerms
|
|
)
|
|
}
|
|
case .received(let model):
|
|
if model.recipientAddressRequired {
|
|
VStack(spacing: 0) {
|
|
GiftRecipientAddressFormView(model: model, viewModel: recipientAddressViewModel)
|
|
if recipientAddressViewModel.hasSubmitted && !viewModel.isLoading && !viewModel.errorMessage.isEmpty {
|
|
retryContent
|
|
}
|
|
}
|
|
} else {
|
|
detailContent {
|
|
ReceivedGiftDetailContentView(model: model)
|
|
}
|
|
.safeAreaInset(edge: .bottom, spacing: 0) {
|
|
if model.status == .undeliverable {
|
|
ReceivedGiftDetailActionBar(onPolicy: openGiftTerms)
|
|
}
|
|
}
|
|
}
|
|
case .canceled(let model):
|
|
detailContent {
|
|
GiftCancelCompleteView(model: model)
|
|
}
|
|
case nil:
|
|
Color.clear.accessibilityHidden(true)
|
|
}
|
|
|
|
if viewModel.renderState == nil && !viewModel.errorMessage.isEmpty {
|
|
retryContent
|
|
}
|
|
}
|
|
}
|
|
.disabled(isRecipientAddressBusy)
|
|
}
|
|
.background(Color.black.ignoresSafeArea())
|
|
.allowsHitTesting(!viewModel.isShowingCancelDialog && !viewModel.isCancelSubmitting)
|
|
.accessibilityHidden(viewModel.isShowingCancelDialog)
|
|
.overlay {
|
|
if viewModel.isShowingCancelDialog {
|
|
SodaV2ActionModal(
|
|
title: "신청 취소",
|
|
message: cancelMessage,
|
|
button1: SodaV2ActionModalButton(label: "취소하기", action: viewModel.confirmCancel),
|
|
button2: SodaV2ActionModalButton(label: "나가기", action: viewModel.dismissCancelDialog),
|
|
onDimmedTap: viewModel.dismissCancelDialog,
|
|
primaryTextColor: .red400
|
|
)
|
|
.disabled(viewModel.isCancelSubmitting)
|
|
.allowsHitTesting(!viewModel.isCancelSubmitting)
|
|
}
|
|
}
|
|
.overlay {
|
|
if viewModel.isCancelSubmitting {
|
|
LoadingView()
|
|
}
|
|
}
|
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage)
|
|
.sodaToast(isPresented: $recipientAddressViewModel.isShowPopup, message: recipientAddressViewModel.errorMessage)
|
|
.sodaToast(isPresented: $recipientAddressViewModel.isShowSuccessToast, message: recipientAddressViewModel.successMessage)
|
|
.onChange(of: recipientAddressViewModel.isShowSuccessToast) { isPresented in
|
|
if !isPresented && isActive {
|
|
recipientAddressViewModel.successToastDidDismiss()
|
|
}
|
|
}
|
|
.onAppear {
|
|
isActive = true
|
|
recipientAddressViewModel.successToastDidDismiss()
|
|
guard !hasAppeared else { return }
|
|
hasAppeared = true
|
|
viewModel.fetch()
|
|
}
|
|
.onDisappear {
|
|
isActive = false
|
|
}
|
|
}
|
|
|
|
private func openGiftTerms() {
|
|
guard let url = GiftConstants.termsURL else { return }
|
|
openURL(url)
|
|
}
|
|
|
|
private var isRecipientAddressBusy: Bool {
|
|
recipientAddressViewModel.isSubmitting || recipientAddressViewModel.isShowSuccessToast
|
|
|| (recipientAddressViewModel.hasSubmitted && viewModel.isLoading)
|
|
}
|
|
|
|
private var retryContent: some View {
|
|
VStack(spacing: SodaSpacing.s16) {
|
|
Text(viewModel.errorMessage)
|
|
.appFont(.body3)
|
|
.foregroundColor(.gray500)
|
|
.multilineTextAlignment(.center)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
Button(action: viewModel.fetch) {
|
|
Text("다시 시도")
|
|
.appFont(.body2)
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, SodaSpacing.s16)
|
|
.frame(minHeight: SodaSpacing.s48)
|
|
.background(Color.gray800)
|
|
.clipShape(Capsule())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(viewModel.isLoading)
|
|
}
|
|
.padding(SodaSpacing.s24)
|
|
}
|
|
|
|
private func detailContent<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
|
ScrollView {
|
|
LazyVStack(alignment: .leading, spacing: SodaSpacing.s32) {
|
|
content()
|
|
}
|
|
.padding(.horizontal, SodaSpacing.s14)
|
|
.padding(.top, SodaSpacing.s24)
|
|
.padding(.bottom, SodaSpacing.s32)
|
|
}
|
|
}
|
|
|
|
private var navigationTitle: String {
|
|
if case .canceled = viewModel.renderState {
|
|
return "취소 완료"
|
|
}
|
|
return "선물 상세"
|
|
}
|
|
|
|
private var cancelMessage: String {
|
|
let firstLine: String
|
|
if case .sent(let model) = viewModel.renderState,
|
|
let nickname = model.recipientCreatorNickname?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!nickname.isEmpty {
|
|
firstLine = "\(nickname)에게 보내는 선물을 취소할까요?"
|
|
} else {
|
|
firstLine = "보내는 선물을 취소할까요?"
|
|
}
|
|
return firstLine + "\n선물 보내기 규정에 따라 선물 취소 및 사용한 캔이 모두 환불됩니다."
|
|
}
|
|
}
|