feat(gift): 보낸 선물 상세를 추가한다
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
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선물 보내기 규정에 따라 선물 취소 및 사용한 캔이 모두 환불됩니다."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user