Add in app purchase.
This commit is contained in:
parent
709888a560
commit
04c25454d6
|
@ -53,6 +53,12 @@
|
|||
F866F6A729604629002E8F88 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A629604629002E8F88 /* SignInView.swift */; };
|
||||
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A929605AFA002E8F88 /* SceneDelegate.swift */; };
|
||||
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */; };
|
||||
F86A42FD299A8B8E00DF7645 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F86A42FC299A8B8E00DF7645 /* StoreKit.framework */; };
|
||||
F86A42FF299A8C5500DF7645 /* InAppPurchaseStoreKitConfiguration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = F86A42FE299A8C5500DF7645 /* InAppPurchaseStoreKitConfiguration.storekit */; };
|
||||
F86A4301299A97F500DF7645 /* ProductIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86A4300299A97F500DF7645 /* ProductIdentifiers.swift */; };
|
||||
F86A4303299A9AF500DF7645 /* Tips.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86A4302299A9AF500DF7645 /* Tips.swift */; };
|
||||
F86A4305299AA12800DF7645 /* PurchaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86A4304299AA12800DF7645 /* PurchaseError.swift */; };
|
||||
F86A4307299AA5E900DF7645 /* ThanksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86A4306299AA5E900DF7645 /* ThanksView.swift */; };
|
||||
F86B7214296BFDCE00EE59EC /* UserProfileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */; };
|
||||
F86B7216296BFFDA00EE59EC /* UserProfileStatuses.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */; };
|
||||
F86B7218296C27C100EE59EC /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7217296C27C100EE59EC /* ActionButton.swift */; };
|
||||
|
@ -180,6 +186,12 @@
|
|||
F866F6A929605AFA002E8F88 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationViewMode.swift; sourceTree = "<group>"; };
|
||||
F86728AD296D3CE200475EC9 /* MastodonKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MastodonKit; sourceTree = "<group>"; };
|
||||
F86A42FC299A8B8E00DF7645 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
F86A42FE299A8C5500DF7645 /* InAppPurchaseStoreKitConfiguration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = InAppPurchaseStoreKitConfiguration.storekit; sourceTree = "<group>"; };
|
||||
F86A4300299A97F500DF7645 /* ProductIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductIdentifiers.swift; sourceTree = "<group>"; };
|
||||
F86A4302299A9AF500DF7645 /* Tips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tips.swift; sourceTree = "<group>"; };
|
||||
F86A4304299AA12800DF7645 /* PurchaseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseError.swift; sourceTree = "<group>"; };
|
||||
F86A4306299AA5E900DF7645 /* ThanksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThanksView.swift; sourceTree = "<group>"; };
|
||||
F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileHeader.swift; sourceTree = "<group>"; };
|
||||
F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileStatuses.swift; sourceTree = "<group>"; };
|
||||
F86B7217296C27C100EE59EC /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = "<group>"; };
|
||||
|
@ -267,6 +279,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F86A42FD299A8B8E00DF7645 /* StoreKit.framework in Frameworks */,
|
||||
F89992C7296D3DF8005994BF /* MastodonKit in Frameworks */,
|
||||
F8210DD52966BB7E001D9973 /* Nuke in Frameworks */,
|
||||
F88E4D4D297EA4290057491A /* EmojiText in Frameworks */,
|
||||
|
@ -287,6 +300,7 @@
|
|||
F802884E297AEED5000BDD51 /* DatabaseError.swift */,
|
||||
F87AEB962986D16D00434FB6 /* AuthorisationError.swift */,
|
||||
F8742FC329990AFB00E9642B /* ClientError.swift */,
|
||||
F86A4304299AA12800DF7645 /* PurchaseError.swift */,
|
||||
);
|
||||
path = Errors;
|
||||
sourceTree = "<group>";
|
||||
|
@ -481,10 +495,12 @@
|
|||
F88C246B295C37B80006098B /* VernissageApp.swift */,
|
||||
F88E4D55297EAD6E0057491A /* AppRouteur.swift */,
|
||||
F87AEB932986C51B00434FB6 /* AppConstants.swift */,
|
||||
F86A4300299A97F500DF7645 /* ProductIdentifiers.swift */,
|
||||
F866F6A929605AFA002E8F88 /* SceneDelegate.swift */,
|
||||
F88C246F295C37BB0006098B /* Assets.xcassets */,
|
||||
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */,
|
||||
F88C2471295C37BB0006098B /* Preview Content */,
|
||||
F86A42FE299A8C5500DF7645 /* InAppPurchaseStoreKitConfiguration.storekit */,
|
||||
);
|
||||
path = Vernissage;
|
||||
sourceTree = "<group>";
|
||||
|
@ -524,6 +540,7 @@
|
|||
F89992C5296D3DF8005994BF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F86A42FC299A8B8E00DF7645 /* StoreKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -537,6 +554,7 @@
|
|||
F86167C5297FE6CC004D1F67 /* AvatarShapesSection.swift */,
|
||||
F8B0885F29943498002AB40A /* OtherSection.swift */,
|
||||
F8B08861299435C9002AB40A /* SupportView.swift */,
|
||||
F86A4306299AA5E900DF7645 /* ThanksView.swift */,
|
||||
);
|
||||
path = SettingsView;
|
||||
sourceTree = "<group>";
|
||||
|
@ -573,6 +591,7 @@
|
|||
F8B9B350298D4B34009CC69C /* Client+Account.swift */,
|
||||
F8B9B352298D4B5D009CC69C /* Client+Search.swift */,
|
||||
F8B9B355298D4C1E009CC69C /* Client+Instance.swift */,
|
||||
F86A4302299A9AF500DF7645 /* Tips.swift */,
|
||||
);
|
||||
path = EnvironmentObjects;
|
||||
sourceTree = "<group>";
|
||||
|
@ -662,6 +681,7 @@
|
|||
files = (
|
||||
F88C2473295C37BB0006098B /* Preview Assets.xcassets in Resources */,
|
||||
F88C2470295C37BB0006098B /* Assets.xcassets in Resources */,
|
||||
F86A42FF299A8C5500DF7645 /* InAppPurchaseStoreKitConfiguration.storekit in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -676,6 +696,7 @@
|
|||
F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */,
|
||||
F87AEB922986C44E00434FB6 /* AuthorizationSession.swift in Sources */,
|
||||
F88E4D44297E82EB0057491A /* Status+MediaAttachmentType.swift in Sources */,
|
||||
F86A4301299A97F500DF7645 /* ProductIdentifiers.swift in Sources */,
|
||||
F89D6C4229717FDC001DA3D4 /* AccountsSection.swift in Sources */,
|
||||
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */,
|
||||
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */,
|
||||
|
@ -707,6 +728,7 @@
|
|||
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */,
|
||||
F8B9B353298D4B5D009CC69C /* Client+Search.swift in Sources */,
|
||||
F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */,
|
||||
F86A4305299AA12800DF7645 /* PurchaseError.swift in Sources */,
|
||||
F89D6C4429718092001DA3D4 /* AccentsSection.swift in Sources */,
|
||||
F88E4D42297E69FD0057491A /* StatusesView.swift in Sources */,
|
||||
F86FB555298BF83F000131F0 /* FavouriteTouch.swift in Sources */,
|
||||
|
@ -765,6 +787,7 @@
|
|||
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */,
|
||||
F8121CA8298A86D600B466C7 /* InstanceRow.swift in Sources */,
|
||||
F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */,
|
||||
F86A4307299AA5E900DF7645 /* ThanksView.swift in Sources */,
|
||||
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
|
||||
F8B9B356298D4C1E009CC69C /* Client+Instance.swift in Sources */,
|
||||
F88E4D56297EAD6E0057491A /* AppRouteur.swift in Sources */,
|
||||
|
@ -778,6 +801,7 @@
|
|||
F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */,
|
||||
F88E4D54297EA7EE0057491A /* MarkdownFormattedText.swift in Sources */,
|
||||
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
|
||||
F86A4303299A9AF500DF7645 /* Tips.swift in Sources */,
|
||||
F8C5E56229892CC300ADF6A7 /* FirstAppear.swift in Sources */,
|
||||
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */,
|
||||
F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */,
|
||||
|
|
|
@ -52,4 +52,16 @@ extension View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func withOverlayDestinations(overlayDestinations: Binding<OverlayDestinations?>) -> some View {
|
||||
self.overlay {
|
||||
switch overlayDestinations.wrappedValue {
|
||||
case .successPayment:
|
||||
ThanksView()
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "40",
|
||||
"green" : "40",
|
||||
"red" : "40"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "247",
|
||||
"green" : "247",
|
||||
"red" : "247"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "247",
|
||||
"green" : "247",
|
||||
"red" : "247"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.157",
|
||||
"green" : "0.157",
|
||||
"red" : "0.157"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
|
||||
@MainActor
|
||||
final class Tips: ObservableObject {
|
||||
|
||||
@Published private(set) var items = [Product]()
|
||||
@Published private(set) var status: ActionStatus? {
|
||||
didSet{
|
||||
switch status {
|
||||
case .failed:
|
||||
self.hasError = true
|
||||
default:
|
||||
self.hasError = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var hasError = false
|
||||
|
||||
var error: PurchaseError? {
|
||||
switch status {
|
||||
case .failed(let error):
|
||||
return error
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private var transactionListener: Task<Void, Error>?
|
||||
|
||||
init() {
|
||||
transactionListener = configureTransactionListener()
|
||||
Task { [weak self] in
|
||||
await self?.retrieve()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
transactionListener?.cancel()
|
||||
}
|
||||
|
||||
/// Purchase new product.
|
||||
public func purchase(_ product: Product) async {
|
||||
do {
|
||||
let result = try await product.purchase()
|
||||
try await self.handlePurchase(from: result)
|
||||
} catch {
|
||||
self.status = .failed(.system(error))
|
||||
ErrorService.shared.handle(error, message: "Purchase failed.", showToastr: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset status of the purchase/action.
|
||||
public func reset() {
|
||||
self.status = nil
|
||||
}
|
||||
|
||||
/// Handle purchase result.
|
||||
private func handlePurchase(from result: Product.PurchaseResult) async throws {
|
||||
switch result {
|
||||
case .success(let verificationResult):
|
||||
let transaction = try self.checkVerified(verificationResult)
|
||||
|
||||
self.status = .successful
|
||||
await transaction.finish()
|
||||
case .userCancelled:
|
||||
print("User click cancel before their transaction started.")
|
||||
case .pending:
|
||||
print("User needs to complete some action on their account before their complete the purchase.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// We have to verify if transaction ends successfuly.
|
||||
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
|
||||
switch result {
|
||||
case .unverified:
|
||||
throw PurchaseError.failedVerification
|
||||
case .verified(let signedType):
|
||||
return signedType
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure listener of interrupted transactions.
|
||||
private func configureTransactionListener() -> Task<Void, Error> {
|
||||
Task.detached(priority: .background) { @MainActor [weak self] in
|
||||
do {
|
||||
for await result in Transaction.updates {
|
||||
let transaction = try self?.checkVerified(result)
|
||||
self?.status = .successful
|
||||
await transaction?.finish()
|
||||
}
|
||||
} catch {
|
||||
self?.status = .failed(.system(error))
|
||||
ErrorService.shared.handle(error, message: "Cannot configure transaction listener.", showToastr: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve products from Apple store.
|
||||
private func retrieve() async {
|
||||
do {
|
||||
let products = try await Product.products(for: ProductIdentifiers.allCases.map({ $0.rawValue }))
|
||||
.sorted(by: { $0.price < $1.price })
|
||||
|
||||
self.items = products
|
||||
} catch {
|
||||
self.status = .failed(.system(error))
|
||||
ErrorService.shared.handle(error, message: "Cannot download in-app products.", showToastr: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Tips {
|
||||
public enum ActionStatus: Equatable {
|
||||
case successful
|
||||
case failed(PurchaseError)
|
||||
|
||||
public static func == (lhs: Tips.ActionStatus, rhs: Tips.ActionStatus) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.successful, .successful):
|
||||
return true
|
||||
case (let .failed(lhsError), let .failed(rhsError)):
|
||||
return lhsError.localizedDescription == rhsError.localizedDescription
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum PurchaseError: Error {
|
||||
case failedVerification
|
||||
case system(Error)
|
||||
}
|
||||
|
||||
extension PurchaseError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .failedVerification:
|
||||
return NSLocalizedString("Purchase verification failed.", comment: "Something went wrong during purchase verification.")
|
||||
case .system(let error):
|
||||
return error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,6 +13,8 @@ extension Color {
|
|||
static let lightGrayColor = Color("LightGrayColor")
|
||||
static let mainTextColor = Color("MainTextColor")
|
||||
static let selectedRowColor = Color("SelectedRowColor")
|
||||
static let viewBackgroundColor = Color("ViewBackgroundColor")
|
||||
static let viewTextColor = Color("ViewTextColor")
|
||||
static let accentColor1 = Color("AccentColor1")
|
||||
static let accentColor2 = Color("AccentColor2")
|
||||
static let accentColor3 = Color("AccentColor3")
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"identifier" : "1FBBF671",
|
||||
"nonRenewingSubscriptions" : [
|
||||
|
||||
],
|
||||
"products" : [
|
||||
{
|
||||
"displayPrice" : "0.99",
|
||||
"familyShareable" : false,
|
||||
"internalID" : "FCCAC94C",
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Treat me to a doughnut.",
|
||||
"displayName" : "Donut",
|
||||
"locale" : "en_US"
|
||||
}
|
||||
],
|
||||
"productID" : "dev.mczachurski.Vernissage.donut",
|
||||
"referenceName" : "Donut",
|
||||
"type" : "Consumable"
|
||||
},
|
||||
{
|
||||
"displayPrice" : "3.99",
|
||||
"familyShareable" : false,
|
||||
"internalID" : "AD04459F",
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Treat me to a coffe.",
|
||||
"displayName" : "Cofee",
|
||||
"locale" : "en_US"
|
||||
}
|
||||
],
|
||||
"productID" : "dev.mczachurski.Vernissage.cofee",
|
||||
"referenceName" : "Cofee",
|
||||
"type" : "Consumable"
|
||||
},
|
||||
{
|
||||
"displayPrice" : "9.99",
|
||||
"familyShareable" : false,
|
||||
"internalID" : "16E4E30C",
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Treat me to a coffe and cake.",
|
||||
"displayName" : "Cofee & cake",
|
||||
"locale" : "en_US"
|
||||
}
|
||||
],
|
||||
"productID" : "dev.mczachurski.Vernissage.cake",
|
||||
"referenceName" : "Cake",
|
||||
"type" : "Consumable"
|
||||
}
|
||||
],
|
||||
"settings" : {
|
||||
"_billingIssuesEnabled" : false,
|
||||
"_failTransactionsEnabled" : false,
|
||||
"_storeKitError" : 0
|
||||
},
|
||||
"subscriptionGroups" : [
|
||||
|
||||
],
|
||||
"version" : {
|
||||
"major" : 2,
|
||||
"minor" : 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum ProductIdentifiers: String, CaseIterable {
|
||||
case donut = "dev.mczachurski.Vernissage.donut"
|
||||
case cofee = "dev.mczachurski.Vernissage.cofee"
|
||||
case cake = "dev.mczachurski.Vernissage.cake"
|
||||
}
|
|
@ -35,12 +35,17 @@ enum SheetDestinations: Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
enum OverlayDestinations {
|
||||
case successPayment
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class RouterPath: ObservableObject {
|
||||
public var urlHandler: ((URL) -> OpenURLAction.Result)?
|
||||
|
||||
@Published public var path: [RouteurDestinations] = []
|
||||
@Published public var presentedSheet: SheetDestinations?
|
||||
@Published public var presentedOverlay: OverlayDestinations?
|
||||
|
||||
public init() {}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ struct VernissageApp: App {
|
|||
@StateObject var applicationState = ApplicationState.shared
|
||||
@StateObject var client = Client.shared
|
||||
@StateObject var routerPath = RouterPath()
|
||||
@StateObject var tips = Tips()
|
||||
|
||||
@State var applicationViewMode: ApplicationViewMode = .loading
|
||||
@State var tintColor = ApplicationState.shared.tintColor.color()
|
||||
|
@ -42,12 +43,14 @@ struct VernissageApp: App {
|
|||
MainView()
|
||||
.withAppRouteur()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.withOverlayDestinations(overlayDestinations: $routerPath.presentedOverlay)
|
||||
}
|
||||
}
|
||||
.environment(\.managedObjectContext, coreDataHandler.container.viewContext)
|
||||
.environmentObject(applicationState)
|
||||
.environmentObject(client)
|
||||
.environmentObject(routerPath)
|
||||
.environmentObject(tips)
|
||||
.tint(self.tintColor)
|
||||
.preferredColorScheme(self.theme)
|
||||
.task {
|
||||
|
|
|
@ -20,6 +20,7 @@ struct MainView: View {
|
|||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@EnvironmentObject var client: Client
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
@EnvironmentObject var tips: Tips
|
||||
|
||||
@State private var navBarTitle: String = "Home"
|
||||
@State private var viewMode: ViewMode = .home {
|
||||
|
@ -43,6 +44,14 @@ struct MainView: View {
|
|||
self.getPrincipalToolbar()
|
||||
self.getTrailingToolbar()
|
||||
}
|
||||
.onChange(of: tips.status) { action in
|
||||
if action == .successful {
|
||||
withAnimation(.spring()) {
|
||||
self.routerPath.presentedOverlay = .successPayment
|
||||
self.tips.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
@ -8,6 +8,9 @@ import SwiftUI
|
|||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
@EnvironmentObject var tips: Tips
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
|
@ -59,11 +62,21 @@ struct SettingsView: View {
|
|||
.preferredColorScheme(self.theme)
|
||||
}
|
||||
.withAppRouteur()
|
||||
.withOverlayDestinations(overlayDestinations: $routerPath.presentedOverlay)
|
||||
}
|
||||
.onChange(of: self.applicationState.theme) { newValue in
|
||||
// Change theme of current modal screen (unformtunatelly it's not changed autmatically.
|
||||
self.theme = self.applicationState.theme.colorScheme() ?? self.getSystemColorScheme()
|
||||
}
|
||||
.onChange(of: tips.status) { status in
|
||||
if status == .successful {
|
||||
withAnimation(.spring()) {
|
||||
self.routerPath.presentedOverlay = .successPayment
|
||||
self.tips.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $tips.hasError, error: tips.error) { }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
@ -3,66 +3,48 @@
|
|||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
struct SupportView: View {
|
||||
|
||||
@EnvironmentObject var tips: Tips
|
||||
|
||||
var body: some View {
|
||||
Section("Support") {
|
||||
HStack(alignment: .center) {
|
||||
Text("🍩")
|
||||
.font(.title)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Donut")
|
||||
.font(.caption)
|
||||
Text("Treat me to a doughnut.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.lightGrayColor)
|
||||
ForEach(tips.items) { product in
|
||||
HStack(alignment: .center) {
|
||||
Text(self.getIcon(for: product))
|
||||
.font(.title)
|
||||
VStack(alignment: .leading) {
|
||||
Text(product.displayName)
|
||||
.font(.caption)
|
||||
Text(product.description)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.lightGrayColor)
|
||||
}
|
||||
Spacer()
|
||||
Button(product.displayPrice) {
|
||||
Task {
|
||||
await tips.purchase(product)
|
||||
}
|
||||
}
|
||||
.font(.footnote)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
Spacer()
|
||||
Button("5.99 PLN") {
|
||||
}
|
||||
.font(.footnote)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text("☕️")
|
||||
.font(.title)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Cofee")
|
||||
.font(.caption)
|
||||
Text("Treat me to a coffe.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.lightGrayColor)
|
||||
}
|
||||
Spacer()
|
||||
Button("17.99 PLN") {
|
||||
}
|
||||
.font(.footnote)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text("🍰")
|
||||
.font(.title)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Cofee & cake")
|
||||
.font(.caption)
|
||||
Text("Treat me to a coffe and cake.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.lightGrayColor)
|
||||
}
|
||||
Spacer()
|
||||
Button("39.99 PLN") {
|
||||
}
|
||||
.font(.footnote)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
private func getIcon(for product: Product) -> String {
|
||||
if product.id == ProductIdentifiers.donut.rawValue {
|
||||
return "🍩"
|
||||
} else if product.id == ProductIdentifiers.cofee.rawValue {
|
||||
return "☕️"
|
||||
} else {
|
||||
return "🍰"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ThanksView: View {
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
Group {
|
||||
Text("Thank you 💕")
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.viewTextColor)
|
||||
.padding(.top, 8)
|
||||
Text("Thanks for your purchase. Purchases both big and small help us keep our dream of providing the best quality products to our customers. We hope you’re loving Vernissage.")
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.viewTextColor)
|
||||
|
||||
Button("Close") {
|
||||
withAnimation(.spring()) {
|
||||
self.routerPath.presentedOverlay = nil
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}.padding(8)
|
||||
}
|
||||
.background(Color.viewBackgroundColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
.padding(8)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue