Skip to content
This repository has been archived by the owner on May 6, 2024. It is now read-only.

Commit

Permalink
Merge branch 'master' into e0d-patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
saeedbashir committed Mar 22, 2024
2 parents 972fe4c + a07db33 commit a0b0fe2
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 45 deletions.
12 changes: 11 additions & 1 deletion Source/CalendarManager.swift
Expand Up @@ -117,13 +117,23 @@ class CalendarManager: NSObject {

func requestAccess(completion: @escaping (Bool, EKAuthorizationStatus, EKAuthorizationStatus) -> ()) {
let previousStatus = EKEventStore.authorizationStatus(for: .event)
eventStore.requestAccess(to: .event) { [weak self] access, _ in
let requestHandler: (Bool, Error?) -> Void = { [weak self] access, _ in
self?.eventStore.reset()
let currentStatus = EKEventStore.authorizationStatus(for: .event)
DispatchQueue.main.async {
completion(access, previousStatus, currentStatus)
}
}

if #available(iOS 17.0, *) {
eventStore.requestFullAccessToEvents { access, error in
requestHandler(access, error)
}
} else {
eventStore.requestAccess(to: .event) { access, error in
requestHandler(access, error)
}
}
}

func addEventsToCalendar(for dateBlocks: [Date : [CourseDateBlock]], completion: @escaping (Bool) -> ()) {
Expand Down
8 changes: 4 additions & 4 deletions Source/CourseDashboardAccessErrorView.swift
Expand Up @@ -11,7 +11,7 @@ import Foundation
protocol CourseDashboardAccessErrorViewDelegate: AnyObject {
func findCourseAction()
func upgradeCourseAction(course: OEXCourse, coursePrice: String, price: NSDecimalNumber?, currencyCode: String?, completion: @escaping ((Bool)->()))
func coursePrice(cell: CourseDashboardAccessErrorView, price: String?, elapsedTime: Int)
func coursePrice(cell: CourseDashboardAccessErrorView, price: String?, error: PurchaseError?, elapsedTime: Int)
}

class CourseDashboardAccessErrorView: UIView {
Expand Down Expand Up @@ -224,20 +224,20 @@ class CourseDashboardAccessErrorView: UIView {
let startTime = CFAbsoluteTimeGetCurrent()
DispatchQueue.main.async { [weak self] in
self?.upgradeButton.startShimeringEffect()
PaymentManager.shared.fetchPrroduct(courseSku) { [weak self] product in
PaymentManager.shared.fetchPrroduct(courseSku) { [weak self] product, error in
guard let weakSelf = self else { return }

if let product = product, let coursePrice = product.localizedPrice {
let elapsedTime = CFAbsoluteTimeGetCurrent() - startTime
weakSelf.localizedCoursePrice = coursePrice
weakSelf.price = product.price
weakSelf.currencyCode = product.priceLocale.currencyCode
weakSelf.delegate?.coursePrice(cell: weakSelf, price: coursePrice, elapsedTime: elapsedTime.millisecond)
weakSelf.delegate?.coursePrice(cell: weakSelf, price: coursePrice, error: nil, elapsedTime: elapsedTime.millisecond)
weakSelf.upgradeButton.setPrice(coursePrice)
weakSelf.upgradeButton.stopShimmerEffect()
}
else {
weakSelf.delegate?.coursePrice(cell: weakSelf, price: nil, elapsedTime: 0)
weakSelf.delegate?.coursePrice(cell: weakSelf, price: nil, error: error, elapsedTime: 0)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Source/EnrolledCoursesViewController+CourseUpgrade.swift
Expand Up @@ -100,7 +100,7 @@ extension EnrolledCoursesViewController {

guard let courseSku = course.sku, environment.serverConfig.iapConfig?.enabledforUser == true else { return }

PaymentManager.shared.fetchPrroduct(courseSku) { [weak self] product in
PaymentManager.shared.fetchPrroduct(courseSku) { [weak self] product, _ in
if let product = product {
self?.upgradeCourse(course: course, localizedCoursePrice: product.localizedPrice, price: product.price, currencyCode: product.priceLocale.currencyCode)
}
Expand Down
1 change: 1 addition & 0 deletions Source/NSError+JSON.swift
Expand Up @@ -36,6 +36,7 @@ enum APIErrorCode: String, CaseIterable {
case JWTMustIncludePreferredClaim = "JWT must include a preferred_username or username claim!"
case JWTUserRetrievalFailed = "User retrieval failed."
case JWTUserDisabled = "account_disabled"
case JWTEMailMissMatch = "Failing JWT authentication due to jwt user email mismatch with lms user email."

var action: APIErrorCodeAction {
switch self {
Expand Down
22 changes: 12 additions & 10 deletions Source/NewCourseDashboardViewController.swift
Expand Up @@ -428,12 +428,12 @@ extension NewCourseDashboardViewController: CourseDashboardAccessErrorViewDelega
redirectToDiscovery()
}

func coursePrice(cell: CourseDashboardAccessErrorView, price: String?, elapsedTime: Int) {
func coursePrice(cell: CourseDashboardAccessErrorView, price: String?, error: PurchaseError?, elapsedTime: Int) {
if let price = price {
trackPriceLoadDuration(price: price, elapsedTime: elapsedTime)
}
else {
trackPriceLoadError(cell: cell)
trackPriceLoadError(cell: cell, error: error)
}
}

Expand Down Expand Up @@ -500,24 +500,26 @@ extension NewCourseDashboardViewController {
environment.analytics.trackCourseUpgradeTimeToLoadPrice(courseID: courseID, pacing: pacing, coursePrice: price, screen: screen, elapsedTime: elapsedTime)
}

private func trackPriceLoadError(cell: CourseDashboardAccessErrorView) {
private func trackPriceLoadError(cell: CourseDashboardAccessErrorView, error: PurchaseError?) {
guard let course = course, let courseID = course.course_id else { return }
environment.analytics.trackCourseUpgradeLoadError(courseID: courseID, pacing: pacing, screen: screen)
showCoursePriceErrorAlert(cell: cell)
showCoursePriceErrorAlert(cell: cell, error: error)
}

private func showCoursePriceErrorAlert(cell: CourseDashboardAccessErrorView) {
private func showCoursePriceErrorAlert(cell: CourseDashboardAccessErrorView, error: PurchaseError?) {
guard let topController = UIApplication.shared.topMostController() else { return }

let alertController = UIAlertController().showAlert(withTitle: Strings.CourseUpgrade.FailureAlert.alertTitle, message: Strings.CourseUpgrade.FailureAlert.priceFetchErrorMessage, cancelButtonTitle: nil, onViewController: topController) { _, _, _ in }


alertController.addButton(withTitle: Strings.CourseUpgrade.FailureAlert.priceFetchError) { [weak self] _ in
cell.fetchCoursePrice()
self?.environment.analytics.trackCourseUpgradeErrorAction(courseID: self?.course?.course_id ?? "" , blockID: "", pacing: self?.pacing ?? "", coursePrice: "", screen: self?.screen ?? .none, errorAction: CourseUpgradeHelper.ErrorAction.reloadPrice.rawValue, upgradeError: "price", flowType: CourseUpgradeHandler.CourseUpgradeMode.userInitiated.rawValue)
if error != .productNotExist {
alertController.addButton(withTitle: Strings.CourseUpgrade.FailureAlert.priceFetchError) { [weak self] _ in
cell.fetchCoursePrice()
self?.environment.analytics.trackCourseUpgradeErrorAction(courseID: self?.course?.course_id ?? "" , blockID: "", pacing: self?.pacing ?? "", coursePrice: "", screen: self?.screen ?? .none, errorAction: CourseUpgradeHelper.ErrorAction.reloadPrice.rawValue, upgradeError: "price", flowType: CourseUpgradeHandler.CourseUpgradeMode.userInitiated.rawValue)
}
}

alertController.addButton(withTitle: Strings.cancel, style: .default) { [weak self] _ in
let cancelButtonTitle = error == .productNotExist ? Strings.ok : Strings.cancel
alertController.addButton(withTitle: cancelButtonTitle, style: .default) { [weak self] _ in
cell.hideUpgradeButton()
self?.environment.analytics.trackCourseUpgradeErrorAction(courseID: self?.course?.course_id ?? "" , blockID: "", pacing: self?.pacing ?? "", coursePrice: "", screen: self?.screen ?? .none, errorAction: CourseUpgradeHelper.ErrorAction.close.rawValue, upgradeError: "price", flowType: CourseUpgradeHandler.CourseUpgradeMode.userInitiated.rawValue)
}
Expand Down
2 changes: 2 additions & 0 deletions Source/OEXAnalytics+Swift.swift
Expand Up @@ -103,6 +103,7 @@ public enum AnalyticsDisplayName : String {
case PrivacyPolicyClicked = "Privacy Policy Clicked"
case CookiePolicyClicked = "Cookie Policy Clicked"
case DataSellConsentClicked = "Do Not Sell Data Clicked"
case SubmitFeedbackClicked = "Submit feedback clicked"
}

public enum AnalyticsEventName: String {
Expand Down Expand Up @@ -194,6 +195,7 @@ public enum AnalyticsEventName: String {
case PrivacyPolicyClicked = "edx.bi.app.profile.privacy_policy.clicked"
case CookiePolicyClicked = "edx.bi.app.profile.cookie_policy.clicked"
case DataSellConsentClicked = "edx.bi.app.profile.do_not_sell_data.clicked"
case SubmitFeedbackClicked = "edx.bi.app.profile.submit_feedback.clicked"
}

public enum AnalyticsScreenName: String {
Expand Down
9 changes: 5 additions & 4 deletions Source/PaymentManager.swift
Expand Up @@ -27,6 +27,7 @@ enum PurchaseError: String {
case basketError // basket API returns error
case checkoutError // checkout API returns error
case verifyReceiptError // verify receipt API returns error
case productNotExist // product not existed on app appstore
case generalError // general error

var errorString: String {
Expand Down Expand Up @@ -153,16 +154,16 @@ enum PurchaseError: String {
}
}

func fetchPrroduct(_ identifier: String, completion: ((SKProduct?) -> Void)? = nil) {
func fetchPrroduct(_ identifier: String, completion: ((SKProduct?, PurchaseError?) -> Void)? = nil) {
SwiftyStoreKit.retrieveProductsInfo([identifier]) { result in
if let product = result.retrievedProducts.first {
completion?(product)
completion?(product, nil)
}
else if let _ = result.invalidProductIDs.first {
completion?(nil)
completion?(nil, .productNotExist)
}
else {
completion?(nil)
completion?(nil, .generalError)
}
}
}
Expand Down
59 changes: 50 additions & 9 deletions Source/ProfileOptionsViewController.swift
Expand Up @@ -21,7 +21,7 @@ class ProfileOptionsViewController: UIViewController {
case personalInformation
case restorePurchase
case privacy
case help(Bool, Bool)
case help(Bool, Bool, Bool)
case signout
case deleteAccount
}
Expand Down Expand Up @@ -139,10 +139,11 @@ class ProfileOptionsViewController: UIViewController {
}

let feedbackEnabled = environment.config.feedbackEmailAddress() != nil
let submitFeedbackEnabled = environment.serverConfig.feedbackFormURL != nil
let faqEnabled = environment.config.faqURL != nil

if feedbackEnabled || faqEnabled {
options.append(.help(feedbackEnabled, faqEnabled))
options.append(.help(feedbackEnabled, submitFeedbackEnabled, faqEnabled))
}

options.append(.signout)
Expand Down Expand Up @@ -217,8 +218,8 @@ extension ProfileOptionsViewController: UITableViewDataSource {
case .privacy:
return privacyCell(tableView, indexPath: indexPath)

case .help(let feedbackEnabled, let faqEnabled):
return helpCell(tableView, indexPath: indexPath, feedbackEnabled: feedbackEnabled, faqEnabled: faqEnabled)
case .help(let feedbackEnabled, let submitFeedbackEnabled, let faqEnabled):
return helpCell(tableView, indexPath: indexPath, feedbackEnabled: feedbackEnabled, submitFeedbackEnabled: submitFeedbackEnabled, faqEnabled: faqEnabled)

case .signout:
return signoutCell(tableView, indexPath: indexPath)
Expand Down Expand Up @@ -271,10 +272,10 @@ extension ProfileOptionsViewController: UITableViewDataSource {
return cell
}

private func helpCell(_ tableView: UITableView, indexPath: IndexPath, feedbackEnabled: Bool, faqEnabled: Bool) -> UITableViewCell {
private func helpCell(_ tableView: UITableView, indexPath: IndexPath, feedbackEnabled: Bool, submitFeedbackEnabled: Bool, faqEnabled: Bool) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: HelpCell.identifier, for: indexPath) as! HelpCell
cell.delegate = self
cell.update(feedbackEnabled: feedbackEnabled, faqEnabled: faqEnabled, platformName: environment.config.platformName())
cell.update(feedbackEnabled: feedbackEnabled, faqEnabled: faqEnabled, submitFeedbackEnabled: submitFeedbackEnabled, platformName: environment.config.platformName())

return cell
}
Expand Down Expand Up @@ -345,6 +346,14 @@ extension ProfileOptionsViewController: HelpCellDelegate {
launchEmailComposer()
}

func didTapSubmitFeedback() {
guard let url = environment.serverConfig.feedbackFormURL else { return }
environment.analytics.trackProfileOptionClcikEvent(displayName: AnalyticsDisplayName.SubmitFeedbackClicked, name: AnalyticsEventName.SubmitFeedbackClicked)
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
}
}

func didTapFAQ() {
guard let faqURL = environment.config.faqURL, let url = URL(string: faqURL) else { return }
environment.analytics.trackProfileOptionClcikEvent(displayName: AnalyticsDisplayName.FAQClicked, name: AnalyticsEventName.FAQClicked)
Expand Down Expand Up @@ -415,7 +424,7 @@ extension ProfileOptionsViewController: RestorePurchasesCellDelegate {

guard let courseSku = course.sku, environment.serverConfig.iapConfig?.enabledforUser == true else { return }

PaymentManager.shared.fetchPrroduct(courseSku) { [weak self] product in
PaymentManager.shared.fetchPrroduct(courseSku) { [weak self] product, _ in
if let product = product {
self?.upgradeCourse(course: course, localizedCoursePrice: product.localizedPrice, price: product.price, currencyCode: product.priceLocale.currencyCode, indicator: indicator)
}
Expand Down Expand Up @@ -1132,6 +1141,7 @@ class PrivacyCell: UITableViewCell {

protocol HelpCellDelegate: AnyObject {
func didTapEmail()
func didTapSubmitFeedback()
func didTapFAQ()
}

Expand All @@ -1142,6 +1152,7 @@ class HelpCell: UITableViewCell {

private var feedbackEnabled: Bool = false
private var faqEnabled: Bool = false
private var submitFeedbackEnabled: Bool = false

private let lineSpacing: CGFloat = 4

Expand Down Expand Up @@ -1199,6 +1210,21 @@ class HelpCell: UITableViewCell {
return button
}()

private lazy var submitFeedbackButton: UIButton = {
let button = UIButton()
button.layer.borderWidth = 1
button.layer.borderColor = OEXStyles.shared().neutralXLight().cgColor
button.oex_addAction({ [weak self] _ in
self?.delegate?.didTapSubmitFeedback()
}, for: .touchUpInside)

let faqButtonTitle = [buttonStyle.attributedString(withText: Strings.ProfileOptions.Help.Heading.feedback), faqButtonIcon]
let attributedText = NSAttributedString.joinInNaturalLayout(attributedStrings: faqButtonTitle)
button.setAttributedTitle(attributedText, for: .normal)
button.accessibilityIdentifier = "HelpCell:submit-feedback-button"
return button
}()

private lazy var supportLabel: UILabel = {
let label = UILabel()
label.attributedText = subtitleTextStyle.attributedString(withText: Strings.ProfileOptions.Help.Heading.support)
Expand Down Expand Up @@ -1253,10 +1279,11 @@ class HelpCell: UITableViewCell {
fatalError("init(coder:) has not been implemented")
}

func update(feedbackEnabled: Bool, faqEnabled: Bool, platformName: String) {
func update(feedbackEnabled: Bool, faqEnabled: Bool, submitFeedbackEnabled: Bool, platformName: String) {
self.feedbackEnabled = feedbackEnabled
self.faqEnabled = faqEnabled
self.platformName = platformName
self.submitFeedbackEnabled = submitFeedbackEnabled
setupViews()
setupConstrains()
}
Expand All @@ -1268,6 +1295,9 @@ class HelpCell: UITableViewCell {
feedbackSupportContainer.addSubview(feedbackLabel)
feedbackSupportContainer.addSubview(feedbackSubtitleLabel)
feedbackSupportContainer.addSubview(emailFeedbackButton)
if submitFeedbackEnabled {
feedbackSupportContainer.addSubview(submitFeedbackButton)
}
contentView.addSubview(feedbackSupportContainer)
}

Expand Down Expand Up @@ -1312,8 +1342,19 @@ class HelpCell: UITableViewCell {
make.height.greaterThanOrEqualTo(StandardVerticalMargin * 2)
}

if submitFeedbackEnabled {
submitFeedbackButton.snp.makeConstraints { make in
make.top.equalTo(feedbackSubtitleLabel.snp.bottom).offset(StandardVerticalMargin)
make.leading.equalTo(feedbackSupportContainer)
make.trailing.equalTo(feedbackSupportContainer)
make.height.equalTo(StandardVerticalMargin * 5)
}
}

let constraintView = submitFeedbackEnabled ? submitFeedbackButton : feedbackSubtitleLabel

emailFeedbackButton.snp.makeConstraints { make in
make.top.equalTo(feedbackSubtitleLabel.snp.bottom).offset(StandardVerticalMargin)
make.top.equalTo(constraintView.snp.bottom).offset(StandardVerticalMargin)
make.leading.equalTo(feedbackSupportContainer)
make.trailing.equalTo(feedbackSupportContainer)
make.height.equalTo(StandardVerticalMargin * 5)
Expand Down
3 changes: 3 additions & 0 deletions Source/ServerConfiguration.swift
Expand Up @@ -23,12 +23,14 @@ public extension ServerConfigProvider {
case valuePropEnabled = "value_prop_enabled"
case config = "config"
case iapConfig = "iap_config"
case feedbackFormURL = "feedback_form_url"
}

@objc static let shared = ServerConfiguration()

private(set) var valuePropEnabled: Bool = false
private(set) var iapConfig: IAPConfig? = nil
private(set) var feedbackFormURL: URL? = nil

private override init() {
super.init()
Expand All @@ -41,6 +43,7 @@ public extension ServerConfigProvider {
let config = try? JSONSerialization.jsonObject(with: configData, options : []) as? Dictionary<String,Any> else { return }

valuePropEnabled = config[Keys.valuePropEnabled] as? Bool ?? false
feedbackFormURL = (config[Keys.feedbackFormURL] as? String).flatMap { URL(string:$0)}

if let iapDict = config[Keys.iapConfig] as? Dictionary<String, Any> {
iapConfig = IAPConfig(dictionary: iapDict)
Expand Down

0 comments on commit a0b0fe2

Please sign in to comment.