Have a question about expiration date of the subscription. I have following code with works without errors where I validate receipt and where I have a little issue. My expiration date doesn't change. So If I buy subscription again and again my expiration date stay the same and I always fall into else clause because expiration date is "older" than "new" one.
What I do wrong with it?
private func complete(transaction: SKPaymentTransaction) {
let receiptValidator = ReceiptValidator()
let result = receiptValidator.validateReceipt()
switch result {
case let .success(receipt):
guard let purchase = receipt.inAppPurchaseReceipts?.filter({ $0.productIdentifier == IAPProducts.autoRenewable.rawValue }).first else {
NotificationCenter.default.post(name: Notification.Name(transaction.payment.productIdentifier), object: nil)
return
}
if purchase.subscriptionExpirationDate?.compare(Date()) == .orderedDescending {
print("expirationDate: \(purchase.subscriptionExpirationDate!)")
print(" now's date: \(Date())")
// true clause stuff
} else {
// false clause stuff
print("Subscription has ended")
}
// other stuff
case let .error(error):
print("receipt is invalid")
print(error.localizedDescription)
}
paymentQueue.finishTransaction(transaction)
}
So the console output like this:
expirationDate: 2017-11-01 09:08:11 +0000
now's date: 2017-11-01
10:35:11 +0000
What do I miss?
Thank you!
Oh I found out that my sandbox user made too many transactions. So I create another one and now code works as expected.
Related
I am testing the auto renewable In-app purchases in swift, I found out that there are some strange problems with my code.
I am testing these functions in sandbox environment
User can purchase either one month, one year auto renewable subscription or permanent permission
App should check if the subscription is still valid every time when user open app, if not, lock all premium functions
User is able to restore the purchased plan, app should get the previous purchased type ie. one month, one year, or permanent.
After long research on the tutorials, I am still confused about the validation
I see that there are two ways to validate receipt, one is locally the other is on the server.
But I don't have a server, does that mean I can only validate it locally
Every time the auto-renewal subscription expires, the local receipt is not updated, so when I reopen the app I got a subscription expiration alert (The method I defined by my self for validation check ), when I click the restore button, the app restored successfully and receipt was updated
After 6 times manually restored and refresh the receipt (the sandbox user can only renew 6 times), when I click the restore button, the part transaction == .purchased is till called, and my app unlocks premium function, however when I reopen my app, my app alerts that the subscription is expired, which is it should.
My core problem is how can I check the validation of subscriptions with Apple every time when I open the app, I don't have a server, and I don't know why the receipt is not refreshing automatically
Here are some parts of my code, I call checkUserSubsriptionStatus() when I open the app, I am using TPInAppReceipt Library
class InAppPurchaseManager {
static var shared = InAppPurchaseManager()
init() {
}
public func getUserPurchaseType() -> PurchaseType {
if let receipt = try? InAppReceipt.localReceipt() {
var purchaseType: PurchaseType = .none
if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneMonth.productID) {
purchaseType = .oneMonth
}
if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneYear.productID) {
purchaseType = .oneYear
}
if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
purchaseType = .permanent
}
return purchaseType
} else {
print("Receipt not found")
return .none
}
}
public func restorePurchase(in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
if SKPaymentQueue.canMakePayments() {
SKPaymentQueue.default().restoreCompletedTransactions()
} else {
self.userIsNotAbleToPurchase()
}
}
public func checkUserSubsriptionStatus() {
DispatchQueue.main.async {
if let receipt = try? InAppReceipt.localReceipt() {
self.checkUserPermanentSubsriptionStatus(with: receipt)
}
}
}
private func checkUserPermanentSubsriptionStatus(with receipt: InAppReceipt) {
if let receipt = try? InAppReceipt.localReceipt() { //Check permsnent subscription
if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
print("User has permament permission")
if !AppEngine.shared.currentUser.isVip {
self.updateAfterAppPurchased(withType: .permanent)
}
} else {
self.checkUserAutoRenewableSubsrption(with: receipt)
}
}
}
private func checkUserAutoRenewableSubsrption(with receipt: InAppReceipt) {
if receipt.hasActiveAutoRenewablePurchases {
print("Subsription still valid")
if !AppEngine.shared.currentUser.isVip {
let purchaseType = InAppPurchaseManager.shared.getUserPurchaseType()
updateAfterAppPurchased(withType: purchaseType)
}
} else {
print("Subsription expired")
if AppEngine.shared.currentUser.isVip {
self.subsrptionCheckFailed()
}
}
}
private func updateAfterAppPurchased(withType purchaseType: PurchaseType) {
AppEngine.shared.currentUser.purchasedType = purchaseType
AppEngine.shared.currentUser.energy += 5
AppEngine.shared.userSetting.hasViewedEnergyUpdate = false
AppEngine.shared.saveUser()
AppEngine.shared.notifyAllUIObservers()
}
public func updateAfterEnergyPurchased() {
AppEngine.shared.currentUser.energy += 3
AppEngine.shared.saveUser()
AppEngine.shared.notifyAllUIObservers()
}
public func purchaseApp(with purchaseType: PurchaseType, in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = purchaseType.productID
SKPaymentQueue.default().add(paymentRequest)
} else {
self.userIsNotAbleToPurchase()
}
}
public func purchaseEnergy(in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
let productID = "com.crazycat.Reborn.threePointOfEnergy"
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
} else {
self.userIsNotAbleToPurchase()
}
}
}
If you do not have the possibility to use a server, you need to validate locally. Since you are already included TPInAppReceipt library, this is relatively easy.
To check if the user has an active premium product and what type it has, you can use the following code:
// Get all active purchases which are convertible to `PurchaseType`.
let premiumPurchases = receipt.activeAutoRenewableSubscriptionPurchases.filter({ PurchaseType(rawValue: $0.productIdentifier) != nil })
// It depends on how your premium access works, but if it doesn't matter what kind of premium the user has, it is enough to take one of the available active premium products.
// Note: with the possibility to share subscriptions via family sharing, the receipt can contain multiple active subscriptions.
guard let product = premiumPurchases.first else {
// User has no active premium product => lock all premium features
return
}
// To be safe you can use a "guard" or a "if let", but since we filtered for products conforming to PurchaseType, this shouldn't fail
let purchaseType = PurchaseType(rawValue: product.productIdentifier)!
// => Setup app corresponding to active premium product type
One point I notice in your code, which could lead to problems, is that you constantly add a new SKPaymentTransactionObserver. You should have one class conforming to SKPaymentTransactionObserver and add this only once on app start and not on every public call. Also, you need to remove it when you no longer need it (if you created it only once, you would do it in the deinit of your class, conforming to the observer protocol.
I assume this is the reason for point 2.
Technically, the behavior described in point 3 is correct because the method you are using asks the payment queue to restore all previously completed purchases (see here).
Apple states restoreCompletedTransactions() should only be used for the following scenarios (see here):
If you use Apple-hosted content, restoring completed transactions gives your app the transaction objects it uses to download the content.
If you need to support versions of iOS earlier than iOS 7, where the app receipt isn’t available, restore completed transactions instead.
If your app uses non-renewing subscriptions, your app is responsible for the restoration process.
For your case, it is recommended to use a SKReceiptRefreshRequest, which requests to update the current receipt.
Get the receipt every time when the app launches by calling the method in AppDelegate.
getAppReceipt(forTransaction: nil)
Now, below is the required method:
func getAppReceipt(forTransaction transaction: SKPaymentTransaction?) {
guard let receiptURL = receiptURL else { /* receiptURL is nil, it would be very weird to end up here */ return }
do {
let receipt = try Data(contentsOf: receiptURL)
receiptValidation(receiptData: receipt, transaction: transaction)
} catch {
// there is no app receipt, don't panic, ask apple to refresh it
let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
appReceiptRefreshRequest.delegate = self
appReceiptRefreshRequest.start()
// If all goes well control will land in the requestDidFinish() delegate method.
// If something bad happens control will land in didFailWithError.
}
}
Here is the method receiptValidation:
func receiptValidation(receiptData: Data?, transaction: SKPaymentTransaction?) {
guard let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) else { return }
verify_in_app_receipt(with_receipt_string: receiptString, transaction: transaction)
}
Next is the final method that verifies receipt and gets the expiry date of subscription:
func verify_in_app_receipt(with_receipt_string receiptString: String, transaction: SKPaymentTransaction?) {
let params: [String: Any] = ["receipt-data": receiptString,
"password": "USE YOUR PASSWORD GENERATED FROM ITUNES",
"exclude-old-transactions": true]
// Below are the url's used for in app receipt validation
//appIsInDevelopment ? "https://sandbox.itunes.apple.com/verifyReceipt" : "https://buy.itunes.apple.com/verifyReceipt"
super.startService(apiType: .verify_in_app_receipt, parameters: params, files: [], modelType: SubscriptionReceipt.self) { (result) in
switch result {
case .Success(let receipt):
if let receipt = receipt {
print("Receipt is: \(receipt)")
if let _ = receipt.latest_receipt, let receiptArr = receipt.latest_receipt_info {
var expiryDate: Date? = nil
for latestReceipt in receiptArr {
if let dateInMilliseconds = latestReceipt.expires_date_ms, let product_id = latestReceipt.product_id {
let date = Date(timeIntervalSince1970: dateInMilliseconds / 1000)
if date >= Date() {
// Premium is valid
}
}
}
if expiryDate == nil {
// Premium is not purchased or is expired
}
}
}
case .Error(let message):
print("Error in api is: \(message)")
}
}
}
I noticed something strange during testing. First, I Erase All Content and Settings on the simulator, and then manually delete all records in CloudKit. When I first run the app, I've noticed that over 2000 records are being deleted. I don't understand why (or even where!) they are being stored. Have I completely missed something? Below is a portion of the CloudKit method that is run as part of a check for updates.
operation.fetchDatabaseChangesCompletionBlock = { (token, more, error) in
if error != nil {
finishClosure(UIBackgroundFetchResult.failed)
} else if !zonesIDs.isEmpty {
changeToken = token
let configuration = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
configuration.previousServerChangeToken = changeZoneToken
let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zonesIDs, configurationsByRecordZoneID: [zonesIDs[0]: configuration])
fetchOperation.recordChangedBlock = { (record) in
listRecordsUpdated.append(record)
}
fetchOperation.recordWithIDWasDeletedBlock = { (recordID, recordType) in
if changeToken != nil {
listRecordsDeleted[recordID.recordName] = recordType
}
}
fetchOperation.recordZoneChangeTokensUpdatedBlock = { (zoneID, token, data) in
changeZoneToken = token
}
fetchOperation.recordZoneFetchCompletionBlock = { (zoneID, token, data, more, error) in
if error != nil {
print("Error")
} else {
changeZoneToken = token
self.updateLocalRecords(listRecordsUpdated: listRecordsUpdated)
self.deleteLocalRecords(listRecordsDeleted: listRecordsDeleted)
listRecordsUpdated.removeAll()
listRecordsDeleted.removeAll()
}
}
etc.
Delete Records
func deleteLocalRecords(listRecordsDeleted: [String : String]) {
for (recordName, recordType) in listRecordsDeleted {
let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "\(recordType)")
request.predicate = NSPredicate(format: "ckrecordname = %#", recordName)
do {
let result = try context.fetch(request)
if !result.isEmpty {
if let data = result[0] as? NSManagedObject {
context.delete(data)
}
}
}
catch {
print("Error fetching")
}
}
coreData.saveContext()
}
It sounds like you're deleting the records through the dashboard but are keeping the record zone. In that case the deletes are part of the history of the zone, and when you first sync with the zone it basically rewinds through all that history for the zone, which at the end includes deletes for all your records.
Keep in mind this also applies to zones - so a zone delete for example stays in the history and can lead to some unwanted situations if you don't account for that. I ran into the situation where I was deleting the zone on one device, but the other one would then try to sync, find no zone and create it again.
I created a basic Google Places app that lets users check-in to a location. When a user tries to check in, I loop through the list of likelihood places to verify that the user is actually at the location in the app. However, when I try to escape the loop after confirming the location is correct, my function still ends up going to my "else" situation (an error message that asks the user to please check in to the correct location).
The following function gets called in viewWillAppear:
func checkIn(handleComplete:#escaping (()->())){
guard let currentUserID = User.current?.key else {return}
// Specify the place data types to return.
let fields: GMSPlaceField = GMSPlaceField(rawValue: UInt(GMSPlaceField.name.rawValue) |
UInt(GMSPlaceField.placeID.rawValue))!
placesClient.findPlaceLikelihoodsFromCurrentLocation(withPlaceFields: fields, callback: {
(placeLikelihoodList: Array<GMSPlaceLikelihood>?, error: Error?) in
if let error = error {
print("An error occurred: \(error.localizedDescription)")
return
}
if let placeLikelihoodList = placeLikelihoodList {
for likelihood in placeLikelihoodList {
let place = likelihood.place
if likelihood.likelihood >= 0.75 && place.placeID! == self.hangoutID {
let place = likelihood.place
print("Current Place name \(String(describing: place.name!)) at likelihood \(likelihood.likelihood)")
print("Current PlaceID \(String(describing: place.placeID!))")
self.delta = 0.0
// update checkin
DispatchQueue.main.async {
let hangoutRef = self.db.collection("users").document(currentUserID).collection("hangout").document(self.hangoutID).updateData([
"lastCheckin": Date()
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
handleComplete()
}
}
self.presentDismissableAlert(title: "", message: "Please check in to the hangout to join this chat", button: "OK", dismissed: { (UIAlertAction) in
self.performSegue(withIdentifier: "unwindSegueToChats", sender: self)
})
}
})
}
If the correct conditions are met, the code will land on the handleComplete() line but then it will still execute the dismissableAlert underneath and segue the user out of the room. How can I fix the flow so that the app will cycle through the list of likely Places and stop the function on handleComplete if the correct condition is met, or else then proceed to the error message if the correct conditions are not met (user is not at the correct Place)?
Thanks
When I call my notification function, it will trigger at the original time. However, I never get another notification after that. I noticed this post was dealing with a similar issue. I changed my code to mimic the solution. I still didn't receive another notification.
Here is the code:
static func scheduleNotification(hour:Int, minutes:Int, completion: #escaping (Bool) -> ()) {
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Hello "
notificationContent.subtitle = "Now might be a good time for a check in"
var dateInfo = DateComponents()
dateInfo.hour = hour
dateInfo.minute = minutes
let trigger = UNCalendarNotificationTrigger(dateMatching: dateInfo, repeats: true)
print(trigger.nextTriggerDate()!)
let request = UNNotificationRequest(identifier: Notifications.notificationIdentifier, content: notificationContent, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: { error in
if error != nil {
print("\(error)")
completion(false)
} else {
completion(true)
}
})
}
The intention is to have it trigger daily at 4:00pm.
scheduleNotification(hour: 16, minutes:00, completion: {success in
if success {
print("Successfully scheduled notification")
} else {
print("Could not schedule notification")
}
})
I printed the result of the nextTriggerDate function to see if it would return the original trigger date or the date of the following day. In this case, the date is 4-18-2018.
The trigger that gets returned is
2018-04-18 23:00:00 +0000
I'm also not sure why the date returned adds 7 hours to my intended trigger time. The notification still fires at 4:00. Thoughts?
short answer
add
dateInfo.timeZone = TimeZone(abbreviation: "UTC")
and it will work like expected.
explanation
I tried myself with hour 16 and minutes 0.
I got 14:00 as result. The reason is because I'm living in Germany and Germany (GMT+2, summer time) has at the moment +2 hours distance to UTC.
The defaults timezone of my device is GMT+2. I add 16 hours and DateComponent handle this date as GMT+2 but converts that value to UTC.
So 16 (input) - 2 (GMT) = 14 UTC.
I'm always try to work with UTC dates... everything else causes me a headache.
I have a little problem grasping HealthKit. I want to get heart rate from HealthKit with specific time.
I have done this in the past (until I noticed that I couldn't fetch data when the phone was locked)
func retrieveMostRecentHeartRateSample(completionHandler: (sample: HKQuantitySample) -> Void) {
let sampleType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
let predicate = HKQuery.predicateForSamplesWithStartDate(NSDate.distantPast() as! NSDate, endDate: NSDate(), options: HKQueryOptions.None)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: 1, sortDescriptors: [sortDescriptor])
{ (query, results, error) in
if error != nil {
println("An error has occured with the following description: \(error.localizedDescription)")
} else {
let mostRecentSample = results[0] as! HKQuantitySample
completionHandler(sample: mostRecentSample)
}
}
healthKitStore.executeQuery(query)
}
var observeQuery: HKObserverQuery!
func startObservingForHeartRateSamples() {
println("startObservingForHeartRateSamples")
let sampleType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
if observeQuery != nil {
healthKitStore.stopQuery(observeQuery)
}
observeQuery = HKObserverQuery(sampleType: sampleType, predicate: nil) {
(query, completionHandler, error) in
if error != nil {
println("An error has occured with the following description: \(error.localizedDescription)")
} else {
self.retrieveMostRecentHeartRateSample {
(sample) in
dispatch_async(dispatch_get_main_queue()) {
let result = sample
let quantity = result.quantity
let count = quantity.doubleValueForUnit(HKUnit(fromString: "count/min"))
println("sample: \(count)")
heartChartDelegate?.updateChartWith(count)
}
}
}
}
healthKitStore.executeQuery(observeQuery)
}
This code will fetch the latest sample every time it is a change in HealthKit. But as I said earlier, it won't update when the phone is locked. I tried using:
self.healthKitStore.enableBackgroundDeliveryForType(HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate), frequency: HKUpdateFrequency.Immediate) { (success, error) in
if success{
println("success")
} else {
println("fail")
}
}
But this didn't work and as I found out there was a bug that Apple said it wasn't working as they wanted. Guess it is some security-thing.
But then I thought, maybe I can request samples between a startTime and endTime. For example i have EndTime(2015-05-31 10:34:45 +0000) and StartTime(2015-05-31 10:34:35 +0000).
So my question is how can I get heart rate samples between these two times.
I guess I must do it in the
HKQuery.predicateForSamplesWithStartDate(myStartTime, endDate: myEndTime, options: HKQueryOptions.None)
But when I tried it didn't find anything. Maybe I got this all wrong...
I am using a heart rate monitor on my chest and I know that I get some values in healthKit within the start and end time.
Edit:
Ok I tried it and it is working some times, not always. Someone has an idea?
func fetchHeartRates(endTime: NSDate, startTime: NSDate){
let sampleType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
let predicate = HKQuery.predicateForSamplesWithStartDate(startTime, endDate: endTime, options: HKQueryOptions.None)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: 100, sortDescriptors: [sortDescriptor])
{ (query, results, error) in
if error != nil {
println("An error has occured with the following description: \(error.localizedDescription)")
} else {
for r in results{
let result = r as! HKQuantitySample
let quantity = result.quantity
let count = quantity.doubleValueForUnit(HKUnit(fromString: "count/min"))
println("sample: \(count) : \(result)")
}
}
}
healthKitStore.executeQuery(query)
}
Edit 2:
It was working but I couldn't call it the way I did. So I fetched it a couple of seconds later and it worked fine :)
...But as I said earlier, it won't update when the phone is locked...Guess it is some security-thing.
You are correct.
From HealthKit Framework Reference:
Because the HealthKit store is encrypted, your app cannot read data from the store when the phone is locked. This means your app may not be able to access the store when it is launched in the background. However, apps can still write data to the store, even when the phone is locked. The store temporarily caches the data and saves it to the encrypted store as soon as the phone is unlocked.
If you want your app to be alerted when there are new results, you should review Managing Background Delivery:
enableBackgroundDeliveryForType:frequency:withCompletion:
Call this method to register your app for background updates. HealthKit wakes your app whenever new samples of the specified type are saved to the store. Your app is called at most once per time period defined by the specified frequency.