field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | LICENSE

commit 5b7bc99f3e8bf78247fd6e85f8284fe34527ddf2
parent 7ca86c3a19e7f50c7a2aa11743dba5ae2c692a3a
Author: triesap <tyson@radroots.org>
Date:   Fri, 19 Jun 2026 18:09:08 -0700

ui: remove deferred Market listing surface

- delete app-authored Market and listing views

- remove the field app listing runtime wrapper

- regenerate the Xcode project without deferred listing sources

- keep generated shared FFI bindings intact for future runtime work

Diffstat:
MRadroots.xcodeproj/project.pbxproj | 16----------------
DRadroots/Runtime/TradeListing.swift | 17-----------------
DRadroots/Views/MarketView.swift | 181-------------------------------------------------------------------------------
DRadroots/Views/TradeListingCreateView.swift | 276-------------------------------------------------------------------------------
DRadroots/Views/TradeListingDetailView.swift | 59-----------------------------------------------------------
5 files changed, 0 insertions(+), 549 deletions(-)

diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj @@ -18,13 +18,10 @@ 2B3886FD26434A54F3726591 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8D19AA8D515FC8F5D2407378 /* Localizable.strings */; }; 2B6ACA26689B355CECBFFB57 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A7641E5C643B4B36CFEDA8 /* SetupView.swift */; }; 2FAE0FC43EB547F2CE7A567D /* FieldBackgroundExecutionUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59AC0543EF8335D691D56BD3 /* FieldBackgroundExecutionUITestProbe.swift */; }; - 33A800AA701C354099623B24 /* MarketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C0B870E44C7B152A7FABE0 /* MarketView.swift */; }; 360F23EFE80FDBDC6983FB15 /* AppEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D448C9655B708CA3FA8712B9 /* AppEntry.swift */; }; 3A7FA9E5BCC7590B2EAC5349 /* RelaySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBB081610305940C7849C7C /* RelaySettings.swift */; }; 3B6020E24A2DAD8ADFC2F155 /* BuildConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */; }; 3FC570AC038C3DC575E5A3E7 /* FieldDocumentInterchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F2BDC19EF9435162BFF5EC /* FieldDocumentInterchange.swift */; }; - 4B44B723FF06ECC363A486BA /* TradeListingDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BAE32CD1D46033DDA1A5BB /* TradeListingDetailView.swift */; }; - 505A5731ACDBBB0296134340 /* TradeListingCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947ADA6D32E42ED2B40A5351 /* TradeListingCreateView.swift */; }; 5AECD474FB2F91855BDD79C0 /* PostFeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21554DA87EEC1E5C5F38365 /* PostFeedViewModel.swift */; }; 5FB3E3E450EE7DFF30F3A005 /* FieldIdentityPolicyUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E150C6C18B2A06F2F3227C6 /* FieldIdentityPolicyUITestProbe.swift */; }; 657BEA5AAFF129E10177FE63 /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63189EB90A86A9929BECD9ED /* Nostr.swift */; }; @@ -51,7 +48,6 @@ D3E08BD0EB07C4E687BDAEF0 /* FieldBackgroundURLSessionEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA5C6DE4731C46D53B757E3 /* FieldBackgroundURLSessionEvents.swift */; }; D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */; }; D57452A5B550E4832913AF02 /* FieldUITestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901B3694434DC803B37651E8 /* FieldUITestSupport.swift */; }; - D5C58A98C950D45AD027962A /* TradeListing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D69D200DB1F5FA7AA561CD7 /* TradeListing.swift */; }; D62E9461833A0AA5E622A1E6 /* ToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227028B4EBDC6703999FB9DA /* ToastModifier.swift */; }; D9BF5BE7E4AB5EACBF342539 /* FieldSecureIdentityStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */; }; DCE468F668A3C346E716B04C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CCF0F7B3C57D8D770F178329 /* Assets.xcassets */; }; @@ -65,7 +61,6 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 08C0B870E44C7B152A7FABE0 /* MarketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketView.swift; sourceTree = "<group>"; }; 08FA88664E5E3ED3A24D56CC /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; 0A0274A0260D1C04F40C71AF /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; }; 138AA7BAA021EE13E829390B /* Bundle+Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Build.swift"; sourceTree = "<group>"; }; @@ -73,7 +68,6 @@ 16A7641E5C643B4B36CFEDA8 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; }; 19D317BC9F759709098490DD /* RadrootsAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsAppDelegate.swift; sourceTree = "<group>"; }; 227028B4EBDC6703999FB9DA /* ToastModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastModifier.swift; sourceTree = "<group>"; }; - 26BAE32CD1D46033DDA1A5BB /* TradeListingDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeListingDetailView.swift; sourceTree = "<group>"; }; 2818363B157125491FB84A1E /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; 2F3541389124A31F5D701A45 /* FieldLocationCheckIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldLocationCheckIn.swift; sourceTree = "<group>"; }; 32238D3DFB4465DEEDE6E003 /* FieldRuntimeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldRuntimeError.swift; sourceTree = "<group>"; }; @@ -93,7 +87,6 @@ 7BCA99336E305EC789152DDE /* radroots.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = radroots.local.xcconfig; sourceTree = "<group>"; }; 7C294E8EF50F5E1E73F5C135 /* Common.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = "<group>"; }; 7CC1D11E2E296B4B54E7E8A9 /* FieldCaptureIntake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCaptureIntake.swift; sourceTree = "<group>"; }; - 7D69D200DB1F5FA7AA561CD7 /* TradeListing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeListing.swift; sourceTree = "<group>"; }; 8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldSecureIdentityStore.swift; sourceTree = "<group>"; }; 8DA5C6DE4731C46D53B757E3 /* FieldBackgroundURLSessionEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldBackgroundURLSessionEvents.swift; sourceTree = "<group>"; }; 8E6A1827AB4419F806CD848F /* FieldTelemetryUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldTelemetryUITestProbe.swift; sourceTree = "<group>"; }; @@ -101,7 +94,6 @@ 901B3694434DC803B37651E8 /* FieldUITestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldUITestSupport.swift; sourceTree = "<group>"; }; 93AA285819DD1269C3EAD80A /* Radroots.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Radroots.app; sourceTree = BUILT_PRODUCTS_DIR; }; 93D729E070C32490545FA837 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; - 947ADA6D32E42ED2B40A5351 /* TradeListingCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeListingCreateView.swift; sourceTree = "<group>"; }; 9AE0EB327C10171444553378 /* PostFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedView.swift; sourceTree = "<group>"; }; 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldLocalState.swift; sourceTree = "<group>"; }; A0B0C9861CD86EAD3CAD549E /* View+Nav.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Nav.swift"; sourceTree = "<group>"; }; @@ -227,7 +219,6 @@ 63189EB90A86A9929BECD9ED /* Nostr.swift */, 8F0F21496E7A8490EB14AC5B /* Radroots.swift */, 6FBB081610305940C7849C7C /* RelaySettings.swift */, - 7D69D200DB1F5FA7AA561CD7 /* TradeListing.swift */, ); path = Runtime; sourceTree = "<group>"; @@ -280,7 +271,6 @@ isa = PBXGroup; children = ( 0A0274A0260D1C04F40C71AF /* HomeView.swift */, - 08C0B870E44C7B152A7FABE0 /* MarketView.swift */, CA8AAF0C0F1723860A8481E0 /* PostCreateView.swift */, F3C0EFACAD213A69C12D5064 /* PostDetailView.swift */, 9AE0EB327C10171444553378 /* PostFeedView.swift */, @@ -289,8 +279,6 @@ C1D9496F9F05A4E79E73A247 /* RelaysView.swift */, E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */, 16A7641E5C643B4B36CFEDA8 /* SetupView.swift */, - 947ADA6D32E42ED2B40A5351 /* TradeListingCreateView.swift */, - 26BAE32CD1D46033DDA1A5BB /* TradeListingDetailView.swift */, ); path = Views; sourceTree = "<group>"; @@ -465,7 +453,6 @@ 25654E50F9519809A237759D /* FieldUserPresenceGate.swift in Sources */, 1E5B41A3E1F9A7D68F63B079 /* HomeView.swift in Sources */, B8A3BBDE3A1FC0248512BF76 /* LoggingSettings.swift in Sources */, - 33A800AA701C354099623B24 /* MarketView.swift in Sources */, 657BEA5AAFF129E10177FE63 /* Nostr.swift in Sources */, F32EFF00A8A852F76657FEE1 /* PostCreateView.swift in Sources */, EB7C19F62D7DAB9C044D53AA /* PostDetailView.swift in Sources */, @@ -483,9 +470,6 @@ 2B6ACA26689B355CECBFFB57 /* SetupView.swift in Sources */, 7FD8FB018DA09568303194B2 /* Strings.swift in Sources */, D62E9461833A0AA5E622A1E6 /* ToastModifier.swift in Sources */, - D5C58A98C950D45AD027962A /* TradeListing.swift in Sources */, - 505A5731ACDBBB0296134340 /* TradeListingCreateView.swift in Sources */, - 4B44B723FF06ECC363A486BA /* TradeListingDetailView.swift in Sources */, B971351ABE8E79A472B4DC7D /* View+Nav.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Radroots/Runtime/TradeListing.swift b/Radroots/Runtime/TradeListing.swift @@ -1,17 +0,0 @@ -import Foundation - -public extension FieldRuntimeService { - func tradeListingPublish(draft: TradeListingDraft) async throws -> NostrEventId { - let id = try await run { try $0.tradeListingPublish(draft: draft) } - return NostrEventId(id) - } - - func tradeListingsFetch(limit: UInt16, sinceUnix: UInt64? = nil) async throws -> [TradeListingSummary] { - try await run { try $0.tradeListingsFetch(limit: limit, sinceUnix: sinceUnix) } - } - -} - -extension TradeListingSummary: Identifiable { - public var id: String { eventId } -} diff --git a/Radroots/Views/MarketView.swift b/Radroots/Views/MarketView.swift @@ -1,181 +0,0 @@ -import SwiftUI - -@MainActor -final class TradeListingsViewModel: ObservableObject { - @Published var listings: [TradeListingSummary] = [] - @Published var isLoading = false - @Published var errorMessage: String? - @Published var searchText: String = "" - - func loadIfNeeded(app: AppState) async { - if listings.isEmpty { - await refresh(app: app) - } - } - - func refresh(app: AppState) async { - guard let service = app.runtimeService else { return } - isLoading = true - errorMessage = nil - - do { - let items = try await service.tradeListingsFetch(limit: 60, sinceUnix: nil) - listings = items - isLoading = false - } catch { - errorMessage = error.fieldRuntimeMessage - isLoading = false - } - } -} - -struct MarketView: View { - @EnvironmentObject private var app: AppState - @StateObject private var vm = TradeListingsViewModel() - @State private var showCreate = false - - var body: some View { - List { - if let error = vm.errorMessage { - Section { - Text(error) - .foregroundStyle(.red) - .font(.footnote) - } - } - - if filteredListings.isEmpty { - if vm.isLoading { - HStack { - Spacer() - ProgressView() - Spacer() - } - .listRowBackground(Color.clear) - } else { - ContentUnavailableView( - "No Listings Yet", - systemImage: "leaf", - description: Text("Connect to relays and pull listings from the network.") - ) - .listRowBackground(Color.clear) - } - } else { - Section { - ForEach(filteredListings) { listing in - NavigationLink { - TradeListingDetailView(listing: listing) - } label: { - TradeListingRow(listing: listing) - } - } - } - } - } - .listStyle(.plain) - .navigationTitle("Market") - .accessibilityIdentifier("field_ios.market") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - if vm.isLoading { - ProgressView() - } - } - ToolbarItem(placement: .topBarTrailing) { - Button { - showCreate = true - } label: { - Image(systemName: "plus") - } - } - } - .searchable(text: $vm.searchText, placement: .navigationBarDrawer(displayMode: .always)) - .task { await vm.loadIfNeeded(app: app) } - .refreshable { await vm.refresh(app: app) } - .sheet(isPresented: $showCreate) { - NavigationStack { - TradeListingCreateView { - Task { await vm.refresh(app: app) } - } - } - } - } - - private var filteredListings: [TradeListingSummary] { - let trimmed = vm.searchText.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return vm.listings } - let needle = trimmed.lowercased() - return vm.listings.filter { listing in - let haystack = [ - listing.title, - listing.description, - listing.productType, - listing.location, - listing.sellerPubkey - ] - .joined(separator: " ") - .lowercased() - return haystack.contains(needle) - } - } -} - -private struct TradeListingRow: View { - let listing: TradeListingSummary - - var body: some View { - VStack(alignment: .leading, spacing: 6) { - HStack(alignment: .firstTextBaseline) { - Text(listing.title) - .font(.headline) - .foregroundStyle(.primary) - Spacer() - Text(listing.availability.capitalized) - .font(.caption.weight(.semibold)) - .foregroundStyle(.secondary) - } - - if !listing.description.isEmpty { - Text(listing.description) - .font(.subheadline) - .foregroundStyle(.secondary) - .lineLimit(2) - } - - HStack { - Text(priceLine) - .font(.subheadline.weight(.semibold)) - .foregroundStyle(.primary) - Spacer() - Text("Inventory \(listing.inventoryAvailable)") - .font(.subheadline) - .foregroundStyle(.secondary) - } - Text(binLine) - .font(.caption) - .foregroundStyle(.secondary) - - HStack(spacing: 12) { - Label(listing.deliveryMethod.capitalized, systemImage: "truck.box") - Label(listing.location, systemImage: "mappin.and.ellipse") - } - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) - } - .padding(.vertical, 4) - } - - private var priceLine: String { - "\(listing.unitPriceAmount) \(listing.unitPriceCurrency) / \(listing.unitPriceUnit)" - } - - private var binLine: String { - let label = listing.binDisplayLabel?.trimmingCharacters(in: .whitespacesAndNewlines) - let base = "Bin \(listing.binDisplayAmount) \(listing.binDisplayUnit)" - if let label, !label.isEmpty { - return "\(base) \(label)" - } - return base - } -} diff --git a/Radroots/Views/TradeListingCreateView.swift b/Radroots/Views/TradeListingCreateView.swift @@ -1,276 +0,0 @@ -import SwiftUI - -struct TradeListingCreateView: View { - @EnvironmentObject private var app: AppState - @Environment(\.dismiss) private var dismiss - private var onCreated: (() -> Void)? - - @State private var draft = ListingDraftState() - @State private var isPosting = false - @State private var errorMessage: String? - @FocusState private var focusedField: Field? - - init(onCreated: (() -> Void)? = nil) { - self.onCreated = onCreated - } - - var body: some View { - Form { - Section("Farm") { - TextField("Farm pubkey", text: $draft.farmPubkey) - .textInputAutocapitalization(.none) - .autocorrectionDisabled() - .submitLabel(.next) - .focused($focusedField, equals: .farmPubkey) - .onSubmit { focusedField = .farmDTag } - - TextField("Farm id", text: $draft.farmDTag) - .textInputAutocapitalization(.none) - .autocorrectionDisabled() - .submitLabel(.next) - .focused($focusedField, equals: .farmDTag) - .onSubmit { focusedField = .title } - } - - Section("Listing") { - TextField("Title", text: $draft.title) - .textInputAutocapitalization(.words) - .submitLabel(.next) - .focused($focusedField, equals: .title) - .onSubmit { focusedField = .description } - - TextEditor(text: $draft.description) - .frame(minHeight: 120) - .focused($focusedField, equals: .description) - } - - Section("Product") { - TextField("Category", text: $draft.category) - .textInputAutocapitalization(.words) - .submitLabel(.next) - .focused($focusedField, equals: .category) - .onSubmit { focusedField = .unitPrice } - } - - Section("Pricing") { - TextField("Unit price", text: $draft.unitPrice) - .keyboardType(.decimalPad) - .focused($focusedField, equals: .unitPrice) - - TextField("Currency", text: $draft.currency) - .textInputAutocapitalization(.characters) - .autocorrectionDisabled() - .focused($focusedField, equals: .currency) - - HStack { - TextField("Bin size", text: $draft.binDisplayAmount) - .keyboardType(.decimalPad) - .focused($focusedField, equals: .binDisplayAmount) - Picker("Unit", selection: $draft.binDisplayUnit) { - ForEach(ListingDraftState.UnitOption.allCases, id: \.self) { unit in - Text(unit.label).tag(unit) - } - } - .pickerStyle(.menu) - } - - TextField("Bin label (optional)", text: $draft.binLabel) - .textInputAutocapitalization(.words) - .focused($focusedField, equals: .binLabel) - - TextField("Inventory available", text: $draft.inventory) - .keyboardType(.decimalPad) - .focused($focusedField, equals: .inventory) - } - - Section("Delivery") { - Picker("Method", selection: $draft.deliveryMethod) { - ForEach(ListingDraftState.DeliveryMethod.allCases, id: \.self) { method in - Text(method.label).tag(method) - } - } - - TextField("Location", text: $draft.locationPrimary) - .textInputAutocapitalization(.words) - .focused($focusedField, equals: .locationPrimary) - - TextField("City", text: $draft.locationCity) - .textInputAutocapitalization(.words) - .focused($focusedField, equals: .locationCity) - - TextField("Region", text: $draft.locationRegion) - .textInputAutocapitalization(.words) - .focused($focusedField, equals: .locationRegion) - - TextField("Country", text: $draft.locationCountry) - .textInputAutocapitalization(.characters) - .focused($focusedField, equals: .locationCountry) - } - - Section { - SectionWideButton("Publish Listing", enabled: canPublish, isProminent: true) { - publish() - } - } footer: { - if let errorMessage { - Text(errorMessage).foregroundStyle(.red) - } else if app.relayConnectedCount == 0 { - Text("No relays connected. Configure relays before publishing.") - .foregroundStyle(.secondary) - } - } - } - .scrollDismissesKeyboard(.interactively) - .inlineNavigationTitle("New Listing") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - if isPosting { ProgressView() } - } - ToolbarItemGroup(placement: .keyboard) { - Spacer() - Button("Done") { focusedField = nil } - } - } - } - - private var canPublish: Bool { - app.relayConnectedCount > 0 && !isPosting && draft.isValid - } - - private func publish() { - guard let service = app.runtimeService else { return } - errorMessage = nil - isPosting = true - let draftValue = draft.toTradeListingDraft() - - Task { @MainActor in - do { - _ = try await service.tradeListingPublish(draft: draftValue) - isPosting = false - onCreated?() - dismiss() - } catch { - isPosting = false - errorMessage = error.fieldRuntimeMessage - } - } - } -} - -private enum Field: Hashable { - case farmPubkey - case farmDTag - case title - case description - case category - case unitPrice - case currency - case binDisplayAmount - case binLabel - case inventory - case locationPrimary - case locationCity - case locationRegion - case locationCountry -} - -private struct ListingDraftState { - enum UnitOption: String, CaseIterable { - case each - case lb - case oz - case g - case kg - case l - case ml - - var label: String { - switch self { - case .each: return "Each" - case .lb: return "lb" - case .oz: return "oz" - case .g: return "g" - case .kg: return "kg" - case .l: return "L" - case .ml: return "mL" - } - } - } - - enum DeliveryMethod: String, CaseIterable { - case pickup - case localDelivery - case shipping - - var label: String { - switch self { - case .pickup: return "Pickup" - case .localDelivery: return "Local delivery" - case .shipping: return "Shipping" - } - } - - var rawValueString: String { - switch self { - case .pickup: return "pickup" - case .localDelivery: return "local_delivery" - case .shipping: return "shipping" - } - } - } - - var title: String = "" - var description: String = "" - var category: String = "" - var farmPubkey: String = "" - var farmDTag: String = "" - var binDisplayUnit: UnitOption = .lb - var binDisplayAmount: String = "1" - var unitPrice: String = "" - var currency: String = "USD" - var binLabel: String = "" - var inventory: String = "" - var deliveryMethod: DeliveryMethod = .shipping - var locationPrimary: String = "" - var locationCity: String = "" - var locationRegion: String = "" - var locationCountry: String = "" - - var isValid: Bool { - !farmPubkey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - !farmDTag.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - !description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - !category.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - !binDisplayAmount.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - !unitPrice.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - !currency.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - !inventory.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - !locationPrimary.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - } - - func toTradeListingDraft() -> TradeListingDraft { - let trimmedLabel = binLabel.trimmingCharacters(in: .whitespacesAndNewlines) - return TradeListingDraft( - listingId: nil, - farmPubkey: farmPubkey, - farmDTag: farmDTag, - title: title, - description: description, - category: category, - binDisplayAmount: binDisplayAmount, - binDisplayUnit: binDisplayUnit.rawValue, - unitPrice: unitPrice, - currency: currency, - binLabel: trimmedLabel.isEmpty ? nil : trimmedLabel, - binId: nil, - inventory: inventory, - deliveryMethod: deliveryMethod.rawValueString, - locationPrimary: locationPrimary, - locationCity: locationCity.isEmpty ? nil : locationCity, - locationRegion: locationRegion.isEmpty ? nil : locationRegion, - locationCountry: locationCountry.isEmpty ? nil : locationCountry - ) - } - -} diff --git a/Radroots/Views/TradeListingDetailView.swift b/Radroots/Views/TradeListingDetailView.swift @@ -1,59 +0,0 @@ -import SwiftUI - -struct TradeListingDetailView: View { - let listing: TradeListingSummary - - init(listing: TradeListingSummary) { - self.listing = listing - } - - var body: some View { - List { - Section("Listing") { - LabeledContent("Title", value: listing.title) - if !listing.description.isEmpty { - LabeledContent("Description", value: listing.description) - } - LabeledContent("Category", value: listing.productType) - if !listing.availability.isEmpty { - LabeledContent("Availability", value: listing.availability.capitalized) - } - } - - Section("Pricing") { - LabeledContent("Unit price", value: priceLine) - LabeledContent("Bin size", value: binLine) - LabeledContent("Inventory", value: listing.inventoryAvailable) - } - - Section("Delivery") { - LabeledContent("Method", value: listing.deliveryMethod.capitalized) - LabeledContent("Location", value: listing.location) - } - - Section { - CopyRow(title: "Listing ID", value: listing.listingId) - CopyRow(title: "Event ID", value: listing.eventId) - CopyRow(title: "Seller", value: listing.sellerPubkey) - } header: { - Text("Event") - } - } - .listStyle(.insetGrouped) - .inlineNavigationTitle(listing.title) - } - - private var priceLine: String { - "\(listing.unitPriceAmount) \(listing.unitPriceCurrency) / \(listing.unitPriceUnit)" - } - - private var binLine: String { - let label = listing.binDisplayLabel?.trimmingCharacters(in: .whitespacesAndNewlines) - let base = "\(listing.binDisplayAmount) \(listing.binDisplayUnit)" - if let label, !label.isEmpty { - return "\(base) \(label)" - } - return base - } - -}