Skip to content

Commit f166b90

Browse files
authored
Merge pull request #9790 from woocommerce/feat/9785-celebrate-first-product
Add product: Show celebratory view when the first product is created
2 parents eaaf30f + be7f1ea commit f166b90

File tree

10 files changed

+177
-11
lines changed

10 files changed

+177
-11
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
13.8
44
-----
5+
- [*] 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]
56
- [*] 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]
67

78
13.7

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.

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/ProductsViewController.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ final class ProductsViewController: UIViewController, GhostableViewController {
179179

180180
private var subscriptions: Set<AnyCancellable> = []
181181

182+
private var addProductCoordinator: AddProductCoordinator?
183+
182184
deinit {
183185
NotificationCenter.default.removeObserver(self)
184186
}
@@ -269,10 +271,12 @@ private extension ProductsViewController {
269271
}
270272

271273
@objc func addProduct(_ sender: UIBarButtonItem) {
272-
addProduct(sourceBarButtonItem: sender)
274+
addProduct(sourceBarButtonItem: sender, isFirstProduct: false)
273275
}
274276

275-
func addProduct(sourceBarButtonItem: UIBarButtonItem? = nil, sourceView: UIView? = nil) {
277+
func addProduct(sourceBarButtonItem: UIBarButtonItem? = nil,
278+
sourceView: UIView? = nil,
279+
isFirstProduct: Bool) {
276280
guard let navigationController = navigationController, sourceBarButtonItem != nil || sourceView != nil else {
277281
return
278282
}
@@ -283,17 +287,20 @@ private extension ProductsViewController {
283287
coordinatingController = AddProductCoordinator(siteID: siteID,
284288
source: source,
285289
sourceBarButtonItem: sourceBarButtonItem,
286-
sourceNavigationController: navigationController)
290+
sourceNavigationController: navigationController,
291+
isFirstProduct: isFirstProduct)
287292
} else if let sourceView = sourceView {
288293
coordinatingController = AddProductCoordinator(siteID: siteID,
289294
source: source,
290295
sourceView: sourceView,
291-
sourceNavigationController: navigationController)
296+
sourceNavigationController: navigationController,
297+
isFirstProduct: isFirstProduct)
292298
} else {
293299
fatalError("No source view for adding a product")
294300
}
295301

296302
coordinatingController.start()
303+
self.addProductCoordinator = coordinatingController
297304
}
298305
}
299306

@@ -1022,7 +1029,7 @@ private extension ProductsViewController {
10221029
details: details,
10231030
buttonTitle: buttonTitle,
10241031
onTap: { [weak self] button in
1025-
self?.addProduct(sourceView: button)
1032+
self?.addProduct(sourceView: button, isFirstProduct: true)
10261033
},
10271034
onPullToRefresh: { [weak self] refreshControl in
10281035
self?.pullToRefresh(sender: refreshControl)

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,6 +2057,8 @@
20572057
DE8C94662646990000C94823 /* PluginListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8C94652646990000C94823 /* PluginListViewController.swift */; };
20582058
DE8C946E264699B600C94823 /* PluginListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8C946D264699B600C94823 /* PluginListViewModel.swift */; };
20592059
DE971219290A9615000C0BD3 /* AddStoreFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE971218290A9615000C0BD3 /* AddStoreFooterView.swift */; };
2060+
DE9F2D292A1B1AB2004E5957 /* FirstProductCreatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE9F2D282A1B1AB2004E5957 /* FirstProductCreatedView.swift */; };
2061+
DE9F2D2C2A1B1F5D004E5957 /* ConfettiSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = DE9F2D2B2A1B1F5D004E5957 /* ConfettiSwiftUI */; };
20602062
DEA4269A2875440500265B0C /* PaymentMethodsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA426992875440500265B0C /* PaymentMethodsScreen.swift */; };
20612063
DEC0293729C418FF00FD0E2F /* ApplicationPasswordAuthorizationWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC0293629C418FF00FD0E2F /* ApplicationPasswordAuthorizationWebViewController.swift */; };
20622064
DEC0293A29C41BC500FD0E2F /* ApplicationPasswordAuthorizationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC0293929C41BC500FD0E2F /* ApplicationPasswordAuthorizationViewModel.swift */; };
@@ -4364,6 +4366,7 @@
43644366
DE8C94652646990000C94823 /* PluginListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListViewController.swift; sourceTree = "<group>"; };
43654367
DE8C946D264699B600C94823 /* PluginListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListViewModel.swift; sourceTree = "<group>"; };
43664368
DE971218290A9615000C0BD3 /* AddStoreFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddStoreFooterView.swift; sourceTree = "<group>"; };
4369+
DE9F2D282A1B1AB2004E5957 /* FirstProductCreatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstProductCreatedView.swift; sourceTree = "<group>"; };
43674370
DEA426992875440500265B0C /* PaymentMethodsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodsScreen.swift; sourceTree = "<group>"; };
43684371
DEC0293629C418FF00FD0E2F /* ApplicationPasswordAuthorizationWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordAuthorizationWebViewController.swift; sourceTree = "<group>"; };
43694372
DEC0293929C41BC500FD0E2F /* ApplicationPasswordAuthorizationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordAuthorizationViewModel.swift; sourceTree = "<group>"; };
@@ -4564,6 +4567,7 @@
45644567
isa = PBXFrameworksBuildPhase;
45654568
buildActionMask = 2147483647;
45664569
files = (
4570+
DE9F2D2C2A1B1F5D004E5957 /* ConfettiSwiftUI in Frameworks */,
45674571
3FFC5EAC2851942F00563C48 /* Charts in Frameworks */,
45684572
D88FDB4525DD223B00CB0DBD /* Hardware.framework in Frameworks */,
45694573
263E37E12641AD8300260D3B /* Codegen in Frameworks */,
@@ -5795,6 +5799,7 @@
57955799
isa = PBXGroup;
57965800
children = (
57975801
02ECD1E324FF5E0B00735BE5 /* AddProductCoordinator.swift */,
5802+
DE9F2D282A1B1AB2004E5957 /* FirstProductCreatedView.swift */,
57985803
02ECD1E524FFB4E900735BE5 /* ProductFactory.swift */,
57995804
);
58005805
path = "Add Product";
@@ -10422,6 +10427,7 @@
1042210427
174CA86927D90A6200126524 /* AutomatticAbout */,
1042310428
3FFC5EAB2851942F00563C48 /* Charts */,
1042410429
4598298028574688003A9AFE /* Inject */,
10430+
DE9F2D2B2A1B1F5D004E5957 /* ConfettiSwiftUI */,
1042510431
);
1042610432
productName = WooCommerce;
1042710433
productReference = B56DB3C62049BFAA00D4AA8E /* WooCommerce.app */;
@@ -10592,6 +10598,7 @@
1059210598
3FFC5EAA2851942F00563C48 /* XCRemoteSwiftPackageReference "Charts" */,
1059310599
4598297F28574688003A9AFE /* XCRemoteSwiftPackageReference "Inject" */,
1059410600
3F2C8A17285B038800B1A5BB /* XCRemoteSwiftPackageReference "test-collector-swift" */,
10601+
DE9F2D2A2A1B1F5D004E5957 /* XCRemoteSwiftPackageReference "ConfettiSwiftUI" */,
1059510602
);
1059610603
productRefGroup = B56DB3C72049BFAA00D4AA8E /* Products */;
1059710604
projectDirPath = "";
@@ -12168,6 +12175,7 @@
1216812175
035DBA45292D0164003E5125 /* CollectOrderPaymentUseCase.swift in Sources */,
1216912176
0282DD96233C960C006A5FDB /* SearchResultCell.swift in Sources */,
1217012177
027F83ED29B046D2002688C6 /* DashboardTopPerformersViewModel.swift in Sources */,
12178+
DE9F2D292A1B1AB2004E5957 /* FirstProductCreatedView.swift in Sources */,
1217112179
260C32BE2527A2DE00157BC2 /* IssueRefundViewModel.swift in Sources */,
1217212180
2678897C270E6E8B00BD249E /* SimplePaymentsAmount.swift in Sources */,
1217312181
02D29A9229F7C39200473D6D /* UIImage+Text.swift in Sources */,
@@ -13941,6 +13949,14 @@
1394113949
minimumVersion = 1.1.1;
1394213950
};
1394313951
};
13952+
DE9F2D2A2A1B1F5D004E5957 /* XCRemoteSwiftPackageReference "ConfettiSwiftUI" */ = {
13953+
isa = XCRemoteSwiftPackageReference;
13954+
repositoryURL = "https://github.com/simibac/ConfettiSwiftUI.git";
13955+
requirement = {
13956+
kind = upToNextMajorVersion;
13957+
minimumVersion = 1.0.0;
13958+
};
13959+
};
1394413960
/* End XCRemoteSwiftPackageReference section */
1394513961

1394613962
/* Begin XCSwiftPackageProductDependency section */
@@ -14026,6 +14042,11 @@
1402614042
isa = XCSwiftPackageProductDependency;
1402714043
productName = TestKit;
1402814044
};
14045+
DE9F2D2B2A1B1F5D004E5957 /* ConfettiSwiftUI */ = {
14046+
isa = XCSwiftPackageProductDependency;
14047+
package = DE9F2D2A2A1B1F5D004E5957 /* XCRemoteSwiftPackageReference "ConfettiSwiftUI" */;
14048+
productName = ConfettiSwiftUI;
14049+
};
1402914050
/* End XCSwiftPackageProductDependency section */
1403014051
};
1403114052
rootObject = B56DB3BE2049BFAA00D4AA8E /* Project object */;

WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductCoordinatorTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ private extension AddProductCoordinatorTests {
4444
return AddProductCoordinator(siteID: 100,
4545
source: .productsTab,
4646
sourceView: view,
47-
sourceNavigationController: navigationController)
47+
sourceNavigationController: navigationController,
48+
isFirstProduct: false)
4849
}
4950
}

0 commit comments

Comments
 (0)