SwiftyStoreKit IAP Purchasing but not remembering it's settings after launch - ios

I am trying to integrate a very basic single IAP in my game, here I am calling from my GameScene
let alert = UIAlertController(title: "Upgrade", message: "Would you like to remove ads?", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Remove Ads", style: .default, handler: { action in
print("Pressed Remove Ads")
GameViewController().buytheIAP()
}))
alert.addAction(UIAlertAction(title: "Restore Purchases", style: .default, handler: { action in
print("Pressed Restore")
GameViewController().restoretheIAP()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { action in
print("Pressed Cancel")
}))
view?.window?.rootViewController?.present(alert, animated: true, completion: nil)
}
Those methods are called correctly, and refer to these inside GameViewController.swift;
func buytheIAP(){
iAPHelper.purchase()
print("OK Lets upgrade")
}
func restoretheIAP(){
iAPHelper.restorePurchase()
print("OK Lets restore")
}
func restoreDidSucceed() {
UserDefaults.setValue(true, forKey: iAPHelper.productID)
//this should have something like hide banner etc.
bannerView.isHidden = true
}
func purchaseDidSucceed() {
UserDefaults.setValue(true, forKey: iAPHelper.productID)
//as above this should have something like hide banner etc.
bannerView.isHidden = true
print("Purchased upgrade ENJOYYYYYYYY")
}
func nothingToRestore() {
}
func paymentCancelled() {
}
The test IAP goes through, it gets the correct information from the app store, and I use my sandbox details to purchase, it goes through correctly with a successful purchase message. However, the bannerView does not hide, and more importantly, upon restarting the game again, everything is forgotten and the game thinks nothing has been purchased. I am guessing it has to be some sort of check that is missing.
I have this in my viewDidLoad
if userDefaults.bool(forKey: iAPHelper.productID) {
bannerView.isHidden = true
print("It is purchased, so DO NOT show the ads")
} else{
bannerView.adSize = getAdaptiveSize()
bannerView.adUnitID = "ca-app-pub-3940256099942544/2934735716"
bannerView.delegate = self
bannerView.rootViewController = self
bannerView.load(GADRequest())
addBannerViewToView(bannerView)
print("Not purchased, so show the ads")
}
And it ALWAYS shows print("Not purchased, so show the ads")
The IAPHelper file, for purchasing is;
func purchase() {
SwiftyStoreKit.purchaseProduct(productID, quantity: 1, atomically: true) { [self] result in
switch result {
case .success:
delegate?.purchaseDidSucceed()
print("OK It's purchased")
case .error(let error):
switch error.code {
case .unknown: print("Unknown error. Please contact support")
case .clientInvalid: print("Not allowed to make the payment")
case .paymentCancelled:
delegate?.paymentCancelled()
case .paymentInvalid: print("The purchase identifier was invalid")
case .paymentNotAllowed: print("The device is not allowed to make the payment")
case .storeProductNotAvailable: print("The product is not available in the current storefront")
case .cloudServicePermissionDenied: print("Access to cloud service information is not allowed")
case .cloudServiceNetworkConnectionFailed: print("Could not connect to the network")
case .cloudServiceRevoked: print("User has revoked permission to use this cloud service")
default: print((error as NSError).localizedDescription)
}
}
}
}
}
And the log DOES show print("OK It's purchased") after the initial purchase - so I am struggling to see what is going wrong.

The IAP delegate functions are not (guaranteed to be) called on the UI/main thread, that's why your view doesn't hide.
Don't you see some iOS warning saying that you try to set a UIView property on non-main thread?
The fact that your custom purchase info was not saved in UserDefaults could be caused by killing the app prematurely from Xcode.
(UserDefaults does take of saving what's being set during normal app flow.)

OK, so I figured the answer, there was a missing line in my AppDelegate.swift section didFinishLaunchingWithOptions
UserDefaults.standard.register(defaults: ["adRemove" : false])
I renamed userDefaults.bool(forKey: iAPHelper.productID) to make it easier to use / understand. So in my original post that has been replaced by UserDefaults.standard.register(defaults: ["adRemove" : false]) instead.
You can then use this anywhere to check, such as;
if !userDefaults.bool(forKey: "adRemove") {
// Do something here
}
Hope this helps anyone in future with the same question!

Related

In-App Purchase is successful, but methods are not called

I'm testing a promoted In-App Purchase, as described here.
Here's how it works:
I construct the system URL.
I send the system URL link to my device.
I tap the link.
My app is automatically opened, and the In App Purchase payment sheet is presented.
I tap the "Purchase" button (or whatever it's called), and enter my password to complete the transaction.
The transaction appears to be successful and an alert appears that says, "You're all set. Your purchase was successful".
The problem is, some code that should run depending on the state of the transaction seems not to. I'm not sure how that's possible, but there you go.
So, the deferredTransactionHandler, handlePurchase, handleFailedPurchase, and handleRestoredPurchase methods appear not to be invoked.
class StoreObserver: NSObject, SKPaymentTransactionObserver, SKProductsRequestDelegate {
var productRequest: SKProductsRequest?
var isAuthorizedForPayments: Bool {
return SKPaymentQueue.canMakePayments()
}
var products = [SKProduct]()
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
//The user has initiated a promoted In-App Purchase directly from the app's product page.
//Show the UI:
SceneDelegate.mainData.showAppStorePromotionUI = true
//"Return true to continue the transaction in your app. Return false to defer or cancel the transaction."
return true
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing: break
case .deferred: deferredTransactionHandler(transaction)
case .purchased: handlePurchase(transaction)
case .failed: handleFailedPurchase(transaction)
case .restored: handleRestoredPurchase(transaction)
#unknown default: fatalError("unknownPaymentTransaction")
}
}
}
}
How do I know these methods aren't being called? 1) Inside these methods, I update the UI to reflect that the transaction has completed (the UI is never updated, though), and 2) Inside these methods I try to present an alert (but the alert is never presented). Here's how I'm doing that:
func handlePurchase(_ transaction: SKPaymentTransaction) {
//Update the UI
SceneDelegate.mainData.showAppStorePromotionUI = false
//Present an alert so you know this function ran
let alert = UIAlertController(title: "Alert", message: "handlePurchase()", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
}))
let topViewController = UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.rootViewController
topViewController?.present(alert, animated: true, completion: nil)
//Complete the transaction
SKPaymentQueue.default().finishTransaction(transaction)
}
Why does it seem that these functions are not being called? What am I missing?
Thank you!
Sigh. It was a stupid mistake on my part.
I'd been testing something, and had forgotten to remove a line in my SceneDelegate's sceneWillResignActive(_:) in which I removed the observer. So, every time the payment sheet was presented, the observer was removed, which messed everything up.
func sceneWillResignActive(_ scene: UIScene) {
//Removing this line solved the problem
//SKPaymentQueue.default().remove(SceneDelegate.inAppPurchases)
}

Use of unresolved identifer - IOS Swift3

I am at my wits end here, I have been working on this for days and cannont seem to resolve this issue. I am relitivly new to the coding world, so I am sure this is a rookie mistake, but I just can't afford to waste any more time on this, its been a week almost.
I am writing in XCode 8.3, Swift 3. I am following tutorials to the letter from Auth0's website. Before I entered the code I am pasting below, my app would complie, but it would not work. Auth0 said I was mising this code.
I am receiving the following error: Use of unresolved identifier 'accessToken' I have no other errors or red !'s but this will not go away. I have tried moving the code around, that doesn't work. I have tried let, else, if statements, that doesn't work. I have spent mind numbing hours googling, and still I just can't seem to grasp what I am doing wrong.
Here is the code:
import UIKit
import Lock
import Auth0
import SimpleKeychain
class HomeViewController: UIViewController {
// MARK: - IBAction
#IBAction func showLoginController(_ sender: UIButton) {
Lock
.classic()
.withOptions {
$0.oidcConformant = true
$0.scope = "openid profile"
}
.onAuth { credentials in
let keychain = A0SimpleKeychain(service: "Auth0")
guard let accessToken = credentials.accessToken else { return }
keychain.setString(accessToken, forKey: "access_token")
self.showSuccessAlert(accessToken)
}
.present(from: self)
Auth0
.authentication()
.userInfo(withAccessToken: accessToken)
.start { result in
switch(result) {
case .sucess(let profile): break
case .failure(let error): break
}
}
Auth0
.webAuth()
.scope("openid profile")
.audience("https://mycompany.auth0.com/userinfo")
.start {
switch $0 {
case .failure(let error):
// Handle the error
print("Error: \(error)")
case .success(let credentials):
// Do something with credentials e.g.: save them.
// Auth0 will automatically dismiss the hosted login page
print("Credentials: \(credentials)")
}
}
Auth0
.webAuth()
.scope("openid profile offline_access")
.start {
switch $0 {
case .failure(let error):
// Handle the error
print("Error: \(error)")
case .success(let credentials):
guard let accessToken = credentials.accessToken, let refreshToken = credentials.refreshToken else { return }
let keychain = A0SimpleKeychain(service: "Auth0")
keychain.setString(accessToken, forKey: "access_token")
keychain.setString(refreshToken, forKey: "refresh_token")
}
}
}
// MARK: - Private
fileprivate func showSuccessAlert(_ accessToken: String) {
let alert = UIAlertController(title: "Success", message: "accessToken: \(accessToken)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
Strategies for Debugging Swift Code:
Thoroughly investigate LLDB debugger to see what happened
Read through existing code (all .swift files that you wrote code in) to check for any spelling errors, etc.
Open a new Xcode project and run each .swift file to pinpoint where the error is and/or use breakpoints in your existing Xcode project
Enable zombies
Code pesticides :P
Good luck with the rest of your project!

iOS permission alert issue

I have a view that:
Creates an observer for UIApplicationDidBecomeActiveNotification with invokes a selector
Sequentially asks the user for permissions to: use the camera, location & receiving push notifications.
The view has three UIButtons with state depending on each permission state, which navigate the user to settings if permissions for anything were rejected
Tapping a button which represents a permission with rejected state navigates the user to settings
Once each alert hides, using the observer action, next alert is triggered and all button states are updated to reflect any changes
Once all permissions are granted it pushes next view with the rest of the signup/in flow.
The problem is: on some devices, when running the app from a clean state (app removed and reinstalled), permissions for location & notifications are set to rejected by default, as if the user was presented an alert that was rejected.
I couldn't pinpoint any rational issue behind this, except for leftover settings from some outdated build that don't get deleted when installing a new one. This view seems to be the only place that can possibly trigger these alerts.
Did anyone have a similar issue and can suggest anything?
I would suggest you to try to check for states of location services and notification services before asking user to use it. Since if user is going to disable these the moment you ask him for permission, he will need to go to the settings and enable it there. You should try to detect if user has disabled location/notification/camera.
For camera use:
func accessToCamera(granted: #escaping (() -> Void)) {
if UIImagePickerController.isSourceTypeAvailable(.camera) {
let status = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeAudio)
if status == .authorized {
granted()
} else if status == .denied {
self.cameraPermissionAlert()
} else if status == .notDetermined {
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo, completionHandler: { (accessAllowed) in
if accessAllowed {
granted()
} else {
self.cameraPermissionAlert()
}
})
} else if status == .restricted {
self.cameraPermissionAlert()
}
} else {
print("Camera not available on this device")
}
}
func cameraPermissionAlert() {
let alert = UIAlertController(title: "Access to camera not available", message: "Please enable access to camera in order to use this feature", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Settings", style: .default, handler: { (action) in
if let url = URL(string: UIApplicationOpenSettingsURLString) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
if let top = UIApplication.topViewController() { // This is extension to UIApplication that finds top view controller and displays it
top.present(alert, animated: true, completion: nil)
}
}
For remote notifications you can use something like this:
Determine on iPhone if user has enabled push notifications
And for location services:
Check if location services are enabled
In both of these cases you can detect if this is disabled or not by user and present user with alert controller that has open settings functionality.

Restore Purchase : Non-Consumable

I have completed a small app where I have a non-consumable purchase option. It is on the App Store.
The purchase of the product runs OK. It's my Restore Purchase function that seems to do nothing.
I have added this code for the Restore Purchase #IBAction:
#IBAction func restorePurchases(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
But nothing happens when I hit the restore purchase button.
I think I have to add a function that checks if the restore was successful or not. Am planning to amend code to the following:
#IBAction func restorePurchases(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
switch trans.transactionState {
case .Restored:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as SKPaymentTransaction)
var alert = UIAlertView(title: "Thank You", message: "Your purchase(s) were restored.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
break;
case .Failed:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as SKPaymentTransaction)
var alert = UIAlertView(title: "Sorry", message: "Your purchase(s) could not be restored.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
break;
default:
break;
}
}
}
Will this do the trick?
I have been through every thread in relation to effecting Restore Purchase transactions, and my research has led me to the above. So I don't think this is a duplicate of a question, but perhaps may clarify how to successfully restore purchases for others facing my similar situation.
Your codes looks pretty fine for the most part, although some parts seem to be from older tutorials . There is some changes you should make, one of them is that you need to call your unlockProduct function again.
This is the code I use (Swift 3).
/// Updated transactions
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
// Transaction is being added to the server queue.
case .purchased:
// Transaction is in queue, user has been charged. Client should complete the transaction.
defer {
queue.finishTransaction(transaction)
}
let productIdentifier = transaction.payment.productIdentifier
unlockProduct(withIdentifier: productIdentifier)
case .failed:
// Transaction was cancelled or failed before being added to the server queue.
defer {
queue.finishTransaction(transaction)
}
let errorCode = (transaction.error as? SKError)?.code
if errorCode == .paymentCancelled {
print("Transaction failed - user cancelled payment")
} else if errorCode == .paymentNotAllowed { // Will show alert automatically
print("Transaction failed - payments are not allowed")
} else {
print("Transaction failed - other error")
// Show alert with localised error description
}
case .restored:
// Transaction was restored from user's purchase history. Client should complete the transaction.
defer {
queue.finishTransaction(transaction)
}
if let productIdentifier = transaction.original?.payment.productIdentifier {
unlockProduct(withIdentifier: productIdentifier)
}
case .deferred:
// The transaction is in the queue, but its final status is pending external action
// e.g family member approval (FamilySharing).
// DO NOT freeze up app. Treate as if transaction has not started yet.
}
}
}
Than use the delegate methods to show the restore alert
/// Restore finished
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
guard queue.transactions.count != 0 else {
// showAlert that nothing restored
return
}
// show restore successful alert
}
/// Restore failed
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: NSError) {
/// handle the restore error if you need to.
}
Unlock product is just a method I am sure you already have too.
func unlockProduct(withIdentifier productIdentifier: String) {
switch productIdentifier {
/// unlock product for correct ID
}
}
As a side note, you should move this line
SKPaymentQueue.default().add(self)
out of your restore and buy function and put it in viewDidLoad.
Apple recommends you add the transaction observer as soon as your app launches and only remove it when your app is closed. A lot of tutorials unfortunately dont teach you this correctly. This way you unsure that any incomplete transactions e.g due to network error, will always resume correctly.
https://developer.apple.com/library/content/technotes/tn2387/_index.html
In my real projects my code for IAPs is in a Singleton class so I would actually using delegation to forward the unlockProduct method to my class that handles gameData. I can than also make sure the observer is added at app launch.
Hope this helps
Took me a while to suss out, but the reason my StoreKit was not updatingTransactions and restoring the purchase was because of a broken Configuration setting in my app's Scheme. When I set that to None, it worked!
In Xcode I went into Edit>Scheme (image1) clicked on the Run>Options tab and selected None for StoreKit Configuration (image2). I also went on my physical device, and logged out of my personal Apple purchase account (Settings >Your Name/Pic at the Top > Media & Purchases > Sign Out) (image3). And finally, this step might not be critical, but I logged into my test sandbox account on my device at the bottom of the Settings>App Store menu(image4 and image5). That is an account that I setup in developer.apple.com under test users.

Warning: Attempt to present <UIAlertController: x> on <x.x:x> whose view is not in the window hierarchy

I am fairly new to Swift but I am creating a app that requires after a certain time to enter either Touch-ID or a PIN. I am checking a timer from AppDelegate.swift to see if it has expired and if it has expired I am making a call to my "BaseTableViewController" which holds my function authenticateUser. Again I am calling this from my AppDelegate.swift file by creating an instance of BaseTableViewController var baseTableVC = BaseTableViewController() and making a call if timer expired to self.baseTableVC.authenticateUser().
Anyways I am getting: Warning: Attempt to present <UIAlertController: 0x7fed5ae1dcf0> on <admin.BaseViewController: 0x7fed5ad279d0> whose view is not in the window hierarchy!
Thank you in advance for you help!
func showPasswordAlert(){
let alertController = UIAlertController(title: "Touch ID Password", message: "Please enter your password", preferredStyle: .Alert)
let defaultAction = UIAlertAction(title: "OK", style: .Cancel) {(action) -> Void in
if let textField = alertController.textFields?.first as UITextField?{
if textField.text == "hello" {
print("Authentication successfull!")
}
else{
self.showPasswordAlert()
}
}
}
alertController.addAction(defaultAction)
alertController.addTextFieldWithConfigurationHandler{(textField) -> Void in
textField.placeholder = "Password"
textField.secureTextEntry = true
}
presentViewController(alertController, animated: true, completion: nil)
}
func authenticateUser(){
let context = LAContext()
var error: NSError?
let reasonString = "Authentication is required for Admin!"
context.localizedFallbackTitle = "Enter your PIN Code"
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error){
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: reasonString, reply: {(success, policyError) ->Void in
if success{
print("Authentication successful!")
}
else{
switch policyError!.code{
case LAError.SystemCancel.rawValue:
print("Authentication was cancelled by the system!")
case LAError.UserCancel.rawValue:
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.showPasswordAlert()
})
print("Authentication was cancelled by the user!")
case LAError.UserFallback.rawValue:
print("User selected to enter password.")
NSOperationQueue.mainQueue().addOperationWithBlock({() -> Void in
self.showPasswordAlert()
})
default:
print("Authentication failed!")
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.showPasswordAlert()
})
}
}
})
}
else{
print(error?.localizedDescription)
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.showPasswordAlert()
})
}
var baseTableVC = BaseTableViewController()
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
let logInStatus = NSUserDefaults.standardUserDefaults()
let currentTime = NSDate().timeIntervalSince1970
let roundCurrentTime = (round(currentTime))
// Pin expire limit
let pinExpLimit: Double = 30
// Set the exact time of expire for pin
let pinExpDate = (currentTime + pinExpLimit)
let newPinExpDate = (round(pinExpDate))
if (logInStatus.doubleForKey("expPinTime") <= roundCurrentTime) {
self.baseTableVC.authenticateUser()
print("AppDelegate Pin Exp Time")
print(logInStatus.doubleForKey("expPinTime"))
//print(newPinExpDate)
print("AppDelegate Current Time")
print(roundCurrentTime)
logInStatus.setDouble(newPinExpDate, forKey: "expPinTime")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
I suspect you simply create an instance of BaseTableViewController but you don't add its view to the view hierarchy before presenting the UIAlertController's instance.
If self.baseTableVC is the root view controller of your app, then a call like this
baseTableVC.presentViewController(instanceOfUIAlertController, animated: true, completion: yourCompletionBlock)
should work from within the AppDelegate.
If self.baseTableVC is not the root view controller, then or you make sure to invoke the previous command on the root VC of your app
window.rootViewController.presentViewController(instanceOfUIAlertController, animated: true, completion: yourCompletionBlock)
or make sure you embed the view of self.baseTableVC in the view hierarchy and then call
baseTableVC.presentViewController(instanceOfUIAlertController, animated: true, completion: yourCompletionBlock)
As a side note, if your alert must be displayed from anywhere in the app, then your approach is ok. If instead your alert must be displayed only from a specific screen, I would remove the timer logic from the app delegate and move it inside the presenting view controller. This would keep your app delegate clean from unnecessary code and would confine the control logic in the right place: the presenting view controller
You can't create instance of view controller just by calling default constructor, use storyboard. Correct me if I'm wrong

Resources