Skip to content

Commit

Permalink
Merge pull request #42 from Isvvc/groups-iap
Browse files Browse the repository at this point in the history
Groups in-app-purchase
  • Loading branch information
skjiisa authored Jun 2, 2021
2 parents 52f992b + 7e90057 commit 904a50b
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 26 deletions.
14 changes: 12 additions & 2 deletions Tickmate/Tickmate.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
59342E1C2603F3DF007E9F64 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59342E1B2603F3DF007E9F64 /* OnboardingView.swift */; };
5939239C2654CE050085DABD /* PageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939239B2654CE050085DABD /* PageView.swift */; };
5939239E2655B6C90085DABD /* Animation+Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939239D2655B6C90085DABD /* Animation+Push.swift */; };
59603D452666C85A001482C6 /* StoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59603D442666C85A001482C6 /* StoreController.swift */; };
5964545225E987C9004FE184 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5964545125E987C9004FE184 /* CloudKit.framework */; };
597A7FDD25F6E1C900256268 /* StateEditButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 597A7FDC25F6E1C900256268 /* StateEditButton.swift */; };
597DF5B025E5E90800DC8D28 /* Color+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 597DF5AF25E5E90800DC8D28 /* Color+RGB.swift */; };
Expand All @@ -36,6 +37,7 @@
59F0980225E081CC00667971 /* TicksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F0980125E081CC00667971 /* TicksView.swift */; };
59F3EDBB25FC46770009CDEC /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 59F3EDBA25FC46770009CDEC /* Introspect */; };
59F3EDC225FC4F270009CDEC /* ViewControllerContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F3EDC125FC4F270009CDEC /* ViewControllerContainer.swift */; };
59F6170926671C2100CC78E1 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F6170826671C2100CC78E1 /* AlertItem.swift */; };
59F6EA9D25E369830069DF40 /* Tickmate+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F6EA9C25E369830069DF40 /* Tickmate+Convenience.swift */; };
59F6EAA425E36D510069DF40 /* TickController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F6EAA325E36D510069DF40 /* TickController.swift */; };
59F6EAA725E36D680069DF40 /* TrackController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F6EAA625E36D680069DF40 /* TrackController.swift */; };
Expand All @@ -58,6 +60,8 @@
59342E1B2603F3DF007E9F64 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
5939239B2654CE050085DABD /* PageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageView.swift; sourceTree = "<group>"; };
5939239D2655B6C90085DABD /* Animation+Push.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Animation+Push.swift"; sourceTree = "<group>"; };
59603D442666C85A001482C6 /* StoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreController.swift; sourceTree = "<group>"; };
59603D462666CA98001482C6 /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = "<group>"; };
5964544E25E987C6004FE184 /* Tickmate.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tickmate.entitlements; sourceTree = "<group>"; };
5964545125E987C9004FE184 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
597A7FDC25F6E1C900256268 /* StateEditButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateEditButton.swift; sourceTree = "<group>"; };
Expand All @@ -78,6 +82,7 @@
59EF633625E5DC93003E7259 /* TextWithCaption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithCaption.swift; sourceTree = "<group>"; };
59F0980125E081CC00667971 /* TicksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicksView.swift; sourceTree = "<group>"; };
59F3EDC125FC4F270009CDEC /* ViewControllerContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerContainer.swift; sourceTree = "<group>"; };
59F6170826671C2100CC78E1 /* AlertItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertItem.swift; sourceTree = "<group>"; };
59F6EA9C25E369830069DF40 /* Tickmate+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tickmate+Convenience.swift"; sourceTree = "<group>"; };
59F6EAA325E36D510069DF40 /* TickController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TickController.swift; sourceTree = "<group>"; };
59F6EAA625E36D680069DF40 /* TrackController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -119,6 +124,7 @@
59FBCF2425E07BD8007B114F /* Assets.xcassets */,
59C23A6625E70D7000AFBC4B /* SymbolsList.swift */,
5991969125F817F7003215CC /* PresetTracks.swift */,
59603D462666CA98001482C6 /* Configuration.storekit */,
);
path = Resources;
sourceTree = "<group>";
Expand All @@ -134,6 +140,7 @@
592CE887264F19B400668684 /* View+Centered.swift */,
5939239B2654CE050085DABD /* PageView.swift */,
5939239D2655B6C90085DABD /* Animation+Push.swift */,
59F6170826671C2100CC78E1 /* AlertItem.swift */,
);
path = "Supplementary Views";
sourceTree = "<group>";
Expand Down Expand Up @@ -219,6 +226,7 @@
59F6EAA625E36D680069DF40 /* TrackController.swift */,
592850F1265EF54B00E682F5 /* GroupController.swift */,
59F3EDC125FC4F270009CDEC /* ViewControllerContainer.swift */,
59603D442666C85A001482C6 /* StoreController.swift */,
);
path = Controllers;
sourceTree = "<group>";
Expand Down Expand Up @@ -322,8 +330,10 @@
59FBCF2125E07BD5007B114F /* TickmateApp.swift in Sources */,
5996360F264F128A00AAF6CA /* GroupsView.swift in Sources */,
59FBCF2D25E07BD8007B114F /* Tickmate.xcdatamodeld in Sources */,
59603D452666C85A001482C6 /* StoreController.swift in Sources */,
5991969225F817F7003215CC /* PresetTracks.swift in Sources */,
599D9F5225EC5A4900DF3795 /* TracksView.swift in Sources */,
59F6170926671C2100CC78E1 /* AlertItem.swift in Sources */,
59F6EA9D25E369830069DF40 /* Tickmate+Convenience.swift in Sources */,
59A0D8932645CF1600E01E61 /* View+DismissKeyboard.swift in Sources */,
59EF633725E5DC93003E7259 /* TextWithCaption.swift in Sources */,
Expand Down Expand Up @@ -463,7 +473,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Tickmate/Tickmate.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"Tickmate/Preview Content\"";
DEVELOPMENT_TEAM = A4525544Q4;
ENABLE_PREVIEWS = YES;
Expand All @@ -488,7 +498,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Tickmate/Tickmate.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"Tickmate/Preview Content\"";
DEVELOPMENT_TEAM = A4525544Q4;
ENABLE_PREVIEWS = YES;
Expand Down
154 changes: 154 additions & 0 deletions Tickmate/Tickmate/Controllers/StoreController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// StoreController.swift
// Tickmate
//
// Created by Isaac Lyons on 6/1/21.
//

import SwiftUI
import StoreKit

class StoreController: NSObject, ObservableObject {

@Published private(set) var products = [SKProduct]()

@Published private(set) var purchased = Set<String>()
@Published private(set) var purchasing = Set<String>()
@Published var restored: AlertItem?

private var isRestoringPurchases = false

var isAuthorizedForPayments: Bool {
SKPaymentQueue.canMakePayments()
}

override init() {
super.init()
SKPaymentQueue.default().add(self)
}

var priceFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.formatterBehavior = .behavior10_4
formatter.numberStyle = .currency
return formatter
}()

func fetchProducts() {
let request = SKProductsRequest(productIdentifiers: Set(Products.allValues))
request.delegate = self
request.start()
}

func purchase(_ product: SKProduct) {
guard isAuthorizedForPayments else { return }

let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}

func restorePurchases() {
isRestoringPurchases = true
SKPaymentQueue.default().restoreCompletedTransactions()
}

#if DEBUG
func removePurchased(id: String) {
purchased.remove(id)
}
#endif

//MARK: Products

enum Products: String, CaseIterable {
case groups = "vc.isv.Tickmate.groups"

static var allValues: [String] {
allCases.map { $0.rawValue }
}
}

}

//MARK: Products Request Delegate

extension StoreController: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
if !response.invalidProductIdentifiers.isEmpty {
NSLog("Invalid product identifiers found: \(response.invalidProductIdentifiers)")
}

DispatchQueue.main.async {
if let locale = response.products.first?.priceLocale {
self.priceFormatter.locale = locale
}
withAnimation {
response.products
.map { $0.productIdentifier }
.filter { UserDefaults.standard.bool(forKey: $0) }
.forEach { self.purchased.insert($0) }
self.products = response.products
}
}
}
}

//MARK: Payment Transaction Observer

extension StoreController: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach { transaction in
let productID = transaction.payment.productIdentifier

switch transaction.transactionState {
case .purchasing:
withAnimation {
_ = purchasing.insert(productID)
}
case .purchased, .restored:
print("Purchased \(productID)!")
UserDefaults.standard.set(true, forKey: productID)
withAnimation {
purchasing.remove(productID)
purchased.insert(productID)
}

queue.finishTransaction(transaction)
case .failed, .deferred:
purchasing.remove(productID)
if let error = transaction.error {
print("Purchase of \(productID) failed: \(error)")
} else {
print("Purchase of \(productID) failed.")
}

queue.finishTransaction(transaction)
@unknown default:
break
}
}

if isRestoringPurchases {
let restoredProducts = transactions
.filter { $0.transactionState == .restored }
.compactMap { transaction in products.first(where: { $0.productIdentifier == transaction.payment.productIdentifier }) }
if !restoredProducts.isEmpty {
let alertBody = restoredProducts
.map { $0.localizedTitle }
.joined(separator: ", ")
restored = AlertItem(title: "Purchases restored!", message: alertBody)
isRestoringPurchases = false
}
}
}

func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
// If there were purchases that were restored, isRestoringPurchases,
// would be set to false in paymentQueue(_:, updatedTransactions:), so
// if it's still true here, that means there were no purchases to restore.
if isRestoringPurchases {
restored = AlertItem(title: "No purchases to restore")
isRestoringPurchases = false
}
}
}
23 changes: 18 additions & 5 deletions Tickmate/Tickmate/Controllers/TrackController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@ class TrackController: NSObject, ObservableObject {
Defaults.relativeDates.rawValue: true
])

if UserDefaults.standard.bool(forKey: Defaults.customDayStart.rawValue) {
date = Date() - UserDefaults.standard.integer(forKey: Defaults.customDayStartMinutes.rawValue).minutes
} else {
date = Date()
}
date = Date() - TrackController.dayOffset
weekday = date.in(region: .current).weekday

weekStartDay = UserDefaults.standard.integer(forKey: Defaults.weekStartDay.rawValue)
Expand Down Expand Up @@ -69,6 +65,10 @@ class TrackController: NSObject, ObservableObject {
}
}

static var dayOffset: DateComponents {
(UserDefaults.standard.bool(forKey: Defaults.customDayStart.rawValue) ? UserDefaults.standard.integer(forKey: Defaults.customDayStartMinutes.rawValue) : 0).minutes
}

//MARK: Date Formatters

private var dateFormatter: DateFormatter = {
Expand Down Expand Up @@ -247,6 +247,19 @@ class TrackController: NSObject, ObservableObject {
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: work)
}

func checkForNewDay() {
// The new date check was done differently in setCustomDayStart, but this works
// too and I don't know if one ways is necissarily better than the other.
let oldDate = TrackController.iso8601.string(from: date)
let newDate = TrackController.iso8601.string(from: Date() - TrackController.dayOffset)
if oldDate != newDate {
objectWillChange.send()
date = Date() - TrackController.dayOffset
tickControllers.values.forEach { $0.loadTicks() }
print("Updated from \(oldDate) to \(newDate)")
}
}

}

//MARK: Fetched Results Controller Delegate
Expand Down
33 changes: 33 additions & 0 deletions Tickmate/Tickmate/Resources/Configuration.storekit
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"identifier" : "6A8C1B93",
"nonRenewingSubscriptions" : [

],
"products" : [
{
"displayPrice" : "1.99",
"familyShareable" : false,
"internalID" : "07E2B154",
"localizations" : [
{
"description" : "Unlock track grouping feature",
"displayName" : "Groups",
"locale" : "en_US"
}
],
"productID" : "vc.isv.Tickmate.groups",
"referenceName" : "Groups",
"type" : "NonConsumable"
}
],
"settings" : {

},
"subscriptionGroups" : [

],
"version" : {
"major" : 1,
"minor" : 1
}
}
36 changes: 36 additions & 0 deletions Tickmate/Tickmate/Supplementary Views/AlertItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// AlertItem.swift
// Tickmate
//
// Created by Isaac Lyons on 6/1/21.
//

import SwiftUI

class AlertItem: Identifiable {
var title: String
var message: String?

init(title: String, message: String? = nil) {
self.title = title
self.message = message
}

var alert: Alert {
if let message = message {
return Alert(title: Text(title), message: Text(message))
}

return Alert(title: Text(title))
}

static func alert(for alertItem: AlertItem) -> Alert {
alertItem.alert
}
}

extension View {
func alert(alertItem: Binding<AlertItem?>) -> some View {
self.alert(item: alertItem) { $0.alert }
}
}
10 changes: 10 additions & 0 deletions Tickmate/Tickmate/Supplementary Views/TextWithCaption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ struct TextWithCaption: View {
var text: String
var caption: String?

init(text: String, caption: String? = nil) {
self.text = text
self.caption = caption
}

init(_ text: String, caption: String? = nil) {
self.text = text
self.caption = caption
}

var body: some View {
VStack(alignment: .leading) {
Text(text)
Expand Down
Loading

0 comments on commit 904a50b

Please sign in to comment.