Revenue Cat for Threaded+
This commit is contained in:
parent
7d72b8e0e1
commit
a90bff172e
@ -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" */;
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
27
Threaded/Data/Accounts/LoggedAccounts.swift
Normal file
27
Threaded/Data/Accounts/LoggedAccounts.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user