Files
sodalive-ios/SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationView.swift
T

294 lines
13 KiB
Swift

import SwiftUI
struct GiftTrackingRegistrationView: View {
@StateObject private var viewModel: GiftTrackingRegistrationViewModel
@State private var hasAppeared = false
@State private var isActive = false
@State private var isShowingCancelDialog = false
@FocusState private var isTrackingNumberFocused: Bool
init(applicationNo: String) {
_viewModel = StateObject(wrappedValue: GiftTrackingRegistrationViewModel(applicationNo: applicationNo, onUnavailable: { unavailableApplicationNo in
AppState.shared.replaceCurrentAppStep(
step: .giftDetail(applicationNo: unavailableApplicationNo),
ifCurrentStep: { step in
guard case .giftTrackingRegistration(let currentApplicationNo) = step else { return false }
return currentApplicationNo == unavailableApplicationNo
}
)
}, onMutationCompleted: { completedApplicationNo, completion in
AppState.shared.replaceCurrentAppStep(
step: .giftDetail(applicationNo: completedApplicationNo),
ifCurrentStep: { step in
guard case .giftTrackingRegistration(let currentApplicationNo) = step else { return false }
return currentApplicationNo == completedApplicationNo
},
completion: completion
)
}))
}
var body: some View {
VStack(spacing: 0) {
DetailNavigationBar(title: "운송장 등록")
.disabled(viewModel.isSubmitting)
BaseView(isLoading: .constant(viewModel.isLoading || viewModel.isSubmitting)) {
if let summary = viewModel.summary, let mailbox = viewModel.mailbox {
ScrollView {
LazyVStack(alignment: .leading, spacing: SodaSpacing.s24) {
summaryCard(summary)
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
sectionHeader("보내실 주소", helper: "아래 주소로 발송한 뒤 운송장 번호를 등록해 주세요")
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
GiftInfoRow(title: "사서함", value: mailbox.name)
GiftInfoRow(title: "주소", value: mailbox.address)
GiftInfoRow(title: "연락처", value: mailbox.phoneNumber)
}
.padding(SodaSpacing.s14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray900)
.clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14))
}
sectionDivider
trackingSection
sectionDivider
guidanceSection
}
.padding(.horizontal, SodaSpacing.s14)
.padding(.top, SodaSpacing.s24)
.padding(.bottom, SodaSpacing.s32)
}
.scrollDismissesKeyboard(.interactively)
.safeAreaInset(edge: .bottom, spacing: 0) {
actionBar
}
} else if !viewModel.isLoading && !viewModel.errorMessage.isEmpty {
ScrollView {
VStack(spacing: SodaSpacing.s16) {
Text(viewModel.errorMessage)
.appFont(.body3)
.foregroundColor(.gray500)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
if !viewModel.shouldReturnToDetail {
Button(action: viewModel.fetch) {
Text("다시 시도")
.appFont(.body2)
.foregroundColor(.white)
.padding(.horizontal, SodaSpacing.s16)
.frame(minHeight: SodaSpacing.s48)
.background(Color.gray800)
.clipShape(Capsule())
}
.buttonStyle(.plain)
}
}
.padding(SodaSpacing.s24)
.frame(maxWidth: .infinity)
}
}
}
.disabled(viewModel.isSubmitting)
}
.background(Color.black.ignoresSafeArea())
.alert("신청 취소", isPresented: $isShowingCancelDialog) {
Button("취소하기", role: .destructive) {
viewModel.confirmCancel()
}
.disabled(viewModel.isSubmitting)
Button("나가기", role: .cancel) {
isShowingCancelDialog = false
}
} message: {
Text(cancelMessage)
}
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage)
.sodaToast(isPresented: $viewModel.isShowSuccessToast, message: viewModel.successMessage)
.onChange(of: viewModel.isShowPopup) { isPresented in
if !isPresented && isActive {
viewModel.errorToastDidDismiss()
}
}
.onChange(of: viewModel.isShowSuccessToast) { isPresented in
if !isPresented && isActive {
viewModel.successToastDidDismiss()
}
}
.onAppear {
isActive = true
viewModel.errorToastDidDismiss()
viewModel.successToastDidDismiss()
guard !hasAppeared else { return }
hasAppeared = true
viewModel.fetch()
}
.onDisappear {
isActive = false
}
}
private func summaryCard(_ summary: SentGiftDetailDisplayModel) -> some View {
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
Text(summary.applicationNo)
.appFont(.caption3)
.foregroundColor(.gray500)
HStack(alignment: .top, spacing: SodaSpacing.s12) {
Circle()
.fill(Color.gray800)
.frame(width: 42, height: 42)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: SodaSpacing.s6) {
if let nickname = summary.recipientCreatorNickname {
Text(nickname)
.appFont(size: 14, weight: .semibold)
.foregroundColor(.white)
}
Text([summary.sizeName, summary.categoryName, summary.applicationDateText.map { "\($0) 신청" }]
.compactMap { $0 }.joined(separator: " · "))
.appFont(.caption3)
.foregroundColor(.gray500)
}
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(SodaSpacing.s14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray900)
.clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14))
}
private func sectionHeader(_ title: String, helper: String) -> some View {
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
Text(title)
.appFont(.body1)
.foregroundColor(.white)
.accessibilityAddTraits(.isHeader)
Text(helper)
.appFont(.caption2)
.foregroundColor(.gray500)
}
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
private var trackingSection: some View {
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
sectionHeader("운송장 정보", helper: "등록 완료 후 수정이 불가능하니 신중히 적어주세요")
GiftCourierPickerView(selectedCourier: $viewModel.selectedCourier)
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
Text("운송장 번호")
.appFont(.body5)
.foregroundColor(.white)
TextField("운송장 번호를 입력해 주세요", text: Binding(
get: { viewModel.trackingNumber },
set: { value in
guard GiftTrackingRegistrationViewModel.acceptsTrackingEditing(value) else { return }
viewModel.trackingNumber = value
}
))
.appFont(.body3)
.foregroundColor(.white)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.numberPad)
.focused($isTrackingNumberFocused)
.submitLabel(.done)
.onSubmit { isTrackingNumberFocused = false }
.padding(SodaSpacing.s14)
.frame(maxWidth: .infinity)
.background(Color.gray900)
.clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14))
.accessibilityLabel("운송장 번호")
}
}
}
private var sectionDivider: some View {
Color.gray900
.frame(height: 7)
.padding(.horizontal, -SodaSpacing.s14)
.accessibilityHidden(true)
}
private var guidanceSection: some View {
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
Text("등록 안내")
.appFont(.body1)
.foregroundColor(.white)
.accessibilityAddTraits(.isHeader)
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
ForEach(registrationGuidance, id: \.self) { guidance in
HStack(alignment: .top, spacing: SodaSpacing.s6) {
Text("•").accessibilityHidden(true)
Text(guidance)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
.appFont(.caption2)
.foregroundColor(.gray500)
}
}
.padding(SodaSpacing.s14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray900)
.clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14))
}
}
private var registrationGuidance: [String] {
[
"신청일로부터 3일 이내에 운송장 번호를 등록해 주세요.",
"3일 이내 운송장 번호를 등록하지 않은 경우, 선물 보내기가 자동으로 취소되며 사용한 캔이 모두 환불됩니다.",
"선물은 반드시 배송비 선불로 발송해 주세요. 착불로 발송된 선물은 수취가 거부됩니다.",
"진행 상태는 신청 상세에서 확인할 수 있습니다.",
"검수를 통과한 선물만 크리에이터에게 재배송됩니다."
]
}
private var actionBar: some View {
HStack(spacing: SodaSpacing.s14) {
Button {
guard viewModel.canCancel else { return }
isTrackingNumberFocused = false
isShowingCancelDialog = true
} label: {
Text("신청 취소")
.appFont(.body2)
.foregroundColor(viewModel.canCancel ? .white : .gray500)
.padding(.vertical, SodaSpacing.s16)
.frame(maxWidth: .infinity, minHeight: SodaSpacing.s48 + SodaSpacing.s4)
.overlay(Capsule().stroke(Color.white.opacity(0.3), lineWidth: 1))
}
.disabled(!viewModel.canCancel)
Button {
isTrackingNumberFocused = false
viewModel.registerTracking()
} label: {
Text("등록 완료")
.appFont(.body2)
.foregroundColor(viewModel.canSubmit ? .white : .gray500)
.padding(.vertical, SodaSpacing.s16)
.frame(maxWidth: .infinity, minHeight: SodaSpacing.s48 + SodaSpacing.s4)
.background(viewModel.canSubmit ? Color.button : Color.gray900)
.clipShape(Capsule())
}
.disabled(!viewModel.canSubmit)
}
.buttonStyle(.plain)
.padding(SodaSpacing.s14)
.background(Color.black)
}
private var cancelMessage: String {
let firstLine: String
if let nickname = viewModel.summary?.recipientCreatorNickname?.trimmingCharacters(in: .whitespacesAndNewlines),
!nickname.isEmpty {
firstLine = "\(nickname)에게 보내는 선물을 취소할까요?"
} else {
firstLine = "보내는 선물을 취소할까요?"
}
return firstLine + "\n선물 보내기 규정에 따라 선물 취소 및 사용한 캔이 모두 환불됩니다."
}
}