Apple has just released Family Sharing for in-app purchases, I'm trying to add this to my apps with auto-renewable subscriptions without luck.
From WWDC2020 video they say everything should be working like a magic without code changes but not for me I guess. Even after restoring purchase on family member device nothing happens.
Do I need any additional setup just to make it work?
I have restore / purchase logic Implemented in the app like this:
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
promotedPayment = nil
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as NSError? {
if transactionError.code != SKError.paymentCancelled.rawValue {
let failedString = "Transaction Error: \(String(describing: transaction.error?.localizedDescription ?? ""))"
print(failedString)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.failedNotification), object: failedString)
}
}
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.failedNotification), object: nil)
SKPaymentQueue.default().finishTransaction(transaction)
promotedPayment = nil
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
UserDefaults.standard.synchronize()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.purchaseNotification), object: identifier)
}
Related
I followed this guide to set up an in-app purchase in my app. I've done everything and set it up in App Store Connect. My problem is the actual purchase at the moment is simply a boolean in UserDefaults for ArePagesUnlocked = true But I am having trouble on where I actually define this in the product. I understand how to set up and manage the purchases and restores but where do I actually define the product as being an instruction to change the value of ArePagesUnlocked from false to true, On the Buy Button Action? I tried that but if there was no connection then the user has still unlocked it. Do I have to build this seperately and send it? I have been just getting a bit confused during my research and not found a clear answer thus far so any help would be appreciated. Thank you.
EDIT: code:
public init(productIds: Set<ProductIdentifier>) {
productIdentifiers = productIds
for productIdentifier in productIds {
let purchased = UserDefaults.standard.bool(forKey: productIdentifier) // this i use to see if purchased
if purchased {
purchasedProductIdentifiers.insert(productIdentifier)
print("Previously purchased: \(productIdentifier)")
} else {
print("Not purchased: \(productIdentifier)")
}
}
super.init()
SKPaymentQueue.default().add(self)
}
}
Do I need to add a seperate UD Value from the one above? It seems It doesnt know its purchased until I restart the app, how can I make it instant?
extension IAPHelper: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(localizedDescription)")
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
}
The usual strategy is that in your paymentQueue(_:updatedTransactions:) you write into user defaults if the user purchases.
The problem that I am experiencing is that the app keeps working if you try to cancel the subscription.
Here is my code snippet:
override func viewDidLoad() {
super.viewDidLoad()
if(SKPaymentQueue.canMakePayments()) {
print("IAP is enabled, loading")
let productID: NSSet = NSSet(objects: "Seb.DiPlus.RenewingSubMonthAuto", "Seb.DiPlus.RenewingSubYearAuto")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
} else {
print("please enable IAPS")
}
}
#IBAction func subscribeMonth(_ sender: Any) {
for product in list {
let prodID = product.productIdentifier
if(prodID == "Seb.DiPlus.RenewingSubMonthAuto") {
p = product
buyProduct()
}
}
}
#IBAction func subscribeYear(_ sender: Any) {
for product in list {
let prodID = product.productIdentifier
if(prodID == "Seb.DiPlus.RenewingSubYearAuto") {
p = product
buyProduct()
}
}
}
#IBAction func restoreComplete(_ sender: UIButton) {
restorePurchases()
}
#IBAction func exit(_ sender: Any) {
_exit(0)
}
func restorePurchases() {
if SKPaymentQueue.canMakePayments(){
print("restored complete")
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
else{
print("restored faild, IAP not activ?")
}
}
func buyProduct() {
print("buy " + p.productIdentifier)
let pay = SKPayment(product: p)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(pay as SKPayment)
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("product request")
let myProduct = response.products
for product in myProduct {
print("product added")
print(product.productIdentifier)
print(product.localizedTitle)
print(product.localizedDescription)
print(product.price)
list.append(product)
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("transactions restored")
for transaction in queue.transactions {
let t: SKPaymentTransaction = transaction
let prodID = t.payment.productIdentifier as String
switch prodID {
case "Seb.DiPlus.RenewingSubMonthAuto":
print("Subscribe Month!")
if abo < 1 {
readySubscribe()
}
abo = 1
break
case "Seb.DiPlus.RenewingSubYearAuto":
print("Subscribe Year!")
if abo < 1 {
readySubscribe()
}
abo = 1
break
default:
print("IAP not found")
}
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("add payment")
for transaction: AnyObject in transactions {
let trans = transaction as! SKPaymentTransaction
print("ERROR: ", trans.error)
switch trans.transactionState {
case .purchased:
print("buy ok, unlock IAP HERE")
print(p.productIdentifier)
let prodID = p.productIdentifier
switch prodID {
case "Seb.DiPlus.RenewingSubMonthAuto":
print("Subscribe Month!!")
if abo < 1 {
readySubscribe()
}
abo = 1
break
case "Seb.DiPlus.RenewingSubYearAuto":
print("Subscribe Year!!")
if abo < 1 {
readySubscribe()
}
abo = 1
break
default:
print("IAP not found")
}
queue.finishTransaction(trans)
case .failed:
print("buy error")
alert(title: "ERROR", message: trans.error?.localizedDescription)
queue.finishTransaction(trans)
break
default:
print("Default")
break
}
}
}
func readySubscribe() {
UserDefaults.standard.setValue(checkSubscribe, forKeyPath: "subscribe")
UserDefaults.standard.synchronize()
self.performSegue(withIdentifier: "readySubscribe", sender: self)
}
func alert (title:String, message:String?){
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: { (action) in
alert.dismiss(animated: true, completion: nil)
}))
self.present(alert, animated: true, completion: nil)
}
Once I click on restore purchases, you can continue to use the app even though the subscription is no longer activated.
So the function readySubscribe() is called.
You should not activate any purchases in paymentQueueRestoreCompletedTransactionsFinished. This function is called when the restoration process is complete. You can use it to update your UI or alert the user.
The actual restoration of products should be handled in your paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) function.
Restored transactions appear with a state of .restored. You should process them exactly as you process a .purchased state.
Since you are using auto-renewing subscription IAPs you also need to check expiration dates from the receipt and be prepared for new transactions to be presented when the subscription renews. For this reason one of the first things your app should do in didFinishLaunchingWithOptions is create a payment queue observer.
I was following the Kilo Loco tutorial for store kit but I can't seem to purchase the product when its is called. This is my class:
import Foundation
import StoreKit
enum GameProducts: String {
case removeAds = "BundleID" // I have registered one but Id rather keep it private
}
class GameIAP: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
static let shared = GameIAP()
var products = [SKProduct]()
let myQueue = SKPaymentQueue.default()
override init() {
}
func getProduct() {
let products: Set = [GameProducts.removeAds.rawValue]
let request = SKProductsRequest(productIdentifiers: products)
request.delegate = self
request.start()
myQueue.add(self)
}
func purchase(product: GameProducts) {
guard let theProduct = products.filter({ $0.productIdentifier == product.rawValue}).first else
{ return }
let payment = SKPayment(product: theProduct)
myQueue.add(payment)
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
products = response.products
}
func restore() {
myQueue.restoreCompletedTransactions()
}
func complete(transaction: SKPaymentTransaction) {
SKPaymentQueue.default().finishTransaction(transaction)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "payment"), object: nil)
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction.transactionState.currentStatus(), transaction.payment.productIdentifier)
switch transaction.transactionState {
case .purchased: complete(transaction: transaction)
default: break
}
}
}
}
extension SKPaymentTransactionState {
func currentStatus() -> String {
switch self {
case .deferred: return "Deferred"
case .failed: return "Failed"
case .purchased: return "Purchased"
case .purchasing: return "Purchasing"
case .restored: return "Restored"
}
}
}
When the ViewController loads it calls getProduct() and then when I hit a button it calls the purchase function but this doesn't seem to work??
The restore function works absolutely fine but the purchase doesn't return anything...
Has anyone done this before and know where I've gone wrong or is this something on app store connect???
I'm doing my first In-App purchase and a weird behaviour is that an consumable product is being restored instead of creating new transaction.
I've followed the tuto https://www.raywenderlich.com/122144/in-app-purchase-tutorial
Which I found an elegant approach.
here is my StoreKit helper:
import StoreKit
public typealias ProductIdentifier = String
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> ()
open class IAPHelper : NSObject {
fileprivate let productIdentifiers: Set<ProductIdentifier>
fileprivate var purchasedProductIdentifiers: Set<ProductIdentifier> = Set()
fileprivate var productsRequest: SKProductsRequest?
fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
static let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
public init(productIds: Set<ProductIdentifier>) {
productIdentifiers = productIds
super.init()
SKPaymentQueue.default().add(self)
}
}
// MARK: - StoreKit API
extension IAPHelper {
public func requestProducts(_ completionHandler: #escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
public func buyProduct(_ product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
return purchasedProductIdentifiers.contains(productIdentifier)
}
public class func canMakePayments() -> Bool {
return SKPaymentQueue.canMakePayments()
}
public func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
// MARK: - SKProductsRequestDelegate
extension IAPHelper: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Loaded list of products...")
let products = response.products
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()
for p in products {
print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products.")
print("Error: \(error.localizedDescription)")
productsRequestCompletionHandler?(false, nil)
clearRequestAndHandler()
}
private func clearRequestAndHandler() {
productsRequest = nil
productsRequestCompletionHandler = nil
}
}
// MARK: - SKPaymentTransactionObserver
extension IAPHelper: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
validateReceipt()
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as? NSError {
if transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(transaction.error?.localizedDescription)")
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
UserDefaults.standard.synchronize()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: identifier)
}
}
I'm able to buy once, second it is restored automatically:
This In-App purchase has already been bought. It will be restored for
free
When I get this message, none of IAPHelper methods is called.
My iTunes showing it is a consumable:
Even uninstalling the app the purchased still being restored.
It really looks like an Apple bug as my firsts tests I could buy 2..3 times without this message.
If it is not a bug, how can I prevent this situation ?
For those stuck in this silly situation, here are some WA and solution:
queue.finishTransaction(transaction)
IMPORTANT: Finish the transaction in your paymentQueue method:
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
queue.finishTransaction(transaction)
break
case .failed:
fail(transaction: transaction)
queue.finishTransaction(transaction)
break
case .restored:
restore(transaction: transaction)
queue.finishTransaction(transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
This will avoid the situation.
If you are already stuck on it, here is the work around, just for tests purposes !
for transaction in SKPaymentQueue.default().transactions {
print(transaction)
SKPaymentQueue.default().finishTransaction(transaction)
}
You can place it on a temporary button and clear all transactions while testing in one tap.
At least it saved my life today.
I include a number of non consumable IAPs but I'm encountering issues when loading my app up as it seems to constantly prompt me to log in to my test account user I setup.
I after looking at similar questions, I believe this was because I had originally failed to add the following line to each .case:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
As you can see from my below code, this is added to each .case. To re-test I created an additional test account, but I'm now getting the exact same results (on each viewDidLoad I am being prompted to log into 2 test accounts...).
Which makes me think that SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction) is not working as expected?
Any help would be appreciated.
override func viewDidLoad() {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
}
Followed by
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("received response ok");
if product_id == "xxxxxxxxx" {
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
switch trans.transactionState {
case .Purchased:
print("Product Purchased");
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
defaults.setBool(true , forKey: "Purchased")
print("Set key ok")
break;
case .Failed:
print("Purchased Failed");
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
break;
case .Restored:
print("Already Purchased");
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
default:
break;
}
}
}
}
else {
//blah blah
}
}
I am encountering this issue due to the finishTransaction not working as expected, or have the original transaction become "stuck" ?
EDIT:
Here's where I call SKPaymentQueue.defaultQueue().addPayment(payment);
func buyProduct(product: SKProduct){
print("Sending the Payment Request to Apple");
let payment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addPayment(payment);
}
func productsRequest (request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
let count : Int = response.products.count
if (count>0) {
var validProducts = response.products
let validProduct: SKProduct = response.products[0] as SKProduct
if (validProduct.productIdentifier == self.product_id) {
print(validProduct.localizedTitle)
print(validProduct.localizedDescription)
print(validProduct.price)
buyProduct(validProduct);
} else {
print(validProduct.productIdentifier)
}
} else {
print("nothing")
}
}
This has "fixed itself". I presume that now I had added:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
each time I was re-logging in, it was finishing the previously "stuck" transactions, and removing them from the backlog?
Either way; it's resolved now.