Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6dfeafb
Added skeleton PlanDetailViewController, presented from PlanListViewC…
frosty Feb 2, 2016
c5624d4
PlanDetailViewController: Added placeholder content rows with ImmuTable.
frosty Feb 2, 2016
56c9fcd
Added Plans storyboard and Plan Detail feature list table view cell.
frosty Feb 3, 2016
4d59137
PlanFeatureListCell: Added a 'disabled' marker and a detail text label.
frosty Feb 3, 2016
f12145e
PlanDetailViewController: Updated cell and header view styles.
frosty Feb 3, 2016
39578ef
Passed selected plan through to PlanDetailViewController and populate…
frosty Feb 4, 2016
5ba92c3
Refactoring PlanFeatureListCell to use stackviews.
frosty Feb 4, 2016
688d490
Extracted PurchaseButton from PlanDetailViewController.
frosty Feb 4, 2016
26c7305
Added Free, Paid, and Business plan feature lists.
frosty Feb 4, 2016
e73952b
PlanDetailViewController: Added border and dropshadow to plan image.
frosty Feb 5, 2016
d103ffb
Plan / PurchaseButton: Small code tidy.
frosty Feb 5, 2016
7e14312
Replaced dropshadow image for PlanDetail image with layer shadows.
frosty Feb 5, 2016
159f450
PurchaseButton: Removed duplicate line.
frosty Feb 8, 2016
254d8eb
Merge branch 'develop' into feature/plan-details
frosty Feb 8, 2016
83c39ac
PlanDetailViewController: Replaced custom cell w/ WPTableViewCellValue1
frosty Feb 8, 2016
19b6727
Added tests for PlanDetailViewController feature list rows.
frosty Feb 8, 2016
95d4df7
Present new plan details from plans list
koke Feb 5, 2016
5ba79f9
Enable selection of plans since they're actionable
koke Feb 8, 2016
fa00c01
Merge branch 'develop' into feature/plan-details-present
koke Feb 8, 2016
c6c1675
Refactor Plan list to use ImmuTablePresenter
koke Feb 8, 2016
8c52ddb
Avoid retain cycle on rowGenerator
koke Feb 8, 2016
988e276
Match file location with Xcode project for "Me"
koke Feb 9, 2016
3acee38
Merge pull request #4790 from wordpress-mobile/feature/plan-details-p…
koke Feb 9, 2016
c9facb9
PlanDetailViewController: Made header 'sticky' and changed table back…
frosty Feb 9, 2016
fb09fb4
Merge branch 'feature/plan-details' of github.com:wordpress-mobile/Wo…
frosty Feb 9, 2016
5ee8be1
Changed plan detail to use a formsheet modal style on iPad.
frosty Feb 9, 2016
a6f36bf
Removed unused `MRProgress` import from PurchaseButton.
frosty Feb 9, 2016
5d192e1
Merge branch 'develop' into feature/plan-details
frosty Feb 9, 2016
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
111 changes: 111 additions & 0 deletions WordPress/Classes/Models/Plan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,46 @@ enum PlanEnum: Int {
return NSLocalizedString("Business websites and ecommerce.", comment: "Description of the Business plan")
}
}

/// An array of the features that this plan offers (e.g. No Ads, Premium Themes)
var features: [PlanFeature] {
switch self {
case .Free:
return [
.WordPressSite,
.FullAccess,
.StorageSpace(NSLocalizedString("3GB", comment: "3 gigabytes of storage")),
.Support(NSLocalizedString("Community", comment: "Noun. Customer support from the community (forums)."))
]
case .Premium:
return [
.WordPressSite,
.FullAccess,
.CustomDomain,
.NoAds,
.CustomFontsAndColors,
.CSSEditing,
.VideoPress,
.StorageSpace(NSLocalizedString("13GB", comment: "13 gigabytes of storage")),
.Support(NSLocalizedString("In-App & Direct Email", comment: "Types of support available to a user."))
]
case .Business:
return [
.WordPressSite,
.FullAccess,
.CustomDomain,
.NoAds,
.CustomFontsAndColors,
.CSSEditing,
.VideoPress,
.ECommerce,
.PremiumThemes,
.GoogleAnalytics,
.StorageSpace(NSLocalizedString("Unlimited", comment: "Unlimited data storage")),
.Support(NSLocalizedString("In-App & Live Chat", comment: "Types of support available to a user."))
]
}
}
}

// We currently need to access the title of a plan in BlogDetailsViewController, which is
Expand Down Expand Up @@ -106,3 +146,74 @@ extension Plan {
return UIImage(named: activeImageName)!
}
}

enum PlanFeature: Equatable {
static let allFeatures: [PlanFeature] = [ .WordPressSite, .FullAccess, .CustomDomain, .NoAds, .CustomFontsAndColors, .CSSEditing, .VideoPress, .ECommerce, .PremiumThemes, .GoogleAnalytics, .StorageSpace(""), .Support("") ]

case WordPressSite
case FullAccess
case CustomDomain
case NoAds
case CustomFontsAndColors
case CSSEditing
case VideoPress
case ECommerce
case PremiumThemes
case GoogleAnalytics
case StorageSpace(String)
case Support(String)

var title: String {
switch self {
case WordPressSite:
return NSLocalizedString("WordPress.com Site", comment: "The site that a user gets by signing up at WordPress.com. A feature provided by every plan.")
case FullAccess:
return NSLocalizedString("Full Access to Web Version", comment: "A WordPress.com feature that every user gets when they select a plan in the mobile app.")
case CustomDomain:
return NSLocalizedString("A Custom Site Address", comment: "A feature that users gain when purchasing a paid plan.")
case NoAds:
return NSLocalizedString("No Ads", comment: "A feature that users gain when purchasing a paid plan. No advertising will be displayed on their site.")
case CustomFontsAndColors:
return NSLocalizedString("Custom Fonts & Colors", comment: "A feature that users gain when purchasing a paid plan. They can set custom fonts and colors on their site.")
case CSSEditing:
return NSLocalizedString("CSS Editing", comment: "A feature that users gain when purchasing a paid plan. They can edit the CSS of their site.")
case VideoPress:
return NSLocalizedString("Video Storage & Hosting", comment: "A feature that users gain when purchasing a paid plan. They can upload and embed videos using VideoPress.")
case ECommerce:
return NSLocalizedString("eCommerce", comment: "A feature that users gain when purchasing a paid plan. They can embed a store in their site.")
case PremiumThemes:
return NSLocalizedString("Premium Themes", comment: "A feature that users gain when purchasing a paid plan. They have unlimited access to premium themes.")
case GoogleAnalytics:
return NSLocalizedString("Google Analytics", comment: "A feature that users gain when purchasing a paid plan. ")
case StorageSpace:
return NSLocalizedString("Storage Space", comment: "A feature that users gain when purchasing a paid plan. They get a certain level of storage space for uploads.")
case Support:
return NSLocalizedString("Support", comment: "A feature that users gain when purchasing a paid plan. Customer support such as live chat or email.")
}
}

var webOnly: Bool {
switch self {
case .CustomDomain, .ECommerce, .GoogleAnalytics:
return true
default:
return false
}
}

var description: String? {
switch self {
case .StorageSpace(let space):
return space
case .Support(let type):
return type
default:
return nil
}
}
}

func ==(lhs: PlanFeature, rhs: PlanFeature) -> Bool {
return lhs.title == rhs.title
Copy link
Member

Choose a reason for hiding this comment

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

I think that this is not correct, even if it works for this case. I believe .Support("3GB") shouldn't be equal to .Support("13GB"), even when they'll share the same title.
You can compare values with pattern matching, although I think you'll have to cover every case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm... the reason I added Equatable was simply so that I could find a plan's matching feature compared to the list of all features, for use here: https://github.com/wordpress-mobile/WordPress-iOS/blob/feature/plan-details/WordPress/Classes/ViewRelated/Plans/PlanDetailViewController.swift#L80

This wouldn't work if it also checked on the associated values etc too. I'm happy to restructure the features and do this differently if we can think of a better way though!

}

202 changes: 202 additions & 0 deletions WordPress/Classes/ViewRelated/Plans/PlanDetailViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import UIKit
import WordPressShared

class PlanDetailViewController: UITableViewController {
var plan: Plan!

private let cellIdentifier = "PlanFeatureListItem"

private let planImageDropshadowRadius: CGFloat = 3.0

private var viewModel: ImmuTable! = nil

@IBOutlet weak var planImageView: UIImageView!
@IBOutlet weak var dropshadowImageView: UIImageView!
@IBOutlet weak var planTitleLabel: UILabel!
@IBOutlet weak var planDescriptionLabel: UILabel!
@IBOutlet weak var planPriceLabel: UILabel!
@IBOutlet weak var purchaseButton: UIButton!

lazy private var cancelXButton: UIBarButtonItem = {
let button = UIBarButtonItem(image: UIImage(named: "gridicons-cross"), style: .Plain, target: self, action: "closeTapped")
button.accessibilityLabel = NSLocalizedString("Close", comment: "Dismiss the current view")

return button
}()

class func controllerWithPlan(plan: Plan) -> PlanDetailViewController {
let storyboard = UIStoryboard(name: "Plans", bundle: NSBundle.mainBundle())
let controller = storyboard.instantiateViewControllerWithIdentifier(NSStringFromClass(self)) as! PlanDetailViewController

controller.plan = plan

return controller
}

override func prefersStatusBarHidden() -> Bool {
return true
}

override func viewDidLoad() {
super.viewDidLoad()

title = plan.title
navigationItem.leftBarButtonItem = cancelXButton

configureAppearance()
configureImmuTable()
populateHeader()
}

private func configureAppearance() {
planTitleLabel.textColor = WPStyleGuide.darkGrey()
planDescriptionLabel.textColor = WPStyleGuide.grey()
planPriceLabel.textColor = WPStyleGuide.grey()

purchaseButton.tintColor = WPStyleGuide.wordPressBlue()

dropshadowImageView.backgroundColor = UIColor.whiteColor()
configurePlanImageDropshadow()
}

private func configurePlanImageDropshadow() {
dropshadowImageView.layer.masksToBounds = false
dropshadowImageView.layer.shadowColor = WPStyleGuide.greyLighten30().CGColor
dropshadowImageView.layer.shadowOpacity = 1.0
dropshadowImageView.layer.shadowRadius = planImageDropshadowRadius
dropshadowImageView.layer.shadowOffset = .zero
dropshadowImageView.layer.shadowPath = UIBezierPath(ovalInRect: dropshadowImageView.bounds).CGPath
}

private func configureImmuTable() {
viewModel = ImmuTable(sections:
[ ImmuTableSection(rows: PlanFeature.allFeatures.map { feature in
Copy link
Member

Choose a reason for hiding this comment

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

One of the motivations behind ImmuTable was to be able to define the table contents in a more declarative way. In other words, when you see the ImmuTable being created, it should be obvious which rows it will display. It's a bit more verbose than this solution, but I think the result is more readable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As we've discussed, I'll leave this for now as we may be restructuring the PlanFeatures setup in the near future.

let available = plan.features.contains(feature)

if available {
// If a feature is 'available', we have to find and use the feature instance
// from the _plan's_ list of features, as it will have the correct associated values
// for any enum case that has associated values.
let index = plan.features.indexOf(feature)
let planFeature = plan.features[index!]
return FeatureListItemRow(feature: planFeature, available: available)
}

return FeatureListItemRow(feature: feature, available: available)
} ) ]
)
}

private func populateHeader() {
planImageView.image = plan.image
planTitleLabel.text = plan.fullTitle
planDescriptionLabel.text = plan.description
planPriceLabel.text = priceDescriptionForPlan(plan)
}

// TODO: Prices should always come from StoreKit
// @frosty 2016-02-04
private func priceDescriptionForPlan(plan: Plan) -> String? {
switch plan {
case .Free:
return "$0 for life"
case .Premium:
return "$99.99 per year"
case .Business:
return "$299.99 per year"
}
}

//MARK: - IBActions

@IBAction private func closeTapped() {
dismissViewControllerAnimated(true, completion: nil)
}

@IBAction private func purchaseTapped() {
}
}

// MARK: Table View Data Source / Delegate
extension PlanDetailViewController {
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return viewModel.sections.count
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.sections[section].rows.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let row = viewModel.rowAtIndexPath(indexPath)
let cell = tableView.dequeueReusableCellWithIdentifier(row.reusableIdentifier, forIndexPath: indexPath)

row.configureCell(cell)

return cell
}

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
// Rows have alternate colors
if indexPath.row % 2 == 0 {
cell.backgroundColor = WPStyleGuide.greyLighten30()
} else {
cell.backgroundColor = WPStyleGuide.lightGrey()
}
}
}

struct FeatureListItemRow : ImmuTableRow {
static let cell = ImmuTableCell.Class(PlanFeatureListCell)

let action: ImmuTableAction? = nil

let feature: PlanFeature
let available: Bool

init(feature: PlanFeature, available: Bool) {
self.feature = feature
self.available = available
}

func configureCell(cell: UITableViewCell) {
let cell = cell as! PlanFeatureListCell
cell.titleLabel.text = feature.title
cell.detailLabel.text = feature.description

cell.titleLabel.textColor = available ? WPStyleGuide.darkGrey() : WPStyleGuide.grey()
cell.titleLabel.alpha = available ? 1.0 : 0.5

cell.webOnlyLabel.hidden = !feature.webOnly
cell.unavailableFeatureMarker.hidden = available

let noDetailText = (feature.description ?? "").isEmpty
cell.availableFeatureStackView.hidden = !available || !noDetailText
cell.detailLabel.hidden = noDetailText
}
}

class PlanFeatureListCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var detailLabel: UILabel!
@IBOutlet weak var webOnlyLabel: UILabel!
@IBOutlet weak var availableFeatureStackView: UIStackView!
@IBOutlet weak var unavailableFeatureMarker: UIView!

override func awakeFromNib() {
super.awakeFromNib()

webOnlyLabel.text = NSLocalizedString("WEB ONLY", comment: "Describes a feature of a WordPress.com plan that is only available to users via the web.")

unavailableFeatureMarker.backgroundColor = WPStyleGuide.grey()
webOnlyLabel.textColor = WPStyleGuide.grey()
detailLabel.textColor = WPStyleGuide.grey()
}

override func prepareForReuse() {
super.prepareForReuse()

detailLabel.text = nil
titleLabel.text = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,11 @@ class PlanListViewController: UITableViewController {

return cell
}

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let planVC = PlanDetailViewController.controllerWithPlan(availablePlans[indexPath.row])
let navigationVC = UINavigationController(rootViewController: planVC)

presentViewController(navigationVC, animated: true, completion: nil)
}
}
Loading