Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

13.8
-----
- [*] 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]
- [*] 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]

13.7
Expand Down
9 changes: 9 additions & 0 deletions WooCommerce.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions WooCommerce/Classes/Analytics/WooAnalyticsStat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,10 @@ public enum WooAnalyticsStat: String {
case productDescriptionAIGenerationSuccess = "product_description_ai_generation_success"
case productDescriptionAIGenerationFailed = "product_description_ai_generation_failed"

// MARK: First created product events
case firstCreatedProductShown = "first_created_product_shown"
case firstCreatedProductShareTapped = "first_created_product_share_tapped"

// MARK: Jetpack Tunnel Events
//
case jetpackTunnelTimeout = "jetpack_tunnel_timeout"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,8 @@ private extension DashboardViewController {
let coordinator = AddProductCoordinator(siteID: siteID,
source: .productOnboarding,
sourceView: announcementView,
sourceNavigationController: navigationController)
sourceNavigationController: navigationController,
isFirstProduct: true)
coordinator.onProductCreated = { [weak self] _ in
guard let self else { return }
self.viewModel.announcementViewModel = nil // Remove the products onboarding banner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ private extension StoreOnboardingCoordinator {
let coordinator = AddProductCoordinator(siteID: site.siteID,
source: .storeOnboarding,
sourceView: nil,
sourceNavigationController: navigationController)
sourceNavigationController: navigationController,
isFirstProduct: true)
self.addProductCoordinator = coordinator
coordinator.onProductCreated = { [weak self] _ in
self?.onTaskCompleted(.addFirstProduct)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ final class AddProductCoordinator: Coordinator {
private let sourceView: UIView?
private let productImageUploader: ProductImageUploaderProtocol
private let storage: StorageManagerType
private let isFirstProduct: Bool

/// ResultController to to track the current product count.
///
Expand All @@ -48,29 +49,33 @@ final class AddProductCoordinator: Coordinator {
sourceBarButtonItem: UIBarButtonItem,
sourceNavigationController: UINavigationController,
storage: StorageManagerType = ServiceLocator.storageManager,
productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader) {
productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader,
isFirstProduct: Bool) {
self.siteID = siteID
self.source = source
self.sourceBarButtonItem = sourceBarButtonItem
self.sourceView = nil
self.navigationController = sourceNavigationController
self.productImageUploader = productImageUploader
self.storage = storage
self.isFirstProduct = isFirstProduct
}

init(siteID: Int64,
source: Source,
sourceView: UIView?,
sourceNavigationController: UINavigationController,
storage: StorageManagerType = ServiceLocator.storageManager,
productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader) {
productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader,
isFirstProduct: Bool) {
self.siteID = siteID
self.source = source
self.sourceBarButtonItem = nil
self.sourceView = sourceView
self.navigationController = sourceNavigationController
self.productImageUploader = productImageUploader
self.storage = storage
self.isFirstProduct = isFirstProduct
}

required init?(coder aDecoder: NSCoder) {
Expand Down Expand Up @@ -235,7 +240,13 @@ private extension AddProductCoordinator {
let viewModel = ProductFormViewModel(product: model,
formType: .add,
productImageActionHandler: productImageActionHandler)
viewModel.onProductCreated = onProductCreated
viewModel.onProductCreated = { [weak self] product in
guard let self else { return }
self.onProductCreated(product)
if self.isFirstProduct, let url = URL(string: product.permalink) {
self.showFirstProductCreatedView(productURL: url)
}
}
let viewController = ProductFormViewController(viewModel: viewModel,
eventLogger: ProductFormEventLogger(),
productImageActionHandler: productImageActionHandler,
Expand Down Expand Up @@ -289,4 +300,11 @@ private extension AddProductCoordinator {
}()
ServiceLocator.analytics.track(event: .ProductsOnboarding.productCreationTypeSelected(type: analyticsType))
}

/// Presents the celebratory view for the first created product.
///
func showFirstProductCreatedView(productURL: URL) {
let viewController = FirstProductCreatedHostingController(productURL: productURL)
navigationController.present(UINavigationController(rootViewController: viewController), animated: true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import ConfettiSwiftUI
import SwiftUI

final class FirstProductCreatedHostingController: UIHostingController<FirstProductCreatedView> {
init(productURL: URL) {
super.init(rootView: FirstProductCreatedView())
rootView.onSharingProduct = { [weak self] in
guard let self else { return }
SharingHelper.shareURL(url: productURL, from: self.view, in: self)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Is it possible to pass onCompletion block to SharingHelper and dismiss this screen from it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't get a callback when the user submitted any content in the share sheet, so it's not technically possible to handle onCompletion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ServiceLocator.analytics.track(.firstCreatedProductShareTapped)
}
}

@available(*, unavailable)
required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
configureTransparentNavigationBar()
navigationItem.leftBarButtonItem = UIBarButtonItem(title: Localization.cancel, style: .plain, target: self, action: #selector(dismissView))
ServiceLocator.analytics.track(.firstCreatedProductShown)
}

@objc
private func dismissView() {
dismiss(animated: true)
}
}

private extension FirstProductCreatedHostingController {
enum Localization {
static let cancel = NSLocalizedString("Dismiss", comment: "Button to dismiss the first created product screen")
}
}

/// Celebratory screen after creating the first product 🎉
///
struct FirstProductCreatedView: View {
var onSharingProduct: () -> Void = {}
@State private var confettiCounter: Int = 0

var body: some View {
GeometryReader { proxy in
ScrollableVStack(spacing: Constants.verticalSpacing) {
Spacer()
Text(Localization.title)
.titleStyle()
Image(uiImage: .welcomeImage)
Text(Localization.message)
.secondaryBodyStyle()
.multilineTextAlignment(.center)
Button(Localization.shareAction,
action: onSharingProduct)
.buttonStyle(PrimaryButtonStyle())
.padding(.horizontal)
Spacer()
}
.padding()
.confettiCannon(counter: $confettiCounter,
num: Constants.confettiCount,
rainHeight: proxy.size.height,
radius: proxy.size.width)
}
.onAppear {
confettiCounter += 1
}
.background(Color(uiColor: .systemBackground))
}
}

private extension FirstProductCreatedView {
enum Constants {
static let verticalSpacing: CGFloat = 40
static let confettiCount: Int = 100
}
enum Localization {
static let title = NSLocalizedString(
"First product created 🎉",
comment: "Title of the celebratory screen after creating the first product"
)
static let message = NSLocalizedString(
"Congratulations! You're one step closer to getting the new store ready.",
comment: "Message on the celebratory screen after creating first product"
)
static let shareAction = NSLocalizedString(
"Share Product",
comment: "Title of the action button to share the first created product"
)
}
}

struct FirstProductCreatedView_Previews: PreviewProvider {
static var previews: some View {
FirstProductCreatedView()
.environment(\.colorScheme, .light)

FirstProductCreatedView()
.environment(\.colorScheme, .dark)
.previewInterfaceOrientation(.landscapeLeft)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ final class ProductsViewController: UIViewController, GhostableViewController {

private var subscriptions: Set<AnyCancellable> = []

private var addProductCoordinator: AddProductCoordinator?

deinit {
NotificationCenter.default.removeObserver(self)
}
Expand Down Expand Up @@ -269,10 +271,12 @@ private extension ProductsViewController {
}

@objc func addProduct(_ sender: UIBarButtonItem) {
addProduct(sourceBarButtonItem: sender)
addProduct(sourceBarButtonItem: sender, isFirstProduct: false)
}

func addProduct(sourceBarButtonItem: UIBarButtonItem? = nil, sourceView: UIView? = nil) {
func addProduct(sourceBarButtonItem: UIBarButtonItem? = nil,
sourceView: UIView? = nil,
isFirstProduct: Bool) {
guard let navigationController = navigationController, sourceBarButtonItem != nil || sourceView != nil else {
return
}
Expand All @@ -283,17 +287,20 @@ private extension ProductsViewController {
coordinatingController = AddProductCoordinator(siteID: siteID,
source: source,
sourceBarButtonItem: sourceBarButtonItem,
sourceNavigationController: navigationController)
sourceNavigationController: navigationController,
isFirstProduct: isFirstProduct)
} else if let sourceView = sourceView {
coordinatingController = AddProductCoordinator(siteID: siteID,
source: source,
sourceView: sourceView,
sourceNavigationController: navigationController)
sourceNavigationController: navigationController,
isFirstProduct: isFirstProduct)
} else {
fatalError("No source view for adding a product")
}

coordinatingController.start()
self.addProductCoordinator = coordinatingController
}
}

Expand Down Expand Up @@ -1022,7 +1029,7 @@ private extension ProductsViewController {
details: details,
buttonTitle: buttonTitle,
onTap: { [weak self] button in
self?.addProduct(sourceView: button)
self?.addProduct(sourceView: button, isFirstProduct: true)
},
onPullToRefresh: { [weak self] refreshControl in
self?.pullToRefresh(sender: refreshControl)
Expand Down
21 changes: 21 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2057,6 +2057,8 @@
DE8C94662646990000C94823 /* PluginListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8C94652646990000C94823 /* PluginListViewController.swift */; };
DE8C946E264699B600C94823 /* PluginListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8C946D264699B600C94823 /* PluginListViewModel.swift */; };
DE971219290A9615000C0BD3 /* AddStoreFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE971218290A9615000C0BD3 /* AddStoreFooterView.swift */; };
DE9F2D292A1B1AB2004E5957 /* FirstProductCreatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE9F2D282A1B1AB2004E5957 /* FirstProductCreatedView.swift */; };
DE9F2D2C2A1B1F5D004E5957 /* ConfettiSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = DE9F2D2B2A1B1F5D004E5957 /* ConfettiSwiftUI */; };
DEA4269A2875440500265B0C /* PaymentMethodsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA426992875440500265B0C /* PaymentMethodsScreen.swift */; };
DEC0293729C418FF00FD0E2F /* ApplicationPasswordAuthorizationWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC0293629C418FF00FD0E2F /* ApplicationPasswordAuthorizationWebViewController.swift */; };
DEC0293A29C41BC500FD0E2F /* ApplicationPasswordAuthorizationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC0293929C41BC500FD0E2F /* ApplicationPasswordAuthorizationViewModel.swift */; };
Expand Down Expand Up @@ -4364,6 +4366,7 @@
DE8C94652646990000C94823 /* PluginListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListViewController.swift; sourceTree = "<group>"; };
DE8C946D264699B600C94823 /* PluginListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListViewModel.swift; sourceTree = "<group>"; };
DE971218290A9615000C0BD3 /* AddStoreFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddStoreFooterView.swift; sourceTree = "<group>"; };
DE9F2D282A1B1AB2004E5957 /* FirstProductCreatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstProductCreatedView.swift; sourceTree = "<group>"; };
DEA426992875440500265B0C /* PaymentMethodsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodsScreen.swift; sourceTree = "<group>"; };
DEC0293629C418FF00FD0E2F /* ApplicationPasswordAuthorizationWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordAuthorizationWebViewController.swift; sourceTree = "<group>"; };
DEC0293929C41BC500FD0E2F /* ApplicationPasswordAuthorizationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordAuthorizationViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4564,6 +4567,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DE9F2D2C2A1B1F5D004E5957 /* ConfettiSwiftUI in Frameworks */,
3FFC5EAC2851942F00563C48 /* Charts in Frameworks */,
D88FDB4525DD223B00CB0DBD /* Hardware.framework in Frameworks */,
263E37E12641AD8300260D3B /* Codegen in Frameworks */,
Expand Down Expand Up @@ -5795,6 +5799,7 @@
isa = PBXGroup;
children = (
02ECD1E324FF5E0B00735BE5 /* AddProductCoordinator.swift */,
DE9F2D282A1B1AB2004E5957 /* FirstProductCreatedView.swift */,
02ECD1E524FFB4E900735BE5 /* ProductFactory.swift */,
);
path = "Add Product";
Expand Down Expand Up @@ -10422,6 +10427,7 @@
174CA86927D90A6200126524 /* AutomatticAbout */,
3FFC5EAB2851942F00563C48 /* Charts */,
4598298028574688003A9AFE /* Inject */,
DE9F2D2B2A1B1F5D004E5957 /* ConfettiSwiftUI */,
);
productName = WooCommerce;
productReference = B56DB3C62049BFAA00D4AA8E /* WooCommerce.app */;
Expand Down Expand Up @@ -10592,6 +10598,7 @@
3FFC5EAA2851942F00563C48 /* XCRemoteSwiftPackageReference "Charts" */,
4598297F28574688003A9AFE /* XCRemoteSwiftPackageReference "Inject" */,
3F2C8A17285B038800B1A5BB /* XCRemoteSwiftPackageReference "test-collector-swift" */,
DE9F2D2A2A1B1F5D004E5957 /* XCRemoteSwiftPackageReference "ConfettiSwiftUI" */,
);
productRefGroup = B56DB3C72049BFAA00D4AA8E /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -12168,6 +12175,7 @@
035DBA45292D0164003E5125 /* CollectOrderPaymentUseCase.swift in Sources */,
0282DD96233C960C006A5FDB /* SearchResultCell.swift in Sources */,
027F83ED29B046D2002688C6 /* DashboardTopPerformersViewModel.swift in Sources */,
DE9F2D292A1B1AB2004E5957 /* FirstProductCreatedView.swift in Sources */,
260C32BE2527A2DE00157BC2 /* IssueRefundViewModel.swift in Sources */,
2678897C270E6E8B00BD249E /* SimplePaymentsAmount.swift in Sources */,
02D29A9229F7C39200473D6D /* UIImage+Text.swift in Sources */,
Expand Down Expand Up @@ -13941,6 +13949,14 @@
minimumVersion = 1.1.1;
};
};
DE9F2D2A2A1B1F5D004E5957 /* XCRemoteSwiftPackageReference "ConfettiSwiftUI" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/simibac/ConfettiSwiftUI.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
Expand Down Expand Up @@ -14026,6 +14042,11 @@
isa = XCSwiftPackageProductDependency;
productName = TestKit;
};
DE9F2D2B2A1B1F5D004E5957 /* ConfettiSwiftUI */ = {
isa = XCSwiftPackageProductDependency;
package = DE9F2D2A2A1B1F5D004E5957 /* XCRemoteSwiftPackageReference "ConfettiSwiftUI" */;
productName = ConfettiSwiftUI;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = B56DB3BE2049BFAA00D4AA8E /* Project object */;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ private extension AddProductCoordinatorTests {
return AddProductCoordinator(siteID: 100,
source: .productsTab,
sourceView: view,
sourceNavigationController: navigationController)
sourceNavigationController: navigationController,
isFirstProduct: false)
}
}