Revenue Cat for Threaded+

This commit is contained in:
Lumaa 2024-02-22 23:16:08 +01:00
parent 7d72b8e0e1
commit a90bff172e
11 changed files with 391 additions and 75 deletions

View File

@ -20,6 +20,9 @@
B93B67782B42E8F0000892E9 /* TextEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93B67772B42E8F0000892E9 /* TextEmoji.swift */; };
B93B677A2B42EC51000892E9 /* MetaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93B67792B42EC51000892E9 /* MetaPicker.swift */; };
B93B677C2B433A6E000892E9 /* PostingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93B677B2B433A6E000892E9 /* PostingView.swift */; };
B95ED2332B8707D60055F5BD /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = B95ED2322B8707D60055F5BD /* RevenueCat */; };
B95ED2352B8707D60055F5BD /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = B95ED2342B8707D60055F5BD /* RevenueCatUI */; };
B95ED2372B87C9550055F5BD /* StoreKitTestCertificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = B95ED2362B87C9550055F5BD /* StoreKitTestCertificate.cer */; };
B97491E32B6E96700098BC48 /* SymbolWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97491E22B6E96700098BC48 /* SymbolWidth.swift */; };
B97798892B853E6600DC869F /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97798882B853E6600DC869F /* UpdateView.swift */; };
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97BCE232B3DD8400044756D /* HapticManager.swift */; };
@ -32,6 +35,7 @@
B9842C142B2F310C00D9F3C1 /* FetchTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */; };
B9842C162B2F363600D9F3C1 /* TimelineFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */; };
B9842C182B2F36F500D9F3C1 /* AccountsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C172B2F36F500D9F3C1 /* AccountsList.swift */; };
B98627312B86F23500844245 /* LoggedAccounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98627302B86F23500844245 /* LoggedAccounts.swift */; };
B98BC7472B46CE6300595441 /* PostDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98BC7462B46CE6300595441 /* PostDetailsView.swift */; };
B98BC7492B46CEDA00595441 /* AppearenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98BC7482B46CEDA00595441 /* AppearenceView.swift */; };
B98BC74B2B46CF0400595441 /* ThreadedStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98BC74A2B46CF0400595441 /* ThreadedStyle.swift */; };
@ -130,6 +134,7 @@
B93B67772B42E8F0000892E9 /* TextEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEmoji.swift; sourceTree = "<group>"; };
B93B67792B42EC51000892E9 /* MetaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaPicker.swift; sourceTree = "<group>"; };
B93B677B2B433A6E000892E9 /* PostingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingView.swift; sourceTree = "<group>"; };
B95ED2362B87C9550055F5BD /* StoreKitTestCertificate.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = StoreKitTestCertificate.cer; path = ../../../../../Downloads/StoreKitTestCertificate.cer; sourceTree = "<group>"; };
B97491E22B6E96700098BC48 /* SymbolWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolWidth.swift; sourceTree = "<group>"; };
B97798882B853E6600DC869F /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
B97BCE232B3DD8400044756D /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = "<group>"; };
@ -142,6 +147,7 @@
B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTimeline.swift; sourceTree = "<group>"; };
B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFilter.swift; sourceTree = "<group>"; };
B9842C172B2F36F500D9F3C1 /* AccountsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsList.swift; sourceTree = "<group>"; };
B98627302B86F23500844245 /* LoggedAccounts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedAccounts.swift; sourceTree = "<group>"; };
B98BC7462B46CE6300595441 /* PostDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailsView.swift; sourceTree = "<group>"; };
B98BC7482B46CEDA00595441 /* AppearenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearenceView.swift; sourceTree = "<group>"; };
B98BC74A2B46CF0400595441 /* ThreadedStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadedStyle.swift; sourceTree = "<group>"; };
@ -219,8 +225,10 @@
B93B676D2B42C94F000892E9 /* Nuke in Frameworks */,
B9DC692D2B79362700E625B9 /* StoreKit.framework in Frameworks */,
B9BF54072B6B6823004B24E7 /* KeychainSwift in Frameworks */,
B95ED2332B8707D60055F5BD /* RevenueCat in Frameworks */,
B93B67732B42C94F000892E9 /* NukeVideo in Frameworks */,
B93B676F2B42C94F000892E9 /* NukeExtensions in Frameworks */,
B95ED2352B8707D60055F5BD /* RevenueCatUI in Frameworks */,
B93B67712B42C94F000892E9 /* NukeUI in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -278,6 +286,7 @@
isa = PBXGroup;
children = (
B9DC692F2B79378400E625B9 /* ThreadedPlus.storekit */,
B95ED2362B87C9550055F5BD /* StoreKitTestCertificate.cer */,
);
path = Store;
sourceTree = "<group>";
@ -435,6 +444,7 @@
B9FB947E2B2E1D5F00D81C07 /* Account.swift */,
B9FB94852B2E211200D81C07 /* Account+Elms.swift */,
B9842C172B2F36F500D9F3C1 /* AccountsList.swift */,
B98627302B86F23500844245 /* LoggedAccounts.swift */,
);
path = Accounts;
sourceTree = "<group>";
@ -465,6 +475,8 @@
B93B67722B42C94F000892E9 /* NukeVideo */,
B93B67752B42E8AB000892E9 /* EmojiText */,
B9BF54062B6B6823004B24E7 /* KeychainSwift */,
B95ED2322B8707D60055F5BD /* RevenueCat */,
B95ED2342B8707D60055F5BD /* RevenueCatUI */,
);
productName = Threaded;
productReference = B9FB94572B2DEECE00D81C07 /* Threaded.app */;
@ -520,6 +532,7 @@
B93B676B2B42C94F000892E9 /* XCRemoteSwiftPackageReference "Nuke" */,
B93B67742B42E8AB000892E9 /* XCRemoteSwiftPackageReference "EmojiText" */,
B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */,
B95ED2312B8707D60055F5BD /* XCRemoteSwiftPackageReference "purchases-ios" */,
);
productRefGroup = B9FB94582B2DEECE00D81C07 /* Products */;
projectDirPath = "";
@ -541,6 +554,7 @@
B9FB94642B2DEECF00D81C07 /* Preview Assets.xcassets in Resources */,
B9FB94612B2DEECF00D81C07 /* Assets.xcassets in Resources */,
B9029FC22B81259400AA9B68 /* Secret.plist in Resources */,
B95ED2372B87C9550055F5BD /* StoreKitTestCertificate.cer in Resources */,
B9FB94902B2E2B0E00D81C07 /* Localizable.xcstrings in Resources */,
B9CFC43B2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard in Resources */,
B97BCE2A2B3ED2C80044756D /* LICENSE in Resources */,
@ -619,6 +633,7 @@
B9EBE8582B474FD600FB594D /* AppDelegate.swift in Sources */,
B93B677C2B433A6E000892E9 /* PostingView.swift in Sources */,
B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */,
B98627312B86F23500844245 /* LoggedAccounts.swift in Sources */,
B9B63B272B449CDC00BBC82D /* SearchResults.swift in Sources */,
B9B63B252B44997400BBC82D /* QuotePostView.swift in Sources */,
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */,
@ -969,6 +984,14 @@
minimumVersion = 3.3.0;
};
};
B95ED2312B8707D60055F5BD /* XCRemoteSwiftPackageReference "purchases-ios" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/RevenueCat/purchases-ios.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.37.0;
};
};
B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/evgenyneu/keychain-swift?tab=readme-ov-file#usage";
@ -1013,6 +1036,16 @@
package = B93B67742B42E8AB000892E9 /* XCRemoteSwiftPackageReference "EmojiText" */;
productName = EmojiText;
};
B95ED2322B8707D60055F5BD /* RevenueCat */ = {
isa = XCSwiftPackageProductDependency;
package = B95ED2312B8707D60055F5BD /* XCRemoteSwiftPackageReference "purchases-ios" */;
productName = RevenueCat;
};
B95ED2342B8707D60055F5BD /* RevenueCatUI */ = {
isa = XCSwiftPackageProductDependency;
package = B95ED2312B8707D60055F5BD /* XCRemoteSwiftPackageReference "purchases-ios" */;
productName = RevenueCatUI;
};
B9BF54062B6B6823004B24E7 /* KeychainSwift */ = {
isa = XCSwiftPackageProductDependency;
package = B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */;

View File

@ -27,6 +27,15 @@
"version" : "12.2.0"
}
},
{
"identity" : "purchases-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/RevenueCat/purchases-ios.git",
"state" : {
"revision" : "e20ebed21693994f76bb33d3f8efbc8002df95b7",
"version" : "4.37.0"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",

View File

@ -33,12 +33,13 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = "en"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
allowLocationSimulation = "NO">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference

View File

@ -2,12 +2,16 @@
import SwiftUI
import UIKit
import RevenueCat
@Observable
public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicationDelegate {
public var window: UIWindow?
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height
public private(set) var secret: [String: String] = [:]
public static var premium: Bool = false
public func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
@ -16,12 +20,47 @@ public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicati
override public init() {
super.init()
if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") {
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
if let plist = try! PropertyListSerialization.propertyList(from: data, options: .mutableContainers, format: nil) as? [String: String] {
secret = plist
}
}
let foundPremium = AppDelegate.hasPlus()
print("User has \(!foundPremium ? "no-" : "")access to Plus")
windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width
windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height
Self.observedSceneDelegate.insert(self)
_ = Self.observer // just for activating the lazy static property
}
static func readSecret() -> [String: String]? {
if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") {
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
if let plist = try! PropertyListSerialization.propertyList(from: data, options: .mutableContainers, format: nil) as? [String: String] {
return plist
}
}
return nil
}
static func hasPlus() -> Bool {
Purchases.shared.getCustomerInfo { (customerInfo, error) in
self.premium = hasActuallyPlus(customerInfo: customerInfo)
}
return self.premium
}
private static func hasActuallyPlus(customerInfo: CustomerInfo?) -> Bool {
return customerInfo?.entitlements[PlusEntitlements.lifetime.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.monthly.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.yearly.getEntitlementId()]?.isActive == true
}
deinit {
Task { @MainActor in
Self.observedSceneDelegate.remove(self)

View File

@ -28,6 +28,7 @@ struct LargeButton: ButtonStyle {
.bold(filled)
.clipShape(.rect(cornerRadius: 15))
.opacity(configuration.isPressed ? 0.3 : 1)
.opacity(!filled && disabled ? 0.5 : 1)
.overlay {
if !filled {
RoundedRectangle(cornerRadius: 15)

View File

@ -91,6 +91,7 @@ public struct AppAccount: Codable, Identifiable, Hashable {
Self.clear()
}
/// This function only works with the given `AppAccount`
public func saveAsCurrent(_ appAccount: AppAccount? = nil) {
let encoder = JSONEncoder()
if let data = try? encoder.encode(appAccount ?? self) {
@ -100,6 +101,7 @@ public struct AppAccount: Codable, Identifiable, Hashable {
}
}
/// This function only works with the last saved `AppAccount` or with the given `Data`
public static func loadAsCurrent(_ data: Data? = nil) -> Self? {
let decoder = JSONDecoder()
if let newData = data ?? keychain.getData(Self.saveKey) {

View File

@ -0,0 +1,27 @@
//Made by Lumaa
import Foundation
import SwiftUI
import SwiftData
@Model
class LoggedAccounts {
let appAccounts: [AppAccount]
let currentAccount: AppAccount
static let shared: LoggedAccounts = LoggedAccounts()
init(appAccounts: [AppAccount] = [], current: AppAccount? = nil) {
let curr: AppAccount = current ?? AppAccount.loadAsCurrent()!
self.appAccounts = appAccounts.count < 1 ? [curr] : appAccounts
self.currentAccount = curr
}
}
public extension View {
@ViewBuilder
func modelData() -> some View {
self
.modelContainer(for: LoggedAccounts.self)
}
}

View File

@ -16,10 +16,7 @@ final class HuggingFace: ObservableObject {
}
static func getToken() -> String? {
guard let path = Bundle.main.path(forResource: "Secret", ofType: "plist") else { return nil }
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
guard let plist = try! PropertyListSerialization.propertyList(from: data, options: .mutableContainers, format: nil) as? [String: String] else { return nil }
guard let plist = AppDelegate.readSecret() else { return nil }
Self.token = plist["AI_Token"] ?? ""
return Self.token
}

View File

@ -1687,18 +1687,50 @@
}
}
},
"shop.threaded-plus.lifetime.header" : {
"shop.threaded-plus.monthly" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "A lifetime support"
"value" : "Threaded+ - Monthly"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Un soutien à vie."
"value" : "Threaded+ - Mensuel"
}
}
}
},
"shop.threaded-plus.monthly.price" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "For $1.99 per month, auto-renewed"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pour 1.99€ par mois, renouvellement automatique"
}
}
}
},
"shop.threaded-plus.subscribe" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Subscribe"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "S'abonner"
}
}
}
@ -1735,6 +1767,38 @@
}
}
},
"shop.threaded-plus.yearly" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Threaded+ - Yearly"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Threaded+ - Annuel"
}
}
}
},
"shop.threaded-plus.yearly.price" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "For $17.99 per year, auto-renewed"
}
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Pour 19.99€ par mois, renouvellement automatique"
}
}
}
},
"status.action.bookmark" : {
"localizations" : {
"en" : {
@ -2676,4 +2740,4 @@
}
},
"version" : "1.0"
}
}

View File

@ -2,9 +2,20 @@
import SwiftUI
import TipKit
import RevenueCat
@main
struct ThreadedApp: App {
init() {
guard let plist = AppDelegate.readSecret() else { return }
if let apiKey = plist["RevenueCat_public"], let deviceId = UIDevice.current.identifierForVendor?.uuidString {
#if DEBUG
Purchases.logLevel = .debug
#endif
Purchases.configure(withAPIKey: apiKey, appUserID: deviceId)
}
}
var body: some Scene {
WindowGroup {
ContentView()
@ -23,6 +34,7 @@ struct ThreadedApp: App {
.datastoreLocation(.applicationDefault)
])
}
.modelData()
}
}
}

View File

@ -2,13 +2,22 @@
import SwiftUI
import StoreKit
import RevenueCat
struct ShopView: View {
@Environment(AppDelegate.self) private var delegate: AppDelegate
@Environment(\.dismiss) private var dismiss
@State private var showSub: Bool = false
@State private var showLifetime: Bool = false
@State private var purchaseError: Bool = false
private var canPay: Bool {
#if DEBUG
return true
#else
return false
#endif
}
var body: some View {
VStack {
@ -29,7 +38,7 @@ struct ShopView: View {
} label: {
Text("shop.threaded-plus.subscription")
}
.buttonStyle(LargeButton(filled: true, disabled: true))
.buttonStyle(LargeButton(filled: true, disabled: !canPay))
.overlay(alignment: .topTrailing) {
Text("shop.best")
.foregroundStyle(Color.white)
@ -41,15 +50,16 @@ struct ShopView: View {
.offset(x: 20.0, y: -25.0)
.rotationEffect(.degrees(25.0))
}
.disabled(true)
.disabled(!canPay)
Button {
showLifetime.toggle()
// showLifetime.toggle()
purchase(entitlement: .lifetime)
} label: {
Text("shop.threaded-plus.lifetime")
}
.buttonStyle(LargeButton(filled: false, disabled: true))
.disabled(true)
.buttonStyle(LargeButton(filled: false, disabled: !canPay))
.disabled(!canPay)
Button {
dismiss()
@ -67,9 +77,6 @@ struct ShopView: View {
.sheet(isPresented: $showSub) {
ShopView.SubView()
}
.sheet(isPresented: $showLifetime) {
ShopView.LifetimeView()
}
}
var features: some View {
@ -140,80 +147,152 @@ struct ShopView: View {
extension ShopView {
struct SubView: View {
@State private var selectedPlan: PlusPlan = .monthly
var body: some View {
NavigationStack {
SubscriptionStoreView(productIDs: ["fr.lumaa.Threaded.Plus.monthly", "fr.lumaa.ThreadedPlus.yearly"]) {
ZStack {
Color.appBackground
.ignoresSafeArea()
VStack {
Spacer()
Image("HeroPlus")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
header
.frame(height: 300)
Spacer()
Text(String("Threaded+")) // Force the name as untranslatable
.font(.title.bold())
.foregroundStyle(.white)
Text("shop.threaded-plus.subscription.description")
.font(.caption)
.foregroundStyle(.gray)
.multilineTextAlignment(.center)
}
.padding(.horizontal)
}
.background(Color.appBackground)
.productViewStyle(.large)
.storeButton(.visible, for: .redeemCode)
.subscriptionStoreControlStyle(.prominentPicker)
.subscriptionStoreControlBackground(Color.appBackground)
.subscriptionStorePolicyDestination(url: URL(string: "https://apps.lumaa.fr/legal/privacy")!, for: .privacyPolicy)
.subscriptionStorePolicyDestination(for: .termsOfService) {
ZStack {
Color.appBackground
.ignoresSafeArea()
Button {
guard selectedPlan != .monthly else { return }
withAnimation(.spring.speed(2.0)) {
selectedPlan = .monthly
}
} label: {
planSelector(.monthly, isSelected: selectedPlan == PlusPlan.monthly)
}
Text("tos.description")
.padding()
.background(Color.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 15))
.padding(.horizontal)
Button {
guard selectedPlan != .yearly else { return }
withAnimation(.spring.speed(2.0)) {
selectedPlan = .yearly
}
} label: {
planSelector(.yearly, isSelected: selectedPlan == PlusPlan.yearly)
}
Spacer()
Button {
purchase(entitlement: selectedPlan.getEntitlement())
} label: {
Text("shop.threaded-plus.subscribe")
}
.buttonStyle(LargeButton(filled: true))
}
.environment(\.colorScheme, ColorScheme.dark)
}
.navigationTitle(String("Subscription"))
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden()
.tint(Color.white)
}
.environment(\.colorScheme, ColorScheme.dark)
}
}
struct LifetimeView: View {
var body: some View {
var header: some View {
VStack {
Text("shop.threaded-plus.lifetime.header")
.font(.title.bold())
.fontWidth(.expanded)
Spacer()
ProductView(id: "fr.lumaa.ThreadedPlus.lifetime", prefersPromotionalIcon: true) {
ZStack {
Color.appBackground
Image("HeroPlus")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
}
.clipShape(RoundedRectangle(cornerRadius: 25.0))
.environment(\.colorScheme, ColorScheme.dark)
}
.productViewStyle(.large)
Image("HeroPlus")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
Spacer()
Text(String("Threaded+")) // Force the name as untranslatable
.font(.title.bold())
.foregroundStyle(.white)
Text("shop.threaded-plus.subscription.description")
.font(.caption)
.foregroundStyle(.gray)
.multilineTextAlignment(.center)
}
.padding(.horizontal)
}
var tos: some View {
ZStack {
Color.appBackground
.ignoresSafeArea()
Text("tos.description")
.padding()
.background(Color.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 15))
.padding(.horizontal)
}
.environment(\.colorScheme, ColorScheme.dark)
}
@ViewBuilder
private func planSelector(_ plan: PlusPlan, isSelected: Bool = false) -> some View {
VStack(alignment: .leading) {
Text(plan.getTitle())
.font(.headline.bold())
.multilineTextAlignment(.leading)
Text(plan.getPrice())
.multilineTextAlignment(.leading)
}
.padding(.vertical, 30)
.frame(width: 350)
.background(Color.gray.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 15))
.overlay {
if isSelected {
RoundedRectangle(cornerRadius: 15)
.stroke(Color.green, lineWidth: 1.5)
} else {
RoundedRectangle(cornerRadius: 15)
.stroke(Color(uiColor: UIColor.label).opacity(0.35), lineWidth: 1.5)
}
}
.overlay(alignment: .topTrailing) {
if isSelected {
Image(systemName: "checkmark.seal.fill")
.foregroundStyle(Color.green)
.font(.title)
}
}
}
private enum PlusPlan {
case monthly
case yearly
func getTitle() -> String {
switch (self) {
case .monthly:
return String(localized: "shop.threaded-plus.monthly")
case .yearly:
return String(localized: "shop.threaded-plus.yearly")
}
}
func getPrice() -> String {
switch (self) {
case .monthly:
return String(localized: "shop.threaded-plus.monthly.price")
case .yearly:
return String(localized: "shop.threaded-plus.yearly.price")
}
}
func getEntitlement() -> PlusEntitlements {
switch (self) {
case .monthly:
return .monthly
case .yearly:
return .yearly
}
}
}
}
}
@ -222,3 +301,55 @@ extension ShopView {
.environment(AppDelegate())
// .environment(\.locale, Locale(identifier: "en-us"))
}
private func hasActuallyPlus(customerInfo: CustomerInfo?) -> Bool {
return customerInfo?.entitlements[PlusEntitlements.lifetime.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.monthly.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.yearly.getEntitlementId()]?.isActive == true
}
private func purchase(entitlement: PlusEntitlements) {
Purchases.shared.getOfferings { (offerings, error) in
if let product = entitlement.toPackage(offerings: offerings) {
Purchases.shared.purchase(package: product) { (transaction, customerInfo, error, userCancelled) in
if hasActuallyPlus(customerInfo: customerInfo) {
print("BOUGHT PLUS")
AppDelegate.premium = true
}
}
}
if let e = error {
print(e)
}
}
}
enum PlusEntitlements: String {
case monthly
case yearly
case lifetime
func toPackage(offerings: Offerings?) -> Package? {
if let offs = offerings {
switch (self) {
case .monthly:
return offs.current?.monthly
case .yearly:
return offs.current?.annual
case .lifetime:
return offs.current?.lifetime
}
} else {
return nil
}
}
func getEntitlementId() -> String {
switch (self) {
case .lifetime:
return "thrd_30$_life"
case .monthly:
return "thrd_2$_1mth_1mth0"
case .yearly:
return "thrd_20$_1y_1mth0"
}
}
}