My app displays prompts when the user clicks a specific button. I'm want to have additional prompt packages for non-consumable in-app purchase available (click the package to purchase, then if purchased you can use that same button to toggle the package on and off).
Below is all the relevant code...
import UIKit
import QuartzCore
import StoreKit
class ViewController: UIViewController, SKPaymentTransactionObserver {
let productID = "com.domain.app.purchase"
override func viewDidLoad() {
super.viewDidLoad()
SKPaymentQueue.default().add(self)
}
var packsUnlocked = false
#IBAction func selectPack1(_ sender: UIButton) {
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
print("Initiating Transaction")
} else {
print("No Purchased")
}
if packsUnlocked == false {
print("It's locked, ‘Pack 1’ not enabled")
} else {
print(“Utilize Purchase”)
//this is where you place code to use the purchased ‘Pack 1’
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions{
if transaction.transactionState == .purchased {
packsUnlocked = true
print("Transaction Successful")
} else if transaction.transactionState == .failed {
print("Transaction Failed")
}
}
}
}
However whenever I run the code and click the button all I get is the below outputs in the debugger:
"
Initiating Transaction
It's locked, 'Pack 1' not enabled
Transaction Failed
"
This is my first app and I've never set up a sandbox tester before, so I'm not sure if the code is the problem or if it's something with my App Store Connect setup.
Thank you - I've been stuck on this for wayyy too long so any help is GREATLY appreciated.
You need to test In-App Purchases on an actual device in the sandbox environment.
Here is the Apple Documentation on it, but I can guide you as well.
https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchase_transactions
Main steps are:
Create a sandbox or test user account in App Store Connect.
For iOS 12 or later — Don't sign out of the App Store; simply build and run your app from Xcode. Sandbox accounts are stored separately, and you can control your sandbox account directly on-device in Settings. (You must run on your attached device.)
Related
My new app has four in app purchases (consumables) and I submitted the first version with these IAPs. These IAP's were marked as "Ready for Review". However, the app got rejected due to another reason, and when I uploaded a new build, I couldn't select these IAP's anymore in the app details page, even though they're still "Ready for Review":
screenshot of the app details page
So after resubmitting a new version of the app for review, I got this rejection information:
We found that your in-app purchase products exhibited one or more bugs when reviewed on iPad running iOS 15.4 on Wi-Fi.
Specifically, we were not able to buy the in app purchases. The buttons did not react to taps
Next Steps
When validating receipts on your server, your server needs to be able to handle a production-signed app getting its receipts from Apple’s test environment. The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code "Sandbox receipt used in production," you should validate against the test environment instead.
I tested everything on Testflight before and all the IAP's were working fine. I know that prior to submitting an app with IAP's, these purchases have to be selected on the app details page, so I'm curious why I can't select them and if that's causing the issue.
Right when the app launches, in the AppDelegate, I fetch the products: IAPManager.shared.fetchProducts()
and the code for the IAPManager is as follows:
final class IAPManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
static let shared = IAPManager()
var products = [SKProduct]()
enum Product: String, CaseIterable {
case firstIdentifier = "com.fahrprueferCreate.tokens_1_1000"
case secondIdentifier = "com.FahrprueferCreate.tokens_5_4000"
case thirdIdentifier = "com.FahrprueferCreate.tokens_10_8000"
case fourthIdentifier = "com.FahrprueferCreate.tokens_20_15000"
var count: Int {
switch self {
case .firstIdentifier:
return 1
case .secondIdentifier:
return 5
case .thirdIdentifier:
return 10
case .fourthIdentifier:
return 20
}
}
}
private var completion: ((Int) -> Void)?
// Fetch Product Objects from Apple
func fetchProducts() {
let request = SKProductsRequest(productIdentifiers: Set(Product.allCases.compactMap({ $0.rawValue})))
request.delegate = self
request.start()
}
// Prompt a product payment transaction
public func purchase(product: Product, completion: #escaping ((Int) -> Void)) {
guard SKPaymentQueue.canMakePayments() else {
// Show some error here
return
}
guard let storeKitProduct = products.first(where: { $0.productIdentifier == product.rawValue }) else {
return
}
self.completion = completion
let payment = SKPayment(product: storeKitProduct)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
}
// Observe the transaction state
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach({ transaction in
switch transaction.transactionState {
case.purchasing:
break
case .purchased:
if let product = Product(rawValue: transaction.payment.productIdentifier) {
completion?(product.count)
}
SKPaymentQueue.default().finishTransaction(transaction)
SKPaymentQueue.default().remove(self)
break
case .restored:
break
case .failed:
break
case .deferred:
break
#unknown default:
break
}
})
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
self.products = response.products
print("products: ", response.products)
}
func request(_ request: SKRequest, didFailWithError error: Error) {
guard request is SKProductsRequest else {
return
}
print("Product fetch request failed")
}
}
Hello dear developers,
I'm currently stuck due to a problem with In-App AppStore rating (SKStoreReviewController).
Here is the situation, I've a screen "FirstScreen" with a button. When I tap on it, I'm going to the next screen "SecondScreen" and an in app alert for AppStore rating pop over.
I'm trying to find a solution for my UITests in order to dismiss this Alert.
I tried many solutions but I'm looking for one which do not depends on string (I don't want to localize the content of this Alert):
override func setUp() {
app = XCUIApplication()
app.launch()
addUIInterruptionMonitor(withDescription: "System Dialog") { (alert) -> Bool in
let allowButton = alert.buttons.element(boundBy: 1)
if allowButton.exists {
allowButton.tap()
}
}
}
I also tried to add an interaction ("app.swipeUp()") when I'm coming to "SecondScreen" in order to trigger this handler.
I've also tried another solution, as I know when this alert will be triggered:
let dismissButton = XCUIApplication(bundleIdentifier: "com.apple.springboard").buttons.element(boundBy: 1)
if dismissButton.exists {
dismissButton.tap()
}
No one worked and I'm still stuck :(
Does anybody found a solution in order to dismiss this alert ?
Thanks
Swiping up doesn't work but ironically swiping down does. Here is a very simplistic example
import UIKit
import StoreKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
SKStoreReviewController.requestReview()
}
}
}
import XCTest
class UITests: XCTestCase {
override func setUp() {
continueAfterFailure = false
}
func test() {
let app = XCUIApplication()
app.launch()
sleep(5)
app.swipeDown()
sleep(3)
}
}
I would like to send a notification to the user whenever he/she starts driving using CoreMotion. I can use CoreMotion to see what the user is doing while my app is on like so...
let activityManager = CMMotionActivityManager()
override func viewDidLoad() {
super.viewDidLoad()
activityManager.startActivityUpdates(to: .main) { (activity) in
guard let activity = activity else {
return
}
if activity.automotive {
print("Driving")
}
if activity.stationary {
print("Not Moving")
}
}
}
}
But how would I be able to detect the change to activity.automotive in the background to send a notification to the user even if my app is not on?
In my app, I have a switch that allows the user to put certain events in their agenda. I handle that as such:
#IBAction func putInAgenda(_ sender: UISwitch) {
let store = manager.store
if (sender.isOn){
store.requestAccess(to: EKEntityType.event, completion: {
(accessGranted: Bool, error: Error?) in
if accessGranted == true {
self.eventsHandler.importEventsInAgenda(id)
} else {
DispatchQueue.main.async {
sender.isOn = false
}
}
})
} else {
//
}
shared?.set(sender.isOn, forKey: "putInAgenda")
shared?.synchronize()
}
However, against my expectation, "store.requestAccess" not only requests, but also SETS.
As a result, when the user CANCELS the dialog, the switch switches back (expected) but any consecutive attempt to switch the switch to the ON position is honored with an OFF position, without a new dialog.
What should I do?
A privacy request is only ever asked once. If you detect that it is currently denied, you could either update the UI or prompt the user to go to Settings and turn it on. You can use UIApplication openSettingsURLString and UIApplication openURL to take the user to your app's settings page in the Settings app.
I am logging into my app using PFFacebookUtils.logInWithPermissions however, the first time I log in the actual login works fine but I get the FB error code = 2500 An active access token must be used to query information about the current user. This results in any graphRequests failing.
If I then recompile the code to the device this error does not occur and all graphRequests work fine. Why is the app not getting an access token upon the first login? Also, if I completely delete the app from the device and reinstall this whole process starts again (error code = 2500 on first attempt).
Also maybe worth noting, this didn't happen with the previous versions of Parse and FB SDKs. I am just starting to move this app into iOS9 and that's when this problem popped up.
Xcode 7.0.1, Parse SDK v1.8.5, FacebookSDKs-iOS-20150910
I was able to resolve this issue by first calling the FBSDK login method followed by the Parse SDK login method (inside the FBSDK callback):
#IBAction func facebookButtonAction(sender: AnyObject) {
// Ensure that PFUser == nil - just in here because I heard it can help with some possible issues
PFUser.logOut()
let permissions = ["public_profile", "email", "user_friends"]
// First, login with FB’s SDK
let login: FBSDKLoginManager = FBSDKLoginManager()
login.logInWithReadPermissions(permissions, handler: {(result: FBSDKLoginManagerLoginResult!, error: NSError!) in
if (error != nil) {
NSLog("Process error")
} else {
if result.isCancelled {
NSLog("Cancelled")
} else {
NSLog("Logged in")
// Then with Parse’s, upon FB login success
PFFacebookUtils.logInWithPermissions(permissions) { (user, error) -> Void in
if let user = user {
if user.isNew {
print("User signed up and logged in through Facebook!”)
//...
} else {
print("User logged in through Facebook!")
//...
}
} else {
print("Uh oh. The user cancelled the Facebook login.")
//...
}
}
}
}
})
}