Skip to content

Commit ff916a5

Browse files
committed
Merge branch 'trunk' into issue/8962-product-bundle-ui
2 parents 45fbb5a + dd180b4 commit ff916a5

File tree

15 files changed

+229
-17
lines changed

15 files changed

+229
-17
lines changed

RELEASE-NOTES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
13.8
44
-----
55
- [Internal] Orders: Bundled products (within a product bundle) are now indented, to show their relationship to the parent bundle. [https://github.com/woocommerce/woocommerce-ios/pull/9778]
6+
- [*] Add Products: A new view is display to celebrate when the first product is created in a store. [https://github.com/woocommerce/woocommerce-ios/pull/9790]
7+
- [*] Product form: a share action is shown in the navigation bar if the product can be shared and no more than one action is displayed, in addition to the more menu > Share. [https://github.com/woocommerce/woocommerce-ios/pull/9789]
68

79
13.7
810
-----

WooCommerce.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
extension WooAnalyticsEvent {
2+
enum ProductForm {
3+
/// Event property keys.
4+
private enum Key {
5+
static let source = "source"
6+
}
7+
8+
/// Tracked when the user taps on the button to share a product.
9+
static func productDetailShareButtonTapped(source: ShareProductSource) -> WooAnalyticsEvent {
10+
WooAnalyticsEvent(statName: .productDetailShareButtonTapped,
11+
properties: [Key.source: source.rawValue])
12+
}
13+
}
14+
}
15+
16+
extension WooAnalyticsEvent.ProductForm {
17+
/// Source of the share product action. The raw value is the event property value.
18+
enum ShareProductSource: String {
19+
/// From product form in the navigation bar.
20+
case productForm = "product_form"
21+
/// From product form > more menu in the navigation bar.
22+
case moreMenu = "more_menu"
23+
}
24+
}

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,10 @@ public enum WooAnalyticsStat: String {
713713
case productDescriptionAIGenerationSuccess = "product_description_ai_generation_success"
714714
case productDescriptionAIGenerationFailed = "product_description_ai_generation_failed"
715715

716+
// MARK: First created product events
717+
case firstCreatedProductShown = "first_created_product_shown"
718+
case firstCreatedProductShareTapped = "first_created_product_share_tapped"
719+
716720
// MARK: Jetpack Tunnel Events
717721
//
718722
case jetpackTunnelTimeout = "jetpack_tunnel_timeout"

WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,8 @@ private extension DashboardViewController {
471471
let coordinator = AddProductCoordinator(siteID: siteID,
472472
source: .productOnboarding,
473473
sourceView: announcementView,
474-
sourceNavigationController: navigationController)
474+
sourceNavigationController: navigationController,
475+
isFirstProduct: true)
475476
coordinator.onProductCreated = { [weak self] _ in
476477
guard let self else { return }
477478
self.viewModel.announcementViewModel = nil // Remove the products onboarding banner

WooCommerce/Classes/ViewRelated/Dashboard/Onboarding/StoreOnboardingCoordinator.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ private extension StoreOnboardingCoordinator {
8686
let coordinator = AddProductCoordinator(siteID: site.siteID,
8787
source: .storeOnboarding,
8888
sourceView: nil,
89-
sourceNavigationController: navigationController)
89+
sourceNavigationController: navigationController,
90+
isFirstProduct: true)
9091
self.addProductCoordinator = coordinator
9192
coordinator.onProductCreated = { [weak self] _ in
9293
self?.onTaskCompleted(.addFirstProduct)

WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductCoordinator.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ final class AddProductCoordinator: Coordinator {
2929
private let sourceView: UIView?
3030
private let productImageUploader: ProductImageUploaderProtocol
3131
private let storage: StorageManagerType
32+
private let isFirstProduct: Bool
3233

3334
/// ResultController to to track the current product count.
3435
///
@@ -48,29 +49,33 @@ final class AddProductCoordinator: Coordinator {
4849
sourceBarButtonItem: UIBarButtonItem,
4950
sourceNavigationController: UINavigationController,
5051
storage: StorageManagerType = ServiceLocator.storageManager,
51-
productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader) {
52+
productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader,
53+
isFirstProduct: Bool) {
5254
self.siteID = siteID
5355
self.source = source
5456
self.sourceBarButtonItem = sourceBarButtonItem
5557
self.sourceView = nil
5658
self.navigationController = sourceNavigationController
5759
self.productImageUploader = productImageUploader
5860
self.storage = storage
61+
self.isFirstProduct = isFirstProduct
5962
}
6063

6164
init(siteID: Int64,
6265
source: Source,
6366
sourceView: UIView?,
6467
sourceNavigationController: UINavigationController,
6568
storage: StorageManagerType = ServiceLocator.storageManager,
66-
productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader) {
69+
productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader,
70+
isFirstProduct: Bool) {
6771
self.siteID = siteID
6872
self.source = source
6973
self.sourceBarButtonItem = nil
7074
self.sourceView = sourceView
7175
self.navigationController = sourceNavigationController
7276
self.productImageUploader = productImageUploader
7377
self.storage = storage
78+
self.isFirstProduct = isFirstProduct
7479
}
7580

7681
required init?(coder aDecoder: NSCoder) {
@@ -235,7 +240,13 @@ private extension AddProductCoordinator {
235240
let viewModel = ProductFormViewModel(product: model,
236241
formType: .add,
237242
productImageActionHandler: productImageActionHandler)
238-
viewModel.onProductCreated = onProductCreated
243+
viewModel.onProductCreated = { [weak self] product in
244+
guard let self else { return }
245+
self.onProductCreated(product)
246+
if self.isFirstProduct, let url = URL(string: product.permalink) {
247+
self.showFirstProductCreatedView(productURL: url)
248+
}
249+
}
239250
let viewController = ProductFormViewController(viewModel: viewModel,
240251
eventLogger: ProductFormEventLogger(),
241252
productImageActionHandler: productImageActionHandler,
@@ -289,4 +300,11 @@ private extension AddProductCoordinator {
289300
}()
290301
ServiceLocator.analytics.track(event: .ProductsOnboarding.productCreationTypeSelected(type: analyticsType))
291302
}
303+
304+
/// Presents the celebratory view for the first created product.
305+
///
306+
func showFirstProductCreatedView(productURL: URL) {
307+
let viewController = FirstProductCreatedHostingController(productURL: productURL)
308+
navigationController.present(UINavigationController(rootViewController: viewController), animated: true)
309+
}
292310
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import ConfettiSwiftUI
2+
import SwiftUI
3+
4+
final class FirstProductCreatedHostingController: UIHostingController<FirstProductCreatedView> {
5+
init(productURL: URL) {
6+
super.init(rootView: FirstProductCreatedView())
7+
rootView.onSharingProduct = { [weak self] in
8+
guard let self else { return }
9+
SharingHelper.shareURL(url: productURL, from: self.view, in: self)
10+
ServiceLocator.analytics.track(.firstCreatedProductShareTapped)
11+
}
12+
}
13+
14+
@available(*, unavailable)
15+
required dynamic init?(coder aDecoder: NSCoder) {
16+
fatalError("init(coder:) has not been implemented")
17+
}
18+
19+
override func viewDidLoad() {
20+
super.viewDidLoad()
21+
configureTransparentNavigationBar()
22+
navigationItem.leftBarButtonItem = UIBarButtonItem(title: Localization.cancel, style: .plain, target: self, action: #selector(dismissView))
23+
ServiceLocator.analytics.track(.firstCreatedProductShown)
24+
}
25+
26+
@objc
27+
private func dismissView() {
28+
dismiss(animated: true)
29+
}
30+
}
31+
32+
private extension FirstProductCreatedHostingController {
33+
enum Localization {
34+
static let cancel = NSLocalizedString("Dismiss", comment: "Button to dismiss the first created product screen")
35+
}
36+
}
37+
38+
/// Celebratory screen after creating the first product 🎉
39+
///
40+
struct FirstProductCreatedView: View {
41+
var onSharingProduct: () -> Void = {}
42+
@State private var confettiCounter: Int = 0
43+
44+
var body: some View {
45+
GeometryReader { proxy in
46+
ScrollableVStack(spacing: Constants.verticalSpacing) {
47+
Spacer()
48+
Text(Localization.title)
49+
.titleStyle()
50+
Image(uiImage: .welcomeImage)
51+
Text(Localization.message)
52+
.secondaryBodyStyle()
53+
.multilineTextAlignment(.center)
54+
Button(Localization.shareAction,
55+
action: onSharingProduct)
56+
.buttonStyle(PrimaryButtonStyle())
57+
.padding(.horizontal)
58+
Spacer()
59+
}
60+
.padding()
61+
.confettiCannon(counter: $confettiCounter,
62+
num: Constants.confettiCount,
63+
rainHeight: proxy.size.height,
64+
radius: proxy.size.width)
65+
}
66+
.onAppear {
67+
confettiCounter += 1
68+
}
69+
.background(Color(uiColor: .systemBackground))
70+
}
71+
}
72+
73+
private extension FirstProductCreatedView {
74+
enum Constants {
75+
static let verticalSpacing: CGFloat = 40
76+
static let confettiCount: Int = 100
77+
}
78+
enum Localization {
79+
static let title = NSLocalizedString(
80+
"First product created 🎉",
81+
comment: "Title of the celebratory screen after creating the first product"
82+
)
83+
static let message = NSLocalizedString(
84+
"Congratulations! You're one step closer to getting the new store ready.",
85+
comment: "Message on the celebratory screen after creating first product"
86+
)
87+
static let shareAction = NSLocalizedString(
88+
"Share Product",
89+
comment: "Title of the action button to share the first created product"
90+
)
91+
}
92+
}
93+
94+
struct FirstProductCreatedView_Previews: PreviewProvider {
95+
static var previews: some View {
96+
FirstProductCreatedView()
97+
.environment(\.colorScheme, .light)
98+
99+
FirstProductCreatedView()
100+
.environment(\.colorScheme, .dark)
101+
.previewInterfaceOrientation(.landscapeLeft)
102+
}
103+
}

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ final class ProductFormViewController<ViewModel: ProductFormViewModelProtocol>:
5353
presentationStyle.createExitForm(viewController: self)
5454
}()
5555

56+
private lazy var shareBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action,
57+
target: self,
58+
action: #selector(shareProduct))
59+
5660
private let presentationStyle: ProductFormPresentationStyle
5761
private let navigationRightBarButtonItemsSubject = PassthroughSubject<[UIBarButtonItem], Never>()
5862
private var navigationRightBarButtonItems: AnyPublisher<[UIBarButtonItem], Never> {
@@ -281,8 +285,7 @@ final class ProductFormViewController<ViewModel: ProductFormViewModelProtocol>:
281285

282286
if viewModel.canShareProduct() {
283287
actionSheet.addDefaultActionWithTitle(ActionSheetStrings.share) { [weak self] _ in
284-
ServiceLocator.analytics.track(.productDetailShareButtonTapped)
285-
self?.displayShareProduct()
288+
self?.displayShareProduct(from: sender, analyticSource: .moreMenu)
286289
}
287290
}
288291

@@ -470,6 +473,10 @@ final class ProductFormViewController<ViewModel: ProductFormViewModelProtocol>:
470473
}
471474
}
472475

476+
@objc private func shareProduct() {
477+
displayShareProduct(from: shareBarButtonItem, analyticSource: .productForm)
478+
}
479+
473480
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
474481
let section = tableViewModel.sections[section]
475482
switch section {
@@ -858,12 +865,14 @@ private extension ProductFormViewController {
858865
WebviewHelper.launch(url, with: self)
859866
}
860867

861-
func displayShareProduct() {
868+
func displayShareProduct(from sourceView: UIBarButtonItem, analyticSource: WooAnalyticsEvent.ProductForm.ShareProductSource) {
869+
ServiceLocator.analytics.track(event: .ProductForm.productDetailShareButtonTapped(source: analyticSource))
870+
862871
guard let url = URL(string: product.permalink) else {
863872
return
864873
}
865874

866-
SharingHelper.shareURL(url: url, title: product.name, from: view, in: self)
875+
SharingHelper.shareURL(url: url, title: product.name, from: sourceView, in: self)
867876
}
868877

869878
func duplicateProduct() {
@@ -1003,6 +1012,8 @@ private extension ProductFormViewController {
10031012
return createSaveBarButtonItem()
10041013
case .more:
10051014
return createMoreOptionsBarButtonItem()
1015+
case .share:
1016+
return shareBarButtonItem
10061017
}
10071018
}
10081019

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewModel.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ final class ProductFormViewModel: ProductFormViewModelProtocol {
168168
buttons.append(.more)
169169
}
170170

171+
// Share button if up to one button is visible.
172+
if canShareProduct() && buttons.count <= 1 {
173+
buttons.insert(.share, at: 0)
174+
}
175+
171176
return buttons
172177
}
173178

0 commit comments

Comments
 (0)