Skip to content

Commit

Permalink
Support custom URL paths in ManageSubscriptionsView (#4382)
Browse files Browse the repository at this point in the history
* Support custom URL paths in ManageSubscriptionsView

* Fix tests

* More test fixes

* Add parsing and processing tests

* Consolidate SafariView

* Rename method

* Another rename

* Fix import issue with SafariView not being compatible to some platforms

* Style fix
  • Loading branch information
tonidero authored Oct 21, 2024
1 parent 099846e commit 95aaf74
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 24 deletions.
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
1E5F8F6E2C4515430041EECD /* View+PresentCustomerCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */; };
1E5F8F782C46BBD90041EECD /* CustomerCenterAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5F8F772C46BBD90041EECD /* CustomerCenterAction.swift */; };
1E99F81F2AC5917F0023E26E /* StoreMessagesHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E99F81D2AC5917F0023E26E /* StoreMessagesHelperTests.swift */; };
1ED4CA9F2CC25A5F0021AB8F /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED4CA9E2CC25A5F0021AB8F /* SafariView.swift */; };
2C0B98CD2797070B00C5874F /* PromotionalOffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0B98CC2797070B00C5874F /* PromotionalOffer.swift */; };
2C2AEB0F2CA64E0E00A50F38 /* Template1Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AEB0E2CA64E0E00A50F38 /* Template1Preview.swift */; };
2C2AEB3B2CA7209F00A50F38 /* PaywallPackageComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AEB3A2CA7209F00A50F38 /* PaywallPackageComponent.swift */; };
Expand Down Expand Up @@ -1161,6 +1162,7 @@
1E5F8F6D2C4515430041EECD /* View+PresentCustomerCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+PresentCustomerCenter.swift"; sourceTree = "<group>"; };
1E5F8F772C46BBD90041EECD /* CustomerCenterAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterAction.swift; sourceTree = "<group>"; };
1E99F81D2AC5917F0023E26E /* StoreMessagesHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreMessagesHelperTests.swift; sourceTree = "<group>"; };
1ED4CA9E2CC25A5F0021AB8F /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
2C0B98CC2797070B00C5874F /* PromotionalOffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalOffer.swift; sourceTree = "<group>"; };
2C2AEB0E2CA64E0E00A50F38 /* Template1Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Template1Preview.swift; sourceTree = "<group>"; };
2C2AEB3A2CA7209F00A50F38 /* PaywallPackageComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallPackageComponent.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4204,6 +4206,7 @@
887A605D2C1D037000E1A461 /* RemoteImage.swift */,
887A605E2C1D037000E1A461 /* TemplateBackgroundImageView.swift */,
88A543E62C37A4C40039C6A5 /* TierSelectorView.swift */,
1ED4CA9E2CC25A5F0021AB8F /* SafariView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -6063,6 +6066,7 @@
88B1BB132C81479F001B7EE5 /* PaywallComponentTypeTransformers.swift in Sources */,
88B1BB042C813A3C001B7EE5 /* StackComponentViewModel.swift in Sources */,
1E5F8F782C46BBD90041EECD /* CustomerCenterAction.swift in Sources */,
1ED4CA9F2CC25A5F0021AB8F /* SafariView.swift in Sources */,
887A60CC2C1D037000E1A461 /* PaywallFontProvider.swift in Sources */,
887A60B82C1D037000E1A461 /* Template1View.swift in Sources */,
887A60C62C1D037000E1A461 /* LoadingPaywallView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ enum CustomerCenterConfigTestData {
.init(
id: "1",
title: "Didn't receive purchase",
url: nil,
openMethod: nil,
type: .missingPurchase,
detail: nil
),
.init(
id: "2",
title: "Request a refund",
url: nil,
openMethod: nil,
type: .refundRequest,
detail: .promotionalOffer(CustomerCenterConfigData.HelpPath.PromotionalOffer(
iosOfferId: "offer_id",
Expand All @@ -48,12 +52,16 @@ enum CustomerCenterConfigTestData {
.init(
id: "3",
title: "Change plans",
url: nil,
openMethod: nil,
type: .changePlans,
detail: nil
),
.init(
id: "4",
title: "Cancel subscription",
url: nil,
openMethod: nil,
type: .cancel,
detail: .feedbackSurvey(.init(
title: "Why are you cancelling?",
Expand Down Expand Up @@ -86,6 +94,8 @@ enum CustomerCenterConfigTestData {
.init(
id: "9q9719171o",
title: "Check purchases",
url: nil,
openMethod: nil,
type: .missingPurchase,
detail: nil
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand All @@ -186,6 +203,18 @@ private extension ManageSubscriptionsViewModel {
} catch {
self.state = .error(error)
}
case .customUrl:
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ struct ManageSubscriptionsView: View {
product: promotionalOfferData.product,
promoOfferDetails: promotionalOfferData.promoOfferDetails)
})
.sheet(item: self.$viewModel.inAppBrowserURL,
onDismiss: {
self.viewModel.onDismissInAppBrowser()
}, content: { inAppBrowserURL in
SafariView(url: inAppBrowserURL.url)
})
.navigationTitle(self.viewModel.screen.title)
.navigationBarTitleDisplayMode(.inline)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@

import Foundation
import RevenueCat
#if canImport(SafariServices)
import SafariServices
#endif
import SwiftUI

#if PAYWALL_COMPONENTS
Expand All @@ -42,7 +39,7 @@ struct ButtonComponentView: View {
action: { try await performAction() },
label: { StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: self.onDismiss) }
)
#if canImport(SafariServices)
#if canImport(SafariServices) && canImport(UIKit)
.sheet(isPresented: .isNotNil($inAppBrowserURL)) {
SafariView(url: inAppBrowserURL!)
}.presentCustomerCenter(isPresented: $showCustomerCenter) {
Expand Down Expand Up @@ -104,23 +101,6 @@ struct ButtonComponentView: View {

}

#if canImport(SafariServices)
private struct SafariView: UIViewControllerRepresentable {
let url: URL

func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
SFSafariViewController(url: url)
}

func updateUIViewController(
_ uiViewController: SFSafariViewController,
context: UIViewControllerRepresentableContext<SafariView>
) {
// No updates needed
}
}
#endif

#if DEBUG

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
Expand Down
32 changes: 32 additions & 0 deletions RevenueCatUI/Views/SafariView.swift
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 {
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
44 changes: 42 additions & 2 deletions Sources/CustomerCenter/CustomerCenterConfigData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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) {
Expand All @@ -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
}
}

}

public struct PromotionalOffer {

public let iosOfferId: String
Expand Down Expand Up @@ -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) }
}

}
Expand Down Expand Up @@ -425,10 +452,23 @@ extension CustomerCenterConfigData.Localization {

extension CustomerCenterConfigData.HelpPath {

init(from response: CustomerCenterConfigResponse.HelpPath) {
init?(from response: CustomerCenterConfigResponse.HelpPath) {
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 {
Expand Down
18 changes: 18 additions & 0 deletions Sources/Networking/Responses/CustomerCenterConfigResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand All @@ -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

}
Expand Down Expand Up @@ -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 {}
Expand Down Expand Up @@ -167,4 +179,10 @@ extension CustomerCenterConfigResponse.HelpPath.PathType: CodableEnumWithUnknown

}

extension CustomerCenterConfigResponse.HelpPath.OpenMethod: CodableEnumWithUnknownCase {

static var unknownCase: Self { .unknown }

}

extension CustomerCenterConfigResponse: HTTPResponseBody {}
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,8 @@ private class Fixtures {
.init(
id: "1",
title: "Didn't receive purchase",
url: nil,
openMethod: nil,
type: .missingPurchase,
detail: .promotionalOffer(CustomerCenterConfigData.HelpPath.PromotionalOffer(
iosOfferId: "offer_id",
Expand Down
Loading

0 comments on commit 95aaf74

Please sign in to comment.