Files
sodalive-ios/SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift
T

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선물 보내기 규정에 따라 선물 취소 및 사용한 캔이 모두 환불됩니다."
}
}