-
Notifications
You must be signed in to change notification settings - Fork 338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support custom URL paths in ManageSubscriptionsView
#4382
Changes from all commits
2be7197
40229aa
4ef91aa
a0b19e7
2730487
3cf3e73
ebcdd90
eb23ef7
487472c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,8 @@ class ManageSubscriptionsViewModel: ObservableObject { | |
@Published | ||
var promotionalOfferData: PromotionalOfferData? | ||
@Published | ||
var inAppBrowserURL: IdentifiableURL? | ||
@Published | ||
var state: CustomerCenterViewState { | ||
didSet { | ||
if case let .error(stateError) = state { | ||
|
@@ -152,17 +154,32 @@ class ManageSubscriptionsViewModel: ObservableObject { | |
self.loadingPath = nil | ||
} | ||
} | ||
|
||
func onDismissInAppBrowser() { | ||
self.inAppBrowserURL = nil | ||
} | ||
#endif | ||
|
||
} | ||
|
||
struct IdentifiableURL: Identifiable { | ||
|
||
var id: String { | ||
return url.absoluteString | ||
} | ||
|
||
let url: URL | ||
|
||
} | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
@available(macOS, unavailable) | ||
@available(tvOS, unavailable) | ||
@available(watchOS, unavailable) | ||
private extension ManageSubscriptionsViewModel { | ||
|
||
#if os(iOS) || targetEnvironment(macCatalyst) | ||
// swiftlint:disable:next cyclomatic_complexity | ||
private func onPathSelected(path: CustomerCenterConfigData.HelpPath) async { | ||
switch path.type { | ||
case .missingPurchase: | ||
|
@@ -186,6 +203,18 @@ private extension ManageSubscriptionsViewModel { | |
} catch { | ||
self.state = .error(error) | ||
} | ||
case .customUrl: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note this is only currently available in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. interesting... It might be worth figuring out a way for those to work eventually since for those it might make even more sense - you can direct users to where you'd actually want them to manage There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think we need to move some of the logic to a shared view for all 3 of them and handle paths in all 3. Right now the buttons are mostly hardcoded in those 2 other views. Probably not something for this PR though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree we need to do that refactor yes |
||
guard let url = path.url, | ||
let openMethod = path.openMethod else { | ||
Logger.warning("Found a custom URL path without a URL or open method. Ignoring tap.") | ||
return | ||
} | ||
switch openMethod { | ||
case .external: | ||
UIApplication.shared.open(url, options: [:], completionHandler: nil) | ||
case .inApp: | ||
self.inAppBrowserURL = .init(url: url) | ||
} | ||
default: | ||
break | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// | ||
// Copyright RevenueCat Inc. All Rights Reserved. | ||
// | ||
// Licensed under the MIT License (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// https://opensource.org/licenses/MIT | ||
// | ||
// SafariView.swift | ||
// | ||
// Created by Antonio Rico Diez on 2024-10-18. | ||
|
||
#if canImport(SafariServices) && canImport(UIKit) | ||
|
||
import SafariServices | ||
import SwiftUI | ||
|
||
struct SafariView: UIViewControllerRepresentable { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lmk what you think of this. Seems to work fine and seemed like an easy solution :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is good yea! One thing though is that there's an existing (and pretty much identical) The only difference between the 2 #if canImport(SafariServices) and the new one uses #if os(iOS) Could we consolidate the 2? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ohh I had missed that one! Thanks for the heads up! I can try to consolidate yeah |
||
let url: URL | ||
|
||
func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController { | ||
return SFSafariViewController(url: url) | ||
} | ||
|
||
func updateUIViewController(_ uiViewController: SFSafariViewController, | ||
context: UIViewControllerRepresentableContext<SafariView>) { | ||
// No need to update the controller, as it's static in this case | ||
} | ||
} | ||
|
||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -177,15 +177,21 @@ public struct CustomerCenterConfigData { | |
|
||
public let id: String | ||
public let title: String | ||
public let url: URL? | ||
public let openMethod: OpenMethod? | ||
public let type: PathType | ||
public let detail: PathDetail? | ||
|
||
public init(id: String, | ||
title: String, | ||
url: URL?, | ||
openMethod: OpenMethod?, | ||
type: PathType, | ||
detail: PathDetail?) { | ||
self.id = id | ||
self.title = title | ||
self.url = url | ||
self.openMethod = openMethod | ||
self.type = type | ||
self.detail = detail | ||
} | ||
|
@@ -203,6 +209,7 @@ public struct CustomerCenterConfigData { | |
case refundRequest = "REFUND_REQUEST" | ||
case changePlans = "CHANGE_PLANS" | ||
case cancel = "CANCEL" | ||
case customUrl = "CUSTOM_URL" | ||
case unknown | ||
|
||
init(from rawValue: String) { | ||
|
@@ -215,13 +222,33 @@ public struct CustomerCenterConfigData { | |
self = .changePlans | ||
case "CANCEL": | ||
self = .cancel | ||
case "CUSTOM_URL": | ||
self = .customUrl | ||
default: | ||
self = .unknown | ||
} | ||
} | ||
|
||
} | ||
|
||
public enum OpenMethod: String { | ||
|
||
case inApp = "IN_APP" | ||
case external = "EXTERNAL" | ||
|
||
init?(from rawValue: String?) { | ||
switch rawValue { | ||
case "IN_APP": | ||
self = .inApp | ||
case "EXTERNAL": | ||
self = .external | ||
default: | ||
return nil | ||
} | ||
} | ||
|
||
} | ||
Comment on lines
+234
to
+250
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should consolidate this with the existing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm right kinda same thinking as you here... It's internal APIs so I guess we can go either way... I would suggest keeping it separate for flexibility for now, and we can always refactor later if needed. For example, we are currently only adding |
||
|
||
public struct PromotionalOffer { | ||
|
||
public let iosOfferId: String | ||
|
@@ -389,7 +416,7 @@ extension CustomerCenterConfigData.Screen { | |
self.type = ScreenType(from: response.type.rawValue) | ||
self.title = response.title | ||
self.subtitle = response.subtitle | ||
self.paths = response.paths.map { CustomerCenterConfigData.HelpPath(from: $0) } | ||
self.paths = response.paths.compactMap { CustomerCenterConfigData.HelpPath(from: $0) } | ||
} | ||
|
||
} | ||
|
@@ -425,10 +452,23 @@ extension CustomerCenterConfigData.Localization { | |
|
||
extension CustomerCenterConfigData.HelpPath { | ||
|
||
init(from response: CustomerCenterConfigResponse.HelpPath) { | ||
init?(from response: CustomerCenterConfigResponse.HelpPath) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that I changed the constructor to be nullable. If we get a custom url path without a url or open method, we can filter it out and this was the easiest way to achieve that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible to make it throw instead? Imo absence of data (null) is not the same as incorrect input data. The latter is an error, the former doesn't have to be. (I also find it odd that Foundation's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm so for customer center I would specially take a very "lax" approach. As in, if there is something misconfigured in the backend, I would prefer to try to display as much as possible instead of failing entirely. For example in this case, it basically means that if somehow a misconfiguration happened, we would just hide the path. I think we should do this since we're still changing the schema every now and then and we should be flexible. Having said that, we could indeed throw in the constructor and handle it when creating the paths. The code becomes a bit less legible IMO, since we need to handle the catch though... I'm not sure TBH, since I know optional constructors are actually quite common in Swift (even if I don't like them as much 😅)... Wdyt @vegaro @aboedo ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I think it also kinda maps better to what's happening underneath - you're using an optional constructor to try to build the URL in any case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, this is essentially validating JSON, which I agree should be lax. We could avoid handling the catch and just doing self.paths = response.paths.compactMap { try? CustomerCenterConfigData.HelpPath(from: $0) } I don't feel super strongly about this btw, especially since optional constructors are common in Swift, as you said. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm yeah that's not bad. I also don't feel too strongly about this TBH... But considering we don't do that many throws along the codebase, I'm thinking on leaving it as is... If you feel we should better throw an error, I'm ok changing to your suggestion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No it's minor imo. Feel free to leave as is! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would also leave this as nullable as well because of the optional constructor it is using underneath |
||
self.id = response.id | ||
self.title = response.title | ||
self.type = CustomerCenterConfigData.HelpPath.PathType(from: response.type.rawValue) | ||
if self.type == .customUrl { | ||
if let responseUrl = response.url, | ||
let url = URL(string: responseUrl), | ||
let openMethod = CustomerCenterConfigData.HelpPath.OpenMethod(from: response.openMethod?.rawValue) { | ||
self.url = url | ||
self.openMethod = openMethod | ||
} else { | ||
return nil | ||
} | ||
} else { | ||
self.url = nil | ||
self.openMethod = nil | ||
} | ||
if let promotionalOfferResponse = response.promotionalOffer { | ||
self.detail = .promotionalOffer(PromotionalOffer(from: promotionalOfferResponse)) | ||
} else if let feedbackSurveyResponse = response.feedbackSurvey { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,8 @@ struct CustomerCenterConfigResponse { | |
let id: String | ||
let title: String | ||
let type: PathType | ||
let url: String? | ||
let openMethod: OpenMethod? | ||
let promotionalOffer: PromotionalOffer? | ||
let feedbackSurvey: FeedbackSurvey? | ||
|
||
|
@@ -53,6 +55,15 @@ struct CustomerCenterConfigResponse { | |
case refundRequest = "REFUND_REQUEST" | ||
case changePlans = "CHANGE_PLANS" | ||
case cancel = "CANCEL" | ||
case customUrl = "CUSTOM_URL" | ||
case unknown | ||
|
||
} | ||
|
||
enum OpenMethod: String { | ||
|
||
case inApp = "IN_APP" | ||
case external = "EXTERNAL" | ||
case unknown | ||
|
||
} | ||
|
@@ -130,6 +141,7 @@ extension CustomerCenterConfigResponse.CustomerCenter: Codable, Equatable {} | |
extension CustomerCenterConfigResponse.Localization: Codable, Equatable {} | ||
extension CustomerCenterConfigResponse.HelpPath: Codable, Equatable {} | ||
extension CustomerCenterConfigResponse.HelpPath.PathType: Equatable {} | ||
extension CustomerCenterConfigResponse.HelpPath.OpenMethod: Equatable {} | ||
extension CustomerCenterConfigResponse.HelpPath.PromotionalOffer: Codable, Equatable {} | ||
extension CustomerCenterConfigResponse.HelpPath.FeedbackSurvey: Codable, Equatable {} | ||
extension CustomerCenterConfigResponse.HelpPath.FeedbackSurvey.Option: Codable, Equatable {} | ||
|
@@ -167,4 +179,10 @@ extension CustomerCenterConfigResponse.HelpPath.PathType: CodableEnumWithUnknown | |
|
||
} | ||
|
||
extension CustomerCenterConfigResponse.HelpPath.OpenMethod: CodableEnumWithUnknownCase { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vegaro Note that I added this for the new enum |
||
|
||
static var unknownCase: Self { .unknown } | ||
|
||
} | ||
|
||
extension CustomerCenterConfigResponse: HTTPResponseBody {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this is the best way but I couldn't make
URL
Identifiable
directly without getting a warning.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you getting the warning that it will not behave correctly if
URL
ever becomesIdentifiable
in the future? If so, yea I think this solution is the safest!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes sense
nitpick but there should be an empty line at the beginning and at the end of the struct