From 47b971894e49c1e30b62344af9046c3a12259011 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 24 Sep 2024 10:41:26 +0700 Subject: [PATCH 01/19] Render webhook button in settings view --- .../Settings/SettingsViewController.swift | 20 ++++++++++++++ .../Settings/Settings/SettingsViewModel.swift | 4 +-- .../Settings/Webhooks/WebhooksView.swift | 7 +++++ .../WooCommerce.xcodeproj/project.pbxproj | 27 +++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift index 1dec3da9ca6..3584324ab0b 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift @@ -164,6 +164,8 @@ private extension SettingsViewController { configureWhatsNew(cell: cell) case let cell as BasicTableViewCell where row == .deviceSettings: configureAppSettings(cell: cell) + case let cell as BasicTableViewCell where row == .webhooks: + configureWebhooks(cell: cell) case let cell as BasicTableViewCell where row == .wormholy: configureWormholy(cell: cell) case let cell as BasicTableViewCell where row == .accountSettings: @@ -260,6 +262,13 @@ private extension SettingsViewController { cell.textLabel?.text = Localization.openDeviceSettings } + func configureWebhooks(cell: BasicTableViewCell) { + cell.accessoryType = .disclosureIndicator + cell.selectionStyle = .default + cell.textLabel?.text = "Webhooks" + cell.accessibilityIdentifier = "settings-webhooks-button" + } + func configureWormholy(cell: BasicTableViewCell) { cell.accessoryType = .disclosureIndicator cell.selectionStyle = .default @@ -488,6 +497,12 @@ private extension SettingsViewController { NotificationCenter.default.post(name: NSNotification.Name(rawValue: "wormholy_fire"), object: nil) } + func webhooksWasPressed() { + // TODO-gm: track event featureWebhooksShown + let viewController = UIHostingController(rootView: WebhooksView()) + navigationController?.pushViewController(viewController, animated: true) + } + func whatsNewWasPressed() { ServiceLocator.analytics.track(event: .featureAnnouncementShown(source: .appSettings)) guard let announcement = viewModel.announcement else { return } @@ -643,6 +658,8 @@ extension SettingsViewController: UITableViewDelegate { aboutWasPressed() case .deviceSettings: deviceSettingsWasPressed() + case .webhooks: + webhooksWasPressed() case .wormholy: wormholyWasPressed() case .whatsNew: @@ -727,6 +744,7 @@ extension SettingsViewController { // Other case deviceSettings + case webhooks case wormholy // Account settings @@ -772,6 +790,8 @@ extension SettingsViewController { return BasicTableViewCell.self case .deviceSettings: return BasicTableViewCell.self + case .webhooks: + return BasicTableViewCell.self case .wormholy: return BasicTableViewCell.self case .whatsNew: diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewModel.swift index 60aa0de08af..f269e1fd273 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewModel.swift @@ -298,9 +298,9 @@ private extension SettingsViewModel { let otherSection: Section = { let rows: [Row] #if DEBUG - rows = [.deviceSettings, .wormholy] + rows = [.deviceSettings, .wormholy, .webhooks] #else - rows = [.deviceSettings] + rows = [.deviceSettings, .webhooks] #endif return Section(title: Localization.otherTitle, rows: rows, diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift new file mode 100644 index 00000000000..3555a5af199 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -0,0 +1,7 @@ +import SwiftUI + +struct WebhooksView: View { + var body: some View { + Text("Webhooks!") + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 68318d7cd56..99cae1a01ab 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1494,6 +1494,7 @@ 57F2C6CD246DECC10074063B /* SummaryTableViewCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F2C6CC246DECC10074063B /* SummaryTableViewCellViewModelTests.swift */; }; 57F42E40253768D600EA87F7 /* TitleAndEditableValueTableViewCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F42E3F253768D600EA87F7 /* TitleAndEditableValueTableViewCellViewModelTests.swift */; }; 581D5052274AA2480089B6AD /* View+AutofocusTextModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581D5051274AA2480089B6AD /* View+AutofocusTextModifier.swift */; }; + 6809106F2CA25FAE0057B02A /* WebhooksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6809106E2CA25FAE0057B02A /* WebhooksView.swift */; }; 680BA59A2A4C377900F5559D /* UpgradeViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680BA5992A4C377900F5559D /* UpgradeViewState.swift */; }; 680E36B52BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 680E36B42BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib */; }; 680E36B72BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680E36B62BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift */; }; @@ -4514,6 +4515,7 @@ 57F2C6CC246DECC10074063B /* SummaryTableViewCellViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryTableViewCellViewModelTests.swift; sourceTree = ""; }; 57F42E3F253768D600EA87F7 /* TitleAndEditableValueTableViewCellViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleAndEditableValueTableViewCellViewModelTests.swift; sourceTree = ""; }; 581D5051274AA2480089B6AD /* View+AutofocusTextModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AutofocusTextModifier.swift"; sourceTree = ""; }; + 6809106E2CA25FAE0057B02A /* WebhooksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhooksView.swift; sourceTree = ""; }; 680BA5992A4C377900F5559D /* UpgradeViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeViewState.swift; sourceTree = ""; }; 680E36B42BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OrderSubscriptionTableViewCell.xib; sourceTree = ""; }; 680E36B62BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderSubscriptionTableViewCellViewModel.swift; sourceTree = ""; }; @@ -9331,6 +9333,14 @@ path = TitleAndEditableValueTableViewCell; sourceTree = ""; }; + 6809106D2CA25F830057B02A /* Webhooks */ = { + isa = PBXGroup; + children = ( + 6809106E2CA25FAE0057B02A /* WebhooksView.swift */, + ); + path = Webhooks; + sourceTree = ""; + }; 682210EB2909664800814E14 /* Customer */ = { isa = PBXGroup; children = ( @@ -11839,6 +11849,7 @@ E138D4F2269ED99A006EA5C6 /* In-Person Payments */, CE27257A219249B5002B22EB /* Help */, CE22E3F821714639005A6BEF /* Privacy */, + 6809106D2CA25F830057B02A /* Webhooks */, 02E4A0842BFB1D1F006D4F87 /* POS */, 03191AE828E20C9200670723 /* PluginDetailsRowView.swift */, ); @@ -15660,6 +15671,7 @@ 02521E11243DC3C400DC7810 /* CancellableMedia.swift in Sources */, DE74A4522BCF87120009C415 /* TopPerformersDashboardView.swift in Sources */, CCCC29E325E576810046B96F /* RenameAttributesViewController.swift in Sources */, + 6809106F2CA25FAE0057B02A /* WebhooksView.swift in Sources */, 0300201029C0EBA400B09777 /* ReaderConnectionUnderlyingErrorDisplaying.swift in Sources */, EEB4E2DA29B5F8FC00371C3C /* CouponLineDetailsViewModel.swift in Sources */, B6C78B8E293BAE68008934A1 /* AnalyticsHubLastMonthRangeData.swift in Sources */, @@ -17384,7 +17396,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension; @@ -17399,6 +17413,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.woocommerce.notificationcontentextension; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.automattic.woocommerce.notificationcontentextension"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "WooCommerce Notification Content Exten Development"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -17483,7 +17498,9 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_OBJC_WEAK = YES; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; + "DEVELOPMENT_TEAM[sdk=watchos*]" = PZYM8XX95Q; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -17501,6 +17518,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.woocommerce.watchkitapp.widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.automattic.woocommerce.watchkitapp.widgets"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=watchos*]" = "WooCommerce Watch App Widgets Extension Developmen"; SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -17603,8 +17621,10 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_OBJC_WEAK = YES; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEVELOPMENT_ASSET_PATHS = "\"Woo Watch App/Preview Content\""; + "DEVELOPMENT_TEAM[sdk=watchos*]" = PZYM8XX95Q; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -17623,6 +17643,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.woocommerce.watchkitapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.automattic.woocommerce.watchkitapp"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=watchos*]" = "WooCommerce Watch App Development"; SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -17717,7 +17738,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_ENTITLEMENTS = "StoreWidgets/Entitlements/StoreWidgets-Release.entitlements"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = StoreWidgets/Info.plist; @@ -17732,6 +17755,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.woocommerce.storewidgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.automattic.woocommerce.storewidgets"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "WooCommerce Store Widgets Development"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -18055,7 +18079,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "Resources/Woo-Release.entitlements"; "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "Resources/Woo-Release-macOS.entitlements"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/Resources/Info.plist"; INFOPLIST_PREFIX_HEADER = DerivedSources/InfoPlist.h; @@ -18068,6 +18094,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.woocommerce; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.automattic.woocommerce"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "WooCommerce Development"; SUPPORTS_MACCATALYST = NO; SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; From 7252fbce92941457cd64ddb7d151b91e69fd28c2 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 24 Sep 2024 13:25:21 +0700 Subject: [PATCH 02/19] Create WebhooksService and networking layer --- .../Networking.xcodeproj/project.pbxproj | 12 ++++ .../Networking/Mapper/WebhookMapper.swift | 33 +++++++++++ Networking/Networking/Model/Webhook.swift | 56 +++++++++++++++++++ .../Networking/Remote/WebhooksRemote.swift | 14 +++++ .../Settings/Webhooks/WebhooksView.swift | 40 ++++++++++++- 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 Networking/Networking/Mapper/WebhookMapper.swift create mode 100644 Networking/Networking/Model/Webhook.swift create mode 100644 Networking/Networking/Remote/WebhooksRemote.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 6bd82200266..3cd9284a041 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -512,6 +512,9 @@ 57E8FED3246616AC0057CD68 /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E8FED2246616AC0057CD68 /* Result+Extensions.swift */; }; 621D043B29C9D4280040EC08 /* product-variation-alternative-types.json in Resources */ = {isa = PBXBuildFile; fileRef = 621D043A29C9D4280040EC08 /* product-variation-alternative-types.json */; }; 6647C0161DAC6AB6570C53A7 /* Pods_Networking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3F25DC15EC1D7C631169CB5 /* Pods_Networking.framework */; }; + 680910712CA279340057B02A /* Webhook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680910702CA279340057B02A /* Webhook.swift */; }; + 680910732CA27B900057B02A /* WebhookMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680910722CA27B900057B02A /* WebhookMapper.swift */; }; + 680910752CA27DC50057B02A /* WebhooksRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680910742CA27DC50057B02A /* WebhooksRemote.swift */; }; 6812FC012A6B27E100D7C625 /* InAppPurchasesTransactionMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6812FC002A6B27E100D7C625 /* InAppPurchasesTransactionMapperTests.swift */; }; 68255E442C60C6AA00090EBD /* products-load-all-for-eligibility-criteria.json in Resources */ = {isa = PBXBuildFile; fileRef = 68255E432C60C6AA00090EBD /* products-load-all-for-eligibility-criteria.json */; }; 6846B0152A619A5C008EB143 /* InAppPurchasesTransactionMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6846B0142A619A5C008EB143 /* InAppPurchasesTransactionMapper.swift */; }; @@ -1661,6 +1664,9 @@ 6132DCC72AA9C070E2033628 /* Pods_NetworkingWatchOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NetworkingWatchOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 61A45743E761BA40FF08B27D /* Pods-NetworkingWatchOS.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkingWatchOS.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-NetworkingWatchOS/Pods-NetworkingWatchOS.release-alpha.xcconfig"; sourceTree = ""; }; 621D043A29C9D4280040EC08 /* product-variation-alternative-types.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-variation-alternative-types.json"; sourceTree = ""; }; + 680910702CA279340057B02A /* Webhook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Webhook.swift; sourceTree = ""; }; + 680910722CA27B900057B02A /* WebhookMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookMapper.swift; sourceTree = ""; }; + 680910742CA27DC50057B02A /* WebhooksRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhooksRemote.swift; sourceTree = ""; }; 6812FC002A6B27E100D7C625 /* InAppPurchasesTransactionMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesTransactionMapperTests.swift; sourceTree = ""; }; 68255E432C60C6AA00090EBD /* products-load-all-for-eligibility-criteria.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "products-load-all-for-eligibility-criteria.json"; sourceTree = ""; }; 6846B0142A619A5C008EB143 /* InAppPurchasesTransactionMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesTransactionMapper.swift; sourceTree = ""; }; @@ -2857,6 +2863,7 @@ 029BA4EF255D7282006171FD /* ShippingLabelRemote.swift */, 311D412F2783C0E200052F64 /* StripeRemote.swift */, D8EDFE1D25EE87F1003D2213 /* WCPayRemote.swift */, + 680910742CA27DC50057B02A /* WebhooksRemote.swift */, FE28F6E5268429B6004465C7 /* UserRemote.swift */, 077F39D526A58E4500ABEADC /* SystemStatusRemote.swift */, AEF94584272974F2001DCCFB /* TelemetryRemote.swift */, @@ -2999,6 +3006,7 @@ 0359EA0C27AAC5F80048DE2D /* WCPayChargeStatus.swift */, 3105470B262E27F000C5C02B /* WCPayPaymentIntentStatusEnum.swift */, 0359EA0E27AAC6410048DE2D /* WCPayPaymentMethodDetails.swift */, + 680910702CA279340057B02A /* Webhook.swift */, 209AD3C22AC196E300825D76 /* WooPaymentsDepositsOverview.swift */, E1BAB2C62913FB5800C3982B /* WordPressApiError.swift */, DE2E8E9C29530EEF002E4B14 /* WordPressSite.swift */, @@ -3572,6 +3580,7 @@ FE28F6E326842848004465C7 /* UserMapper.swift */, 077F39D326A58DE700ABEADC /* SystemStatusMapper.swift */, DEC51AE827687AAF009F3DF4 /* SystemPluginMapper.swift */, + 680910722CA27B900057B02A /* WebhookMapper.swift */, 02C11275274285FF00F4F0B4 /* WooCommerceAvailabilityMapper.swift */, 02BE0A7A274B695F001176D2 /* WordPressMediaMapper.swift */, 02C112772742862600F4F0B4 /* WordPressSiteSettingsMapper.swift */, @@ -4886,6 +4895,7 @@ DEFBA74E29485A7600C35BA9 /* RESTRequest.swift in Sources */, 456930A9264EB576009ED69D /* ShippingLabelCarriersAndRates.swift in Sources */, 741B950120EBC8A700DD6E2D /* OrderCouponLine.swift in Sources */, + 680910732CA27B900057B02A /* WebhookMapper.swift in Sources */, 020D07BA23D8542000FD9580 /* UploadableMedia.swift in Sources */, DEC2961C26BBE764005A056B /* ShippingLabelCustomsForm.swift in Sources */, 31D27C8B26028D96002EDB1D /* SitePlugin.swift in Sources */, @@ -5097,6 +5107,7 @@ 4568E2222459ADC60007E478 /* SitePostsRemote.swift in Sources */, 02C11276274285FF00F4F0B4 /* WooCommerceAvailabilityMapper.swift in Sources */, DEB387762C2A9A140025256E /* GoogleAdsConnectionMapper.swift in Sources */, + 680910752CA27DC50057B02A /* WebhooksRemote.swift in Sources */, CC0786C5267BAF0F00BA9AC1 /* ShippingLabelStatusMapper.swift in Sources */, 4515280D257A7EEC0076B03C /* ProductAttributeListMapper.swift in Sources */, 93D8BBFD226BBEE800AD2EB3 /* AccountSettingsMapper.swift in Sources */, @@ -5242,6 +5253,7 @@ 45150A9E26836A57006922EA /* CountryListMapper.swift in Sources */, DE34051928BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift in Sources */, CE6BFEE82236D133005C79FB /* ProductDimensions.swift in Sources */, + 680910712CA279340057B02A /* Webhook.swift in Sources */, 077F39D426A58DE700ABEADC /* SystemStatusMapper.swift in Sources */, 45152811257A81730076B03C /* ProductAttributeMapper.swift in Sources */, DEB387732C2A8F9A0025256E /* GoogleAdsConnection.swift in Sources */, diff --git a/Networking/Networking/Mapper/WebhookMapper.swift b/Networking/Networking/Mapper/WebhookMapper.swift new file mode 100644 index 00000000000..c733e43416e --- /dev/null +++ b/Networking/Networking/Mapper/WebhookMapper.swift @@ -0,0 +1,33 @@ +import Foundation + +struct WebhookListMapper: Mapper { + /// Identifier associated to the webhooks that will be parsed from a given site + /// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints for webhook don't return the SiteID + /// + let siteID: Int64 + + /// Attempts to convert a dictionary into a `[Webhook]` object + /// + func map(response: Data) throws -> [Webhook] { + let decoder = JSONDecoder() + decoder.userInfo = [ + .siteID: siteID + ] + + if hasDataEnvelope(in: response) { + let decodedResponse = try decoder.decode(WebhookListEnvelope.self, from: response).webhooks + return decodedResponse + } else { + let decodedResponse = try decoder.decode([Webhook].self, from: response) + return decodedResponse + } + } +} + +struct WebhookListEnvelope: Decodable { + let webhooks: [Webhook] + + private enum CodingKeys: String, CodingKey { + case webhooks = "data" + } +} diff --git a/Networking/Networking/Model/Webhook.swift b/Networking/Networking/Model/Webhook.swift new file mode 100644 index 00000000000..d2358d35ce0 --- /dev/null +++ b/Networking/Networking/Model/Webhook.swift @@ -0,0 +1,56 @@ +import Foundation +import Codegen + +/// Represents a Webhook entity: +/// https://woocommerce.github.io/woocommerce-rest-api-docs/#webhooks +/// +public struct Webhook: Codable { + /// The siteID for the webhook + public let siteID: Int64 + + public let name: String? + public let topic: String + public let deliveryURL: URL + + /// Webhook struct initializer + /// + public init(siteID: Int64, + name: String?, + topic: String, + deliveryURL: URL) { + self.siteID = siteID + self.name = name + self.topic = topic + self.deliveryURL = deliveryURL + } + + /// Public initializer for the Webhook + /// + public init(from decoder: any Decoder) throws { + guard let siteID = decoder.userInfo[.siteID] as? Int64 else { + throw WebhookDecodingError.missingSiteID + } + let container = try decoder.container(keyedBy: CodingKeys.self) + + let name = try container.decodeIfPresent(String.self, forKey: .name) + let topic = try container.decode(String.self, forKey: .topic) + let deliveryURL = try container.decode(URL.self, forKey: .deliveryURL) + + self.init(siteID: siteID, + name: name, + topic: topic, + deliveryURL: deliveryURL) + } +} + +extension Webhook { + enum CodingKeys: String, CodingKey { + case name + case topic + case deliveryURL = "delivery_url" + } + + enum WebhookDecodingError: Error { + case missingSiteID + } +} diff --git a/Networking/Networking/Remote/WebhooksRemote.swift b/Networking/Networking/Remote/WebhooksRemote.swift new file mode 100644 index 00000000000..b20f9484057 --- /dev/null +++ b/Networking/Networking/Remote/WebhooksRemote.swift @@ -0,0 +1,14 @@ +import Foundation + +public class WebhooksRemote: Remote { + public func listAllWebhooks(for siteID: Int64) async throws -> [Webhook] { + let request = JetpackRequest(wooApiVersion: .mark3, + method: .get, + siteID: siteID, + path: "webhooks", + availableAsRESTRequest: true) + let mapper = WebhookListMapper(siteID: siteID) + + return try await enqueue(request, mapper: mapper) + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 3555a5af199..e55c743b1ea 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -1,7 +1,45 @@ import SwiftUI +import Networking + +final class WebhooksService: ObservableObject { + let credentials = ServiceLocator.stores.sessionManager.defaultCredentials + let siteID = ServiceLocator.stores.sessionManager.defaultSite?.siteID + var remote: WebhooksRemote + + var webhooks: [Webhook] = [] + + init() { + self.remote = WebhooksRemote(network: AlamofireNetwork(credentials: credentials)) + } + + @MainActor + func listAllWebhooks() async { + guard let siteID = ServiceLocator.stores.sessionManager.defaultSite?.siteID else { + debugPrint("🍍 Couldn't retrieve site ID") + return + } + Task { + webhooks = try await remote.listAllWebhooks(for: siteID) + debugPrint("🍍 Webhooks: \(webhooks)") + } + } +} struct WebhooksView: View { + @ObservedObject private var service = WebhooksService() + var body: some View { - Text("Webhooks!") + VStack { + Text("Webhooks") + Text("(Check the console!)") + .font(.caption) + } + .task { + await service.listAllWebhooks() + } } } + +#Preview { + WebhooksView() +} From cf588d4104b648b37dc9076481df35cc688e1e3a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 24 Sep 2024 15:59:31 +0700 Subject: [PATCH 03/19] Move webhook service to Yosemite --- .../Settings/SettingsViewController.swift | 3 +- .../Settings/Webhooks/WebhooksView.swift | 33 ++++--------------- .../Settings/Webhooks/WebhooksViewModel.swift | 26 +++++++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 4 +++ Yosemite/Yosemite.xcodeproj/project.pbxproj | 16 +++++++++ .../Yosemite/Tools/Webhooks/Webhook.swift | 13 ++++++++ .../Tools/Webhooks/WebhooksService.swift | 27 +++++++++++++++ 7 files changed, 94 insertions(+), 28 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift create mode 100644 Yosemite/Yosemite/Tools/Webhooks/Webhook.swift create mode 100644 Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift index 3584324ab0b..85a4c78141d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift @@ -499,7 +499,8 @@ private extension SettingsViewController { func webhooksWasPressed() { // TODO-gm: track event featureWebhooksShown - let viewController = UIHostingController(rootView: WebhooksView()) + let viewModel = WebhooksViewModel() + let viewController = UIHostingController(rootView: WebhooksView(viewModel: viewModel)) navigationController?.pushViewController(viewController, animated: true) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index e55c743b1ea..239a0cbc4ca 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -1,33 +1,12 @@ import SwiftUI -import Networking -final class WebhooksService: ObservableObject { - let credentials = ServiceLocator.stores.sessionManager.defaultCredentials - let siteID = ServiceLocator.stores.sessionManager.defaultSite?.siteID - var remote: WebhooksRemote - - var webhooks: [Webhook] = [] +struct WebhooksView: View { + @ObservedObject private var viewModel: WebhooksViewModel - init() { - self.remote = WebhooksRemote(network: AlamofireNetwork(credentials: credentials)) + init(viewModel: WebhooksViewModel) { + self.viewModel = viewModel } - @MainActor - func listAllWebhooks() async { - guard let siteID = ServiceLocator.stores.sessionManager.defaultSite?.siteID else { - debugPrint("🍍 Couldn't retrieve site ID") - return - } - Task { - webhooks = try await remote.listAllWebhooks(for: siteID) - debugPrint("🍍 Webhooks: \(webhooks)") - } - } -} - -struct WebhooksView: View { - @ObservedObject private var service = WebhooksService() - var body: some View { VStack { Text("Webhooks") @@ -35,11 +14,11 @@ struct WebhooksView: View { .font(.caption) } .task { - await service.listAllWebhooks() + await viewModel.listAllWebhooks() } } } #Preview { - WebhooksView() + WebhooksView(viewModel: WebhooksViewModel()) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift new file mode 100644 index 00000000000..89fa278bcdd --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift @@ -0,0 +1,26 @@ +import Foundation +import Yosemite +import SwiftUI + +final class WebhooksViewModel: ObservableObject { + var siteID: Int64 = ServiceLocator.stores.sessionManager.defaultSite?.siteID ?? 0 + var credentials: Credentials = ServiceLocator.stores.sessionManager.defaultCredentials ?? .init(authToken: "") + var service: WebhooksService + + var webhooks: [Webhook] = [] + + init() { + service = WebhooksService(siteID: siteID, credentials: credentials) + } + + @MainActor + func listAllWebhooks() async { + do { + webhooks = try await service.listAllWebhooks() + debugPrint("🍍 Webhooks: \(webhooks)") + } catch { + // TODO-gm: Modal with error + debugPrint(error) + } + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 99cae1a01ab..c797504166a 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1495,6 +1495,7 @@ 57F42E40253768D600EA87F7 /* TitleAndEditableValueTableViewCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F42E3F253768D600EA87F7 /* TitleAndEditableValueTableViewCellViewModelTests.swift */; }; 581D5052274AA2480089B6AD /* View+AutofocusTextModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581D5051274AA2480089B6AD /* View+AutofocusTextModifier.swift */; }; 6809106F2CA25FAE0057B02A /* WebhooksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6809106E2CA25FAE0057B02A /* WebhooksView.swift */; }; + 6809107A2CA2B37F0057B02A /* WebhooksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680910792CA2B37F0057B02A /* WebhooksViewModel.swift */; }; 680BA59A2A4C377900F5559D /* UpgradeViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680BA5992A4C377900F5559D /* UpgradeViewState.swift */; }; 680E36B52BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 680E36B42BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib */; }; 680E36B72BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680E36B62BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift */; }; @@ -4516,6 +4517,7 @@ 57F42E3F253768D600EA87F7 /* TitleAndEditableValueTableViewCellViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleAndEditableValueTableViewCellViewModelTests.swift; sourceTree = ""; }; 581D5051274AA2480089B6AD /* View+AutofocusTextModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AutofocusTextModifier.swift"; sourceTree = ""; }; 6809106E2CA25FAE0057B02A /* WebhooksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhooksView.swift; sourceTree = ""; }; + 680910792CA2B37F0057B02A /* WebhooksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhooksViewModel.swift; sourceTree = ""; }; 680BA5992A4C377900F5559D /* UpgradeViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeViewState.swift; sourceTree = ""; }; 680E36B42BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OrderSubscriptionTableViewCell.xib; sourceTree = ""; }; 680E36B62BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderSubscriptionTableViewCellViewModel.swift; sourceTree = ""; }; @@ -9337,6 +9339,7 @@ isa = PBXGroup; children = ( 6809106E2CA25FAE0057B02A /* WebhooksView.swift */, + 680910792CA2B37F0057B02A /* WebhooksViewModel.swift */, ); path = Webhooks; sourceTree = ""; @@ -15015,6 +15018,7 @@ 68E952D22875A44B0095A23D /* CardReaderType+Manual.swift in Sources */, 035DBA4B292E248C003E5125 /* CardPresentPaymentAlertsPresenter.swift in Sources */, 02D9EFCB2B69F91B00AE8968 /* ProductsSplitViewCoordinator.swift in Sources */, + 6809107A2CA2B37F0057B02A /* WebhooksViewModel.swift in Sources */, 0211259F2578DE310075AD2A /* ShippingLabelPrintingStepView.swift in Sources */, B58B4AB22108F01700076FDD /* NoticeView.swift in Sources */, 20D2CCA52C7E328300051705 /* POSModalCloseButton.swift in Sources */, diff --git a/Yosemite/Yosemite.xcodeproj/project.pbxproj b/Yosemite/Yosemite.xcodeproj/project.pbxproj index 0a789ed0c0d..382a8c79c84 100644 --- a/Yosemite/Yosemite.xcodeproj/project.pbxproj +++ b/Yosemite/Yosemite.xcodeproj/project.pbxproj @@ -251,6 +251,8 @@ 578CE7942475F52F00492EBF /* MockNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578CE7932475F52F00492EBF /* MockNote.swift */; }; 578CE7972475FD8200492EBF /* MockProductReview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578CE7962475FD8200492EBF /* MockProductReview.swift */; }; 57DFCC7925003C4000251E0C /* FetchResultSnapshotsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DFCC7825003C4000251E0C /* FetchResultSnapshotsProvider.swift */; }; + 680910782CA2B0AF0057B02A /* WebhooksService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680910772CA2B0AF0057B02A /* WebhooksService.swift */; }; + 6809107C2CA2B5900057B02A /* Webhook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6809107B2CA2B5900057B02A /* Webhook.swift */; }; 681D952B28E0F62B00C4039E /* CustomerAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681D952A28E0F62B00C4039E /* CustomerAction.swift */; }; 687F83722C0EBF8900460AB3 /* POSProductProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 687F83712C0EBF8900460AB3 /* POSProductProviderTests.swift */; }; 6889089F28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */; }; @@ -754,6 +756,8 @@ 578CE7962475FD8200492EBF /* MockProductReview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProductReview.swift; sourceTree = ""; }; 57DFCC7825003C4000251E0C /* FetchResultSnapshotsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchResultSnapshotsProvider.swift; sourceTree = ""; }; 585B973F61632665297738A3 /* Pods-Yosemite.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Yosemite.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-Yosemite/Pods-Yosemite.release-alpha.xcconfig"; sourceTree = ""; }; + 680910772CA2B0AF0057B02A /* WebhooksService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhooksService.swift; sourceTree = ""; }; + 6809107B2CA2B5900057B02A /* Webhook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Webhook.swift; sourceTree = ""; }; 681D952A28E0F62B00C4039E /* CustomerAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerAction.swift; sourceTree = ""; }; 687F83712C0EBF8900460AB3 /* POSProductProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSProductProviderTests.swift; sourceTree = ""; }; 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Customer+ReadOnlyConvertible.swift"; sourceTree = ""; }; @@ -1428,6 +1432,15 @@ path = SnapshotsProvider; sourceTree = ""; }; + 680910762CA2B08B0057B02A /* Webhooks */ = { + isa = PBXGroup; + children = ( + 6809107B2CA2B5900057B02A /* Webhook.swift */, + 680910772CA2B0AF0057B02A /* WebhooksService.swift */, + ); + path = Webhooks; + sourceTree = ""; + }; 687F83702C0EBF3E00460AB3 /* PointOfSale */ = { isa = PBXGroup; children = ( @@ -1600,6 +1613,7 @@ 02E262BB238CE45300B79588 /* ShippingSettings */, 209AD3CA2AC1A65700825D76 /* Payments */, B52E002C2119E6C000700FDE /* Internal */, + 680910762CA2B08B0057B02A /* Webhooks */, B5631ECC2114DF8C008D3535 /* EntityListener.swift */, B56C1EBD20EABD2B00D749F9 /* ResultsController.swift */, B56C1EC120EAE2E500D749F9 /* ReadOnlyConvertible.swift */, @@ -2307,6 +2321,7 @@ 02E3B623290267D3007E0F13 /* AccountCreationStore.swift in Sources */, 02FF056523DE9C8B0058E6E7 /* MediaStore.swift in Sources */, 7493751022498AB1007D85D1 /* ProductDefaultAttribute+ReadOnlyConvertible.swift in Sources */, + 6809107C2CA2B5900057B02A /* Webhook.swift in Sources */, 261F94E6242EFF8700762B58 /* ProductCategoryStore.swift in Sources */, 57DFCC7925003C4000251E0C /* FetchResultSnapshotsProvider.swift in Sources */, 7492FAD9217FAD1000ED2C69 /* SiteSetting+ReadOnlyConvertible.swift in Sources */, @@ -2355,6 +2370,7 @@ E18FDAFE28F97EB9008519BA /* AppAccountToken.swift in Sources */, 02FF055023D983F30058E6E7 /* ExportableAsset.swift in Sources */, 02C255022563C76A00A04423 /* ShippingLabel+ReadOnlyConvertible.swift in Sources */, + 680910782CA2B0AF0057B02A /* WebhooksService.swift in Sources */, 7493750E224988DE007D85D1 /* ProductImage+ReadOnlyConvertible.swift in Sources */, DA3F0E1C2C218EF100B8C2F8 /* POSOrderService.swift in Sources */, 26E5A08225A66868000DF8F6 /* ProductAttributeTermStore.swift in Sources */, diff --git a/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift b/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift new file mode 100644 index 00000000000..f94173ce59c --- /dev/null +++ b/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct Webhook { + public let name: String? + public let topic: String + public let deliveryURL: URL + + public init(name: String?, topic: String, deliveryURL: URL) { + self.name = name + self.topic = topic + self.deliveryURL = deliveryURL + } +} diff --git a/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift b/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift new file mode 100644 index 00000000000..1a9d4909ead --- /dev/null +++ b/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift @@ -0,0 +1,27 @@ +import Foundation +import Networking + +public final class WebhooksService: ObservableObject { + private let siteID: Int64 + public var remote: WebhooksRemote + + public init(siteID: Int64, credentials: Credentials) { + self.siteID = siteID + self.remote = WebhooksRemote(network: AlamofireNetwork(credentials: credentials)) + } + + /// Lists all site's webhooks by mapping `Networking.Webhook` to `Yosemite.Webhook` objects + /// + @MainActor + public func listAllWebhooks() async throws -> [Webhook] { + let webhooksFromRemote = try await remote.listAllWebhooks(for: siteID) + + let webhooks = webhooksFromRemote.map { + Webhook(name: $0.name, + topic: $0.topic, + deliveryURL: $0.deliveryURL) + } + + return webhooks + } +} From b6e53a08e7a2bdaf916642442b7250614462e263 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 24 Sep 2024 16:25:44 +0700 Subject: [PATCH 04/19] Render existing webhooks in view --- .../Settings/Webhooks/WebhooksView.swift | 77 ++++++++++++++++++- .../Settings/Webhooks/WebhooksViewModel.swift | 2 +- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 239a0cbc4ca..79bb5def043 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -1,19 +1,90 @@ import SwiftUI +import Yosemite + +struct WebhookRowViewModel: Identifiable { + var id = UUID() + let webhook: Webhook + + init(webhook: Webhook) { + self.webhook = webhook + } +} + +struct WebhookRowView: View { + private let viewModel: WebhookRowViewModel + + init(viewModel: WebhookRowViewModel) { + self.viewModel = viewModel + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + if let name = viewModel.webhook.name { + Text(name) + } + Spacer() + // TODO-gm: Forgot to model wh status! + Text("Active") + .background(Color.green) + } + Text("Topic: \(viewModel.webhook.topic)") + .font(.caption) + Text("Delivery: \(viewModel.webhook.deliveryURL)") + .font(.caption) + } + .padding(.vertical, 2) + .padding(.horizontal) + Divider() + } +} + +enum WebhooksViewState: String, CaseIterable, Identifiable { + case listAll = "List all Webhooks" + case createNew = "Create new Webhook" + + // Identifiable conformance + var id: String { self.rawValue } +} struct WebhooksView: View { @ObservedObject private var viewModel: WebhooksViewModel + @State private var viewState: WebhooksViewState = .listAll init(viewModel: WebhooksViewModel) { self.viewModel = viewModel } + var rowViewModels: [WebhookRowViewModel] { + viewModel.webhooks.map { + WebhookRowViewModel(webhook: $0) + } + } + var body: some View { VStack { - Text("Webhooks") - Text("(Check the console!)") - .font(.caption) + Picker("", selection: $viewState) { + ForEach(WebhooksViewState.allCases) { viewState in + Text(viewState.rawValue) + .tag(viewState) + } + } + .pickerStyle(SegmentedPickerStyle()) + switch viewState { + case .listAll: + List { + ForEach(rowViewModels) { viewModel in + WebhookRowView(viewModel: viewModel) + } + } + case .createNew: + Text("Create new") + } } .task { + // TODO-gm: + // Loading screen + // load only when in the correct viewState await viewModel.listAllWebhooks() } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift index 89fa278bcdd..2ad1f3ba3aa 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift @@ -7,7 +7,7 @@ final class WebhooksViewModel: ObservableObject { var credentials: Credentials = ServiceLocator.stores.sessionManager.defaultCredentials ?? .init(authToken: "") var service: WebhooksService - var webhooks: [Webhook] = [] + @Published var webhooks: [Webhook] = [] init() { service = WebhooksService(siteID: siteID, credentials: credentials) From 74c577f502a7e41d5cd02a523373f050bc2b53d7 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 10:51:08 +0700 Subject: [PATCH 05/19] Add logic for createWebhook() --- .../Networking.xcodeproj/project.pbxproj | 4 ++ .../Networking/Mapper/WebhookListMapper.swift | 33 +++++++++++++++ .../Networking/Mapper/WebhookMapper.swift | 16 +++---- .../Networking/Remote/WebhooksRemote.swift | 17 ++++++++ .../Settings/Webhooks/WebhooksView.swift | 42 ++++++++++++++++--- .../Settings/Webhooks/WebhooksViewModel.swift | 11 +++-- .../Tools/Webhooks/WebhooksService.swift | 8 ++++ 7 files changed, 114 insertions(+), 17 deletions(-) create mode 100644 Networking/Networking/Mapper/WebhookListMapper.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 3cd9284a041..a4bd8de9e43 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -512,6 +512,7 @@ 57E8FED3246616AC0057CD68 /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E8FED2246616AC0057CD68 /* Result+Extensions.swift */; }; 621D043B29C9D4280040EC08 /* product-variation-alternative-types.json in Resources */ = {isa = PBXBuildFile; fileRef = 621D043A29C9D4280040EC08 /* product-variation-alternative-types.json */; }; 6647C0161DAC6AB6570C53A7 /* Pods_Networking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3F25DC15EC1D7C631169CB5 /* Pods_Networking.framework */; }; + 6800B44C2CA3BDE900A64B4F /* WebhookListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6800B44B2CA3BDE900A64B4F /* WebhookListMapper.swift */; }; 680910712CA279340057B02A /* Webhook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680910702CA279340057B02A /* Webhook.swift */; }; 680910732CA27B900057B02A /* WebhookMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680910722CA27B900057B02A /* WebhookMapper.swift */; }; 680910752CA27DC50057B02A /* WebhooksRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680910742CA27DC50057B02A /* WebhooksRemote.swift */; }; @@ -1664,6 +1665,7 @@ 6132DCC72AA9C070E2033628 /* Pods_NetworkingWatchOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NetworkingWatchOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 61A45743E761BA40FF08B27D /* Pods-NetworkingWatchOS.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkingWatchOS.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-NetworkingWatchOS/Pods-NetworkingWatchOS.release-alpha.xcconfig"; sourceTree = ""; }; 621D043A29C9D4280040EC08 /* product-variation-alternative-types.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-variation-alternative-types.json"; sourceTree = ""; }; + 6800B44B2CA3BDE900A64B4F /* WebhookListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookListMapper.swift; sourceTree = ""; }; 680910702CA279340057B02A /* Webhook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Webhook.swift; sourceTree = ""; }; 680910722CA27B900057B02A /* WebhookMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookMapper.swift; sourceTree = ""; }; 680910742CA27DC50057B02A /* WebhooksRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhooksRemote.swift; sourceTree = ""; }; @@ -3581,6 +3583,7 @@ 077F39D326A58DE700ABEADC /* SystemStatusMapper.swift */, DEC51AE827687AAF009F3DF4 /* SystemPluginMapper.swift */, 680910722CA27B900057B02A /* WebhookMapper.swift */, + 6800B44B2CA3BDE900A64B4F /* WebhookListMapper.swift */, 02C11275274285FF00F4F0B4 /* WooCommerceAvailabilityMapper.swift */, 02BE0A7A274B695F001176D2 /* WordPressMediaMapper.swift */, 02C112772742862600F4F0B4 /* WordPressSiteSettingsMapper.swift */, @@ -4881,6 +4884,7 @@ 09EA564B27C75FCE00407D40 /* ProductVariationsBulkUpdateMapper.swift in Sources */, CE227093228DD44C00C0626C /* ProductStatus.swift in Sources */, 451A97E9260B657D0059D135 /* ShippingLabelPredefinedOption.swift in Sources */, + 6800B44C2CA3BDE900A64B4F /* WebhookListMapper.swift in Sources */, 263659DC2A264A3E00607A0D /* IPLocationRemote.swift in Sources */, CEB9BF432BB199600007978A /* ProductBundleStatsMapper.swift in Sources */, 02C2548425635BD000A04423 /* ShippingLabelPaperSize.swift in Sources */, diff --git a/Networking/Networking/Mapper/WebhookListMapper.swift b/Networking/Networking/Mapper/WebhookListMapper.swift new file mode 100644 index 00000000000..c733e43416e --- /dev/null +++ b/Networking/Networking/Mapper/WebhookListMapper.swift @@ -0,0 +1,33 @@ +import Foundation + +struct WebhookListMapper: Mapper { + /// Identifier associated to the webhooks that will be parsed from a given site + /// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints for webhook don't return the SiteID + /// + let siteID: Int64 + + /// Attempts to convert a dictionary into a `[Webhook]` object + /// + func map(response: Data) throws -> [Webhook] { + let decoder = JSONDecoder() + decoder.userInfo = [ + .siteID: siteID + ] + + if hasDataEnvelope(in: response) { + let decodedResponse = try decoder.decode(WebhookListEnvelope.self, from: response).webhooks + return decodedResponse + } else { + let decodedResponse = try decoder.decode([Webhook].self, from: response) + return decodedResponse + } + } +} + +struct WebhookListEnvelope: Decodable { + let webhooks: [Webhook] + + private enum CodingKeys: String, CodingKey { + case webhooks = "data" + } +} diff --git a/Networking/Networking/Mapper/WebhookMapper.swift b/Networking/Networking/Mapper/WebhookMapper.swift index c733e43416e..b9f1108bcdb 100644 --- a/Networking/Networking/Mapper/WebhookMapper.swift +++ b/Networking/Networking/Mapper/WebhookMapper.swift @@ -1,33 +1,33 @@ import Foundation -struct WebhookListMapper: Mapper { +struct WebhookMapper: Mapper { /// Identifier associated to the webhooks that will be parsed from a given site /// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints for webhook don't return the SiteID /// let siteID: Int64 - /// Attempts to convert a dictionary into a `[Webhook]` object + /// Attempts to convert a dictionary into a `Webhook` object /// - func map(response: Data) throws -> [Webhook] { + func map(response: Data) throws -> Webhook { let decoder = JSONDecoder() decoder.userInfo = [ .siteID: siteID ] if hasDataEnvelope(in: response) { - let decodedResponse = try decoder.decode(WebhookListEnvelope.self, from: response).webhooks + let decodedResponse = try decoder.decode(WebhookEnvelope.self, from: response).webhook return decodedResponse } else { - let decodedResponse = try decoder.decode([Webhook].self, from: response) + let decodedResponse = try decoder.decode(Webhook.self, from: response) return decodedResponse } } } -struct WebhookListEnvelope: Decodable { - let webhooks: [Webhook] +struct WebhookEnvelope: Decodable { + let webhook: Webhook private enum CodingKeys: String, CodingKey { - case webhooks = "data" + case webhook = "data" } } diff --git a/Networking/Networking/Remote/WebhooksRemote.swift b/Networking/Networking/Remote/WebhooksRemote.swift index b20f9484057..01b84684243 100644 --- a/Networking/Networking/Remote/WebhooksRemote.swift +++ b/Networking/Networking/Remote/WebhooksRemote.swift @@ -11,4 +11,21 @@ public class WebhooksRemote: Remote { return try await enqueue(request, mapper: mapper) } + + public func createWebhook(for siteID: Int64) async throws -> Webhook { + let parameters = [ + "topic": "", + "delivery_url": "" + ] + + let request = JetpackRequest(wooApiVersion: .mark3, + method: .post, + siteID: siteID, + path: "webhooks", + parameters: parameters, + availableAsRESTRequest: true) + let mapper = WebhookMapper(siteID: siteID) + + return try await enqueue(request, mapper: mapper) + } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 79bb5def043..81c2643b17a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -51,6 +51,10 @@ struct WebhooksView: View { @ObservedObject private var viewModel: WebhooksViewModel @State private var viewState: WebhooksViewState = .listAll + @State private var orderCreatedToggle: Bool = false + @State private var deliveryURLString: String = "" + @State private var showErrorModal: Bool = false + init(viewModel: WebhooksViewModel) { self.viewModel = viewModel } @@ -77,15 +81,43 @@ struct WebhooksView: View { WebhookRowView(viewModel: viewModel) } } + .listStyle(.plain) case .createNew: - Text("Create new") + VStack { + Toggle(isOn: $orderCreatedToggle, label: { Text("Order created")} ) + Spacer() + TextField("Delivery URL", text: $deliveryURLString) + .border(.black, width: 1.0) + Button(action: { + Task { + do { + try await viewModel.createWebhook() + } catch { + showErrorModal = true + } + } + }, label: { + Text("Create") + }) + .buttonStyle(PrimaryButtonStyle()) + Spacer() + } + .padding() } } .task { - // TODO-gm: - // Loading screen - // load only when in the correct viewState - await viewModel.listAllWebhooks() + if viewState == .listAll { + do { + try await viewModel.listAllWebhooks() + } catch { + showErrorModal = true + } + } + } + .alert(isPresented: $showErrorModal) { + Alert(title: Text("Error"), + message: Text("Error message"), + dismissButton: .default(Text("Dismiss"))) } } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift index 2ad1f3ba3aa..7bc41bcf8f4 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift @@ -14,13 +14,16 @@ final class WebhooksViewModel: ObservableObject { } @MainActor - func listAllWebhooks() async { + func listAllWebhooks() async throws { do { webhooks = try await service.listAllWebhooks() - debugPrint("🍍 Webhooks: \(webhooks)") } catch { - // TODO-gm: Modal with error - debugPrint(error) + throw NSError(domain: error.localizedDescription, code: 0) } } + + func createWebhook() async throws { + let webhook = try await service.createWebhook() + debugPrint("🍍 \(webhook)") + } } diff --git a/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift b/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift index 1a9d4909ead..ee85a03cd2e 100644 --- a/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift +++ b/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift @@ -24,4 +24,12 @@ public final class WebhooksService: ObservableObject { return webhooks } + + @MainActor + public func createWebhook() async throws -> Webhook { + let response = try await remote.createWebhook(for: siteID) + return Webhook(name: response.name, + topic: response.topic, + deliveryURL: response.deliveryURL) + } } From 1769adecddd23a95e7cf2966cd179f99d05761df Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 11:08:08 +0700 Subject: [PATCH 06/19] Pass delivery_url from UI. Disable button when toggle off --- Networking/Networking/Remote/WebhooksRemote.swift | 8 ++++---- .../Dashboard/Settings/Webhooks/WebhooksView.swift | 3 ++- .../Settings/Webhooks/WebhooksViewModel.swift | 11 ++++++++--- .../Yosemite/Tools/Webhooks/WebhooksService.swift | 6 ++++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Networking/Networking/Remote/WebhooksRemote.swift b/Networking/Networking/Remote/WebhooksRemote.swift index 01b84684243..d96181f04f1 100644 --- a/Networking/Networking/Remote/WebhooksRemote.swift +++ b/Networking/Networking/Remote/WebhooksRemote.swift @@ -12,15 +12,15 @@ public class WebhooksRemote: Remote { return try await enqueue(request, mapper: mapper) } - public func createWebhook(for siteID: Int64) async throws -> Webhook { + public func createWebhook(for siteID: Int64, topic: String, url: URL) async throws -> Webhook { let parameters = [ - "topic": "", - "delivery_url": "" + "topic": "\(topic)", + "delivery_url": "\(url.absoluteString)" ] let request = JetpackRequest(wooApiVersion: .mark3, method: .post, - siteID: siteID, + siteID: siteID, path: "webhooks", parameters: parameters, availableAsRESTRequest: true) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 81c2643b17a..a99d9a7da8d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -91,7 +91,7 @@ struct WebhooksView: View { Button(action: { Task { do { - try await viewModel.createWebhook() + try await viewModel.createWebhook( $deliveryURLString.wrappedValue) } catch { showErrorModal = true } @@ -99,6 +99,7 @@ struct WebhooksView: View { }, label: { Text("Create") }) + .disabled(orderCreatedToggle ? false : true) .buttonStyle(PrimaryButtonStyle()) Spacer() } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift index 7bc41bcf8f4..0d50a797f72 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift @@ -22,8 +22,13 @@ final class WebhooksViewModel: ObservableObject { } } - func createWebhook() async throws { - let webhook = try await service.createWebhook() - debugPrint("🍍 \(webhook)") + func createWebhook(_ deliveryURLString: String) async throws { + // At the moment we only allow for the order.updated webhook, so it's hardcoded + // On further iterations we can pass different selectable topics or custom actions down to the service. + let topic = "order.updated" + guard let url = URL(string: deliveryURLString) else { + throw NSError(domain: "Invalid URL", code: 0) + } + let webhook = try await service.createWebhook(topic: topic, url: url) } } diff --git a/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift b/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift index ee85a03cd2e..7f76eccfe62 100644 --- a/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift +++ b/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift @@ -26,8 +26,10 @@ public final class WebhooksService: ObservableObject { } @MainActor - public func createWebhook() async throws -> Webhook { - let response = try await remote.createWebhook(for: siteID) + public func createWebhook(topic: String, url: URL) async throws -> Webhook { + let response = try await remote.createWebhook(for: siteID, + topic: topic, + url: url) return Webhook(name: response.name, topic: response.topic, deliveryURL: response.deliveryURL) From 2401dd6f86803bb9e95b81f0587670ffee77b99e Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 11:13:51 +0700 Subject: [PATCH 07/19] update text field style --- .../ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index a99d9a7da8d..1d03675293a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -87,7 +87,7 @@ struct WebhooksView: View { Toggle(isOn: $orderCreatedToggle, label: { Text("Order created")} ) Spacer() TextField("Delivery URL", text: $deliveryURLString) - .border(.black, width: 1.0) + .textFieldStyle(RoundedBorderTextFieldStyle(focused: true)) Button(action: { Task { do { From 0daf1518afa95f99211982583c62e58da3adedec Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 11:38:46 +0700 Subject: [PATCH 08/19] Add webhook status --- Networking/Networking/Model/Webhook.swift | 6 ++++++ .../Settings/Webhooks/WebhooksView.swift | 18 +++++++++++++++--- .../Settings/Webhooks/WebhooksViewModel.swift | 4 ++-- Yosemite/Yosemite/Tools/Webhooks/Webhook.swift | 4 +++- .../Tools/Webhooks/WebhooksService.swift | 2 ++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Networking/Networking/Model/Webhook.swift b/Networking/Networking/Model/Webhook.swift index d2358d35ce0..484616e9cf3 100644 --- a/Networking/Networking/Model/Webhook.swift +++ b/Networking/Networking/Model/Webhook.swift @@ -9,6 +9,7 @@ public struct Webhook: Codable { public let siteID: Int64 public let name: String? + public let status: String public let topic: String public let deliveryURL: URL @@ -16,10 +17,12 @@ public struct Webhook: Codable { /// public init(siteID: Int64, name: String?, + status: String, topic: String, deliveryURL: URL) { self.siteID = siteID self.name = name + self.status = status self.topic = topic self.deliveryURL = deliveryURL } @@ -33,11 +36,13 @@ public struct Webhook: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) let name = try container.decodeIfPresent(String.self, forKey: .name) + let status = try container.decode(String.self, forKey: .status) let topic = try container.decode(String.self, forKey: .topic) let deliveryURL = try container.decode(URL.self, forKey: .deliveryURL) self.init(siteID: siteID, name: name, + status: status, topic: topic, deliveryURL: deliveryURL) } @@ -46,6 +51,7 @@ public struct Webhook: Codable { extension Webhook { enum CodingKeys: String, CodingKey { case name + case status case topic case deliveryURL = "delivery_url" } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 1d03675293a..4e3b5f76e07 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -16,6 +16,19 @@ struct WebhookRowView: View { init(viewModel: WebhookRowViewModel) { self.viewModel = viewModel } + + var statusLabelColor: Color { + switch viewModel.webhook.status { + case "active": + Color.green + case "paused": + Color.yellow + case "disabled": + Color.gray + default: + Color.white + } + } var body: some View { VStack(alignment: .leading) { @@ -24,9 +37,8 @@ struct WebhookRowView: View { Text(name) } Spacer() - // TODO-gm: Forgot to model wh status! - Text("Active") - .background(Color.green) + Text(viewModel.webhook.status) + .background(statusLabelColor) } Text("Topic: \(viewModel.webhook.topic)") .font(.caption) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift index 0d50a797f72..3907913e317 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift @@ -23,9 +23,9 @@ final class WebhooksViewModel: ObservableObject { } func createWebhook(_ deliveryURLString: String) async throws { - // At the moment we only allow for the order.updated webhook, so it's hardcoded + // At the moment we only allow for the order.created webhook, so it's hardcoded // On further iterations we can pass different selectable topics or custom actions down to the service. - let topic = "order.updated" + let topic = "order.created" guard let url = URL(string: deliveryURLString) else { throw NSError(domain: "Invalid URL", code: 0) } diff --git a/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift b/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift index f94173ce59c..0879f81594f 100644 --- a/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift +++ b/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift @@ -2,11 +2,13 @@ import Foundation public struct Webhook { public let name: String? + public let status: String public let topic: String public let deliveryURL: URL - public init(name: String?, topic: String, deliveryURL: URL) { + public init(name: String?, status: String, topic: String, deliveryURL: URL) { self.name = name + self.status = status self.topic = topic self.deliveryURL = deliveryURL } diff --git a/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift b/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift index 7f76eccfe62..954347ac1a7 100644 --- a/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift +++ b/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift @@ -18,6 +18,7 @@ public final class WebhooksService: ObservableObject { let webhooks = webhooksFromRemote.map { Webhook(name: $0.name, + status: $0.status, topic: $0.topic, deliveryURL: $0.deliveryURL) } @@ -31,6 +32,7 @@ public final class WebhooksService: ObservableObject { topic: topic, url: url) return Webhook(name: response.name, + status: response.status, topic: response.topic, deliveryURL: response.deliveryURL) } From ecfe2cb99c99a6a38a9062342d2e01a565e4925a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 11:54:47 +0700 Subject: [PATCH 09/19] Add loading indicator. delete dividers between items --- .../Settings/Webhooks/WebhooksView.swift | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 4e3b5f76e07..1df96f63a15 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -16,7 +16,7 @@ struct WebhookRowView: View { init(viewModel: WebhookRowViewModel) { self.viewModel = viewModel } - + var statusLabelColor: Color { switch viewModel.webhook.status { case "active": @@ -47,7 +47,6 @@ struct WebhookRowView: View { } .padding(.vertical, 2) .padding(.horizontal) - Divider() } } @@ -62,10 +61,12 @@ enum WebhooksViewState: String, CaseIterable, Identifiable { struct WebhooksView: View { @ObservedObject private var viewModel: WebhooksViewModel @State private var viewState: WebhooksViewState = .listAll + @State private var isLoading: Bool = false @State private var orderCreatedToggle: Bool = false @State private var deliveryURLString: String = "" @State private var showErrorModal: Bool = false + @State private var showSuccessModal: Bool = false init(viewModel: WebhooksViewModel) { self.viewModel = viewModel @@ -88,12 +89,18 @@ struct WebhooksView: View { .pickerStyle(SegmentedPickerStyle()) switch viewState { case .listAll: - List { - ForEach(rowViewModels) { viewModel in - WebhookRowView(viewModel: viewModel) + if isLoading { + ProgressView() + .padding() + Spacer() + } else { + List { + ForEach(rowViewModels) { viewModel in + WebhookRowView(viewModel: viewModel) + } } + .listStyle(.plain) } - .listStyle(.plain) case .createNew: VStack { Toggle(isOn: $orderCreatedToggle, label: { Text("Order created")} ) @@ -103,7 +110,8 @@ struct WebhooksView: View { Button(action: { Task { do { - try await viewModel.createWebhook( $deliveryURLString.wrappedValue) + try await viewModel.createWebhook($deliveryURLString.wrappedValue) + showSuccessModal = true } catch { showErrorModal = true } @@ -121,10 +129,12 @@ struct WebhooksView: View { .task { if viewState == .listAll { do { + isLoading = true try await viewModel.listAllWebhooks() } catch { showErrorModal = true } + isLoading = false } } .alert(isPresented: $showErrorModal) { @@ -132,6 +142,11 @@ struct WebhooksView: View { message: Text("Error message"), dismissButton: .default(Text("Dismiss"))) } + .alert(isPresented: $showSuccessModal) { + Alert(title: Text("Success!"), + message: Text("A new webhook has been created in your site."), + dismissButton: .default(Text("Ok"))) + } } } From e6504c39b2687e405841e026fe1230354adab2a1 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 14:31:02 +0700 Subject: [PATCH 10/19] Preset webhooks from selection. Add refreshable --- .../Settings/Webhooks/WebhooksView.swift | 31 ++++++++++++++++--- .../Settings/Webhooks/WebhooksViewModel.swift | 17 +++++++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 1df96f63a15..643cdc5cdd2 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -58,16 +58,24 @@ enum WebhooksViewState: String, CaseIterable, Identifiable { var id: String { self.rawValue } } +enum AvailableWebhook: String, CaseIterable { + case orderCreated = "Order created" + case couponCreated = "Coupon created" + case customerCreated = "Customer created" + case productCreated = "Product created" +} + struct WebhooksView: View { @ObservedObject private var viewModel: WebhooksViewModel @State private var viewState: WebhooksViewState = .listAll @State private var isLoading: Bool = false - @State private var orderCreatedToggle: Bool = false @State private var deliveryURLString: String = "" @State private var showErrorModal: Bool = false @State private var showSuccessModal: Bool = false + @State private var selectedOption: AvailableWebhook = .orderCreated + init(viewModel: WebhooksViewModel) { self.viewModel = viewModel } @@ -103,14 +111,21 @@ struct WebhooksView: View { } case .createNew: VStack { - Toggle(isOn: $orderCreatedToggle, label: { Text("Order created")} ) + Picker("option", selection: $selectedOption) { + ForEach(AvailableWebhook.allCases, id: \.self) { option in + Text(option.rawValue) + .tag(option) + } + } + .pickerStyle(.menu) Spacer() TextField("Delivery URL", text: $deliveryURLString) .textFieldStyle(RoundedBorderTextFieldStyle(focused: true)) Button(action: { Task { do { - try await viewModel.createWebhook($deliveryURLString.wrappedValue) + try await viewModel.createWebhook($selectedOption.wrappedValue, + $deliveryURLString.wrappedValue) showSuccessModal = true } catch { showErrorModal = true @@ -119,7 +134,6 @@ struct WebhooksView: View { }, label: { Text("Create") }) - .disabled(orderCreatedToggle ? false : true) .buttonStyle(PrimaryButtonStyle()) Spacer() } @@ -137,6 +151,15 @@ struct WebhooksView: View { isLoading = false } } + .refreshable { + do { + isLoading = true + try await viewModel.listAllWebhooks() + isLoading = false + } catch { + showErrorModal = true + } + } .alert(isPresented: $showErrorModal) { Alert(title: Text("Error"), message: Text("Error message"), diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift index 3907913e317..0a7d5d04c3d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift @@ -22,10 +22,19 @@ final class WebhooksViewModel: ObservableObject { } } - func createWebhook(_ deliveryURLString: String) async throws { - // At the moment we only allow for the order.created webhook, so it's hardcoded - // On further iterations we can pass different selectable topics or custom actions down to the service. - let topic = "order.created" + func createWebhook(_ webhook: AvailableWebhook, _ deliveryURLString: String) async throws { + var topic: String + switch webhook { + case .orderCreated: + topic = "order.created" + case .couponCreated: + topic = "coupon.created" + case .customerCreated: + topic = "customer.created" + case .productCreated: + topic = "product.created" + } + guard let url = URL(string: deliveryURLString) else { throw NSError(domain: "Invalid URL", code: 0) } From 3cb13df52485f18ecaa1c81160971db1a514f680 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 14:37:09 +0700 Subject: [PATCH 11/19] Analytics event for settingsWebhooksTapped --- WooCommerce/Classes/Analytics/WooAnalyticsStat.swift | 1 + .../Dashboard/Settings/Settings/SettingsViewController.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift index 7a4015df13c..a11f3d1f769 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift @@ -331,6 +331,7 @@ enum WooAnalyticsStat: String { case settingsLogoutTapped = "settings_logout_button_tapped" case settingsLogoutConfirmation = "settings_logout_confirmation_dialog_result" case settingsWereHiringTapped = "settings_we_are_hiring_button_tapped" + case settingsWebhooksTapped = "settings_webhooks_tapped" // MARK: Domain Settings // diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift index 85a4c78141d..f7fda1220d6 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift @@ -498,7 +498,7 @@ private extension SettingsViewController { } func webhooksWasPressed() { - // TODO-gm: track event featureWebhooksShown + ServiceLocator.analytics.track(.settingsWebhooksTapped) let viewModel = WebhooksViewModel() let viewController = UIHostingController(rootView: WebhooksView(viewModel: viewModel)) navigationController?.pushViewController(viewController, animated: true) From 9281205aaaf101771b88526256b4984bed24eb8f Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 15:09:16 +0700 Subject: [PATCH 12/19] Add text & image to empty list of webhooks --- .../Settings/Webhooks/WebhooksView.swift | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 643cdc5cdd2..9a98b6c14ad 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -102,12 +102,25 @@ struct WebhooksView: View { .padding() Spacer() } else { - List { - ForEach(rowViewModels) { viewModel in - WebhookRowView(viewModel: viewModel) + if rowViewModels.isEmpty { + VStack(alignment: .center) { + Spacer() + Image(.emptyBox) + Text("Webhooks are event notifications sent to URLs of your choice.") + .subheadlineStyle() + Text("They can be used to integrate with third-party services which support them.") + .subheadlineStyle() + Spacer() } + .padding() + } else { + List { + ForEach(rowViewModels) { viewModel in + WebhookRowView(viewModel: viewModel) + } + } + .listStyle(.plain) } - .listStyle(.plain) } case .createNew: VStack { From 1dc30c90a12a8cc9130990464c5a827a4886aa00 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 15:28:01 +0700 Subject: [PATCH 13/19] Extract vms to their own files --- .../Settings/Webhooks/WebhookRowView.swift | 42 ++++++++++++++++ .../Webhooks/WebhookRowViewModel.swift | 11 +++++ .../Settings/Webhooks/WebhooksView.swift | 49 ------------------- .../WooCommerce.xcodeproj/project.pbxproj | 8 +++ 4 files changed, 61 insertions(+), 49 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift create mode 100644 WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowViewModel.swift diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift new file mode 100644 index 00000000000..9a8623f9d55 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift @@ -0,0 +1,42 @@ +import Foundation +import SwiftUI + +struct WebhookRowView: View { + private let viewModel: WebhookRowViewModel + + init(viewModel: WebhookRowViewModel) { + self.viewModel = viewModel + } + + var statusLabelColor: Color { + switch viewModel.webhook.status { + case "active": + Color.green + case "paused": + Color.yellow + case "disabled": + Color.gray + default: + Color.white + } + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + if let name = viewModel.webhook.name { + Text(name) + } + Spacer() + Text(viewModel.webhook.status) + .background(statusLabelColor) + } + Text("Topic: \(viewModel.webhook.topic)") + .font(.caption) + Text("Delivery: \(viewModel.webhook.deliveryURL)") + .font(.caption) + } + .padding(.vertical, 2) + .padding(.horizontal) + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowViewModel.swift new file mode 100644 index 00000000000..0b063ab260a --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowViewModel.swift @@ -0,0 +1,11 @@ +import Foundation +import Yosemite + +struct WebhookRowViewModel: Identifiable { + var id = UUID() + let webhook: Webhook + + init(webhook: Webhook) { + self.webhook = webhook + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 9a98b6c14ad..6e5bc22a131 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -1,55 +1,6 @@ import SwiftUI import Yosemite -struct WebhookRowViewModel: Identifiable { - var id = UUID() - let webhook: Webhook - - init(webhook: Webhook) { - self.webhook = webhook - } -} - -struct WebhookRowView: View { - private let viewModel: WebhookRowViewModel - - init(viewModel: WebhookRowViewModel) { - self.viewModel = viewModel - } - - var statusLabelColor: Color { - switch viewModel.webhook.status { - case "active": - Color.green - case "paused": - Color.yellow - case "disabled": - Color.gray - default: - Color.white - } - } - - var body: some View { - VStack(alignment: .leading) { - HStack { - if let name = viewModel.webhook.name { - Text(name) - } - Spacer() - Text(viewModel.webhook.status) - .background(statusLabelColor) - } - Text("Topic: \(viewModel.webhook.topic)") - .font(.caption) - Text("Delivery: \(viewModel.webhook.deliveryURL)") - .font(.caption) - } - .padding(.vertical, 2) - .padding(.horizontal) - } -} - enum WebhooksViewState: String, CaseIterable, Identifiable { case listAll = "List all Webhooks" case createNew = "Create new Webhook" diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index c797504166a..8bf20ea5ddf 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1499,6 +1499,8 @@ 680BA59A2A4C377900F5559D /* UpgradeViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680BA5992A4C377900F5559D /* UpgradeViewState.swift */; }; 680E36B52BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 680E36B42BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib */; }; 680E36B72BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680E36B62BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift */; }; + 681842D62CA4006700246C90 /* WebhookRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681842D52CA4006700246C90 /* WebhookRowView.swift */; }; + 681842D82CA400AD00246C90 /* WebhookRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681842D72CA400AD00246C90 /* WebhookRowViewModel.swift */; }; 682210ED2909666600814E14 /* CustomerSearchUICommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682210EC2909666600814E14 /* CustomerSearchUICommandTests.swift */; }; 6827140F28A3988300E6E3F6 /* DismissableNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6827140E28A3988300E6E3F6 /* DismissableNoticeView.swift */; }; 6832C7CA26DA5C4500BA4088 /* LabeledTextViewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6832C7C926DA5C4500BA4088 /* LabeledTextViewTableViewCell.swift */; }; @@ -4521,6 +4523,8 @@ 680BA5992A4C377900F5559D /* UpgradeViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeViewState.swift; sourceTree = ""; }; 680E36B42BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OrderSubscriptionTableViewCell.xib; sourceTree = ""; }; 680E36B62BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderSubscriptionTableViewCellViewModel.swift; sourceTree = ""; }; + 681842D52CA4006700246C90 /* WebhookRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookRowView.swift; sourceTree = ""; }; + 681842D72CA400AD00246C90 /* WebhookRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookRowViewModel.swift; sourceTree = ""; }; 682210EC2909666600814E14 /* CustomerSearchUICommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerSearchUICommandTests.swift; sourceTree = ""; }; 6827140E28A3988300E6E3F6 /* DismissableNoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableNoticeView.swift; sourceTree = ""; }; 6832C7C926DA5C4500BA4088 /* LabeledTextViewTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabeledTextViewTableViewCell.swift; sourceTree = ""; }; @@ -9339,7 +9343,9 @@ isa = PBXGroup; children = ( 6809106E2CA25FAE0057B02A /* WebhooksView.swift */, + 681842D52CA4006700246C90 /* WebhookRowView.swift */, 680910792CA2B37F0057B02A /* WebhooksViewModel.swift */, + 681842D72CA400AD00246C90 /* WebhookRowViewModel.swift */, ); path = Webhooks; sourceTree = ""; @@ -14826,6 +14832,7 @@ 027EB56E29C0602D003CE551 /* StoreOnboardingLaunchStoreViewModel.swift in Sources */, 26C6439327B5DBE900DD00D1 /* OrderSynchronizer.swift in Sources */, CE0F17D222A8308900964A63 /* FancyAlertController+PurchaseNote.swift in Sources */, + 681842D82CA400AD00246C90 /* WebhookRowViewModel.swift in Sources */, 684AB83A2870677F003DFDD1 /* CardReaderManualsView.swift in Sources */, CEA455C12BB3446D00D932CF /* BundlesReportCardViewModel.swift in Sources */, 0230B4D62C33454900F2F660 /* PointOfSaleCardPresentPaymentCaptureErrorMessageView.swift in Sources */, @@ -16201,6 +16208,7 @@ EE1905842B579B6700617C53 /* BlazeCampaignCreationLoadingView.swift in Sources */, 020DD49123239DD6005822B1 /* PaginatedListViewControllerStateCoordinator.swift in Sources */, EE66BB0E2B29BF2800518DAF /* ThemeInstaller.swift in Sources */, + 681842D62CA4006700246C90 /* WebhookRowView.swift in Sources */, 0202B6952387AD1B00F3EBE0 /* UITabBar+Order.swift in Sources */, 26C98F9829C1247000F96503 /* WPComSitePlan+FreeTrial.swift in Sources */, 3120491726DD807900A4EC4F /* LabelAndButtonTableViewCell.swift in Sources */, From 1d78e43d40eef7a448c14cf5a4f309b1a17ebdad Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 16:33:01 +0700 Subject: [PATCH 14/19] UI updates --- .../Settings/Webhooks/WebhookRowView.swift | 7 +++- .../Settings/Webhooks/WebhooksView.swift | 39 +++++++++++++------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift index 9a8623f9d55..b2e1d7b2455 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift @@ -28,8 +28,11 @@ struct WebhookRowView: View { Text(name) } Spacer() - Text(viewModel.webhook.status) - .background(statusLabelColor) + Text(viewModel.webhook.status.capitalized) + .font(.caption) + .padding(2) + .background(RoundedRectangle(cornerRadius: CGFloat(8)).fill(statusLabelColor)) + .foregroundColor(.primary) } Text("Topic: \(viewModel.webhook.topic)") .font(.caption) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 6e5bc22a131..e8b99d356a5 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -2,7 +2,7 @@ import SwiftUI import Yosemite enum WebhooksViewState: String, CaseIterable, Identifiable { - case listAll = "List all Webhooks" + case listAll = "All Webhooks" case createNew = "Create new Webhook" // Identifiable conformance @@ -46,6 +46,7 @@ struct WebhooksView: View { } } .pickerStyle(SegmentedPickerStyle()) + .padding() switch viewState { case .listAll: if isLoading { @@ -56,10 +57,9 @@ struct WebhooksView: View { if rowViewModels.isEmpty { VStack(alignment: .center) { Spacer() - Image(.emptyBox) - Text("Webhooks are event notifications sent to URLs of your choice.") - .subheadlineStyle() - Text("They can be used to integrate with third-party services which support them.") + Image(.magnifyingGlassNotFound) + .padding(.bottom) + Text("No webhooks have been configured yet on your site.") .subheadlineStyle() Spacer() } @@ -75,16 +75,32 @@ struct WebhooksView: View { } case .createNew: VStack { - Picker("option", selection: $selectedOption) { - ForEach(AvailableWebhook.allCases, id: \.self) { option in - Text(option.rawValue) - .tag(option) + Group { + Text("Webhooks are event notifications sent to URLs of your choice.") + Text("They can be used to integrate with third-party services which support them.") + } + .subheadlineStyle() + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom) + + HStack() { + Text("Select a topic:") + .subheadlineStyle() + Spacer() + Picker("option", selection: $selectedOption) { + ForEach(AvailableWebhook.allCases, id: \.self) { option in + Text(option.rawValue) + .tag(option) + } } + .pickerStyle(.menu) } - .pickerStyle(.menu) - Spacer() + TextField("Delivery URL", text: $deliveryURLString) .textFieldStyle(RoundedBorderTextFieldStyle(focused: true)) + + Spacer() + Button(action: { Task { do { @@ -99,7 +115,6 @@ struct WebhooksView: View { Text("Create") }) .buttonStyle(PrimaryButtonStyle()) - Spacer() } .padding() } From 740d9f438d467304007e42e9b647af33806ef365 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 25 Sep 2024 17:05:41 +0700 Subject: [PATCH 15/19] Localized strings --- .../Settings/Webhooks/WebhookRowView.swift | 29 +++++- .../Settings/Webhooks/WebhooksView.swift | 91 ++++++++++++++++--- 2 files changed, 105 insertions(+), 15 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift index b2e1d7b2455..2847f6f6220 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhookRowView.swift @@ -21,6 +21,16 @@ struct WebhookRowView: View { } } + private var formattedTopicSelectionText: String { + String.localizedStringWithFormat(Localization.topicSelectionText, + "Topic: ", viewModel.webhook.topic) + } + + private var formattedDeliveryURLText: String { + String.localizedStringWithFormat(Localization.deliveryURLText, + "Delivery URL: ", viewModel.webhook.deliveryURL.absoluteString) + } + var body: some View { VStack(alignment: .leading) { HStack { @@ -34,12 +44,27 @@ struct WebhookRowView: View { .background(RoundedRectangle(cornerRadius: CGFloat(8)).fill(statusLabelColor)) .foregroundColor(.primary) } - Text("Topic: \(viewModel.webhook.topic)") + Text(formattedTopicSelectionText) .font(.caption) - Text("Delivery: \(viewModel.webhook.deliveryURL)") + Text(formattedDeliveryURLText) .font(.caption) } .padding(.vertical, 2) .padding(.horizontal) } } + +private extension WebhookRowView { + enum Localization { + static let topicSelectionText = + NSLocalizedString("settings.webhookRowView.topicSelectionText", + value: "%1$@ %2$@", + comment: "Hint shown to the merchant to choose one of different webhook options" + + "Reads as: 'Topic: {selection}'") + static let deliveryURLText = + NSLocalizedString("settings.webhookRowView.deliveryURLText", + value: "%1$@ %2$@", + comment: "Hint shown to the merchant to type a delivery URL for a webhook" + + "Reads as: 'Delivery URL: {URL}'") + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index e8b99d356a5..19a55cbcac4 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -59,7 +59,7 @@ struct WebhooksView: View { Spacer() Image(.magnifyingGlassNotFound) .padding(.bottom) - Text("No webhooks have been configured yet on your site.") + Text(Localization.noWebhooksFoundMessage) .subheadlineStyle() Spacer() } @@ -76,18 +76,18 @@ struct WebhooksView: View { case .createNew: VStack { Group { - Text("Webhooks are event notifications sent to URLs of your choice.") - Text("They can be used to integrate with third-party services which support them.") + Text(Localization.addNewWebhookHint1) + Text(Localization.addNewWebhookHint2) } .subheadlineStyle() .frame(maxWidth: .infinity, alignment: .leading) .padding(.bottom) HStack() { - Text("Select a topic:") + Text(Localization.topicSelectionHint) .subheadlineStyle() Spacer() - Picker("option", selection: $selectedOption) { + Picker("", selection: $selectedOption) { ForEach(AvailableWebhook.allCases, id: \.self) { option in Text(option.rawValue) .tag(option) @@ -96,7 +96,7 @@ struct WebhooksView: View { .pickerStyle(.menu) } - TextField("Delivery URL", text: $deliveryURLString) + TextField(Localization.deliveryURLPlaceholder, text: $deliveryURLString) .textFieldStyle(RoundedBorderTextFieldStyle(focused: true)) Spacer() @@ -112,7 +112,7 @@ struct WebhooksView: View { } } }, label: { - Text("Create") + Text(Localization.createWebhookButtonTitle) }) .buttonStyle(PrimaryButtonStyle()) } @@ -140,18 +140,83 @@ struct WebhooksView: View { } } .alert(isPresented: $showErrorModal) { - Alert(title: Text("Error"), - message: Text("Error message"), - dismissButton: .default(Text("Dismiss"))) + Alert(title: Text(Localization.createWebhookErrorTitle), + message: Text(Localization.createWebhookErrorMessage), + dismissButton: .default(Text(Localization.createWebhookErrorDismiss))) } .alert(isPresented: $showSuccessModal) { - Alert(title: Text("Success!"), - message: Text("A new webhook has been created in your site."), - dismissButton: .default(Text("Ok"))) + Alert(title: Text(Localization.createWebhookSuccessTitle), + message: Text(Localization.createWebhookSuccessMessage), + dismissButton: .default(Text(Localization.createWebhookSuccessOkButton))) } } } +private extension WebhooksView { + enum Localization { + static let noWebhooksFoundMessage = NSLocalizedString( + "settings.webhooksview.noWebhooksFoundMessage", + value: "No webhooks have been configured yet on your site.", + comment: "Message shown to the merchant when no webhooks are found on their site." + ) + static let addNewWebhookHint1 = NSLocalizedString( + "settings.webhooksview.addNewWebhookHint1", + value: "Webhooks are event notifications sent to URLs of your choice.", + comment: "Message shown to the merchant in order to setup their webhooks" + ) + static let addNewWebhookHint2 = NSLocalizedString( + "settings.webhooksview.addNewWebhookHint2", + value: "They can be used to integrate with third-party services which support them.", + comment: "Message shown to the merchant in order to setup their webhooks" + ) + static let deliveryURLPlaceholder = NSLocalizedString( + "settings.webhooksview.deliveryURLPlaceholder", + value: "Delivery URL:", + comment: "Texfield's placeholder message indicating an URL must be typed" + ) + static let createWebhookButtonTitle = NSLocalizedString( + "settings.webhooksview.createWebhookButtonTitle", + value: "Create", + comment: "Title for the button to create a webhook" + ) + static let createWebhookErrorTitle = NSLocalizedString( + "settings.webhooksview.createWebhookErrorTitle", + value: "Error", + comment: "Error title when webhook creation fails" + ) + static let createWebhookErrorMessage = NSLocalizedString( + "settings.webhooksview.createWebhookErrorMessage", + value: "There was an error creating the webhook. Please try again.", + comment: "Error message when webhook creation fails" + ) + static let createWebhookErrorDismiss = NSLocalizedString( + "settings.webhooksview.createWebhookErrorDismiss", + value: "Dismiss", + comment: "Title for the dismiss button when a webhook creation fails" + ) + static let createWebhookSuccessTitle = NSLocalizedString( + "settings.webhooksview.createWebhookSuccessTitle", + value: "Success!", + comment: "Title shown in an alert when a webhook creation succeeds" + ) + static let createWebhookSuccessMessage = NSLocalizedString( + "settings.webhooksview.createWebhookSuccessMessage", + value: "A new webhook has been created in your site.", + comment: "Message shown in an alert when a webhook creation succeeds" + ) + static let createWebhookSuccessOkButton = NSLocalizedString( + "settings.webhooksview.createWebhookSuccessOkButton", + value: "Ok", + comment: "Title for the button that dismisses the alert when a webhook creation succeeds" + ) + static let topicSelectionHint = NSLocalizedString( + "settings.webhooksview.topicSelectionHint", + value: "Select a topic:", + comment: "Message shown to the merchant so they can select between different available webhook options" + ) + } +} + #Preview { WebhooksView(viewModel: WebhooksViewModel()) } From bbafb40b8a4c25101521e36dca49168685ba7eb3 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 26 Sep 2024 09:49:40 +0700 Subject: [PATCH 16/19] listAllWebhooks unit tests --- .../Networking.xcodeproj/project.pbxproj | 12 +++ Networking/Networking/Model/Webhook.swift | 2 +- .../Remote/WebhooksRemoteTests.swift | 101 ++++++++++++++++++ .../Responses/webhooks-multiple.json | 61 +++++++++++ .../Responses/webhooks-single.json | 31 ++++++ 5 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 Networking/NetworkingTests/Remote/WebhooksRemoteTests.swift create mode 100644 Networking/NetworkingTests/Responses/webhooks-multiple.json create mode 100644 Networking/NetworkingTests/Responses/webhooks-single.json diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index a4bd8de9e43..c71821469bd 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -518,6 +518,9 @@ 680910752CA27DC50057B02A /* WebhooksRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680910742CA27DC50057B02A /* WebhooksRemote.swift */; }; 6812FC012A6B27E100D7C625 /* InAppPurchasesTransactionMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6812FC002A6B27E100D7C625 /* InAppPurchasesTransactionMapperTests.swift */; }; 68255E442C60C6AA00090EBD /* products-load-all-for-eligibility-criteria.json in Resources */ = {isa = PBXBuildFile; fileRef = 68255E432C60C6AA00090EBD /* products-load-all-for-eligibility-criteria.json */; }; + 682820512CA4F5C600295107 /* WebhooksRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682820502CA4F5C600295107 /* WebhooksRemoteTests.swift */; }; + 682820532CA4F98F00295107 /* webhooks-single.json in Resources */ = {isa = PBXBuildFile; fileRef = 682820522CA4F98F00295107 /* webhooks-single.json */; }; + 682820552CA4FEB400295107 /* webhooks-multiple.json in Resources */ = {isa = PBXBuildFile; fileRef = 682820542CA4FEB400295107 /* webhooks-multiple.json */; }; 6846B0152A619A5C008EB143 /* InAppPurchasesTransactionMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6846B0142A619A5C008EB143 /* InAppPurchasesTransactionMapper.swift */; }; 6846B01B2A61B590008EB143 /* iap-transaction-handled.json in Resources */ = {isa = PBXBuildFile; fileRef = 6846B01A2A61B590008EB143 /* iap-transaction-handled.json */; }; 684AB6D82AC2E93100106D7C /* order-with-special-character-currency.json in Resources */ = {isa = PBXBuildFile; fileRef = 684AB6D72AC2E93100106D7C /* order-with-special-character-currency.json */; }; @@ -1671,6 +1674,9 @@ 680910742CA27DC50057B02A /* WebhooksRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhooksRemote.swift; sourceTree = ""; }; 6812FC002A6B27E100D7C625 /* InAppPurchasesTransactionMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesTransactionMapperTests.swift; sourceTree = ""; }; 68255E432C60C6AA00090EBD /* products-load-all-for-eligibility-criteria.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "products-load-all-for-eligibility-criteria.json"; sourceTree = ""; }; + 682820502CA4F5C600295107 /* WebhooksRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhooksRemoteTests.swift; sourceTree = ""; }; + 682820522CA4F98F00295107 /* webhooks-single.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "webhooks-single.json"; sourceTree = ""; }; + 682820542CA4FEB400295107 /* webhooks-multiple.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "webhooks-multiple.json"; sourceTree = ""; }; 6846B0142A619A5C008EB143 /* InAppPurchasesTransactionMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesTransactionMapper.swift; sourceTree = ""; }; 6846B01A2A61B590008EB143 /* iap-transaction-handled.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "iap-transaction-handled.json"; sourceTree = ""; }; 684AB6D72AC2E93100106D7C /* order-with-special-character-currency.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "order-with-special-character-currency.json"; sourceTree = ""; }; @@ -2706,6 +2712,7 @@ DE34051E28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift */, 68BD37B228D9B8BD00C2A517 /* CustomerRemoteTests.swift */, 68F48B0E28E3BB850045C15B /* WCAnalyticsCustomerRemoteTests.swift */, + 682820502CA4F5C600295107 /* WebhooksRemoteTests.swift */, 0239306A291A96F800B2632F /* DomainRemoteTests.swift */, 02616F8B292132800095BC00 /* SiteRemoteTests.swift */, 02EF166D292F0C5800D90AD6 /* PaymentRemoteTests.swift */, @@ -3432,6 +3439,8 @@ 31B8D6B526583970008E3DB2 /* wcpay-account-implicitly-not-eligible.json */, 31799AFB2705189200D78179 /* wcpay-location.json */, 31799AFD270518AD00D78179 /* wcpay-location-error.json */, + 682820522CA4F98F00295107 /* webhooks-single.json */, + 682820542CA4FEB400295107 /* webhooks-multiple.json */, 02A26F1A2744F5FC008E4EDB /* wp-site-settings.json */, DEC51AEC2768A0AD009F3DF4 /* systemStatusWithPluginsOnly.json */, 077F39D726A58EB600ABEADC /* systemStatus.json */, @@ -4347,6 +4356,7 @@ 45AF57A924AB42CD0088E2F7 /* product-tags-extra.json in Resources */, 74AB5B4F21AF3F0E00859C12 /* site-api-no-woo.json in Resources */, DE66C559297799D000DAA978 /* add-on-groups-without-data.json in Resources */, + 682820552CA4FEB400295107 /* webhooks-multiple.json in Resources */, 265BCA02243056E3004E53EE /* categories-all.json in Resources */, D8FBFF2422D52815006E3336 /* order-stats-v4-daily.json in Resources */, EEE846A22A54745F005239B6 /* identify-language-success.json in Resources */, @@ -4532,6 +4542,7 @@ DE2004702BFB535500660A72 /* product-report.json in Resources */, EE57C13E297FBEE200BC31E7 /* product-tags-created-without-data.json in Resources */, CE21FB242C2C16DA00303832 /* google-ads-reports-programs.json in Resources */, + 682820532CA4F98F00295107 /* webhooks-single.json in Resources */, EEA658462966C67C00112DF0 /* products-ids-only-without-data.json in Resources */, DE2004762C001F7500660A72 /* variation-report.json in Resources */, DE2095C127966EC800171F1C /* coupon-reports.json in Resources */, @@ -5466,6 +5477,7 @@ 74002D6C2118B88200A63C19 /* SiteStatsRemoteTests.swift in Sources */, 0212683524C046CB00F8A892 /* MockNetwork+Path.swift in Sources */, EE1217DC2AFE04A500E6CAB1 /* ProductVariationEncoderTests.swift in Sources */, + 682820512CA4F5C600295107 /* WebhooksRemoteTests.swift in Sources */, 68BD37B328D9B8BD00C2A517 /* CustomerRemoteTests.swift in Sources */, B554FA932180C17200C54DFF /* NoteHashListMapperTests.swift in Sources */, EEC312C52AFE01BC004369F7 /* ProductEncoderTests.swift in Sources */, diff --git a/Networking/Networking/Model/Webhook.swift b/Networking/Networking/Model/Webhook.swift index 484616e9cf3..03a83f35e45 100644 --- a/Networking/Networking/Model/Webhook.swift +++ b/Networking/Networking/Model/Webhook.swift @@ -4,7 +4,7 @@ import Codegen /// Represents a Webhook entity: /// https://woocommerce.github.io/woocommerce-rest-api-docs/#webhooks /// -public struct Webhook: Codable { +public struct Webhook: Codable, Equatable { /// The siteID for the webhook public let siteID: Int64 diff --git a/Networking/NetworkingTests/Remote/WebhooksRemoteTests.swift b/Networking/NetworkingTests/Remote/WebhooksRemoteTests.swift new file mode 100644 index 00000000000..22e85da84fb --- /dev/null +++ b/Networking/NetworkingTests/Remote/WebhooksRemoteTests.swift @@ -0,0 +1,101 @@ +import XCTest +@testable import Networking + +final class WebhooksRemoteTests: XCTestCase { + private var network: MockNetwork! + private var sut: WebhooksRemote! + + override func setUp() { + super.setUp() + network = MockNetwork() + sut = WebhooksRemote(network: network) + } + + override func tearDown() { + network = nil + sut = nil + super.tearDown() + } + + func test_listAllWebhooks_when_no_webhooks_then_returns_empty_response() async { + // Given + var webhooks: [Webhook] = [] + network.simulateResponse(requestUrlSuffix: "webhooks", filename: "empty-data-array") + + do { + // When + webhooks = try await sut.listAllWebhooks(for: 1) + // Then + XCTAssertEqual(webhooks, []) + } catch { + XCTFail("Expected empty. Found \(webhooks)") + } + } + + func test_listAllWebhooks_when_single_webhook_then_parses_and_returns_single_webhook_successfully() async { + // Given + var webhooks: [Webhook] = [] + let expectedWebhooks = [Self.makeWebhookForTesting()] + network.simulateResponse(requestUrlSuffix: "webhooks", filename: "webhooks-single") + + do { + // When + webhooks = try await sut.listAllWebhooks(for: 1) + // Then + XCTAssertEqual(webhooks, expectedWebhooks) + } catch { + XCTFail("Expected \(expectedWebhooks). Got \(webhooks)") + } + } + + func test_listAllWebhooks_when_multiple_webhooks_then_parses_and_returns_multiple_webhooks_successfully() async { + // Given + var webhooks: [Webhook] = [] + let expectedWebhooks = Self.makeMultipleWebhooksForTesting() + network.simulateResponse(requestUrlSuffix: "webhooks", filename: "webhooks-multiple") + + do { + // When + webhooks = try await sut.listAllWebhooks(for: 1) + // Then + XCTAssertEqual(webhooks, expectedWebhooks) + } catch { + XCTFail("Expected \(expectedWebhooks). Got \(webhooks)") + } + } + + func test_listAllWebhooks_when_fails_then_throws_error() async { + // Given + let expectedError = NSError(domain: "Some error", code: 0) + network.simulateError(requestUrlSuffix: "webhooks", error: expectedError) + + do { + // When + _ = try await sut.listAllWebhooks(for: 1) + XCTFail("Expected an error, but got success.") + } catch { + // Then + XCTAssertEqual(error as NSError, expectedError) + } + } +} + +private extension WebhooksRemoteTests { + static func makeWebhookForTesting() -> Webhook { + Webhook(siteID: 1, + name: "Webhook created on Sep 26, 2024 @ 02:16 AM", + status: "active", + topic: "order.updated", + deliveryURL: URL(string: "https://server.site/1234")!) + } + + static func makeMultipleWebhooksForTesting() -> [Webhook] { + let firstWebhook = Self.makeWebhookForTesting() + let secondWebhook = Webhook(siteID: 1, + name: "Webhook created on Sep 26, 2024 @ 02:30 AM", + status: "active", + topic: "order.created", + deliveryURL: URL(string: "https://server.site/1234")!) + return [secondWebhook, firstWebhook] + } +} diff --git a/Networking/NetworkingTests/Responses/webhooks-multiple.json b/Networking/NetworkingTests/Responses/webhooks-multiple.json new file mode 100644 index 00000000000..f0466c0e5b6 --- /dev/null +++ b/Networking/NetworkingTests/Responses/webhooks-multiple.json @@ -0,0 +1,61 @@ +[ + { + "id": 2, + "name": "Webhook created on Sep 26, 2024 @ 02:30 AM", + "status": "active", + "topic": "order.created", + "resource": "order", + "event": "updated", + "hooks": [ + "woocommerce_new_order", + "wcs_webhook_order_created" + ], + "delivery_url": "https://server.site/1234", + "date_created": "2024-09-26T09:30:56", + "date_created_gmt": "2024-09-26T02:30:56", + "date_modified": "2024-09-26T09:30:57", + "date_modified_gmt": "2024-09-26T02:30:57", + "_links": { + "self": [ + { + "href": "https://example.exampletestsite.com/wp-json/wc/v3/webhooks/15" + } + ], + "collection": [ + { + "href": "https://example.exampletestsite.com/wp-json/wc/v3/webhooks" + } + ] + } + }, + { + "id": 1, + "name": "Webhook created on Sep 26, 2024 @ 02:16 AM", + "status": "active", + "topic": "order.updated", + "resource": "order", + "event": "updated", + "hooks": [ + "woocommerce_update_order", + "woocommerce_order_refunded" + ], + "delivery_url": "https://server.site/1234", + "date_created": "2024-09-26T09:16:56", + "date_created_gmt": "2024-09-26T02:16:56", + "date_modified": "2024-09-26T09:16:57", + "date_modified_gmt": "2024-09-26T02:16:57", + "_links": { + "self": [ + { + "href": "https://example.exampletestsite.com/wp-json/wc/v3/webhooks/15" + } + ], + "collection": [ + { + "href": "https://example.exampletestsite.com/wp-json/wc/v3/webhooks" + } + ] + } + } +] + diff --git a/Networking/NetworkingTests/Responses/webhooks-single.json b/Networking/NetworkingTests/Responses/webhooks-single.json new file mode 100644 index 00000000000..899ea6633b2 --- /dev/null +++ b/Networking/NetworkingTests/Responses/webhooks-single.json @@ -0,0 +1,31 @@ +[ + { + "id": 1, + "name": "Webhook created on Sep 26, 2024 @ 02:16 AM", + "status": "active", + "topic": "order.updated", + "resource": "order", + "event": "updated", + "hooks": [ + "woocommerce_update_order", + "woocommerce_order_refunded" + ], + "delivery_url": "https://server.site/1234", + "date_created": "2024-09-26T09:16:56", + "date_created_gmt": "2024-09-26T02:16:56", + "date_modified": "2024-09-26T09:16:57", + "date_modified_gmt": "2024-09-26T02:16:57", + "_links": { + "self": [ + { + "href": "https://example.exampletestsite.com/wp-json/wc/v3/webhooks/15" + } + ], + "collection": [ + { + "href": "https://example.exampletestsite.com/wp-json/wc/v3/webhooks" + } + ] + } + } +] From a4ff8fcd7726a940ae2dd28de15518fd1d7c2908 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 26 Sep 2024 10:07:55 +0700 Subject: [PATCH 17/19] unit tests for createWebhook --- .../Remote/WebhooksRemoteTests.swift | 62 +++++++++++++++---- .../Responses/webhooks-multiple.json | 36 +++++------ .../Responses/webhooks-single.json | 56 ++++++++--------- 3 files changed, 94 insertions(+), 60 deletions(-) diff --git a/Networking/NetworkingTests/Remote/WebhooksRemoteTests.swift b/Networking/NetworkingTests/Remote/WebhooksRemoteTests.swift index 22e85da84fb..02fe9561697 100644 --- a/Networking/NetworkingTests/Remote/WebhooksRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/WebhooksRemoteTests.swift @@ -4,6 +4,7 @@ import XCTest final class WebhooksRemoteTests: XCTestCase { private var network: MockNetwork! private var sut: WebhooksRemote! + private let sampleSiteID: Int64 = 1 override func setUp() { super.setUp() @@ -17,14 +18,14 @@ final class WebhooksRemoteTests: XCTestCase { super.tearDown() } - func test_listAllWebhooks_when_no_webhooks_then_returns_empty_response() async { + func test_listAllWebhooks_when_site_has_no_webhooks_then_returns_empty_response() async { // Given var webhooks: [Webhook] = [] network.simulateResponse(requestUrlSuffix: "webhooks", filename: "empty-data-array") do { // When - webhooks = try await sut.listAllWebhooks(for: 1) + webhooks = try await sut.listAllWebhooks(for: sampleSiteID) // Then XCTAssertEqual(webhooks, []) } catch { @@ -32,23 +33,23 @@ final class WebhooksRemoteTests: XCTestCase { } } - func test_listAllWebhooks_when_single_webhook_then_parses_and_returns_single_webhook_successfully() async { + func test_listAllWebhooks_when_site_has_single_webhook_then_parses_and_returns_single_webhook_successfully() async { // Given - var webhooks: [Webhook] = [] - let expectedWebhooks = [Self.makeWebhookForTesting()] - network.simulateResponse(requestUrlSuffix: "webhooks", filename: "webhooks-single") + var webhook: Webhook? + let expectedWebhook = Self.makeWebhookForTesting() + network.simulateResponse(requestUrlSuffix: "webhooks", filename: "webhooks-multiple") do { // When - webhooks = try await sut.listAllWebhooks(for: 1) + webhook = try await sut.listAllWebhooks(for: sampleSiteID).first // Then - XCTAssertEqual(webhooks, expectedWebhooks) + XCTAssertEqual(webhook, expectedWebhook) } catch { - XCTFail("Expected \(expectedWebhooks). Got \(webhooks)") + XCTFail("Expected \(expectedWebhook). Got \(String(describing: webhook))") } } - func test_listAllWebhooks_when_multiple_webhooks_then_parses_and_returns_multiple_webhooks_successfully() async { + func test_listAllWebhooks_when_site_has_multiple_webhooks_then_parses_and_returns_multiple_webhooks_successfully() async { // Given var webhooks: [Webhook] = [] let expectedWebhooks = Self.makeMultipleWebhooksForTesting() @@ -56,7 +57,7 @@ final class WebhooksRemoteTests: XCTestCase { do { // When - webhooks = try await sut.listAllWebhooks(for: 1) + webhooks = try await sut.listAllWebhooks(for: sampleSiteID) // Then XCTAssertEqual(webhooks, expectedWebhooks) } catch { @@ -71,7 +72,42 @@ final class WebhooksRemoteTests: XCTestCase { do { // When - _ = try await sut.listAllWebhooks(for: 1) + _ = try await sut.listAllWebhooks(for: sampleSiteID) + XCTFail("Expected an error, but got success.") + } catch { + // Then + XCTAssertEqual(error as NSError, expectedError) + } + } + + func test_createWebhook_then_creates_and_returns_single_webhook_successfully() async { + // Given + let topic = "order.created" + let url = URL(string: "https://server.site/1234")! + var webhook: Webhook? + let expectedWebhook = Self.makeWebhookForTesting() + network.simulateResponse(requestUrlSuffix: "webhooks", filename: "webhooks-single") + + do { + // When + webhook = try await sut.createWebhook(for: sampleSiteID, topic: topic, url: url) + // Then + XCTAssertEqual(webhook, expectedWebhook) + } catch { + XCTFail(error.localizedDescription) + } + } + + func test_createWebhook_when_fails_then_throws_error() async { + // Given + let topic = "order.created" + let url = URL(string: "https://server.site/1234")! + let expectedError = NSError(domain: "Some error", code: 0) + network.simulateError(requestUrlSuffix: "webhooks", error: expectedError) + + do { + // When + _ = try await sut.createWebhook(for: sampleSiteID, topic: topic, url: url) XCTFail("Expected an error, but got success.") } catch { // Then @@ -96,6 +132,6 @@ private extension WebhooksRemoteTests { status: "active", topic: "order.created", deliveryURL: URL(string: "https://server.site/1234")!) - return [secondWebhook, firstWebhook] + return [firstWebhook, secondWebhook] } } diff --git a/Networking/NetworkingTests/Responses/webhooks-multiple.json b/Networking/NetworkingTests/Responses/webhooks-multiple.json index f0466c0e5b6..96c4a9a8ab5 100644 --- a/Networking/NetworkingTests/Responses/webhooks-multiple.json +++ b/Networking/NetworkingTests/Responses/webhooks-multiple.json @@ -1,20 +1,20 @@ [ { - "id": 2, - "name": "Webhook created on Sep 26, 2024 @ 02:30 AM", + "id": 1, + "name": "Webhook created on Sep 26, 2024 @ 02:16 AM", "status": "active", - "topic": "order.created", + "topic": "order.updated", "resource": "order", "event": "updated", "hooks": [ - "woocommerce_new_order", - "wcs_webhook_order_created" + "woocommerce_update_order", + "woocommerce_order_refunded" ], "delivery_url": "https://server.site/1234", - "date_created": "2024-09-26T09:30:56", - "date_created_gmt": "2024-09-26T02:30:56", - "date_modified": "2024-09-26T09:30:57", - "date_modified_gmt": "2024-09-26T02:30:57", + "date_created": "2024-09-26T09:16:56", + "date_created_gmt": "2024-09-26T02:16:56", + "date_modified": "2024-09-26T09:16:57", + "date_modified_gmt": "2024-09-26T02:16:57", "_links": { "self": [ { @@ -29,21 +29,21 @@ } }, { - "id": 1, - "name": "Webhook created on Sep 26, 2024 @ 02:16 AM", + "id": 2, + "name": "Webhook created on Sep 26, 2024 @ 02:30 AM", "status": "active", - "topic": "order.updated", + "topic": "order.created", "resource": "order", "event": "updated", "hooks": [ - "woocommerce_update_order", - "woocommerce_order_refunded" + "woocommerce_new_order", + "wcs_webhook_order_created" ], "delivery_url": "https://server.site/1234", - "date_created": "2024-09-26T09:16:56", - "date_created_gmt": "2024-09-26T02:16:56", - "date_modified": "2024-09-26T09:16:57", - "date_modified_gmt": "2024-09-26T02:16:57", + "date_created": "2024-09-26T09:30:56", + "date_created_gmt": "2024-09-26T02:30:56", + "date_modified": "2024-09-26T09:30:57", + "date_modified_gmt": "2024-09-26T02:30:57", "_links": { "self": [ { diff --git a/Networking/NetworkingTests/Responses/webhooks-single.json b/Networking/NetworkingTests/Responses/webhooks-single.json index 899ea6633b2..538a797566c 100644 --- a/Networking/NetworkingTests/Responses/webhooks-single.json +++ b/Networking/NetworkingTests/Responses/webhooks-single.json @@ -1,31 +1,29 @@ -[ - { - "id": 1, - "name": "Webhook created on Sep 26, 2024 @ 02:16 AM", - "status": "active", - "topic": "order.updated", - "resource": "order", - "event": "updated", - "hooks": [ - "woocommerce_update_order", - "woocommerce_order_refunded" +{ + "id": 1, + "name": "Webhook created on Sep 26, 2024 @ 02:16 AM", + "status": "active", + "topic": "order.updated", + "resource": "order", + "event": "updated", + "hooks": [ + "woocommerce_update_order", + "woocommerce_order_refunded" + ], + "delivery_url": "https://server.site/1234", + "date_created": "2024-09-26T09:16:56", + "date_created_gmt": "2024-09-26T02:16:56", + "date_modified": "2024-09-26T09:16:57", + "date_modified_gmt": "2024-09-26T02:16:57", + "_links": { + "self": [ + { + "href": "https://example.exampletestsite.com/wp-json/wc/v3/webhooks/15" + } ], - "delivery_url": "https://server.site/1234", - "date_created": "2024-09-26T09:16:56", - "date_created_gmt": "2024-09-26T02:16:56", - "date_modified": "2024-09-26T09:16:57", - "date_modified_gmt": "2024-09-26T02:16:57", - "_links": { - "self": [ - { - "href": "https://example.exampletestsite.com/wp-json/wc/v3/webhooks/15" - } - ], - "collection": [ - { - "href": "https://example.exampletestsite.com/wp-json/wc/v3/webhooks" - } - ] - } + "collection": [ + { + "href": "https://example.exampletestsite.com/wp-json/wc/v3/webhooks" + } + ] } -] +} From 7fe358af4cdcf75934c74c55397ef8ed55195453 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 26 Sep 2024 10:43:37 +0700 Subject: [PATCH 18/19] webhook vm tests, protocol, and mock --- .../Settings/SettingsViewController.swift | 9 ++++- .../Settings/Webhooks/WebhooksView.swift | 2 +- .../Settings/Webhooks/WebhooksViewModel.swift | 8 ++--- .../WooCommerce.xcodeproj/project.pbxproj | 12 +++++++ .../Webhooks/WebhooksViewModelTests.swift | 34 +++++++++++++++++++ .../Yosemite/Tools/Webhooks/Webhook.swift | 2 +- .../Tools/Webhooks/WebhooksService.swift | 7 +++- 7 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 WooCommerce/WooCommerceTests/ViewRelated/Webhooks/WebhooksViewModelTests.swift diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift index f7fda1220d6..d772db933ec 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift @@ -499,7 +499,14 @@ private extension SettingsViewController { func webhooksWasPressed() { ServiceLocator.analytics.track(.settingsWebhooksTapped) - let viewModel = WebhooksViewModel() + + guard let siteID = stores.sessionManager.defaultSite?.siteID, + let credentials = stores.sessionManager.defaultCredentials else { + DDLogError("⛔️ Cannot find siteID or credentials needed for webhooks!") + return + } + let webhookService = WebhooksService(siteID: siteID, credentials: credentials) + let viewModel = WebhooksViewModel(service: webhookService) let viewController = UIHostingController(rootView: WebhooksView(viewModel: viewModel)) navigationController?.pushViewController(viewController, animated: true) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift index 19a55cbcac4..636d401812b 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksView.swift @@ -218,5 +218,5 @@ private extension WebhooksView { } #Preview { - WebhooksView(viewModel: WebhooksViewModel()) + WebhooksView(viewModel: WebhooksViewModel(service: WebhooksService(siteID: 1234, credentials: .init(authToken: "")))) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift index 0a7d5d04c3d..311df51a164 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift @@ -3,14 +3,12 @@ import Yosemite import SwiftUI final class WebhooksViewModel: ObservableObject { - var siteID: Int64 = ServiceLocator.stores.sessionManager.defaultSite?.siteID ?? 0 - var credentials: Credentials = ServiceLocator.stores.sessionManager.defaultCredentials ?? .init(authToken: "") - var service: WebhooksService + private let service: WebhooksServiceProtocol @Published var webhooks: [Webhook] = [] - init() { - service = WebhooksService(siteID: siteID, credentials: credentials) + init(service: WebhooksServiceProtocol) { + self.service = service } @MainActor diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 8bf20ea5ddf..f8610277066 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1503,6 +1503,7 @@ 681842D82CA400AD00246C90 /* WebhookRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681842D72CA400AD00246C90 /* WebhookRowViewModel.swift */; }; 682210ED2909666600814E14 /* CustomerSearchUICommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682210EC2909666600814E14 /* CustomerSearchUICommandTests.swift */; }; 6827140F28A3988300E6E3F6 /* DismissableNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6827140E28A3988300E6E3F6 /* DismissableNoticeView.swift */; }; + 682820582CA5084B00295107 /* WebhooksViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682820572CA5084B00295107 /* WebhooksViewModelTests.swift */; }; 6832C7CA26DA5C4500BA4088 /* LabeledTextViewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6832C7C926DA5C4500BA4088 /* LabeledTextViewTableViewCell.swift */; }; 6832C7CC26DA5FDF00BA4088 /* LabeledTextViewTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6832C7CB26DA5FDE00BA4088 /* LabeledTextViewTableViewCell.xib */; }; 683421642ACE9391009021D7 /* ProductDiscountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683421632ACE9391009021D7 /* ProductDiscountView.swift */; }; @@ -4527,6 +4528,7 @@ 681842D72CA400AD00246C90 /* WebhookRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookRowViewModel.swift; sourceTree = ""; }; 682210EC2909666600814E14 /* CustomerSearchUICommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerSearchUICommandTests.swift; sourceTree = ""; }; 6827140E28A3988300E6E3F6 /* DismissableNoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableNoticeView.swift; sourceTree = ""; }; + 682820572CA5084B00295107 /* WebhooksViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhooksViewModelTests.swift; sourceTree = ""; }; 6832C7C926DA5C4500BA4088 /* LabeledTextViewTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabeledTextViewTableViewCell.swift; sourceTree = ""; }; 6832C7CB26DA5FDE00BA4088 /* LabeledTextViewTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LabeledTextViewTableViewCell.xib; sourceTree = ""; }; 683421632ACE9391009021D7 /* ProductDiscountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDiscountView.swift; sourceTree = ""; }; @@ -9358,6 +9360,14 @@ path = Customer; sourceTree = ""; }; + 682820562CA5081C00295107 /* Webhooks */ = { + isa = PBXGroup; + children = ( + 682820572CA5084B00295107 /* WebhooksViewModelTests.swift */, + ); + path = Webhooks; + sourceTree = ""; + }; 6850C5EC2B69E6460026A93B /* Receipts */ = { isa = PBXGroup; children = ( @@ -12235,6 +12245,7 @@ 2619FA2A25C897720006DAFF /* Add Attributes */, 454453C62755170100464AC5 /* HubMenu */, 261E91A129C9880C00A5C118 /* Upgrades */, + 682820562CA5081C00295107 /* Webhooks */, CE6302442BAB527600E3325C /* Customers */, D85B8335222FCDA1002168F3 /* StatusListTableViewCellTests.swift */, D816DDBB22265DA300903E59 /* OrderTrackingTableViewCellTests.swift */, @@ -16934,6 +16945,7 @@ DE001323279A793A00EB0350 /* CouponWooTests.swift in Sources */, 45B98E1F25DECC1C00A1232B /* ShippingLabelAddressFormViewModelTests.swift in Sources */, EE4C75DF2C86D2F500F9D860 /* BlazeLocalNotificationSchedulerSpy.swift in Sources */, + 682820582CA5084B00295107 /* WebhooksViewModelTests.swift in Sources */, 028E1F722833E954001F8829 /* DashboardViewModelTests.swift in Sources */, 02BC5AA424D27F8900C43326 /* ProductVariationFormViewModel+ObservablesTests.swift in Sources */, CCCC5B1326CC2B9F0034FB63 /* ShippingLabelCustomPackageFormViewModelTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Webhooks/WebhooksViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Webhooks/WebhooksViewModelTests.swift new file mode 100644 index 00000000000..f81a8b63df7 --- /dev/null +++ b/WooCommerce/WooCommerceTests/ViewRelated/Webhooks/WebhooksViewModelTests.swift @@ -0,0 +1,34 @@ +import XCTest +import Yosemite +@testable import WooCommerce + +final class MockWebhooksService: WebhooksServiceProtocol { + var webhooks: [Webhook] = [] + + func listAllWebhooks() async throws -> [Yosemite.Webhook] { + webhooks + } + + func createWebhook(topic: String, url: URL) async throws -> Yosemite.Webhook { + Webhook(name: "name", + status: "status", + topic: "topic", + deliveryURL: URL(string: "https://server.site/1234")!) + } +} + +final class WebhooksViewModelTests: XCTestCase { + + func test_listAllWebhooks_when_site_has_no_webhooks_then_returns_empty() async { + let sut = WebhooksViewModel(service: MockWebhooksService()) + var expectedWebhooks: [Webhook] = [] + + do { + try await sut.listAllWebhooks() + XCTAssertEqual(sut.webhooks, expectedWebhooks) + } catch { + XCTFail(error.localizedDescription) + } + + } +} diff --git a/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift b/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift index 0879f81594f..cd18a4a6f83 100644 --- a/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift +++ b/Yosemite/Yosemite/Tools/Webhooks/Webhook.swift @@ -1,6 +1,6 @@ import Foundation -public struct Webhook { +public struct Webhook: Equatable { public let name: String? public let status: String public let topic: String diff --git a/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift b/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift index 954347ac1a7..62ef2384c1e 100644 --- a/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift +++ b/Yosemite/Yosemite/Tools/Webhooks/WebhooksService.swift @@ -1,7 +1,12 @@ import Foundation import Networking -public final class WebhooksService: ObservableObject { +public protocol WebhooksServiceProtocol { + func listAllWebhooks() async throws -> [Webhook] + func createWebhook(topic: String, url: URL) async throws -> Webhook +} + +public final class WebhooksService: WebhooksServiceProtocol, ObservableObject { private let siteID: Int64 public var remote: WebhooksRemote From fab68badd18b535d3c86298f5deea8575db4ffbc Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 26 Sep 2024 12:12:08 +0700 Subject: [PATCH 19/19] Add MockWebhooksService. Tests for createWebhook --- .../Settings/Webhooks/WebhooksViewModel.swift | 11 +- .../Webhooks/WebhooksViewModelTests.swift | 131 ++++++++++++++++-- 2 files changed, 124 insertions(+), 18 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift index 311df51a164..fb4b7b85fb8 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Webhooks/WebhooksViewModel.swift @@ -13,14 +13,11 @@ final class WebhooksViewModel: ObservableObject { @MainActor func listAllWebhooks() async throws { - do { - webhooks = try await service.listAllWebhooks() - } catch { - throw NSError(domain: error.localizedDescription, code: 0) - } + webhooks = try await service.listAllWebhooks() } - func createWebhook(_ webhook: AvailableWebhook, _ deliveryURLString: String) async throws { + @discardableResult + func createWebhook(_ webhook: AvailableWebhook, _ deliveryURLString: String) async throws -> Webhook { var topic: String switch webhook { case .orderCreated: @@ -36,6 +33,6 @@ final class WebhooksViewModel: ObservableObject { guard let url = URL(string: deliveryURLString) else { throw NSError(domain: "Invalid URL", code: 0) } - let webhook = try await service.createWebhook(topic: topic, url: url) + return try await service.createWebhook(topic: topic, url: url) } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Webhooks/WebhooksViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Webhooks/WebhooksViewModelTests.swift index f81a8b63df7..078216b5d6c 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Webhooks/WebhooksViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Webhooks/WebhooksViewModelTests.swift @@ -3,32 +3,141 @@ import Yosemite @testable import WooCommerce final class MockWebhooksService: WebhooksServiceProtocol { - var webhooks: [Webhook] = [] + var hasWebhooks: Bool = true + var hasError: Bool = false func listAllWebhooks() async throws -> [Yosemite.Webhook] { - webhooks + if hasError { + throw NSError(domain: "Error", code: 0) + } + if hasWebhooks { + return makeWebhooksForTesting() + } else { + return [] + } + } + + func createWebhook(topic: String = "order.created", + url: URL = URL(string: "https://server.site/1234")!) async throws -> Yosemite.Webhook { + let webhook = Webhook(name: "some name", + status: "active", + topic: topic, + deliveryURL: url) + if hasError { + throw NSError(domain: "Error", code: 0) + } else { + return webhook + } } - func createWebhook(topic: String, url: URL) async throws -> Yosemite.Webhook { - Webhook(name: "name", - status: "status", - topic: "topic", - deliveryURL: URL(string: "https://server.site/1234")!) + private func makeWebhooksForTesting() -> [Webhook] { + let firstWebhook = Webhook(name: "Webhook created on Sep 26, 2024 @ 02:16 AM", + status: "active", + topic: "order.updated", + deliveryURL: URL(string: "https://server.site/1234")!) + let secondWebhook = Webhook(name: "Webhook created on Sep 26, 2024 @ 02:30 AM", + status: "active", + topic: "order.created", + deliveryURL: URL(string: "https://server.site/1234")!) + return [firstWebhook, secondWebhook] } } final class WebhooksViewModelTests: XCTestCase { - func test_listAllWebhooks_when_site_has_no_webhooks_then_returns_empty() async { - let sut = WebhooksViewModel(service: MockWebhooksService()) - var expectedWebhooks: [Webhook] = [] - + // Given + let service = MockWebhooksService() + service.hasWebhooks = false + + let sut = WebhooksViewModel(service: service) + let expectedWebhooks: [Webhook] = [] + do { + // When try await sut.listAllWebhooks() + // Then XCTAssertEqual(sut.webhooks, expectedWebhooks) } catch { XCTFail(error.localizedDescription) } + } + func test_listAllWebhooks_when_site_has_webhooks_then_returns_expected_webhooks() async { + // Given + let service = MockWebhooksService() + service.hasWebhooks = true + + let sut = WebhooksViewModel(service: service) + let expectedWebhooks: [Webhook] = [ + Webhook(name: "Webhook created on Sep 26, 2024 @ 02:16 AM", + status: "active", + topic: "order.updated", + deliveryURL: URL(string: "https://server.site/1234")!), + Webhook(name: "Webhook created on Sep 26, 2024 @ 02:30 AM", + status: "active", + topic: "order.created", + deliveryURL: URL(string: "https://server.site/1234")!) + ] + + do { + // When + try await sut.listAllWebhooks() + // Then + XCTAssertEqual(sut.webhooks, expectedWebhooks) + } catch { + XCTFail(error.localizedDescription) + } + } + + func test_listAllWebhooks_when_there_is_an_error_then_returns_error() async { + // Given + let service = MockWebhooksService() + service.hasError = true + + let sut = WebhooksViewModel(service: service) + let expectedError = NSError(domain: "Error", code: 0) + + do { + // When + try await sut.listAllWebhooks() + XCTFail("Expected error, but got success.") + } catch { + // Then + XCTAssertEqual(error as NSError, expectedError) + } + } + + func test_createWebhook_ok() async throws { + // Given + let service = MockWebhooksService() + let sut = WebhooksViewModel(service: service) + let topic = AvailableWebhook.orderCreated + let deliveryURLString = "https://server.site/1234" + + // When + let expectedWebhook = try await sut.createWebhook(topic, deliveryURLString) + + // Then + XCTAssertEqual(expectedWebhook.name, "some name") + XCTAssertEqual(expectedWebhook.topic, "order.created") + XCTAssertEqual(expectedWebhook.status, "active") + XCTAssertEqual(expectedWebhook.deliveryURL, URL(string: "https://server.site/1234")!) + } + + func test_createWebhook_when_there_is_an_error_then_returns_error() async { + // Given + let service = MockWebhooksService() + service.hasError = true + let expectedError = NSError(domain: "Error", code: 0) + let sut = WebhooksViewModel(service: service) + + do { + // When + try await sut.createWebhook(.couponCreated, "some delivery url") + // Then + XCTFail("Expected failure, but got success.") + } catch { + XCTAssertEqual(error as NSError, expectedError) + } } }