diff --git a/Sources/Packages/AppRouter/Sources/AppRouter/SheetDestination.swift b/Sources/Packages/AppRouter/Sources/AppRouter/SheetDestination.swift index 0ce0e41..faae476 100644 --- a/Sources/Packages/AppRouter/Sources/AppRouter/SheetDestination.swift +++ b/Sources/Packages/AppRouter/Sources/AppRouter/SheetDestination.swift @@ -10,7 +10,6 @@ import Foundation public enum SheetDestination: String { case userProfile case newShow - case paywall } extension SheetDestination: Identifiable { @@ -24,8 +23,6 @@ extension SheetDestination: Identifiable { "Sheet.UserProfile" case .newShow: "Sheet.NewShow" - case .paywall: - "Sheet.paywall" } } } diff --git a/Sources/iHog/iHog.xcodeproj/project.pbxproj b/Sources/iHog/iHog.xcodeproj/project.pbxproj index d81755d..5533068 100644 --- a/Sources/iHog/iHog.xcodeproj/project.pbxproj +++ b/Sources/iHog/iHog.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 692677A32D0943500006895D /* AppRouter in Frameworks */ = {isa = PBXBuildFile; productRef = 692677A22D0943500006895D /* AppRouter */; }; 693137A42CE1579000562B73 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 693137A32CE1579000562B73 /* PostHog */; }; 69666DE92CDD774B008A0057 /* TelemetryDeck in Frameworks */ = {isa = PBXBuildFile; productRef = 69666DE82CDD774B008A0057 /* TelemetryDeck */; }; + 69C8F9752E8E269900F3C4D9 /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = 69C8F9742E8E269900F3C4D9 /* RevenueCatUI */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -95,6 +96,7 @@ 3730E00D27A17B27006FCA5E /* OctoKit in Frameworks */, 15C8285125FA4B83003FAEFA /* StoreKit.framework in Frameworks */, 69666DE92CDD774B008A0057 /* TelemetryDeck in Frameworks */, + 69C8F9752E8E269900F3C4D9 /* RevenueCatUI in Frameworks */, 377087D727AABFF000B0754A /* CoffeeToast in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -221,6 +223,7 @@ 69666DE82CDD774B008A0057 /* TelemetryDeck */, 693137A32CE1579000562B73 /* PostHog */, 692677A22D0943500006895D /* AppRouter */, + 69C8F9742E8E269900F3C4D9 /* RevenueCatUI */, ); productName = iHog; productReference = 15DA2673251237D60063E236 /* iHog.app */; @@ -580,7 +583,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2025.1.0; + MARKETING_VERSION = 2025.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.appsbymw.hogosc; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -610,7 +613,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2025.1.0; + MARKETING_VERSION = 2025.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.appsbymw.hogosc; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -763,7 +766,7 @@ repositoryURL = "https://github.com/RevenueCat/purchases-ios.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.0.0; + minimumVersion = 5.0.0; }; }; 377087D527AABFF000B0754A /* XCRemoteSwiftPackageReference "CoffeeToast" */ = { @@ -776,7 +779,7 @@ }; 37EAFA8E27AF31530092C11E /* XCRemoteSwiftPackageReference "OSCKit" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/maeganwilson/OSCKit"; + repositoryURL = "https://github.com/heyjaywilson/OSCKit"; requirement = { kind = upToNextMajorVersion; minimumVersion = 3.0.0; @@ -835,6 +838,11 @@ package = 69666DE72CDD774B008A0057 /* XCRemoteSwiftPackageReference "SwiftSDK" */; productName = TelemetryDeck; }; + 69C8F9742E8E269900F3C4D9 /* RevenueCatUI */ = { + isa = XCSwiftPackageProductDependency; + package = 3769730A28A52743004D9BEF /* XCRemoteSwiftPackageReference "purchases-ios" */; + productName = RevenueCatUI; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 15DA266B251237D60063E236 /* Project object */; diff --git a/Sources/iHog/iHog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sources/iHog/iHog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bb380c6..5a6b399 100644 --- a/Sources/iHog/iHog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sources/iHog/iHog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "8c3679f60ab478d8a0179d07bc4403c63f88dd1def0d37f6f3f683fe91d99bd9", + "originHash" : "0b947bc129fb4282b8415427d1b535d96249f317a408d208acee677622f7f59e", "pins" : [ { "identity" : "cocoaasyncsocket", @@ -34,13 +34,13 @@ "location" : "https://github.com/nerdishbynature/octokit.swift", "state" : { "branch" : "main", - "revision" : "1e63f582d69a2c78335af532171e2fb2749251a6" + "revision" : "99aea9cfb39d65d339ba3ec4674e22ae08ebcaaa" } }, { "identity" : "osckit", "kind" : "remoteSourceControl", - "location" : "https://github.com/maeganwilson/OSCKit", + "location" : "https://github.com/heyjaywilson/OSCKit", "state" : { "revision" : "f5982778ddfe573d82d9a1137dbfea17bb52989b", "version" : "3.1.0" @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/PostHog/posthog-ios.git", "state" : { - "revision" : "0e3845ef83640701afb820837fe59f95e95e819e", - "version" : "3.14.2" + "revision" : "f381818bc153f63fbe13d6822e65cc7233ba4647", + "version" : "3.31.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/RevenueCat/purchases-ios.git", "state" : { - "revision" : "2f559c69a255da5cd5f66d5a6b9a1dfe6bc61ead", - "version" : "4.32.0" + "revision" : "a315ca87fcec9fa6290c538e88e2f0a62f796458", + "version" : "5.40.0" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/TelemetryDeck/SwiftSDK", "state" : { - "revision" : "06bd6c3005da9113ae3766f8e23bb0fd07f8852f", - "version" : "2.5.0" + "revision" : "a7fd1eb469589a3c8fd8cc3abe19b092b5d674dc", + "version" : "2.9.4" } } ], diff --git a/Sources/iHog/iHog.xcodeproj/project.xcworkspace/xcuserdata/jay.xcuserdatad/UserInterfaceState.xcuserstate b/Sources/iHog/iHog.xcodeproj/project.xcworkspace/xcuserdata/jay.xcuserdatad/UserInterfaceState.xcuserstate index 075208f..926c487 100644 Binary files a/Sources/iHog/iHog.xcodeproj/project.xcworkspace/xcuserdata/jay.xcuserdatad/UserInterfaceState.xcuserstate and b/Sources/iHog/iHog.xcodeproj/project.xcworkspace/xcuserdata/jay.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sources/iHog/iHog.xcodeproj/xcshareddata/xcschemes/Xcode Cloud.xcscheme b/Sources/iHog/iHog.xcodeproj/xcshareddata/xcschemes/Xcode Cloud.xcscheme index abcdb2d..7bcb9a8 100644 --- a/Sources/iHog/iHog.xcodeproj/xcshareddata/xcschemes/Xcode Cloud.xcscheme +++ b/Sources/iHog/iHog.xcodeproj/xcshareddata/xcschemes/Xcode Cloud.xcscheme @@ -70,8 +70,8 @@ + + + + + + + + + + + + + + diff --git a/Sources/iHog/iHog/Analytics/AnalyticEvent.swift b/Sources/iHog/iHog/Analytics/AnalyticEvent.swift index 7e6129b..24b8a8a 100644 --- a/Sources/iHog/iHog/Analytics/AnalyticEvent.swift +++ b/Sources/iHog/iHog/Analytics/AnalyticEvent.swift @@ -13,6 +13,8 @@ enum AnalyticEvent: String { case subscribeButtonTapped /// Track when a purchase is made case purchase + case purchaseRestoreCompleted + case paywallDismissed /// Track when the connect to console button is tapped case connectToConsoleTapped /// Track when the disconnect from console button is tapped @@ -31,8 +33,8 @@ enum AnalyticEvent: String { case showDeleted /// Button to delete show was tapped case showDeleteTapped - /// Add show button tapped -case addShowTapped + /// Add show button tapped + case addShowTapped /// Hyphen separated var value: String { diff --git a/Sources/iHog/iHog/Analytics/AnalyticEventParameter.swift b/Sources/iHog/iHog/Analytics/AnalyticEventParameter.swift index 72f0f00..b21935e 100644 --- a/Sources/iHog/iHog/Analytics/AnalyticEventParameter.swift +++ b/Sources/iHog/iHog/Analytics/AnalyticEventParameter.swift @@ -13,13 +13,14 @@ enum UserProperty: String { enum AnalyticEventParameter: String { case numberOfShows case paywallSource + case paywallTrigger + case restoreSuccessful + case activeSubscriptions case errorLevel case purchaseState case purchaseAmount case onboardingStep case userIsSandBoxed = "user.isSandboxed" - case featureFlagValue = "featureFlag.value" - case featureFlagKey = "featureFlag.key" } enum PaywallSource: String { diff --git a/Sources/iHog/iHog/Analytics/Analytics.swift b/Sources/iHog/iHog/Analytics/Analytics.swift index d434430..affe00c 100644 --- a/Sources/iHog/iHog/Analytics/Analytics.swift +++ b/Sources/iHog/iHog/Analytics/Analytics.swift @@ -139,14 +139,4 @@ class Analytics { HogLogger.log(category: .analytics) .debug("πŸ”¦ Logged event: \(AnalyticEvent.userCodeLoaded.rawValue) | \(codes)") } - - func logFeatureFlagToggle(flag: FeatureFlagKey, value: Bool) { - logEvent( - with: .featureFlagToggled, - parameters: [ - .featureFlagKey: flag.rawValue, - .featureFlagValue: value, - ] - ) - } } diff --git a/Sources/iHog/iHog/FeatureFlags/FeatureFlagKey.swift b/Sources/iHog/iHog/FeatureFlags/FeatureFlagKey.swift deleted file mode 100644 index f8523c4..0000000 --- a/Sources/iHog/iHog/FeatureFlags/FeatureFlagKey.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// FeatureFlag.swift -// iHog -// -// Created by Jay Wilson on 11/10/24. -// - -/// Feature flag keys defined in PostHog -enum FeatureFlagKey: String, CaseIterable, Identifiable { - // Enables the use of SwiftData instead of CoreData in the app - case swiftdata -} - -extension FeatureFlagKey { - var id: String { rawValue } - - var listLabel: String { - switch self { - case .swiftdata: - "Replace data storage with Swift Data" - } - } - - var isAvailable: Bool { - switch self { - case .swiftdata: - false - } - } -} diff --git a/Sources/iHog/iHog/Features/AppPaymentService/AppPaymentService.swift b/Sources/iHog/iHog/Features/AppPaymentService/AppPaymentService.swift new file mode 100644 index 0000000..322ac1d --- /dev/null +++ b/Sources/iHog/iHog/Features/AppPaymentService/AppPaymentService.swift @@ -0,0 +1,193 @@ +// +// AppPaymentService.swift +// iHog +// +// Created by Jay Wilson on 10/1/25. +// + +import Foundation +import OSLog +import RevenueCat + +/// Determines when to show a paywall based on a trigger +@Observable +class AppPaymentService { + static let buildNumberWithSubscription = 193 + var shouldShowPaywall = false + + private var customerInfo: CustomerInfo? = nil { + didSet { + determineIsPro() + determineDateOriginallySubscribed() + Task { + do { + try await determineIfNeedsEarlyAdopterEntitlement() + } catch { + Logger.appPaymentService.error("Could not check for early adopter entitlement: \(error)") + } + } + } + } + + @ObservationIgnored + var lastUsedTrigger: PaywallTrigger? = nil + + var isPro: Bool + var dateOriginallySubscribed: Date? = nil + + init( + shouldShowPaywall: Bool = false, + customerInfo: CustomerInfo? = nil, + lastUsedTrigger: PaywallTrigger? = nil, + isPro: Bool = false + ) { + self.shouldShowPaywall = shouldShowPaywall + self.customerInfo = customerInfo + self.lastUsedTrigger = lastUsedTrigger + self.isPro = isPro + } + + func configure() { + let handler: (RevenueCat.LogLevel, String) -> Void = { level, message in + switch level { + case .debug: + Logger.purchases.debug("\(message)") + case .verbose: + Logger.purchases.trace("\(message)") + case .info: + Logger.purchases.info("\(message)") + case .warn: + Logger.purchases.warning("\(message)") + case .error: + Logger.purchases.error("\(message)") + } + } + Purchases.logHandler = handler + #if DEBUG + Purchases.logLevel = .debug + #else + Purchases.logLevel = .error + #endif + Purchases.configure(withAPIKey: AppInfo.revenueCatKey) + Task { + for await newCustomerInfo in Purchases.shared.customerInfoStream { + await MainActor.run { + customerInfo = newCustomerInfo + Logger.appPaymentService.info( + """ + πŸ’Έ Subscription status updated: + IsPro: \(self.isPro, privacy: .public) + """ + ) + } + } + } + } + + func determineIsPro() { + for entitlement in HogEntitlement.allCases { + guard + let isActive = customerInfo?.entitlements.active + .contains(where: { $0.key == entitlement.rawValue }) + else { + // Did not find entitlement on user + continue + } + if isActive { + isPro = true + // TODO: Setup analytics to mark the person's entitlement property + return + } + } + isPro = false + } + + func determineDateOriginallySubscribed() { + // Entitlement.pro use the date of purchase for pro + if let isPro = customerInfo?.entitlements.active + .contains( + where: { $0.key == HogEntitlement.pro.rawValue }) + { + if isPro { + self.dateOriginallySubscribed = + customerInfo? + .entitlements[HogEntitlement.pro.rawValue]? + .originalPurchaseDate + } + } + // Entitlement.puntPage use the date of purchase for puntPage + if let puntPageUser = customerInfo?.entitlements.active + .contains( + where: { $0.key == HogEntitlement.puntPage.rawValue }) + { + if puntPageUser { + self.dateOriginallySubscribed = + customerInfo? + .entitlements[HogEntitlement.puntPage.rawValue]? + .originalPurchaseDate + } + } + // Entitlement.earlyAdopter use the original purchase for app + if let earlyAdopter = customerInfo?.entitlements.active + .contains( + where: { $0.key == HogEntitlement.earlyAdopter.rawValue }) + { + if earlyAdopter { + self.dateOriginallySubscribed = + customerInfo? + .entitlements[HogEntitlement.earlyAdopter.rawValue]? + .originalPurchaseDate + } + } + + self.dateOriginallySubscribed = nil + } + + func triggerPaywall(for trigger: PaywallTrigger) { + guard isPro == false else { + shouldShowPaywall = false + return + } + self.lastUsedTrigger = trigger + switch trigger { + case .addShow(let showCount): + // Want true to show paywall + // Want to show paywall when a non-pro user has more than 1 show. + // So any showcount > 0 should trigger the paywall + shouldShowPaywall = showCount > 0 + case .thisIsAdamsFault, .customIcons, .userRequest, .puntPage: + shouldShowPaywall = true + } + } + + func determineIfNeedsEarlyAdopterEntitlement() async throws { + let userId = Purchases.shared.appUserID + // Get original app build number + guard let customerInfo else { + Logger.appPaymentService.info("No customer info") + return + } + // DO NOT CONTINUE IF THE USER HAS THE ENTITLEMENT + guard + customerInfo.entitlements.active + .contains(where: { $0.key == HogEntitlement.earlyAdopter.rawValue }) == false + else { + Logger.appPaymentService.info("Customer does not have early adopter entitlement") + return + } + guard let originalAppBuildNumberString = customerInfo.originalApplicationVersion, + let originalAppBuildNumber = Int(originalAppBuildNumberString) + else { + Logger.appPaymentService.info("Build number not able to be made into an Int") + return + } + if originalAppBuildNumber <= Self.buildNumberWithSubscription { + // Need to grant intitlement now that + let response = try await BaconService().grantLifetimeEntitlement(for: userId) + Logger.appPaymentService + .info( + "Granted entitlement for \(response.userId) at \(response.grantedAt.formatted())" + ) + } + } +} diff --git a/Sources/iHog/iHog/Features/AppPaymentService/HogEntitlement.swift b/Sources/iHog/iHog/Features/AppPaymentService/HogEntitlement.swift new file mode 100644 index 0000000..8c0868a --- /dev/null +++ b/Sources/iHog/iHog/Features/AppPaymentService/HogEntitlement.swift @@ -0,0 +1,12 @@ +// +// Entitlement.swift +// iHog +// +// Created by Jay Wilson on 10/1/25. +// + +enum HogEntitlement: String, CaseIterable { + case puntPage = "punt-page" + case pro = "pro" + case earlyAdopter = "early_adopter" +} diff --git a/Sources/iHog/iHog/Features/AppPaymentService/HogPaywall/HogPaywall.swift b/Sources/iHog/iHog/Features/AppPaymentService/HogPaywall/HogPaywall.swift new file mode 100644 index 0000000..b5ae1e7 --- /dev/null +++ b/Sources/iHog/iHog/Features/AppPaymentService/HogPaywall/HogPaywall.swift @@ -0,0 +1,117 @@ +// +// HogPaywall.swift +// iHog +// +// Created by Jay Wilson on 10/1/25. +// + +import RevenueCat +import RevenueCatUI +import SwiftUI + +struct HogPaywallView: View { + @Environment(AppPaymentService.self) var appPaymentService + + var showDismiss: Bool + var source: PaywallSource + + var body: some View { + @Bindable var appPaymentService = appPaymentService + + PaywallView(displayCloseButton: showDismiss) + .onRequestedDismissal { + appPaymentService.shouldShowPaywall.toggle() + Analytics.shared + .logEvent(with: .paywallDismissed, parameters: [.paywallSource: source]) + } + .onRestoreCompleted { customerInfo in + analyticsHookForRestoreSuccess(customerInfo: customerInfo) + } + .onRestoreFailure { _ in + analyticsHookRestoreFailed() + } + .onPurchaseCompleted { transaction, customerInfo in + Analytics.shared.logEvent( + with: .purchase, + parameters: [ + .activeSubscriptions: customerInfo.activeSubscriptions.map({ $0 }) + .joined( + separator: "," + ), + .paywallTrigger: appPaymentService.lastUsedTrigger ?? "NO_TRIGGER", + .paywallSource: source, + ] + ) + } + } + + func analyticsHookForRestoreSuccess(customerInfo: CustomerInfo) { + Analytics.shared.logEvent( + with: .purchaseRestoreCompleted, + parameters: [ + .restoreSuccessful: true, + .paywallSource: source, + .paywallTrigger: appPaymentService.lastUsedTrigger ?? "NO_TRIGGER", + .activeSubscriptions: customerInfo.activeSubscriptions.map({ $0 }).joined(separator: ","), + ] + ) + } + + func analyticsHookRestoreFailed() { + Analytics.shared.logEvent( + with: .purchaseRestoreCompleted, + parameters: [ + .restoreSuccessful: false, + .paywallSource: source, + .paywallTrigger: appPaymentService.lastUsedTrigger ?? "NO_TRIGGER", + ] + ) + } +} + +// Viewmodifier that will control when to show or hide paywall in a sheet based on AppPaymentService +struct HogPaywallModifier: ViewModifier { + @Environment(AppPaymentService.self) var appPaymentService + + var showDismiss: Bool + var source: PaywallSource + + func body(content: Content) -> some View { + @Bindable var appPaymentService = appPaymentService + content + .sheet(isPresented: $appPaymentService.shouldShowPaywall) { + HogPaywallView(showDismiss: showDismiss, source: source) + } + } + + func analyticsHookForRestoreSuccess(customerInfo: CustomerInfo) { + Analytics.shared.logEvent( + with: .purchaseRestoreCompleted, + parameters: [ + .restoreSuccessful: true, + .paywallSource: source, + .paywallTrigger: appPaymentService.lastUsedTrigger ?? "NO_TRIGGER", + .activeSubscriptions: customerInfo.activeSubscriptions.map({ $0 }).joined(separator: ","), + ] + ) + } + + func analyticsHookRestoreFailed() { + Analytics.shared.logEvent( + with: .purchaseRestoreCompleted, + parameters: [ + .restoreSuccessful: false, + .paywallSource: source, + .paywallTrigger: appPaymentService.lastUsedTrigger ?? "NO_TRIGGER", + ] + ) + } +} + +extension View { + func hogPaywall(showDismiss: Bool = true, source: PaywallSource) -> some View { + modifier( + HogPaywallModifier(showDismiss: showDismiss, source: source) + ) + } +} diff --git a/Sources/iHog/iHog/Features/AppPaymentService/PaywallTrigger.swift b/Sources/iHog/iHog/Features/AppPaymentService/PaywallTrigger.swift new file mode 100644 index 0000000..965c2ad --- /dev/null +++ b/Sources/iHog/iHog/Features/AppPaymentService/PaywallTrigger.swift @@ -0,0 +1,29 @@ +// +// PaywallTrigger.swift +// iHog +// +// Created by Jay Wilson on 10/1/25. +// + +enum PaywallTrigger { + case thisIsAdamsFault + case addShow(showCount: Int) + case customIcons + case userRequest + case puntPage + + var analyticValues: String { + switch self { + case .thisIsAdamsFault: + "onboarding_completed" + case .addShow: + "adding_show" + case .customIcons: + "chaning_show_icons" + case .userRequest: + "user_requested" + case .puntPage: + "punt_page" + } + } +} diff --git a/Sources/iHog/iHog/Features/BaconService/BaconService.swift b/Sources/iHog/iHog/Features/BaconService/BaconService.swift new file mode 100644 index 0000000..f704612 --- /dev/null +++ b/Sources/iHog/iHog/Features/BaconService/BaconService.swift @@ -0,0 +1,37 @@ +// +// BaconService.swift +// iHog +// +// Created by Jay Wilson on 10/1/25. +// + +import Foundation + +struct BaconService { + func grantLifetimeEntitlement(for userID: String) async throws -> BaconSuccessResponse { + guard var url = URL(string: AppInfo.api) else { throw URLError(.badURL) } + url = url.appendingPathComponent("bacon") + url = url.appending(path: "grant-entitlement") + url = url.appending(queryItems: [.init(name: "entitlement", value: "early-adopter")]) + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue(userID, forHTTPHeaderField: "x-user-id") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + if (200...299).contains(httpResponse.statusCode) { + return try decoder.decode(BaconSuccessResponse.self, from: data) + } else { + let errorResponse = try decoder.decode(BaconErrorResponse.self, from: data) + throw errorResponse + } + } +} diff --git a/Sources/iHog/iHog/Features/BaconService/Models.swift b/Sources/iHog/iHog/Features/BaconService/Models.swift new file mode 100644 index 0000000..6d68f4a --- /dev/null +++ b/Sources/iHog/iHog/Features/BaconService/Models.swift @@ -0,0 +1,53 @@ +// +// Models.swift +// iHog +// +// Created by Jay Wilson on 10/1/25. +// + +import Foundation + +struct BaconSuccessResponse: Codable, Sendable { + let success: Bool + let message: String + let entitlement: String + let userId: String + let grantedAt: Date + let expiresAt: Date + + init(message: String, entitlement: String, userId: String, grantedAt: Date, expiresAt: Date) { + self.success = true + self.message = message + self.entitlement = entitlement + self.userId = userId + self.grantedAt = grantedAt + self.expiresAt = expiresAt + } +} + +struct BaconErrorResponse: Codable, Error { + let success: Bool + let error: String + let code: BaconErrorCode + let details: String? + + init(error: String, code: BaconErrorCode, details: String? = nil) { + self.success = false + self.error = error + self.code = code + self.details = details + } +} + +enum BaconErrorCode: String, CaseIterable, Codable { + case missingUserIdHeader = "MISSING_USER_ID_HEADER" + case invalidEntitlement = "INVALID_ENTITLEMENT" + case emptyUserId = "EMPTY_USER_ID" + case customerNotFound = "CUSTOMER_NOT_FOUND" + case entitlementNotFound = "ENTITLEMENT_NOT_FOUND" + case networkError = "NETWORK_ERROR" + case serverError = "SERVER_ERROR" + case invalidApiKey = "INVALID_API_KEY" + case invalidResponse = "INVALID_RESPONSE" + case invalidDate = "INVALID_DATE" +} diff --git a/Sources/iHog/iHog/Features/ExperimentalFeatureManagement/ExperimentalFeatureView.swift b/Sources/iHog/iHog/Features/ExperimentalFeatureManagement/ExperimentalFeatureView.swift deleted file mode 100644 index fe2a328..0000000 --- a/Sources/iHog/iHog/Features/ExperimentalFeatureManagement/ExperimentalFeatureView.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ExperimentalFeatureView.swift -// iHog -// -// Created by Jay Wilson on 12/9/24. -// - -import SwiftData -import SwiftUI - -struct ExperimentalFeatureView: View { - @Environment(\.modelContext) var context - - @Query(sort: \UserCode.dateCreated) var codes: [UserCode] - - @AppStorage(FeatureFlagKey.swiftdata.rawValue) var featureSwiftData = FeatureFlagKey.swiftdata - .isAvailable - - var body: some View { - Section { - // Feature flags that are available to internal and external users - if codes.contains(where: { $0.code == "IH241208ID" }) - || codes.contains(where: { $0.code == "IH241208SBU" }) - { - Toggle(FeatureFlagKey.swiftdata.listLabel, isOn: $featureSwiftData) - } - } header: { - Text("Experimental Features") - } footer: { - Text("Enabling these features may cause data loss.") - } - .onChange(of: featureSwiftData) { _, newValue in - Analytics.shared.logFeatureFlagToggle(flag: .swiftdata, value: newValue) - } - } -} - -#Preview { - if #available(iOS 17, *) { - ExperimentalFeatureView() - } else { - // Fallback on earlier versions - } -} diff --git a/Sources/iHog/iHog/Features/Logging/Logger.swift b/Sources/iHog/iHog/Features/Logging/Logger.swift index 78e5d0c..ec4986d 100644 --- a/Sources/iHog/iHog/Features/Logging/Logger.swift +++ b/Sources/iHog/iHog/Features/Logging/Logger.swift @@ -19,8 +19,21 @@ enum LogCategory: String { case swiftData case show case userProfile + case appPaymentService } +extension Logger { + static let subsystem = Bundle.main.bundleIdentifier! + + static let appPaymentService = Logger( + subsystem: Self.subsystem, + category: LogCategory.appPaymentService.rawValue + ) + static let purchases = Logger( + subsystem: Self.subsystem, + category: LogCategory.purchases.rawValue + ) +} struct HogLogger { static func log(category: LogCategory = .default) -> Logger { let bundleID = diff --git a/Sources/iHog/iHog/Features/Settings/SettingsView.swift b/Sources/iHog/iHog/Features/Settings/SettingsView.swift index a4a1703..f69893d 100644 --- a/Sources/iHog/iHog/Features/Settings/SettingsView.swift +++ b/Sources/iHog/iHog/Features/Settings/SettingsView.swift @@ -15,6 +15,8 @@ struct SettingsView: View { @Environment(\.modelContext) var modelContext @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(AppPaymentService.self) var appPaymentService + @EnvironmentObject var user: UserState @EnvironmentObject var osc: OSCHelper @EnvironmentObject var toast: ToastNotification @@ -35,10 +37,6 @@ struct SettingsView: View { var networkMonitor = NetworkMonitor.shared - private var foundProducts: [Package] { - return user.offerings?[RCConstants.Offerings.year.name]?.availablePackages ?? [] - } - var showRepository: ShowRepository? = nil var body: some View { @@ -183,6 +181,7 @@ struct SettingsView: View { } } } + .hogPaywall(source: .settings) .sheet(item: $router.selectedSheet) { sheet in switch sheet { case .userProfile: @@ -200,11 +199,7 @@ struct SettingsView: View { NavigationStack { NewShowView() .environment(\.modelContext, modelContext) - } - case .paywall: - NavigationStack { - CurrentPaywallView(issue: 0, analyticsSource: .newShowView) - .environmentObject(user) + .environment(appPaymentService) } } } @@ -245,6 +240,15 @@ struct SettingsView: View { networkMonitor.stopMonitoring() notificationTask?.cancel() } + .onChange(of: appPaymentService.isPro) { oldValue, newValue in + // If the user became a pro while adding a show, then show the new show sheet + if newValue == true, + case .addShow(let showCount) = appPaymentService.lastUsedTrigger, + showCount >= 1 + { + router.selectedSheet = .newShow + } + } } func listenForShowCreated() async { @@ -266,24 +270,19 @@ struct SettingsView: View { } func addShow() { - Analytics.shared.logEvent(with: .addShowTapped) - if user.isPro { - router.openSheet(.newShow) - } else { - Task { - let showRepository = - self.showRepository - ?? ShowSwiftDataRepository( - modelContainer: modelContext.container - ) - let showsCount: Int = (try? await showRepository.getCountOfShows()) ?? 0 - await MainActor.run { - if showsCount >= 1 { - router.openSheet(.paywall) - } else { - router.openSheet(.newShow) - } - } + Analytics.shared.logEvent(with: .addShowTapped) + Task { + let showRepository = + self.showRepository + ?? ShowSwiftDataRepository( + modelContainer: modelContext.container + ) + let showsCount: Int = (try? await showRepository.getCountOfShows()) ?? 0 + await MainActor.run { + appPaymentService.triggerPaywall(for: .addShow(showCount: showsCount)) + if appPaymentService.isPro || showsCount == 0 { + router.openSheet(.newShow) + } } } } @@ -303,5 +302,6 @@ struct SettingsView_Previews: PreviewProvider { SettingsView() .environmentObject(UserState()) .environmentObject(OSCHelper(ip: "120.000.000.012", inputPort: 1234, outputPort: 1235)) + .environment(AppPaymentService()) } } diff --git a/Sources/iHog/iHog/Features/UserProfile/UserProfileView.swift b/Sources/iHog/iHog/Features/UserProfile/UserProfileView.swift index 424fd5b..30438cf 100644 --- a/Sources/iHog/iHog/Features/UserProfile/UserProfileView.swift +++ b/Sources/iHog/iHog/Features/UserProfile/UserProfileView.swift @@ -5,11 +5,14 @@ // Created by Jay Wilson on 12/6/24. // +import OSLog +import RevenueCat import SwiftData import SwiftUI /// Used to access a user's profile from the Settings view struct UserProfileView: View { + @Environment(AppPaymentService.self) var appPaymentService @EnvironmentObject var user: UserState var showRepository: ShowRepository @@ -20,31 +23,50 @@ struct UserProfileView: View { List { // MARK: Level Section { - if user.isPro { - HStack { - // Premium Icon - Image(systemName: "star.square.on.square") - .foregroundStyle(.orange, .blue) - .font(.title2) - - // Text Stack - VStack(alignment: .leading, spacing: 4) { - Text("Premium Subscriber") - .font(.headline) - if let proSince = user.proSinceDate { - Text("Since \(proSince.formatted(date: .abbreviated, time: .omitted))") - .foregroundStyle(.secondary) - .font(.subheadline) + if appPaymentService.isPro { + Button { + Task { @MainActor in + do { + try await Purchases.shared.showManageSubscriptions() + } catch { + Logger.appPaymentService.error("Cannot manage subscription \(error)") } } + } label: { + HStack { + // Premium Icon + Image(systemName: "star.square.on.square") + .foregroundStyle(.orange, .blue) + .font(.title2) + + // Text Stack + VStack(alignment: .leading, spacing: 4) { + Text("Pro Subscriber") + .font(.headline) + if let proSince = appPaymentService.dateOriginallySubscribed { + Text("Since \(proSince.formatted(date: .abbreviated, time: .omitted))") + .foregroundStyle(.secondary) + .font(.subheadline) + } + } - Spacer() + Spacer() + } + .padding(.vertical, 4) } - .padding(.vertical, 4) } else { - NavigationLink("Sign up for pro") { - CurrentPaywallView(analyticsSource: .settings) + Button { + appPaymentService.triggerPaywall(for: .userRequest) + } label: { + VStack(alignment: .leading) { + Text("Go PRO") + .font(.title) + .fontWeight(.black) + Text("Unlimited shows, custom controls, and premium features await") + } } + .buttonStyle(.plain) + .foregroundStyle(.primary) } } @@ -54,19 +76,6 @@ struct UserProfileView: View { } header: { Text("Data management") } - // MARK: Analytics - Section { - UserCodeView() - // .environment(\.modelContext, SwiftDataManager.modelContainer.mainContext) - } header: { - Text("User codes") - } footer: { - Text( - "You'll only be entering codes here if you have received specific instruction to enter one or have ran the beta version of iHog." - ) - } - ExperimentalFeatureView() - // .environment(\.modelContext, SwiftDataManager.modelContainer.mainContext) } } } diff --git a/Sources/iHog/iHog/Helpers/AppInfo.swift b/Sources/iHog/iHog/Helpers/AppInfo.swift index 83a58e8..2164c14 100644 --- a/Sources/iHog/iHog/Helpers/AppInfo.swift +++ b/Sources/iHog/iHog/Helpers/AppInfo.swift @@ -24,4 +24,7 @@ enum AppInfo { ) return Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" }() + + static let revenueCatKey = "appl_iuNkLloJrdhvmSrVYSoZPrfJcOp" + static let api = "https://api.cctplus.app" } diff --git a/Sources/iHog/iHog/State/UserState.swift b/Sources/iHog/iHog/State/UserState.swift index 1082f2d..8de4b56 100644 --- a/Sources/iHog/iHog/State/UserState.swift +++ b/Sources/iHog/iHog/State/UserState.swift @@ -14,55 +14,6 @@ class UserState: NSObject, ObservableObject { @Published var navigation: Routes? = .osc - @Published var offerings: Offerings? = nil - @Published var customerInfo: CustomerInfo? { - didSet { - guard let customerInfo else { - purchasedApp = false - proSubscriptionActive = false - purchasedApp = false - proSinceDate = nil - return - } - purchasedApp = - (customerInfo.originalPurchaseDate ?? Date.now) < RCConstants() - .getConvertedToSubscriptionDate()! - puntPageActive = - customerInfo.entitlements[RCConstants.Entitlements.puntPage.rawValue]?.isActive == true - proSubscriptionActive = - customerInfo.entitlements[RCConstants.Entitlements.pro.rawValue]?.isActive == true - if isPro { - if purchasedApp { - proSinceDate = customerInfo.originalPurchaseDate - } else if let proEntitlement = customerInfo.entitlements[ - RCConstants.Entitlements.pro.rawValue - ] { - proSinceDate = proEntitlement.originalPurchaseDate - } - } - } - } - - var puntPageActive: Bool = false - var proSubscriptionActive: Bool = false - var purchasedApp: Bool = false - @Published var proSinceDate: Date? = nil - - var isPro: Bool { - if purchasedApp { - return true - } else if puntPageActive { - return true - } else if proSubscriptionActive { - return true - } - return false - } - - func unlockPro() { - proSubscriptionActive = true - } - func setNavigation(to route: Routes) { DispatchQueue.main.async { self.navigation = route diff --git a/Sources/iHog/iHog/StoreKitTestCertificate.cer b/Sources/iHog/iHog/StoreKitTestCertificate.cer deleted file mode 100644 index 5896156..0000000 Binary files a/Sources/iHog/iHog/StoreKitTestCertificate.cer and /dev/null differ diff --git a/Sources/iHog/iHog/Views/Onboarding/OBIPAddress.swift b/Sources/iHog/iHog/Views/Onboarding/OBIPAddress.swift index 8a0e6a7..7bc8732 100644 --- a/Sources/iHog/iHog/Views/Onboarding/OBIPAddress.swift +++ b/Sources/iHog/iHog/Views/Onboarding/OBIPAddress.swift @@ -5,12 +5,15 @@ // Created by Jay Wilson on 2/14/22. // +import RevenueCatUI import SwiftUI struct OBIPAddress: View { @AppStorage(AppStorageKey.showOnboarding.rawValue) var showOnboarding: Bool = true @AppStorage(AppStorageKey.isOSCOn.rawValue) var isOSCOn: Bool = false + @Environment(AppPaymentService.self) var paymentService: AppPaymentService + @EnvironmentObject var osc: OSCHelper @Binding var setting: SettingsNav @@ -46,7 +49,7 @@ struct OBIPAddress: View { HStack { Spacer() Button("Enable OSC") { - showPaywall = true + paymentService.triggerPaywall(for: .thisIsAdamsFault) } .padding() .foregroundColor(.white) @@ -57,14 +60,17 @@ struct OBIPAddress: View { Spacer() } .multilineTextAlignment(.center) - .sheet(isPresented: $showPaywall) { - OnboardingPaywall() - .onDisappear { - enableOSC() - } - } + .hogPaywall(source: .onboarding) } .padding() + .onChange( + of: paymentService.shouldShowPaywall, + initial: false + ) { oldValue, newValue in + if newValue == false { + enableOSC() + } + } } func enableOSC() { diff --git a/Sources/iHog/iHog/Views/Paywalls/CurrentPaywallView.swift b/Sources/iHog/iHog/Views/Paywalls/CurrentPaywallView.swift deleted file mode 100644 index 0cdbb22..0000000 --- a/Sources/iHog/iHog/Views/Paywalls/CurrentPaywallView.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// AllOptionsPaywallView.swift -// iHog -// -// Created by Jay Wilson on 8/12/22. -// - -import RevenueCat -import SwiftUI - -struct CurrentPaywallView: View { - @EnvironmentObject var user: UserState - - @State private var selectedPackage = 1 - - var issue: Int? = nil - var packages: [Package] { - return user.offerings?.current?.availablePackages ?? [] - } - var showTitle: Bool { - return issue != nil - } - - var analyticsSource: PaywallSource - - var body: some View { - VStack { - PaidFeaturesView(issue: issue) - Spacer() - ForEach(0.. String { - let price = package.localizedPriceString - let duration = package.storeProduct.subscriptionPeriod!.durationTitle - return "then \(price) per \(duration)" - } -} - -struct OnboardingPaywall_Previews: PreviewProvider { - static var previews: some View { - OnboardingPaywall() - .environmentObject(UserState()) - } -} diff --git a/Sources/iHog/iHog/Views/Paywalls/PaidFeaturesView.swift b/Sources/iHog/iHog/Views/Paywalls/PaidFeaturesView.swift deleted file mode 100644 index e555b4f..0000000 --- a/Sources/iHog/iHog/Views/Paywalls/PaidFeaturesView.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// PaidFeaturesView.swift -// iHog -// -// Created by Jay Wilson on 8/11/22. -// - -import SwiftUI - -struct Feature: Identifiable { - let id: Int - let name: String - let symbol: String - var freeAlternative: String? = nil - let color: Color -} - -struct PaidFeaturesView: View { - var issue: Int? = nil - - let features = [ - Feature( - id: 0, - name: "Unlimited shows", - symbol: SFSymbol._folderbadgeplus.name, - freeAlternative: "1 show", - color: .orange - ), - Feature( - id: 4, - name: "Custom icons for shows", - symbol: SFSymbol._questionmarkfolder.name, - freeAlternative: "1 show", - color: .teal - ), - Feature( - id: 1, - name: "Punt Page", - symbol: SFSymbol._sliderhorizontalbelowsquareandsquarefilled.name, - color: .pink - ), - Feature( - id: 3, - name: "Continuous bug fixes and feature additions", - symbol: SFSymbol._clockarrow2circlepath.name, - color: .purple - ), - Feature( - id: 2, - name: "Support the longevity of iHog", - symbol: SFSymbol._cupandsaucer.name, - color: .gray - ), - ] - var body: some View { - ScrollView { - ForEach(features) { feature in - HStack(alignment: .center) { - Image(systemName: feature.symbol) - .foregroundColor(feature.color) - HStack { - Text(feature.name) - .font(.body) - if (issue == feature.id) == true { - Spacer() - Text("Unlock with subscription") - .font(.custom("San Fransisco", size: 10, relativeTo: .footnote)) - .padding(BASE_PADDING) - .background { - RoundedRectangle(cornerRadius: DOUBLE_CORNER_RADIUS) - .fill(.regularMaterial) - } - } - } - Spacer() - } - .padding(.vertical, BASE_PADDING) - } - } - .padding() - } -} - -struct PaidFeaturesView_Previews: PreviewProvider { - static var previews: some View { - PaidFeaturesView(issue: 1) - } -} diff --git a/Sources/iHog/iHog/Views/Paywalls/PaywallView.swift b/Sources/iHog/iHog/Views/Paywalls/PaywallView.swift deleted file mode 100644 index 3932237..0000000 --- a/Sources/iHog/iHog/Views/Paywalls/PaywallView.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// PaywallView.swift -// iHog -// -// Created by Jay Wilson on 9/13/22. -// - -import SwiftUI - -struct PaywallView: View { - var analtycsSource: PaywallSource - var paywall: Paywall - - var body: some View { - switch paywall { - case .onboarding: - OnboardingView(setting: .constant(.device)) - default: - CurrentPaywallView(analyticsSource: analtycsSource) - } - } -} - -struct PaywallView_Previews: PreviewProvider { - static var previews: some View { - PaywallView(analtycsSource: .preview, paywall: .currentPaywall) - } -} diff --git a/Sources/iHog/iHog/Views/Paywalls/PurchaseButtonView.swift b/Sources/iHog/iHog/Views/Paywalls/PurchaseButtonView.swift deleted file mode 100644 index 04c4dde..0000000 --- a/Sources/iHog/iHog/Views/Paywalls/PurchaseButtonView.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// PurchaseButtonView.swift -// iHog -// -// Created by Jay Wilson on 8/23/22. -// - -import RevenueCat -import SwiftUI - -struct PurchaseButtonView: View { - @EnvironmentObject var user: UserState - @Environment(\.dismiss) var dismiss - var isSelected: Bool - - let samplePrice = "$0.99" - let sampleUnit = "month" - let sampleFreeTrial = "2 weeks free" - - var package: Package? = nil - - var price: String { - package?.localizedPriceString ?? samplePrice - } - var units: String { - if package?.storeProduct.subscriptionPeriod != nil { - return "per \(package?.storeProduct.subscriptionPeriod?.durationTitle ?? sampleUnit)" - } else { - return "one time purchase" - } - } - var freeTrial: String { - package?.terms(for: package!) ?? "" - } - var backgroundColor: Color { - return isSelected ? .accentColor : .gray - } - var icon: SFSymbol { - return isSelected ? ._checkmarkcirclefill : ._circle - } - var buttonText: String { - if package?.hasFreeTrial == true { - return "Start my \(freeTrial)" - } else { - return "Start now" - } - } - - var analyticsSource: PaywallSource - - var body: some View { - VStack { - HStack(alignment: .top) { - HStack(alignment: .top) { - VStack(alignment: .leading) { - if package?.isSubscription == true { - Text( - package?.storeProduct.subscriptionPeriod?.durationAdverb.localizedCapitalized ?? "" - ) - .font(.headline) - if package?.hasFreeTrial == true { - Text( - "First \(package?.discountTerms ?? freeTrial) are free,\nthen \(price) \(units)" - ) - .lineLimit(nil) - .padding(.top, HALF_PADDING * 0.25) - } else { - Text("\(price) \(units)") - .padding(.top, HALF_PADDING * 0.25) - } - } else { - Text("Lifetime") - .font(.headline) - Text(price) - Text("One time purchase") - .font(.footnote) - } - } - } - Spacer() - if package?.hasFreeTrial == true { - Text("Free trial") - .font(.subheadline) - .padding(HALF_PADDING) - .background(RoundedRectangle(cornerRadius: BASE_CORNER_RADIUS).fill(Color.orange)) - } - } - if isSelected { - Button(buttonText) { - purchase() - Analytics.shared.logEvent( - with: .subscribeButtonTapped, - parameters: [.paywallSource: analyticsSource.analyticsLabel] - ) - } - .buttonStyle(.borderedProminent) - } - } - .padding(BASE_PADDING) - .background( - RoundedRectangle(cornerRadius: BASE_CORNER_RADIUS).fill(backgroundColor.opacity(0.2)) - ) - .background( - RoundedRectangle(cornerRadius: BASE_CORNER_RADIUS) - .stroke(style: StrokeStyle(lineWidth: 2)).foregroundColor(backgroundColor) - ) - } - - func purchase() { - Purchases.shared.purchase(package: package!) { (storeTransaction, customerInfo, error, _) in - if let error = error { - Analytics.shared.logError(with: error, for: .purchases, level: .critical) - dismiss() - } - - if let storeTransaction = storeTransaction { - if let transaction = storeTransaction.sk2Transaction { - Analytics.shared.logPurchase(transacion: transaction) - } - } - - if customerInfo?.entitlements[RCConstants.Entitlements.pro.name]?.isActive == true { - user.unlockPro() - dismiss() - } - } - } -} - -struct PurchaseButtonView_Previews: PreviewProvider { - static var previews: some View { - VStack { - PurchaseButtonView(isSelected: true, analyticsSource: .preview) - .padding() - PurchaseButtonView(isSelected: false, analyticsSource: .preview) - .padding() - } - } -} diff --git a/Sources/iHog/iHog/Views/Paywalls/SettingsSectionPaywall.swift b/Sources/iHog/iHog/Views/Paywalls/SettingsSectionPaywall.swift deleted file mode 100644 index 2685a8c..0000000 --- a/Sources/iHog/iHog/Views/Paywalls/SettingsSectionPaywall.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// SettingsSectionPaywall.swift -// iHog -// -// Created by Jay Wilson on 11/8/24. -// - -import RevenueCat -import StoreKit -import SwiftUI - -struct SettingsSectionPaywall: View { - @EnvironmentObject var user: UserState - - private var foundProducts: [Package] { - return user.offerings?[RCConstants.Offerings.year.name]?.availablePackages ?? [] - } - - var body: some View { - VStack(alignment: .center) { - HStack { - Image(symbol: ._wandandstars) - Text("Be a PRO-grammer") - Image(symbol: ._wandandstars) - } - Text("Unlock unlimited shows and more") - .lineLimit(nil) - ForEach(foundProducts, id: \.self) { package in - Button("Start your \(package.terms(for: package))") { - Purchases.shared.purchase(package: package) { (_, customerInfo, error, _) in - if let error = error { - print(error.localizedDescription) - } - if customerInfo?.entitlements[RCConstants.Entitlements.pro.name]?.isActive == true { - user.unlockPro() - } - Analytics.shared.logEvent( - with: .subscribeButtonTapped, - parameters: [.paywallSource: PaywallSource.settings] - ) - } - } - .buttonStyle(.borderedProminent) - Text(setPrice(package)) - .font(.footnote) - } - NavigationLink("Learn more", value: Routes.paywall(.currentPaywall)) - .font(.footnote) - .foregroundColor(.blue) - } - - } -} - -#Preview { - SettingsSectionPaywall() - .environmentObject(UserState()) -} - -extension SettingsSectionPaywall { - func setPrice(_ package: Package) -> String { - let price = package.localizedPriceString - let duration = package.storeProduct.subscriptionPeriod!.durationTitle - return "then \(price) per \(duration)" - } - -} diff --git a/Sources/iHog/iHog/Views/Settings/ChooseShowSettings/NewShowView.swift b/Sources/iHog/iHog/Views/Settings/ChooseShowSettings/NewShowView.swift index 13a82d8..c8c6ef4 100644 --- a/Sources/iHog/iHog/Views/Settings/ChooseShowSettings/NewShowView.swift +++ b/Sources/iHog/iHog/Views/Settings/ChooseShowSettings/NewShowView.swift @@ -11,6 +11,7 @@ import SwiftUI struct NewShowView: View { @Environment(\.dismiss) var dismiss @Environment(\.modelContext) var context + @Environment(AppPaymentService.self) var appPaymentService @EnvironmentObject var user: UserState @@ -20,10 +21,6 @@ struct NewShowView: View { var showRepository: ShowRepository? = nil - var showIconChoice: Bool { - return presentIconChoice && user.isPro - } - let gridItems = [ GridItem(.flexible(), spacing: 10, alignment: .center), GridItem(.flexible(), spacing: 10, alignment: .center), @@ -35,7 +32,10 @@ struct NewShowView: View { VStack { HStack { Button { - presentIconChoice.toggle() + appPaymentService.triggerPaywall(for: .customIcons) + if appPaymentService.shouldShowPaywall == false { + presentIconChoice = true + } } label: { Image(symbol: selectedIcon) .font(.title) @@ -50,26 +50,22 @@ struct NewShowView: View { .padding(.leading) Spacer() if presentIconChoice { - if showIconChoice { - ScrollView { - LazyVGrid(columns: gridItems) { - ForEach(SFSymbol.ALL_ICONS, id: \.self) { icon in - Button { - self.selectedIcon = icon - } label: { - Image(symbol: icon) - .font(.title) - } - .tint(.gray) - .padding(.all, HALF_PADDING) - + ScrollView { + LazyVGrid(columns: gridItems) { + ForEach(SFSymbol.ALL_ICONS, id: \.self) { icon in + Button { + self.selectedIcon = icon + } label: { + Image(symbol: icon) + .font(.title) } + .tint(.gray) + .padding(.all, HALF_PADDING) + } } - .padding(.bottom) - } else { - CurrentPaywallView(issue: 4, analyticsSource: .addIconView) } + .padding(.bottom) } } .navigationTitle("\(showName)") diff --git a/Sources/iHog/iHog/Views/ShowViews/ShowNavigation.swift b/Sources/iHog/iHog/Views/ShowViews/ShowNavigation.swift index ffb5383..5364965 100644 --- a/Sources/iHog/iHog/Views/ShowViews/ShowNavigation.swift +++ b/Sources/iHog/iHog/Views/ShowViews/ShowNavigation.swift @@ -5,10 +5,12 @@ // Created by Jay Wilson on 10/15/20. // +import RevenueCatUI import SwiftUI /// TabView for the selected show struct ShowNavigation: View { + @Environment(AppPaymentService.self) var appPaymentService: AppPaymentService @EnvironmentObject var user: UserState @ObservedObject var chosenShow: ChosenShow @@ -35,7 +37,7 @@ struct ShowNavigation: View { Image(systemName: "play.rectangle") } .tag(Views.playbackObjects) - if user.isPro { + if appPaymentService.isPro { PPPlayback(show: chosenShow) .tabItem { Image(symbol: ._sliderhorizontalbelowsquareandsquarefilled) @@ -52,7 +54,10 @@ struct ShowNavigation: View { } .tag(Views.puntPageProgramming) } else { - CurrentPaywallView(issue: 1, analyticsSource: .puntPage) + HogPaywallView(showDismiss: false, source: .puntPage) + .task { + appPaymentService.lastUsedTrigger = .puntPage + } .tabItem { Image(symbol: ._sliderhorizontalbelowsquareandsquarefilled) } diff --git a/Sources/iHog/iHog/iHog - OSC Remote for Hog 4.storekit b/Sources/iHog/iHog/iHog - OSC Remote for Hog 4.storekit deleted file mode 100644 index 0cee312..0000000 --- a/Sources/iHog/iHog/iHog - OSC Remote for Hog 4.storekit +++ /dev/null @@ -1,250 +0,0 @@ -{ - "identifier" : "C2185313", - "nonRenewingSubscriptions" : [ - - ], - "products" : [ - { - "displayPrice" : "4.99", - "familyShareable" : false, - "internalID" : "1510373420", - "localizations" : [ - { - "description" : "Used to purchase an extra feature in the app", - "displayName" : "High Price for features", - "locale" : "en_US" - } - ], - "productID" : "fp3", - "referenceName" : "fp1-high", - "type" : "NonConsumable" - }, - { - "displayPrice" : "9.99", - "familyShareable" : false, - "internalID" : "1510373807", - "localizations" : [ - { - "description" : "Used to purchase extra features in the app", - "displayName" : "Max price for features", - "locale" : "en_US" - } - ], - "productID" : "fp4", - "referenceName" : "fp1-highest", - "type" : "NonConsumable" - }, - { - "displayPrice" : "0.99", - "familyShareable" : false, - "internalID" : "1510357334", - "localizations" : [ - { - "description" : "Used to purchase extra features in the app", - "displayName" : "Lowest price for features", - "locale" : "en_US" - } - ], - "productID" : "fp1", - "referenceName" : "fp1-low", - "type" : "NonConsumable" - }, - { - "displayPrice" : "2.99", - "familyShareable" : false, - "internalID" : "1510373031", - "localizations" : [ - { - "description" : "Used to purchase extra features in the app", - "displayName" : "Mid price for Punt Page", - "locale" : "en_US" - } - ], - "productID" : "fp2", - "referenceName" : "fp1-mid", - "type" : "NonConsumable" - }, - { - "displayPrice" : "19.99", - "familyShareable" : false, - "internalID" : "1638595838", - "localizations" : [ - { - "description" : "Lifetime updates for iHog", - "displayName" : "Lifetime Unlock", - "locale" : "en_US" - } - ], - "productID" : "IH_LIFETIME_1999", - "referenceName" : "IH_LIFETIME_1999", - "type" : "NonConsumable" - }, - { - "displayPrice" : "0.99", - "familyShareable" : false, - "internalID" : "1491226622", - "localizations" : [ - { - "description" : "Make Maegan smile!", - "displayName" : "Tip - Smiles", - "locale" : "en_US" - } - ], - "productID" : "h4tier1", - "referenceName" : "tier 1", - "type" : "Consumable" - }, - { - "displayPrice" : "2.99", - "familyShareable" : false, - "internalID" : "1491227630", - "localizations" : [ - { - "description" : "Maegan greatly appreciates you!", - "displayName" : "Tip - hearts", - "locale" : "en_US" - } - ], - "productID" : "h4tier2", - "referenceName" : "tier 2", - "type" : "Consumable" - }, - { - "displayPrice" : "4.99", - "familyShareable" : false, - "internalID" : "1491693716", - "localizations" : [ - { - "description" : "Buy Maegan a cup of β˜• to keep developing!", - "displayName" : "Tip - buy a coffee for Maegan", - "locale" : "en_US" - } - ], - "productID" : "h4tier3", - "referenceName" : "tier 3", - "type" : "Consumable" - }, - { - "displayPrice" : "9.99", - "familyShareable" : false, - "internalID" : "1491693717", - "localizations" : [ - { - "description" : "Maegan appreciates this!! πŸ’―for you!", - "displayName" : "Tip - 10 for Maegan", - "locale" : "en_US" - } - ], - "productID" : "h4tier4", - "referenceName" : "tier 4", - "type" : "Consumable" - } - ], - "settings" : { - "_applicationInternalID" : "1487580623", - "_compatibilityTimeRate" : 6, - "_developerTeamID" : "ZDK82FGR3Z", - "_lastSynchronizedDate" : 684803784.08642006, - "_timeRate" : 15 - }, - "subscriptionGroups" : [ - { - "id" : "20571170", - "localizations" : [ - - ], - "name" : "Pro", - "subscriptions" : [ - { - "adHocOffers" : [ - - ], - "codeOffers" : [ - - ], - "displayPrice" : "0.99", - "familyShareable" : false, - "groupNumber" : 1, - "internalID" : "1636503644", - "introductoryOffer" : null, - "localizations" : [ - { - "description" : "Unlock all features of the app.", - "displayName" : "Monthly Subscription", - "locale" : "en_US" - } - ], - "productID" : "ih_0099_month", - "recurringSubscriptionPeriod" : "P1M", - "referenceName" : "IH4_099_month", - "subscriptionGroupID" : "20571170", - "type" : "RecurringSubscription" - }, - { - "adHocOffers" : [ - - ], - "codeOffers" : [ - - ], - "displayPrice" : "2.99", - "familyShareable" : false, - "groupNumber" : 1, - "internalID" : "1636504553", - "introductoryOffer" : { - "internalID" : "5D62BCA5", - "numberOfPeriods" : 1, - "paymentMode" : "free", - "subscriptionPeriod" : "P2W" - }, - "localizations" : [ - { - "description" : "Unlock all the features for the year", - "displayName" : "Yearly Unlock", - "locale" : "en_US" - } - ], - "productID" : "ih_0299_yr", - "recurringSubscriptionPeriod" : "P1Y", - "referenceName" : "IH_0299_year", - "subscriptionGroupID" : "20571170", - "type" : "RecurringSubscription" - }, - { - "adHocOffers" : [ - - ], - "codeOffers" : [ - - ], - "displayPrice" : "4.99", - "familyShareable" : false, - "groupNumber" : 1, - "internalID" : "1644601494", - "introductoryOffer" : { - "internalID" : "47D03467", - "numberOfPeriods" : 1, - "paymentMode" : "free", - "subscriptionPeriod" : "P2W" - }, - "localizations" : [ - { - "description" : "Pro subscription to iHog", - "displayName" : "Yearly unlock", - "locale" : "en_US" - } - ], - "productID" : "ih_0499_yr", - "recurringSubscriptionPeriod" : "P1Y", - "referenceName" : "IH_0499_year", - "subscriptionGroupID" : "20571170", - "type" : "RecurringSubscription" - } - ] - } - ], - "version" : { - "major" : 2, - "minor" : 0 - } -} diff --git a/Sources/iHog/iHog/iHogApp.swift b/Sources/iHog/iHog/iHogApp.swift index 965b248..ada4c0a 100644 --- a/Sources/iHog/iHog/iHogApp.swift +++ b/Sources/iHog/iHog/iHogApp.swift @@ -10,6 +10,7 @@ // import CoffeeToast +import OSLog import RevenueCat import StoreKit import SwiftUI @@ -45,14 +46,11 @@ struct iHogApp: App { @StateObject var osc = OSCHelper(ip: "192.168.0.101", inputPort: 9009, outputPort: 9009) @StateObject var user = UserState() + @State private var paymentService = AppPaymentService() + let analtyics = Analytics.shared init() { - Purchases.logLevel = .debug - Purchases.configure( - with: Configuration.Builder(withAPIKey: RCConstants.apiKey) - .build() - ) // Setup the model container _ = SwiftDataManager.modelContainer } @@ -72,30 +70,21 @@ struct iHogApp: App { OnboardingView(setting: $settings) .environmentObject(osc) .environmentObject(user) + .environment(paymentService) } else { SettingsView() .environmentObject(osc) .environmentObject(user) .environmentObject(toastNotification) + .environment(paymentService) } } } .modelContainer(SwiftDataManager.modelContainer) .task { + paymentService.configure() analtyics.logEvent(with: .appLaunched) increaseTimesLaunchedAndAskReview() - do { - try await getCustomerInfo() - user.offerings = try await Purchases.shared.offerings() - for await customerInfo in Purchases.shared.customerInfoStream { - for _ in customerInfo.activeSubscriptions { - HogLogger.log(category: .purchases).debug("πŸ› \(customerInfo.activeSubscriptions)") - } - user.customerInfo = customerInfo - } - } catch { - Analytics.shared.logError(with: error, for: .purchases, level: .critical) - } } } } @@ -109,9 +98,4 @@ extension iHogApp { requestReview() } } - - func getCustomerInfo() async throws { - user.customerInfo = try await Purchases.shared.restorePurchases() - analtyics.identifyUser(with: Purchases.shared.appUserID) - } }