From 7c6fbc6a06248b8ae922a036f87002ed0525c51e Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 29 Jul 2025 11:17:03 +0700 Subject: [PATCH 1/5] make tipkit render for ios17+ --- .../AddOrderComponentsSection.swift | 1 + .../CollapsibleProductCard.swift | 66 +++++++++++++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/AddOrderComponentsSection/AddOrderComponentsSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/AddOrderComponentsSection/AddOrderComponentsSection.swift index 149357e415f..65c033bc3a4 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/AddOrderComponentsSection/AddOrderComponentsSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/AddOrderComponentsSection/AddOrderComponentsSection.swift @@ -82,6 +82,7 @@ private extension AddOrderComponentsSection { .renderedIf(couponLineViewModel.couponLineRows.isEmpty) .padding() .accessibilityIdentifier("add-coupon-button") + // Usage #2 .tooltip(isPresented: $shouldShowCouponsInfoTooltip, toolTipTitle: Localization.couponsTooltipTitle, toolTipDescription: Localization.couponsTooltipDescription, diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift index e1f1f2237f3..ec58d7ee118 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift @@ -1,6 +1,23 @@ import Yosemite import SwiftUI +import TipKit + +/// TipKit tip for explaining when product discounts are unavailable due to coupons +@available(iOS 17.0, *) +struct ProductDiscountTip: Tip { + + var title: Text { + Text("Tipkit: Discounts unavailable") + } + + var message: Text? { + Text("To add a Product Discount, please remove all Coupons from your order") + } + var image: Image? { + Image(systemName: "questionmark.circle") + } +} /// Displays a single collapsible product row or grouped parent and child product rows struct CollapsibleProductCard: View { private let viewModel: CollapsibleProductCardViewModel @@ -127,6 +144,17 @@ private struct CollapsibleProductRowCard: View { private var shouldShowBadgeCounter: Bool { ServiceLocator.featureFlagService.isFeatureFlagEnabled(.subscriptionsInOrderCreationUI) } + + @available(iOS 17.0, *) + private func configureTips() { + do { + // Passing a configuration is necessary, despite not enforced on compile-time + // If no config is passed, the tip will not render, and show no feedback of why + try Tips.configure([.displayFrequency(.immediate)]) + } catch { + debugPrint("🍍 Configure TipKit failed") + } + } init(viewModel: CollapsibleProductRowCardViewModel, flow: WooAnalyticsEvent.Orders.Flow, @@ -292,7 +320,7 @@ private extension CollapsibleProductRowCard { } } -private extension CollapsibleProductRowCard { +extension CollapsibleProductRowCard { // Subscription details section. Renders all elements for a Subscription-type product @ViewBuilder var subscriptionDetailsSection: some View { VStack(spacing: Layout.subscriptionDetailsPadding) { @@ -372,7 +400,11 @@ private extension CollapsibleProductRowCard { Group { Spacer() Button { + if #available(iOS 17.0, *) { + configureTips() + } shouldShowInfoTooltip.toggle() + debugPrint("🍍 toggle tapped - shouldShowInfoTooltip: \(shouldShowInfoTooltip)") } label: { Image(systemName: "questionmark.circle") .foregroundColor(Color(.wooCommercePurple(.shade60))) @@ -380,9 +412,35 @@ private extension CollapsibleProductRowCard { } .renderedIf(shouldDisallowDiscounts) } - .tooltip(isPresented: $shouldShowInfoTooltip, - toolTipTitle: Localization.discountTooltipTitle, - toolTipDescription: Localization.discountTooltipDescription) + // Usage #1 + .modifier(CustomTooltipModifier(shouldShowInfoTooltip: $shouldShowInfoTooltip)) + } +} + +/// ViewModifier that handles both TipKit (iOS 17+) and custom tooltip (iOS 16-) +private struct CustomTooltipModifier: ViewModifier { + @Binding var shouldShowInfoTooltip: Bool + + func body(content: Content) -> some View { + if #available(iOS 17.0, *) { + // TipKit + content + .overlay(alignment: .topTrailing) { + if shouldShowInfoTooltip { + TipView(ProductDiscountTip(), arrowEdge: .leading) + .tipBackground(.regularMaterial) + .onTapGesture { + shouldShowInfoTooltip = false + } + } + } + } else { + // iOS 16 and below continues using the custom tooltip + content + .tooltip(isPresented: $shouldShowInfoTooltip, + toolTipTitle: "Custom: Discounts unavailable", + toolTipDescription: "To add a Product Discount, please remove all Coupons from your order.") + } } } From 9a2aab8fe6ba375756632ff0f9af40b5bea9fb15 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 29 Jul 2025 11:42:18 +0700 Subject: [PATCH 2/5] regenerate tip id to invalidate seen state. update bg color --- .../CollapsibleProductCard.swift | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift index ec58d7ee118..f2a43da8d1a 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift @@ -5,17 +5,21 @@ import TipKit /// TipKit tip for explaining when product discounts are unavailable due to coupons @available(iOS 17.0, *) struct ProductDiscountTip: Tip { - + // Forcing a new ID recreates invalidates the existing tip marked as `seen` by the system, + // since we're creating a new instance each time is toggled + var id: String = UUID().uuidString + var title: Text { Text("Tipkit: Discounts unavailable") + .font(.body) + .foregroundColor(.white) + .fontWeight(.bold) } var message: Text? { Text("To add a Product Discount, please remove all Coupons from your order") - } - - var image: Image? { - Image(systemName: "questionmark.circle") + .font(.body) + .foregroundColor(.gray) } } /// Displays a single collapsible product row or grouped parent and child product rows @@ -144,7 +148,7 @@ private struct CollapsibleProductRowCard: View { private var shouldShowBadgeCounter: Bool { ServiceLocator.featureFlagService.isFeatureFlagEnabled(.subscriptionsInOrderCreationUI) } - + @available(iOS 17.0, *) private func configureTips() { do { @@ -427,8 +431,8 @@ private struct CustomTooltipModifier: ViewModifier { content .overlay(alignment: .topTrailing) { if shouldShowInfoTooltip { - TipView(ProductDiscountTip(), arrowEdge: .leading) - .tipBackground(.regularMaterial) + TipView(ProductDiscountTip(), arrowEdge: .top) + .tipBackground(Color.black) .onTapGesture { shouldShowInfoTooltip = false } From 7fc52881be616d415c5a0349bdc64fbc204523b3 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 29 Jul 2025 11:52:51 +0700 Subject: [PATCH 3/5] Move tip to render in conditional Zstack --- .../CollapsibleProductCard.swift | 142 +++++++++++++----- 1 file changed, 102 insertions(+), 40 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift index f2a43da8d1a..128dd30c591 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift @@ -368,56 +368,118 @@ extension CollapsibleProductRowCard { } @ViewBuilder var discountRow: some View { - HStack { - if !viewModel.hasDiscount || shouldDisallowDiscounts { - Button(Localization.addDiscountLabel) { - trackAddDiscountTapped() - onAddDiscount(viewModel.id) - } - .buttonStyle(PlusButtonStyle()) - .disabled(shouldDisallowDiscounts || isLoading) - } else { + // Always render the row, overlay the tip if needed (iOS 17+), else fallback to .tooltip (iOS 16-) + if #available(iOS 17.0, *) { + ZStack { + // The row content HStack { - Button(action: { - trackEditDiscountTapped() - onAddDiscount(viewModel.id) - }, label: { + if !viewModel.hasDiscount || shouldDisallowDiscounts { + Button(Localization.addDiscountLabel) { + trackAddDiscountTapped() + onAddDiscount(viewModel.id) + } + .buttonStyle(PlusButtonStyle()) + .disabled(shouldDisallowDiscounts || isLoading) + } else { HStack { - Text(Localization.discountLabel) - Image(uiImage: .pencilImage) - .resizable() - .frame(width: Layout.iconSize, height: Layout.iconSize) + Button(action: { + trackEditDiscountTapped() + onAddDiscount(viewModel.id) + }, label: { + HStack { + Text(Localization.discountLabel) + Image(uiImage: .pencilImage) + .resizable() + .frame(width: Layout.iconSize, height: Layout.iconSize) + } + }) + .disabled(isLoading) + Spacer() + if let discountLabel = viewModel.discountLabel { + Text(minusSign + discountLabel) + .foregroundColor(.green) + .shimmering(active: isLoading) + } + } + .redacted(reason: shouldDisableDiscountEditing ? .placeholder : [] ) + } + Group { + Spacer() + Button { + configureTips() + shouldShowInfoTooltip.toggle() + debugPrint("🍍 toggle tapped - shouldShowInfoTooltip: \(shouldShowInfoTooltip)") + } label: { + Image(systemName: "questionmark.circle") + .foregroundColor(Color(.wooCommercePurple(.shade60))) } - }) - .disabled(isLoading) - Spacer() - if let discountLabel = viewModel.discountLabel { - Text(minusSign + discountLabel) - .foregroundColor(.green) - .shimmering(active: isLoading) } + .renderedIf(shouldDisallowDiscounts) + } + // Overlay the TipView and a mostly opaque black background if tooltip is visible + if shouldShowInfoTooltip { + Color.black.opacity(0.85) + .ignoresSafeArea() + .transition(.opacity) + .onTapGesture { + shouldShowInfoTooltip = false + } + TipView(ProductDiscountTip(), arrowEdge: .top) + .padding() + .onTapGesture { + shouldShowInfoTooltip = false + } } - // Redacts the discount editing row while product data is reloaded during remote sync. - // This avoids showing an out-of-date discount while hasn't synched - .redacted(reason: shouldDisableDiscountEditing ? .placeholder : [] ) } - Group { - Spacer() - Button { - if #available(iOS 17.0, *) { - configureTips() + } else { + // iOS 16 and below: use .tooltip as before + HStack { + if !viewModel.hasDiscount || shouldDisallowDiscounts { + Button(Localization.addDiscountLabel) { + trackAddDiscountTapped() + onAddDiscount(viewModel.id) + } + .buttonStyle(PlusButtonStyle()) + .disabled(shouldDisallowDiscounts || isLoading) + } else { + HStack { + Button(action: { + trackEditDiscountTapped() + onAddDiscount(viewModel.id) + }, label: { + HStack { + Text(Localization.discountLabel) + Image(uiImage: .pencilImage) + .resizable() + .frame(width: Layout.iconSize, height: Layout.iconSize) + } + }) + .disabled(isLoading) + Spacer() + if let discountLabel = viewModel.discountLabel { + Text(minusSign + discountLabel) + .foregroundColor(.green) + .shimmering(active: isLoading) + } + } + .redacted(reason: shouldDisableDiscountEditing ? .placeholder : [] ) + } + Group { + Spacer() + Button { + shouldShowInfoTooltip.toggle() + debugPrint("🍍 toggle tapped - shouldShowInfoTooltip: \(shouldShowInfoTooltip)") + } label: { + Image(systemName: "questionmark.circle") + .foregroundColor(Color(.wooCommercePurple(.shade60))) } - shouldShowInfoTooltip.toggle() - debugPrint("🍍 toggle tapped - shouldShowInfoTooltip: \(shouldShowInfoTooltip)") - } label: { - Image(systemName: "questionmark.circle") - .foregroundColor(Color(.wooCommercePurple(.shade60))) } + .renderedIf(shouldDisallowDiscounts) } - .renderedIf(shouldDisallowDiscounts) + .tooltip(isPresented: $shouldShowInfoTooltip, + toolTipTitle: Localization.discountTooltipTitle, + toolTipDescription: Localization.discountTooltipDescription) } - // Usage #1 - .modifier(CustomTooltipModifier(shouldShowInfoTooltip: $shouldShowInfoTooltip)) } } From 69b2484f3d47c05c796bd81727d3273f58cff956 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 29 Jul 2025 11:57:13 +0700 Subject: [PATCH 4/5] update tip style --- .../Orders/Order Creation/CollapsibleProductCard.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift index 128dd30c591..6495ccf56f6 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift @@ -418,13 +418,15 @@ extension CollapsibleProductRowCard { } // Overlay the TipView and a mostly opaque black background if tooltip is visible if shouldShowInfoTooltip { - Color.black.opacity(0.85) + Color(.systemBackground).opacity(0.01) .ignoresSafeArea() + .contentShape(Rectangle()) .transition(.opacity) .onTapGesture { shouldShowInfoTooltip = false } TipView(ProductDiscountTip(), arrowEdge: .top) + .tipBackground(Color.black) .padding() .onTapGesture { shouldShowInfoTooltip = false From c87d33df7371e81869be981f5b81d6a884d15d96 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 29 Jul 2025 16:07:59 +0700 Subject: [PATCH 5/5] set tip to trailing --- .../Orders/Order Creation/CollapsibleProductCard.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift index 6495ccf56f6..3bc310e3b1f 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CollapsibleProductCard.swift @@ -425,7 +425,7 @@ extension CollapsibleProductRowCard { .onTapGesture { shouldShowInfoTooltip = false } - TipView(ProductDiscountTip(), arrowEdge: .top) + TipView(ProductDiscountTip(), arrowEdge: .trailing) .tipBackground(Color.black) .padding() .onTapGesture {