Skip to content

Commit ba6160b

Browse files
committed
Merge branch 'trunk' into issue/8122-iap-debug-warning-sandbox-mode
# Conflicts: # WooCommerce/Classes/ViewRelated/InAppPurchases/InAppPurchasesDebugView.swift
2 parents 7b1a155 + 4b21955 commit ba6160b

File tree

38 files changed

+979
-297
lines changed

38 files changed

+979
-297
lines changed

Networking/Networking/Model/WordPressApiError.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ public enum WordPressApiError: Error, Decodable, Equatable {
88
///
99
case unknown(code: String, message: String)
1010

11+
/// An order already exists for this IAP receipt
12+
///
13+
case productPurchased
14+
1115
/// Decodable Initializer.
1216
///
1317
public init(from decoder: Decoder) throws {
@@ -16,6 +20,8 @@ public enum WordPressApiError: Error, Decodable, Equatable {
1620
let message = try container.decode(String.self, forKey: .message)
1721

1822
switch code {
23+
case Constants.productPurchased:
24+
self = .productPurchased
1925
default:
2026
self = .unknown(code: code, message: message)
2127
}
@@ -25,6 +31,7 @@ public enum WordPressApiError: Error, Decodable, Equatable {
2531
/// Constants for Possible Error Identifiers
2632
///
2733
private enum Constants {
34+
static let productPurchased = "product_purchased"
2835
}
2936

3037
/// Coding Keys
@@ -50,6 +57,10 @@ extension WordPressApiError: CustomStringConvertible {
5057

5158
public var description: String {
5259
switch self {
60+
case .productPurchased:
61+
return NSLocalizedString(
62+
"An order aready exists for this receipt",
63+
comment: "Error message when an order already exists in the backend for a given receipt")
5364
case .unknown(let code, let message):
5465
let messageFormat = NSLocalizedString(
5566
"WordPress API Error: [%1$@] %2$@",
@@ -59,3 +70,11 @@ extension WordPressApiError: CustomStringConvertible {
5970
}
6071
}
6172
}
73+
74+
// MARK: - LocalizedError Conformance
75+
//
76+
extension WordPressApiError: LocalizedError {
77+
public var errorDescription: String? {
78+
description
79+
}
80+
}

RELEASE-NOTES.txt

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

33
11.3
44
-----
5-
5+
- [*] In-Person Payments: Show spinner while preparing reader for payment, instead of saying it's ready before it is. [https://github.com/woocommerce/woocommerce-ios/pull/8115]
66

77
11.2
88
-----
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import SwiftUI
2+
3+
/// Displays a vertical list of features included in the WPCOM plan during the store creation flow.
4+
struct StoreCreationPlanFeaturesView: View {
5+
/// Features to show in a vertical list.
6+
let features: [StoreCreationPlanViewModel.Feature]
7+
8+
var body: some View {
9+
VStack(alignment: .leading, spacing: 16) {
10+
ForEach(features, id: \.title) { feature in
11+
HStack(spacing: 12) {
12+
Image(uiImage: feature.icon)
13+
.renderingMode(.template)
14+
.foregroundColor(Color(.wooCommercePurple(.shade90)))
15+
Text(feature.title)
16+
.foregroundColor(Color(.label))
17+
.bodyStyle()
18+
}
19+
}
20+
}
21+
}
22+
}
23+
24+
struct StoreCreationPlanFeaturesView_Previews: PreviewProvider {
25+
static var previews: some View {
26+
StoreCreationPlanFeaturesView(features: [.init(icon: .megaphoneIcon, title: "Get updates!")])
27+
}
28+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import SwiftUI
2+
3+
/// Hosting controller that wraps the `StoreCreationPlanView`.
4+
final class StoreCreationPlanHostingController: UIHostingController<StoreCreationPlanView> {
5+
private let onPurchase: () -> Void
6+
private let onClose: () -> Void
7+
8+
init(viewModel: StoreCreationPlanViewModel,
9+
onPurchase: @escaping () -> Void,
10+
onClose: @escaping () -> Void) {
11+
self.onPurchase = onPurchase
12+
self.onClose = onClose
13+
super.init(rootView: StoreCreationPlanView(viewModel: viewModel))
14+
15+
rootView.onPurchase = { [weak self] in
16+
self?.onPurchase()
17+
}
18+
}
19+
20+
@available(*, unavailable)
21+
required dynamic init?(coder aDecoder: NSCoder) {
22+
fatalError("init(coder:) has not been implemented")
23+
}
24+
25+
override func viewDidLoad() {
26+
super.viewDidLoad()
27+
28+
configureNavigationBarAppearance()
29+
}
30+
31+
/// Shows a transparent navigation bar without a bottom border and with a close button to dismiss.
32+
func configureNavigationBarAppearance() {
33+
addCloseNavigationBarButton(target: self, action: #selector(closeButtonTapped))
34+
35+
let appearance = UINavigationBarAppearance()
36+
appearance.configureWithTransparentBackground()
37+
appearance.backgroundColor = .withColorStudio(.wooCommercePurple, shade: .shade90)
38+
39+
navigationItem.standardAppearance = appearance
40+
navigationItem.scrollEdgeAppearance = appearance
41+
navigationItem.compactAppearance = appearance
42+
}
43+
44+
@objc private func closeButtonTapped() {
45+
onClose()
46+
}
47+
}
48+
49+
/// Displays the WPCOM eCommerce plan for purchase during the store creation flow.
50+
struct StoreCreationPlanView: View {
51+
/// Set in the hosting controller.
52+
var onPurchase: (() -> Void) = {}
53+
54+
let viewModel: StoreCreationPlanViewModel
55+
56+
var body: some View {
57+
VStack(alignment: .leading, spacing: 0) {
58+
ScrollView {
59+
VStack(alignment: .leading, spacing: 0) {
60+
HStack(alignment: .center) {
61+
VStack(alignment: .leading, spacing: 12) {
62+
// Plan name.
63+
Text(Localization.planTitle)
64+
.fontWeight(.semibold)
65+
.font(.title3)
66+
.foregroundColor(.white)
67+
68+
// Price information.
69+
HStack(alignment: .bottom) {
70+
Text(viewModel.plan.displayPrice)
71+
.fontWeight(.bold)
72+
.foregroundColor(.white)
73+
.largeTitleStyle()
74+
Text(Localization.priceDuration)
75+
.foregroundColor(Color(.secondaryLabel))
76+
.bodyStyle()
77+
}
78+
}
79+
.padding(.horizontal, insets: .init(top: 0, leading: 24, bottom: 0, trailing: 0))
80+
81+
Spacer()
82+
83+
Image(uiImage: .storeCreationPlanImage)
84+
}
85+
86+
Divider()
87+
.frame(height: Layout.dividerHeight)
88+
.foregroundColor(Color(Layout.dividerColor))
89+
.padding(.horizontal, insets: Layout.defaultPadding)
90+
91+
VStack(alignment: .leading, spacing: 0) {
92+
Spacer()
93+
.frame(height: 8)
94+
95+
// Header label.
96+
Text(Localization.subtitle)
97+
.fontWeight(.bold)
98+
.foregroundColor(Color(.white))
99+
.titleStyle()
100+
101+
Spacer()
102+
.frame(height: 16)
103+
104+
// Powered by WPCOM.
105+
HStack(spacing: 5) {
106+
Text(Localization.poweredByWPCOMPrompt)
107+
.foregroundColor(Color(.secondaryLabel))
108+
.footnoteStyle()
109+
Image(uiImage: .wpcomLogoImage)
110+
}
111+
112+
Spacer()
113+
.frame(height: 32)
114+
115+
// Plan features.
116+
StoreCreationPlanFeaturesView(features: viewModel.features)
117+
}
118+
.padding(Layout.defaultPadding)
119+
}
120+
}
121+
122+
VStack(spacing: 0) {
123+
Divider()
124+
.frame(height: Layout.dividerHeight)
125+
.foregroundColor(Color(Layout.dividerColor))
126+
127+
// Continue button.
128+
Button(String(format: Localization.continueButtonTitleFormat, viewModel.plan.displayPrice)) {
129+
onPurchase()
130+
}
131+
.buttonStyle(PrimaryButtonStyle())
132+
.padding(Layout.defaultButtonPadding)
133+
134+
// Refund information.
135+
Text(Localization.refundableNote)
136+
.multilineTextAlignment(.center)
137+
.foregroundColor(Color(.secondaryLabel))
138+
.bodyStyle()
139+
140+
Spacer()
141+
.frame(height: 24)
142+
}
143+
}
144+
.background(Color(.withColorStudio(.wooCommercePurple, shade: .shade90)))
145+
// This screen is using the dark theme for both light and dark modes.
146+
.environment(\.colorScheme, .dark)
147+
}
148+
}
149+
150+
private extension StoreCreationPlanView {
151+
enum Layout {
152+
static let dividerHeight: CGFloat = 1
153+
static let defaultPadding: EdgeInsets = .init(top: 16, leading: 16, bottom: 16, trailing: 16)
154+
static let defaultButtonPadding: EdgeInsets = .init(top: 16, leading: 16, bottom: 16, trailing: 16)
155+
static let dividerColor: UIColor = .separator
156+
}
157+
158+
enum Localization {
159+
static let planTitle = NSLocalizedString(
160+
"eCommerce",
161+
comment: "Title of the store creation plan on the plan screen.")
162+
static let priceDuration = NSLocalizedString(
163+
"/month",
164+
comment: "The text is preceded by the monthly price on the store creation plan screen.")
165+
static let subtitle = NSLocalizedString(
166+
"All the featues you need, already built in",
167+
comment: "Subtitle of the store creation plan screen.")
168+
static let poweredByWPCOMPrompt = NSLocalizedString(
169+
"Powered by",
170+
comment: "The text is followed by a WordPress.com logo on the store creation plan screen.")
171+
static let continueButtonTitleFormat = NSLocalizedString(
172+
"Create Store for %1$@/month",
173+
comment: "Title of the button on the store creation plan view to purchase the plan. " +
174+
"%1$@ is replaced by the monthly price."
175+
)
176+
static let refundableNote = NSLocalizedString(
177+
"There’s no risk, you can cancel for a full refund within 30 days.",
178+
comment: "Refund policy under the purchase button on the store creation plan screen."
179+
)
180+
}
181+
}
182+
183+
#if DEBUG
184+
185+
/// Only used for `StoreCreationPlanView` preview.
186+
private struct Plan: WPComPlanProduct {
187+
let displayName: String
188+
let description: String
189+
let id: String
190+
let displayPrice: String
191+
}
192+
193+
struct StoreCreationPlanView_Previews: PreviewProvider {
194+
static var previews: some View {
195+
StoreCreationPlanView(viewModel:
196+
.init(plan: Plan(displayName: "Debug Monthly",
197+
description: "1 Month of Debug Woo",
198+
id: "debug.woocommerce.ecommerce.monthly",
199+
displayPrice: "$69.99")))
200+
}
201+
}
202+
203+
#endif
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import UIKit
2+
3+
/// View model for `StoreCreationPlanView`.
4+
struct StoreCreationPlanViewModel {
5+
/// Describes a feature for the WPCOM plan with an icon.
6+
struct Feature {
7+
let icon: UIImage
8+
let title: String
9+
}
10+
11+
/// The WPCOM plan to purchase.
12+
let plan: WPComPlanProduct
13+
14+
/// A list of features included in the WPCOM plan.
15+
let features: [Feature] = [
16+
.init(icon: .gridicon(.starOutline), title: Localization.themeFeature),
17+
.init(icon: .gridicon(.product), title: Localization.productsFeature),
18+
.init(icon: .gridicon(.gift), title: Localization.subscriptionsFeature),
19+
.init(icon: .gridicon(.statsAlt), title: Localization.reportFeature),
20+
// TODO: 8108 - update icon
21+
.init(icon: .gridicon(.money), title: Localization.paymentOptionsFeature),
22+
.init(icon: .gridicon(.shipping), title: Localization.shippingLabelsFeature),
23+
.init(icon: .megaphoneIcon, title: Localization.salesChannelsFeature),
24+
]
25+
}
26+
27+
private extension StoreCreationPlanViewModel {
28+
enum Localization {
29+
static let themeFeature = NSLocalizedString(
30+
"Premium themes",
31+
comment: "Title of eCommerce plan feature on the store creation plan screen."
32+
)
33+
static let productsFeature = NSLocalizedString(
34+
"Unlimited products",
35+
comment: "Title of eCommerce plan feature on the store creation plan screen."
36+
)
37+
static let subscriptionsFeature = NSLocalizedString(
38+
"Subscriptions & giftcards",
39+
comment: "Title of eCommerce plan feature on the store creation plan screen."
40+
)
41+
static let reportFeature = NSLocalizedString(
42+
"Ecommerce reports",
43+
comment: "Title of eCommerce plan feature on the store creation plan screen."
44+
)
45+
static let paymentOptionsFeature = NSLocalizedString(
46+
"Multiple payment options",
47+
comment: "Title of eCommerce plan feature on the store creation plan screen."
48+
)
49+
static let shippingLabelsFeature = NSLocalizedString(
50+
"Shipping labels",
51+
comment: "Title of eCommerce plan feature on the store creation plan screen."
52+
)
53+
static let salesChannelsFeature = NSLocalizedString(
54+
"Sales channels",
55+
comment: "Title of eCommerce plan feature on the store creation plan screen."
56+
)
57+
}
58+
}

0 commit comments

Comments
 (0)