It is important to check are In-App Purchases enabled to block UI properly, RayWenderlich blog says:
Apple requires that you handle this situation gracefully; not doing so will likely result in an app rejection.
When you disable In-App Purchase, the Restrictions SKPaymentQueue.canMakePayments() should return false but it always returns true no matter what. I tried with 2 different projects, including, this one from RayWenderlich.
I tested this only with iOS 9.
How to recognise that In-App Purchases disabled with parental restrictions?
Update.
Somebody requested to share my code. I don't think it is necessary, the code is obvious and doesn't have errors. I can reproduce this problems with the Ray's project too.
// This function is called in from viewDidLoad()
// And after SKProduct is updated.
func addTextFromProduct(p: SKProduct) {
if let title = p.localizedTitle as String? {
self.navigationBar.topItem?.title = title
}
if let description = p.localizedDescription as String? {
if dailyLimit {
self.informationLabel.text? = "\(waitingTime)\(description)"
} else {
self.informationLabel.text? = "\(description)"
}
if SKPaymentQueue.canMakePayments() {
unblockButtons()
}
} else {
self.informationLabel.text? = "\(waitingTime)\(description)\n\nIn-App Purchase is unavailable at this moment."
blockButtons()
}
if SKPaymentQueue.canMakePayments() {
self.priceFormatter.locale = p.priceLocale
let localPrice: String! = self.priceFormatter.stringFromNumber(p.price)
let label = "\(localPrice)"
self.buyButton.setTitle(label, forState: UIControlState.Normal)
} else {
blockButtons()
buyButton.setTitle("Not Available", forState: UIControlState.Disabled)
}
}
You need to check in your viewDidLoad method if the user has enabled IAP.
if(SKPaymentQueue.canMakePayments()) {
print("IAP is enabled, loading")
// Your Products
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
}
else {
print("IAP not allowed")
//Do something like show an alert, the user has not allowed in app purchases
}
Related
The test ad works fine when I don't include the in app purchase. I click on the cell, it takes me to the next view controller, and the ad pops up. However, when I include in-app purchases the ad doesn't show up even if the user didn't pay to remove ads.
The ad shows up with this function:
func showAd() {
self.interstitial = createInterstitialAd()
}
But when I add this, the ad doesn't show even if the user hasn't paid to remove ads.
func showAd() {
if let purchased = UserDefaults.standard.value(forKey: "payment") as? Bool{
if purchased == true{
interstitial = nil
print("there is no ad!!!!")
}else{
self.interstitial = createInterstitialAd()
print("there is an ad!!!")
}
}
Your problem is that initially there will be no value in UserDefaults for the payment key. This will cause the outer if statement to fall through, resulting in no ad.
You can make your code simpler by using bool(forKey:) - This will return false where the key is not present in UserDefaults rather than nil:
func showAd() {
if UserDefaults.standard.bool(forKey: "payment") {
interstitial = nil
print("there is no ad!!!!")
} else {
self.interstitial = createInterstitialAd()
print("there is an ad!!!")
}
}
I implemented apple pay with stripe in ios using swift. So when the time of testing a program it is runs fine with simulators but not in real device. To test the apple pay with real device it doesn't show the payment sheet to process the transaction. My doubt is to test with apple pay with real device can we add test cards to wallet or have the sandbox account ? is required adding test cards to wallet when using stripe gate way? Could you please give me a clarity?
This is my code :
import UIKit
import PassKit
import Stripe
class ViewController: UIViewController {
let request = PKPaymentRequest()
var paymentStatus = PKPaymentAuthorizationStatus.failure
var paymentController: PKPaymentAuthorizationController?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func applepay(_ sender: Any) {
let merchantIdentifier = "merchant.Appplepay"
let paymentNetworks = [PKPaymentNetwork.masterCard, PKPaymentNetwork.visa,PKPaymentNetwork.amex,PKPaymentNetwork.discover,PKPaymentNetwork.interac]
if PKPaymentAuthorizationViewController.canMakePayments(usingNetworks: paymentNetworks)
{
request.merchantIdentifier = "merchant.Appplepay"
request.countryCode = "US"
request.currencyCode = "USD"
request.supportedNetworks = paymentNetworks
request.merchantCapabilities = .capability3DS
request.requiredShippingAddressFields = [.phone, .email,.postalAddress,.all]
request.paymentSummaryItems = [PKPaymentSummaryItem(label: "Fancy Hat", amount: 50.00),
// The final line should represent your company;
// it'll be prepended with the word "Pay" (i.e. "Pay iHats, Inc $50")
PKPaymentSummaryItem(label: "iHats, Inc", amount: 50.00),]
} else {
// Traditional checkout flow
}
func applePaySupported() -> Bool {
return PKPaymentAuthorizationViewController.canMakePayments() && PKPaymentAuthorizationViewController.canMakePayments(usingNetworks: [.amex, .visa, .masterCard])
}
// Setup payment authorization view controller
if Stripe.canSubmitPaymentRequest(request) {
// Setup payment authorization view controller
let paymentAuthorizationViewController = PKPaymentAuthorizationViewController(paymentRequest: request)
paymentAuthorizationViewController?.delegate = self
// Present payment authorization view controller
self.present((paymentAuthorizationViewController)!, animated: true,completion: nil)
}
else {
// There is a problem with your Apple Pay configuration
print("Failed to present PaymentSheet");
}
}
}
extension ViewController : PKPaymentAuthorizationViewControllerDelegate {
func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, completion: #escaping (PKPaymentAuthorizationStatus) -> Void) {
if payment.shippingContact?.emailAddress == nil || payment.shippingContact?.phoneNumber == nil {
paymentStatus = .invalidShippingContact
} else {
STPAPIClient.shared().createToken(with: payment) { (token: STPToken?, error)-> Void in
print("Stripe token is \(String(describing: token!))")
//self.paymentStatus = .success
completion(.success)
}
}
}
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
controller.dismiss(animated: true, completion: nil)
}
}
To answer your questions about testing on a real device: You can only add real cards to your wallet to use with Apple Pay, however if you use a test stripe api key, that is detected by stripe and your real card will not be charged
Not sure why the payment sheet isn't displaying on the real device without seeing some code, but you would want to make sure that the device is set up for apple pay, mainly that it has cards added to the wallet (that you support)
Not all countries support Apple Pay so there isn't always an option to add a card to the wallet, this would mean that the Apple Pay sheet wouldn't display due to if Stripe.canSubmitPaymentRequest(request) returning false.
I am developing IAP functionality of non consumable (removing Ads).
I've created an helper for all the operation and everything works fine.
When a user buy/restore the purchase i set this:
let save = UserDefaults.standard
save.set(true, forKey: "Purchase")
save.synchronize()
This works fine as long as the user never delete the app.
I was wondering...
Is there a way to know if the user (after deleting and reinstalling the app) already had made a purchase? so to change the title of the button from "purchase" to "restore"?
For every app, Apple requires that you include a "Restore Purchases" button. This is exactly for the problem you are facing. It will recreate the SKPaymentTransaction for every previously purchased IAP with a "restored" state for the iCloud account that is currently signed in at the App Store, and put on the SKPaymentQueue. Read more...
Maybe it would make sense to have a receipt validation in your code, when the user is interacting in your app. (Apple docs: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html)
Just have a look at this small example:
Init your product requests:
if SKPaymentQueue.canMakePayments() {
print("starting IAPS")
let productIdentifiers = Set(["YOUR_IAPP_IDENTIFIER#1", "YOUR_IAPP_IDENTIFIER#1"])
let request = SKProductsRequest(productIdentifiers: productIdentifiers as Set<String>)
request.delegate = self
request.start()
} else {
print("please enable IAPS")
}
Set your products in your app and do the receipt validation
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("product request")
let myProduct = response.products
for product in myProduct {
if product.productIdentifier == "YOUR_IAPP_IDENTIFIER#1" {
self.productYear = product
} else if product.productIdentifier == "YOUR_IAPP_IDENTIFIER#2" {
self.productMonth = product
}
print("product added")
print(product.productIdentifier)
print(product.localizedTitle)
print(product.localizedDescription)
print(product.price)
self.receiptValidation()
}
Validation example
func receiptValidation() {
self.logger.log.debug("In receipt validation")
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString(options: [])
let dict = ["receipt-data" : receiptString, "password" : "YOUR_PASSWORD"] as [String : Any]
// Getting the receipt data from the JSON file and other logic
// ...
// ...
}
}
}
Please let me know, if you need more clarification on this. Furthermore you can simply restore it with the PaymentQueue (Apple Doc: https://developer.apple.com/documentation/storekit/skpaymentqueue)
If you provide a login system you will know easily , otherwise
no other way you should put buy and restore buttons for payment Or you store this key-value in
device key-chain and read them in first setup ,but you should know from ios 10.3 If you deleted the app the associated key-chain items will be deleted
Note: Apple states that all apps that have IAP should put a restore payment functionality so, If there is a workaround app will be rejected by Apple
I implement IAP using Swift to unlock my game stage. It works well on IPv4. So I submit this binary for review and get rejected by Apple when they tested on IPv6 network.
Reject binary reason:
We discovered one or more bugs in your app when reviewed on iPhone running iOS 9.3.2 on Wi-Fi connected to an IPv6 network.
Specifically, after we purchase the In App Purchase, the level does not unlock.
I put break point for every case but the program didn't go inside any 1 of them when I running on IPv6 network.
Here my code for do purchase:
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("Received Payment Transaction Response from Apple");
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)
self.levelButtonHalloween.enabled = true
lockImage.removeFromSuperview()
Overlay.removeFromSuperview()
userSettingDefaults.setBool(true, forKey: "enableHalloween")
userSettingDefaults.synchronize()
DesertOver50 = userSettingDefaults.boolForKey("enableHalloween")
buyBottom.removeFromSuperview()
backgroundImage.removeFromParent()
backgroundImage = SKSpriteNode(imageNamed: "StageSelect_Background2")
backgroundImage.size = self.frame.size
backgroundImage.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
backgroundImage.anchorPoint = CGPointMake(0.5, 0.5)
backgroundImage.zPosition = 0
addChild(backgroundImage)
break;
case .Failed:
print("Purchase Failed");
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
break;
case .Restored:
print("Transaction restored")
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
self.levelButtonHalloween.enabled = true
lockImage.removeFromSuperview()
Overlay.removeFromSuperview()
userSettingDefaults.setBool(true, forKey: "enableHalloween")
userSettingDefaults.synchronize()
DesertOver50 = userSettingDefaults.boolForKey("enableHalloween")
buyBottom.removeFromSuperview()
backgroundImage.removeFromParent()
backgroundImage = SKSpriteNode(imageNamed: "StageSelect_Background2")
backgroundImage.size = self.frame.size
backgroundImage.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
backgroundImage.anchorPoint = CGPointMake(0.5, 0.5)
backgroundImage.zPosition = 0
addChild(backgroundImage)
default:
break;
}
}
}
SKPaymentQueue.defaultQueue().removeTransactionObserver(self)
}
I also implement button for restore purchase and it work well for both ipv4 and ipv6.
Here the code for restore purchase:
func restorePurchaseButtonAction(){
button_Clicked()
if (DesertOver50 == false){
if (SKPaymentQueue.canMakePayments()) {
// Enable SKPayment as soon as possible during viewdidload
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
}else{
let alert = UIAlertController(title: "It's already unlocked ¬_¬", message: "Your have already unlocked or purchased the item(s)", preferredStyle: .ActionSheet)
let ok = UIAlertAction(title: "OK", style: .Cancel) { action -> Void in
}
alert.addAction(ok)
alert.popoverPresentationController?.sourceView = view
alert.popoverPresentationController?.sourceRect = self.frame
self.view?.window?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}
}
Anyone have experience this before? Any different implement IAP on IPv4 vs IPv6? What should I do next?
Do you check your settings in ItunesConnect whether in-app purchase is registered correctly? Remember to put
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
in viewDidLoad. This issue has been plaguing me too.
Thank you for your advice. My ituneConnect seem ok.
that SKPaymentQueue.defaultQueue().addTransactionObserver(self) is root cause
of my problem.
Scenario 1:
Item completely purchase but stage not unlock =>Fail
func buyNonConsumable() {
button_Clicked()
print("About to fetch the products");
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
// We check that we are allow to make the purchase.
if (SKPaymentQueue.canMakePayments()) {
let productID:NSSet = NSSet(object: self.product_id!);
let productsRequest:SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>);
productsRequest.delegate = self;
productsRequest.start();
print("Fething Products");
} else {
print("can't make purchases");
}
}
Scenario 2: Item auto unlock itself when app start without need to login =>Fail
override func didMoveToView(view: SKView) {
// In-App button and function call
if(DesertOver50 == false) {
product_id = "xxxxxxxx";
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
addBotton();
Scenario 3: Working properly
func buyProduct(product: SKProduct) {
print("Sending the Payment Request to Apple");
let payment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addPayment(payment);
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
}
From what I understand scenario 2 should best solution but it fail.
Since code now working perfectly, I assume scenario 3 is the solution.
I keep getting this invalid product identifiers. Is my code fine? what could be the problem? Ive written the code to print out my in app purchases but it keeps going to this if statement:
if response.invalidProductIdentifiers.count != 0 {
print(response)
print(response.invalidProductIdentifiers.description)
}
In my in-app purchase page on Itunes connect I have the exact ID for the product copied into xcode yet it still does not work.
My full code:
//global variables
var productsArray: Array<SKProduct!> = []
var productIdentifiers = Set<NSObject>()
func requestProductInfo() {
if SKPaymentQueue.canMakePayments() {
//let productIdentifiers = NSSet(array: productIDs)
let productRequest = SKProductsRequest(productIdentifiers: self.productIdentifiers as! Set<String>)
productRequest.delegate = self
productRequest.start()
}
else {
print("Cannot perform In App Purchases.")
}
}
func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
if response.invalidProductIdentifiers.count != 0 {
print("here")
print(response)
print(response.invalidProductIdentifiers.description)
print("here2")
}
if response.products.count != 0 {
for product in response.products {
productsArray.append(product)
print(productsArray)
}
}
else {
print("There are no products.")
}
}
Your code is good, I guess you have problem with app bundle ID or Setting provisional profiles.
You must have to set same BundleId and latest provisional profile(Certificates) with enabled of "In-App purchase".