diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index ec1edab20f..5aab592445 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 2D4D6AF624F7193700B656BE /* verifyReceiptSample1.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2DDE559A24C8B5E300DCB087 /* verifyReceiptSample1.txt */; }; 2D4D6AF724F7193700B656BE /* base64encodedreceiptsample1.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2DDE559B24C8B5E300DCB087 /* base64encodedreceiptsample1.txt */; }; 2D4E926526990AB1000E10B0 /* StoreKit1Wrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4E926426990AB1000E10B0 /* StoreKit1Wrapper.swift */; }; + 2D6AA7B22C99D11A001DD27A /* RevenueCatDeeplinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6AA7B12C99D11A001DD27A /* RevenueCatDeeplinkHandler.swift */; }; 2D735F7E26EFF198004E82A7 /* UnitTestsConfiguration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 2D43017726EBFD7100BAB891 /* UnitTestsConfiguration.storekit */; }; 2D803F6326F144830069D717 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 2D803F6226F144830069D717 /* Nimble */; }; 2D803F6626F144BF0069D717 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 2D803F6526F144BF0069D717 /* Nimble */; }; @@ -1168,6 +1169,7 @@ 2D4E926426990AB1000E10B0 /* StoreKit1Wrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit1Wrapper.swift; sourceTree = ""; }; 2D5BB46A24C8E8ED00E27537 /* PurchasesReceiptParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesReceiptParser.swift; sourceTree = ""; }; 2D69384426DFF93300FCDBC0 /* StoreProductTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreProductTests.swift; sourceTree = ""; }; + 2D6AA7B12C99D11A001DD27A /* RevenueCatDeeplinkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevenueCatDeeplinkHandler.swift; sourceTree = ""; }; 2D84458826B9CD270033B5A3 /* ReceiptFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcherTests.swift; sourceTree = ""; }; 2D8D03B42799A2B90044C2ED /* DocCDocumentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = DocCDocumentation.docc; sourceTree = ""; }; 2D8DB34A24072AAE00BE3D31 /* SubscriberAttributeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriberAttributeTests.swift; sourceTree = ""; }; @@ -4083,6 +4085,7 @@ 887A605D2C1D037000E1A461 /* RemoteImage.swift */, 887A605E2C1D037000E1A461 /* TemplateBackgroundImageView.swift */, 88A543E62C37A4C40039C6A5 /* TierSelectorView.swift */, + 2D6AA7B12C99D11A001DD27A /* RevenueCatDeeplinkHandler.swift */, ); path = Views; sourceTree = ""; @@ -5974,6 +5977,7 @@ 353756682C382C2800A1B8D6 /* CustomerCenterViewModel.swift in Sources */, 887A60BD2C1D037000E1A461 /* TemplateViewType.swift in Sources */, 887A606D2C1D037000E1A461 /* Localization.swift in Sources */, + 2D6AA7B22C99D11A001DD27A /* RevenueCatDeeplinkHandler.swift in Sources */, 887A60CA2C1D037000E1A461 /* RemoteImage.swift in Sources */, 88B1BAF42C813A3C001B7EE5 /* SpacerComponentViewModel.swift in Sources */, 887A607B2C1D037000E1A461 /* Bundle+Extensions.swift in Sources */, diff --git a/RevenueCatUI/Views/RevenueCatDeeplinkHandler.swift b/RevenueCatUI/Views/RevenueCatDeeplinkHandler.swift new file mode 100644 index 0000000000..bed58dcc92 --- /dev/null +++ b/RevenueCatUI/Views/RevenueCatDeeplinkHandler.swift @@ -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: 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 + if let extractedOfferingID = extractOfferingID(from: url) { + 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) + } 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)") + } + } +} diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/Info.plist b/Tests/TestingApps/PaywallsTester/PaywallsTester/Info.plist index 2d4540095c..cfff3c0b3b 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester/Info.plist +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/Info.plist @@ -2,6 +2,19 @@ + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.revenuecat.PaywallsTester + CFBundleURLSchemes + + paywallsTester + + + ITSAppUsesNonExemptEncryption NSAppTransportSecurity diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/AppContentView.swift b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/AppContentView.swift index 5028c0891d..e6d5d938ad 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/AppContentView.swift +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/AppContentView.swift @@ -44,6 +44,7 @@ struct AppContentView: View { } #endif } + .handleRevenueCatDeeplinks() } private var background: some View {