-
Notifications
You must be signed in to change notification settings - Fork 319
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
Paywalls: Open up paywalls from deeplinks #4285
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
---|---|---|
@@ -0,0 +1,107 @@ | ||
// | ||
// 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 | ||
// | ||
// RevenueCatDeeplinkHandler.swift | ||
// | ||
// Created by Andrés Boedo on 9/17/24. | ||
|
||
import Foundation | ||
import RevenueCat | ||
import SwiftUI | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
@available(macOS, unavailable, message: "RevenueCatUI does not support macOS yet") | ||
@available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") | ||
struct RevenueCatDeeplinkHandlerView<Content: View>: View { | ||
@State private var isShowingPaywall: Bool = false | ||
@State private var offeringID: String? | ||
|
||
let content: Content | ||
|
||
init(@ViewBuilder content: () -> Content) { | ||
self.content = content() | ||
} | ||
|
||
var body: some View { | ||
content | ||
.onOpenURL { url in | ||
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. Not sure if we would like to also support UIKit apps... but I guess that can come in a separate PR. 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 figured for UIKit we'd basically do the appDelegate + present from root view controller path 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 just adding a method that can be called by a developer in the app delegate method would work 👍 |
||
if let extractedOfferingID = extractOfferingID(from: url) { | ||
tonidero marked this conversation as resolved.
Show resolved
Hide resolved
|
||
offeringID = extractedOfferingID | ||
isShowingPaywall = true | ||
} | ||
} | ||
.sheet( | ||
isPresented: $isShowingPaywall, | ||
onDismiss: { | ||
offeringID = nil | ||
}, | ||
content: { | ||
if let offeringID = offeringID { | ||
OfferingLoaderView(offeringID: offeringID) | ||
} else { | ||
Text("Invalid offering ID.") | ||
} | ||
}) | ||
} | ||
|
||
private func extractOfferingID(from url: URL) -> String? { | ||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) | ||
return components?.queryItems?.first(where: { $0.name == "offeringID" })?.value | ||
} | ||
} | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
@available(macOS, unavailable, message: "RevenueCatUI does not support macOS yet") | ||
@available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") | ||
extension View { | ||
func handleRevenueCatDeeplinks() -> some View { | ||
RevenueCatDeeplinkHandlerView { | ||
self | ||
} | ||
} | ||
} | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
@available(macOS, unavailable, message: "RevenueCatUI does not support macOS yet") | ||
@available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") | ||
struct OfferingLoaderView: View { | ||
let offeringID: String | ||
@State private var offering: Offering? | ||
@State private var isLoading = true | ||
|
||
var body: some View { | ||
Group { | ||
if let offering = offering { | ||
PaywallView(offering: offering) | ||
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. Thinking about it, we might also want to offer a way to not display the paywall if the user has an entitlement (passing a 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, that's a good point. It could be done as an extra query param in the deeplink too |
||
} else if isLoading { | ||
ProgressView() | ||
} else { | ||
Text("Could not load offering.") | ||
} | ||
} | ||
.task { | ||
await fetchOffering() | ||
} | ||
} | ||
|
||
private func fetchOffering() async { | ||
do { | ||
let offerings = try await Purchases.shared.offerings() | ||
if let offering = offerings.offering(identifier: offeringID) { | ||
self.offering = offering | ||
} else { | ||
isLoading = false | ||
print("Offering with ID \(offeringID) not found.") | ||
} | ||
} catch { | ||
isLoading = false | ||
print("Error fetching offering: \(error.localizedDescription)") | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ struct AppContentView: View { | |
} | ||
#endif | ||
} | ||
.handleRevenueCatDeeplinks() | ||
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. Nothing to do here but I must say, I'm still not used to having these things as modifiers of a view, seems like a very weird API to me 😅 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. 🤔 what would a more natural API look like to you as a dev? I'm open to anything as long as its easy to do. I know others do the setup in the app delegate, but the industry seems to be moving away from 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. Yeah, I think this is the way it's supposed to be in SwiftUI (like 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. Yes I feel you Toni haha, but indeed this seems to be the SwiftUI way. In Compose it would be more natural to add a route to a navigation graph. The downside of that is that there are multiple ways to build such a graph, as mentioned on Slack. |
||
} | ||
|
||
private var background: some View { | ||
|
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.
to get this through the finish line, we really need to:
/paywall
in the examples but I'm not even enforcing it, mayberevenuecatui
is more appropriate?