Skip to content

Commit

Permalink
Forever edit code (#111)
Browse files Browse the repository at this point in the history
* fix forever deep link

* Add possibility to edit forever code

* Move spacing to hCore

* GraphQL implementation

* Fix tests

* Always use same icloud container

* Change iCloud container for prod build aswell

* Fix icloud containers

* Fix com.apple.developer.ubiquity-kvstore-identifier

* Fix com.apple.developer.ubiquity-kvstore-identifier for prod

* Fix quirks

* Fix price section not rendering

* turn off autocorrection and autocapitalization

* Add text field value

* Add clear button

* DelayedDisposer

* URLEncode discountCode inside link
  • Loading branch information
sampettersson authored Jul 22, 2020
1 parent 562dd9e commit 2a18ede
Show file tree
Hide file tree
Showing 18 changed files with 435 additions and 43 deletions.
4 changes: 2 additions & 2 deletions Projects/App/Config/Production/Hedvig.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</array>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.$(CFBundleIdentifier)</string>
<string>iCloud.com.hedvig.app</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
Expand All @@ -23,7 +23,7 @@
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>iCloud.$(CFBundleIdentifier)</string>
<string>iCloud.com.hedvig.app</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
Expand Down
4 changes: 2 additions & 2 deletions Projects/App/Config/Test/Ugglan.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</array>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.$(CFBundleIdentifier)</string>
<string>iCloud.com.hedvig.test.app</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
Expand All @@ -22,7 +22,7 @@
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>iCloud.$(CFBundleIdentifier)</string>
<string>iCloud.com.hedvig.test.app</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
Expand Down
9 changes: 9 additions & 0 deletions Projects/App/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
options: [.defaults]
)

return true
} else if dynamicLinkUrl.pathComponents.contains("forever") {
guard ApplicationState.currentState?.isOneOf([.loggedIn]) == true else { return false }
bag += hasFinishedLoading.atOnce().filter { $0 }.onValue { _ in
NotificationCenter.default.post(Notification(name: .shouldOpenReferrals))
}

Mixpanel.mainInstance().track(event: "DEEP_LINK_FOREVER")

return true
}

Expand Down
19 changes: 19 additions & 0 deletions Projects/Forever/GraphQL/ForeverUpdateDiscountCode.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
mutation ForeverUpdateDiscountCode($code: String!) {
updateReferralCampaignCode(code: $code) {
... on SuccessfullyUpdatedCode {
code
}
... on CodeAlreadyTaken {
code
}
... on CodeTooLong {
maxCharacters
}
... on CodeTooShort {
minCharacters
}
... on ExceededMaximumUpdates {
maximumNumberOfUpdates
}
}
}
188 changes: 188 additions & 0 deletions Projects/Forever/Sources/ChangeCode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//
// ChangeCode.swift
// Forever
//
// Created by sam on 15.7.20.
// Copyright © 2020 Hedvig AB. All rights reserved.
//

import Flow
import Form
import Foundation
import hCore
import hCoreUI
import Presentation
import UIKit

struct ChangeCode {
let service: ForeverService

enum ResultError: Error {
case cancelled
}
}

extension ChangeCode: Presentable {
func makeClearButton(_ row: RowView) -> UIControl {
let clearButton = UIControl()
clearButton.backgroundColor = .brand(.secondaryBackground())
clearButton.layer.cornerRadius = 12
row.viewRepresentation.addSubview(clearButton)

let clearButtonImageView = UIImageView()
clearButtonImageView.contentMode = .scaleAspectFit
clearButton.addSubview(clearButtonImageView)

clearButtonImageView.snp.makeConstraints { make in
make.top.bottom.trailing.leading.equalToSuperview().inset(7.5)
}

clearButtonImageView.image = hCoreUIAssets.close.image

clearButton.snp.makeConstraints { make in
make.width.height.equalTo(24)
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().inset(15)
}

return clearButton
}

func materialize() -> (UIViewController, Future<Void>) {
let viewController = UIViewController()
let bag = DisposeBag()

let cancelBarButtonItem = UIBarButtonItem(title: L10n.NavBar.cancel, style: .brand(.body(color: .primary)))
viewController.navigationItem.leftBarButtonItem = cancelBarButtonItem

let saveBarButtonItem = UIBarButtonItem(title: L10n.NavBar.save, style: .brand(.body(color: .link)))
viewController.navigationItem.rightBarButtonItem = saveBarButtonItem

let form = FormView()
bag += viewController.install(form)

form.appendSpacing(.top)
form.append(L10n.ReferralsChangeCodeSheet.headline)
form.appendSpacing(.inbetween)
bag += form.append(
MultilineLabel(
value: L10n.ReferralsChangeCodeSheet.body,
style: TextStyle.brand(.body(color: .tertiary)).centerAligned
)
)
form.appendSpacing(.top)

let textFieldSection = form.appendSection()
let textFieldRow = textFieldSection.appendRow()

let normalFieldStyle = FieldStyle.default.restyled { (style: inout FieldStyle) in
style.text.alignment = .center
style.autocorrection = .no
style.autocapitalization = .none
}

let textField = UITextField(
value: "",
placeholder: L10n.ReferralsChangeCodeSheet.textFieldPlaceholder,
style: normalFieldStyle
)
textFieldRow.append(textField)

let clearButton = makeClearButton(textFieldRow.row)

bag += clearButton.signal(for: .touchUpInside).atValue {
textField.value = ""
}.animated(style: .easeOut(duration: 0.25)) {
clearButton.alpha = 0
}

bag += service.dataSignal
.atOnce()
.compactMap { $0?.discountCode }
.take(first: 1)
.bindTo(textField, \.value)

textField.becomeFirstResponder()

let textFieldErrorSignal: ReadWriteSignal<ForeverChangeCodeError?> = ReadWriteSignal(nil).distinct()

bag += textField.atValue { _ in
textFieldErrorSignal.value = nil
}.animated(style: .easeOut(duration: 0.25)) { value in
clearButton.alpha = value.isEmpty ? 0 : 1
}

let errorMessageLabel = MultilineLabel(
value: "",
style: TextStyle.brand(.footnote(color: .destructive)).centerAligned
)

bag += textFieldErrorSignal
.compactMap { $0?.localizedDescription }
.bindTo(errorMessageLabel.valueSignal)

form.appendSpacing(.inbetween)
bag += form.append(errorMessageLabel) { errorMessageLabelView in
func alphaAnimation(_ error: Error?) {
errorMessageLabelView.alpha = error == nil ? 0 : 1
}

func isHiddenAnimation(_ error: Error?) {
errorMessageLabelView.animationSafeIsHidden = error == nil
}

bag += textFieldErrorSignal
.atOnce()
.animated(style: .easeOut(duration: 0.15)) { error in
if error == nil {
alphaAnimation(error)
} else {
isHiddenAnimation(error)
}
}.animated(style: .easeOut(duration: 0.15)) { error in
if error == nil {
isHiddenAnimation(error)
} else {
alphaAnimation(error)
}
}.onValue { _ in
viewController.navigationItem.setRightBarButton(saveBarButtonItem, animated: true)
}
}

func onSave() -> Signal<Void> {
let activityIndicator = UIActivityIndicatorView()
activityIndicator.startAnimating()
viewController.navigationItem.setRightBarButton(UIBarButtonItem(customView: activityIndicator), animated: true)

return service.changeDiscountCode(textField.value).delay(by: 0.25).atValue { result in
if let error = result.right {
textFieldErrorSignal.value = error
}
}
.filter(predicate: { $0.left != nil })
.toVoid()
}

return (viewController, Future { completion in
bag += cancelBarButtonItem.onValue {
completion(.failure(ResultError.cancelled))
}

bag += textField.shouldReturn.set { _ -> Bool in
bag += onSave().onValue {
completion(.success)
}
return true
}

bag += saveBarButtonItem.onValue {
bag += onSave().onValue {
completion(.success)
}
}

return DelayedDisposer(bag, delay: 2)
})
}
}
39 changes: 32 additions & 7 deletions Projects/Forever/Sources/DiscountCodeSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,33 @@ import hCoreUI
import UIKit

struct DiscountCodeSection {
let discountCodeSignal: ReadSignal<String?>
let potentialDiscountAmountSignal: ReadSignal<MonetaryAmount?>
var service: ForeverService
}

extension DiscountCodeSection: Viewable {
func materialize(events _: ViewableEvents) -> (SectionView, Disposable) {
let bag = DisposeBag()
let section = SectionView(
headerView: UILabel(value: L10n.ReferralsEmpty.Code.headline, style: .default),
headerView: {
let stackView = UIStackView()
stackView.axis = .horizontal

let label = UILabel(value: L10n.ReferralsEmpty.Code.headline, style: .default)
stackView.addArrangedSubview(label)

let changeButton = Button(
title: L10n.ReferralsEmpty.Edit.Code.button,
type: .outline(borderColor: .clear, textColor: .brand(.link))
)

bag += changeButton.onTapSignal.onValue { _ in
stackView.viewController?.present(ChangeCode(service: self.service), style: .modal)
}

bag += stackView.addArranged(changeButton.wrappedIn(UIStackView()))

return stackView
}(),
footerView: {
let stackView = UIStackView()

Expand All @@ -31,7 +49,7 @@ extension DiscountCodeSection: Viewable {
style: TextStyle.brand(.footnote(color: .tertiary)).aligned(to: .center)
)

bag += potentialDiscountAmountSignal.atOnce().compactMap { $0 }.onValue { monetaryAmount in
bag += self.service.dataSignal.atOnce().compactMap { $0?.potentialDiscountAmount }.onValue { monetaryAmount in
label.valueSignal.value = L10n.ReferralsEmpty.Code.footer(monetaryAmount.formattedAmount)
}

Expand All @@ -50,16 +68,23 @@ extension DiscountCodeSection: Viewable {
)
codeRow.append(codeLabel)

bag += discountCodeSignal.atOnce().compactMap { $0 }.animated(style: SpringAnimationStyle.lightBounce()) { code in
bag += service.dataSignal.atOnce().compactMap { $0?.discountCode }.animated(style: SpringAnimationStyle.lightBounce()) { code in
section.animationSafeIsHidden = false
codeLabel.value = code
}

bag += section.append(codeRow).trackedSignal.onValue { _ in
bag += section.append(codeRow).trackedSignal.onValueDisposePrevious { _ in
let innerBag = DisposeBag()

section.viewController?.presentConditionally(PushNotificationReminder(), style: .modal).onResult { _ in
UIPasteboard.general.string = self.discountCodeSignal.value ?? ""
innerBag += self.service.dataSignal
.atOnce()
.compactMap { $0?.discountCode }
.bindTo(UIPasteboard.general, \.string)
bag += section.viewController?.displayToast(title: L10n.ReferralsActiveToast.text)
}

return innerBag
}

return (section, bag)
Expand Down
12 changes: 4 additions & 8 deletions Projects/Forever/Sources/Forever.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,8 @@ extension Forever: Presentable {
}

tableKit.view.refreshControl = refreshControl

bag += tableKit.view.addTableHeaderView(Header(
grossAmountSignal: service.dataSignal.map { $0?.grossAmount },
netAmountSignal: service.dataSignal.map { $0?.netAmount },
discountCodeSignal: service.dataSignal.map { $0?.discountCode },
potentialDiscountAmountSignal: service.dataSignal.map { $0?.potentialDiscountAmount }
), animated: false)

bag += tableKit.view.addTableHeaderView(Header(service: service), animated: false)

let containerView = UIView()
viewController.view = containerView
Expand Down Expand Up @@ -113,8 +108,9 @@ extension Forever: Presentable {
}.withLatestFrom(self.service.dataSignal.atOnce().compactMap { $0?.discountCode }).onValue { buttonView, discountCode in
shareButton.loadableButton.startLoading()
viewController.presentConditionally(PushNotificationReminder(), style: .modal).onResult { _ in
let encodedDiscountCode = discountCode.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
let activity = ActivityView(
activityItems: [URL(string: L10n.referralsLink(discountCode)) ?? ""],
activityItems: [URL(string: L10n.referralsLink(encodedDiscountCode)) ?? ""],
applicationActivities: nil,
sourceView: buttonView,
sourceRect: nil
Expand Down
26 changes: 25 additions & 1 deletion Projects/Forever/Sources/ForeverService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,35 @@ public struct ForeverData: Codable {
let grossAmount: MonetaryAmount
let netAmount: MonetaryAmount
let potentialDiscountAmount: MonetaryAmount
let discountCode: String
var discountCode: String
let invitations: [ForeverInvitation]

public mutating func updateDiscountCode(_ newValue: String) {
self.discountCode = newValue
}
}

public enum ForeverChangeCodeError: LocalizedError, Equatable {
case nonUnique, tooLong, tooShort, exceededMaximumUpdates(amount: Int), unknown

var localizedDescription: String {
switch self {
case .nonUnique:
return L10n.ReferralsChange.Code.Sheet.Error.Claimed.code
case .tooLong:
return L10n.ReferralsChange.Code.Sheet.Error.Max.length
case .tooShort:
return L10n.ReferralsChange.Code.Sheet.General.error
case .exceededMaximumUpdates(let amount):
return L10n.ReferralsChange.Code.Sheet.Error.Change.Limit.reached(amount)
case .unknown:
return L10n.ReferralsChange.Code.Sheet.General.error
}
}
}

public protocol ForeverService {
var dataSignal: ReadSignal<ForeverData?> { get }
func refetch()
func changeDiscountCode(_ value: String) -> Signal<Either<Void, ForeverChangeCodeError>>
}
Loading

0 comments on commit 2a18ede

Please sign in to comment.