Problem with in-app purchases in apple watch Swift - swift

I have been trying in vain for several days to make a test purchase of a subscription from the watch, but nothing works out for me, all the methods that I found have been tried, and these are: apphud, rebooting devices, logging out with Apple ID, another Apple ID, test / not test apple id, sandbox is logged in, the appstore is also logged in, the card is linked, but, unfortunately, every time I try to buy, I get an error message: "Unable to Purchase App. Sign in with your Apple ID from the Apple Watch app on your iPhone".
Moreover, when I do all the same from the phone, everything works fine, and the purchase is made.
There is an option to try revenuecat, but honestly, I don't think it will be different from apphud. There is also an assumption that it is simply IMPOSSIBLE to make a test purchase from the watch.
Here is a piece of my code from in app manager (recipes are coming):
func startProducts() {
let bundle = appBundleID
let products = Set([
bundle + ".1m",
bundle + ".1y"
])
request = SKProductsRequest(productIdentifiers: Set(products))
request?.delegate = self
request?.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
guard firstRequestFinish else {
return
}
firstRequestFinish = false
defer {
finishProducts()
}
if response.invalidProductIdentifiers.count > 0 {
print("Invalid Products IDs: \(response.invalidProductIdentifiers)")
}
self.allProducts = response.products
let subscriptions = (response.products.compactMap { item in
return IAProduct(id: item.productIdentifier,
title: item.localizedTitle,
price: item.price,
localePrice: item.localizedPrice,
locale: item.priceLocale)
}.sorted { item1, item2 in
return item1.price.doubleValue < item2.price.doubleValue
})
self.availableSubscriptions = subscriptions
if subscriptions.count > 0 {
print("Available products IDs: \(subscriptions.map({ item in return item.id }))")
}
self.monthProduct = subscriptions.first { item in
return item.id.contains(".1m")
}
self.yearProduct = subscriptions.first { item in
return item.id.contains(".1y")
}
delegate?.inappWasLoaded()
}
I would be glad for any advice! Many thanks to everyone who will take part in the discussion of this issue.

Related

Cloudkit notifications only when certain parts of record are changed

I am creating an app that has profiles stored as records on CloudKit. When one user adds another user. The Requester is added to the requestees. Request list. (this is where I would like a notification with the requesters name going to the requestee).
Then once the requestee accepts they are added to the requesters friendlist property on CloudKit and visa-versa.
However all the documentation I have found only shows how to send a notification anytime the CKRecord/Profile is updated in anyway. Which is what I currently have implemented but too many notifications are sent (I.e. when they change their profile name, avatar, etc...) Here is my current implementation:
func subscribeToNotifications(profile: OKGNProfile) async {
let predicate = NSPredicate(format: "name == %#", profile.name)
let subscription = CKQuerySubscription(recordType: "OKGNProfile", predicate: predicate, subscriptionID: "friendRequestAddedToDatabase", options: .firesOnRecordUpdate)
let notification = CKSubscription.NotificationInfo()
notification.title = "Friend Request"
notification.alertBody = "Open friend feed in app to see new friend request from \(profile.name)!"
notification.soundName = "default"
subscription.notificationInfo = notification
CKContainer.default().publicCloudDatabase.save(subscription) { returnedSub, returnedError in
if let error = returnedError {
print(error)
} else {
print("✅ sucessfully subscribed to notifications")
}
}
}

Swift How to handle Auto-renewable Subscription receipt and validation

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)")
}
}
}

Swift In-App purchases sandbox tester error

I am implementing the In-App purchases function today, and I just followed the tutorial step by step, created sandbox testers, wrote the code, and it says
<SKPaymentQueue: 0x282e50860>: Payment completed with error: Error Domain=ASDServerErrorDomain Code=3502 "This item is not available." UserInfo={NSLocalizedDescription=This item is not available.
Why is "This item is not available."? I searched the relevant information online, but there is no answer for it.
Here is my code
#IBAction func purchaseButtonPressed(_ sender: UIButton) {
print("PRESSED")
purchaseApp()
}
func purchaseApp() {
let productID = "com.crazycat.Reborn.FullFuctionalities"
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
} else {
print("Can't make payments")
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
if transaction.transactionState == .purchased {
print("Thanks for shopping")
} else if transaction.transactionState == .failed {
print("purchase Failed")
}
}
}
I had the same issue.
Please make sure your paid application agreement in Appstore connect is Active and not Expired. Check if there are any warnings in the App Store connect. Complete all of the bank, tax, and contact information on your App Store Connect Paid Apps Agreements.
Then relaunch the app from Xcode on your physical device.
The transaction should be successful then.
Check below points
Use the same test account you specified in developer console.
Make sure the In-App product shows a status of Ready to Submit on the developer console.
Make sure the In-App product id matches what your using in your app.

how to validate document paths in firestore?

So, in my previous question, I ended up figuring out my own issue, (I would recommend taking a look at that before reading this one), but the 20 seconds of glory was cut short when I realized that the outcome was similar across all users on the app, which is what I didn't want and totally forgot about.
With the function down below, I can purchase the event and the buttons will show up for that event and go away if I cancel, and it's unique for each event, which I adore. Now, the problem with the function down below is that if I make a purchase on user1 account and the buttons show up and stay there how they're supposed to, when I log into user2 account and perhaps want to purchase that same event, the buttons are already showing up even though user2 hasn't done anything.
getSchoolDocumentID { (schoolDocID) in
if let schID = schoolDocID {
self.db.document("school_users/\(schID)/events/\(self.selectedEventID!)").getDocument { (documentSnapshot, error) in
if let error = error {
print("There was an error fetching the document: \(error)")
} else {
guard let docSnap = documentSnapshot!.get("purchased") else {
return
}
if docSnap as! Bool == true {
self.viewPurchaseButton.isHidden = false
self.cancelPurchaseButton.isHidden = false
self.creditCard.isHidden = true
self.purchaseTicketButton.isHidden = true
} else {
self.creditCard.isHidden = false
self.purchaseTicketButton.isHidden = false
}
}
}
}
}
So i tried to solve the problem on my own but ran into a roadblock. I tried to make a subcollection of events_bought when users purchase an event and have the details stored in fields that I can call later on in a query. This was something I thought I could use to make the purchases unique amongst all users.
The function below looks through events_bought subcollection and pulls up a field and matches it with a piece of data on the displayedVC, the issue is if the event hasn't been purchased and I go on it with that user, it crashes and says how the document reference path has the wrong number of segments which I don't get because it's the same as the function above, so I realized that the path wouldn't exist and tried to figure out ways to validate the path and came up with the function down below.
getEventsBoughtEventID { (eventBought) in
if let idOfEventBought = eventBought {
let docPath = self.db.document("student_users/\(self.user?.uid)/events_bought/\(idOfEventBought)")
if docPath.path.isEmpty {
self.creditCard.isHidden = false
self.purchaseTicketButton.isHidden = false
} else {
self.db.document("student_users/\(self.user?.uid)/events_bought/\(idOfEventBought)").getDocument { (documentSnapshot, error) in
if let error = error {
print("There was an error trying to fetch this document: \(error)")
} else {
guard let docSnapEventName = documentSnapshot!.get("event_name") else {
return
}
if docSnapEventName as! String == self.selectedEventName! {
self.viewPurchaseButton.isHidden = false
self.cancelPurchaseButton.isHidden = false
self.creditCard.isHidden = true
self.purchaseTicketButton.isHidden = true
}
}
}
}
}
}
I wasn't really sure if it would work or not so I tried my luck, but I still end up getting the same document reference errors. If anyone can figure out how I can validate a document path and use logic to make certain things happen, that would be great. Thanks.
So i finally figured out how to come about doing this. It was a 4 hour grind and struggle but i got it, with a few bugs of course. So i found out the reason my app crashed was not just because of the path segments, but cause of the fact that the idOfEventBought didn't exist for some events because those events weren't purchased yet and that there was no subcollection called events_bought even created yet.
Firstly, I added a test document in a subcollection called events_bought when a user signs up, which makes sense because it would have to be made eventually anyways.
db.document("student_users/\(result?.user.uid)/events_bought/test_document").setData(["test": "test"])
This line of code allowed me to come up with my next method, that can verify if an event was bought or not.
func checkIfUserMadePurchase(shouldBeginQuery: Bool) -> Bool {
if shouldBeginQuery == true {
getEventsBoughtEventID { (eventBought) in
if let idOfEventBought = eventBought {
self.docListener = self.db.document("student_users/\(self.user?.uid)/events_bought/\(idOfEventBought)").addSnapshotListener(includeMetadataChanges: true) { (documentSnapshot, error) in
if let documentSnapshot = documentSnapshot {
if documentSnapshot.exists {
self.creditCard.isHidden = true
self.purchaseTicketButton.isHidden = true
self.viewPurchaseButton.isHidden = false
self.cancelPurchaseButton.isHidden = false
}
}
}
}
}
return true
} else {
creditCard.isHidden = false
purchaseTicketButton.isHidden = false
viewPurchaseButton.isHidden = true
cancelPurchaseButton.isHidden = true
return false
}
}
I used this method to verify if the event has been purchased yet, and if it hasn't show the right buttons.
I then call it in the process of when the purchase button in my UIAlertController is pressed.
self.checkIfUserMadePurchase(shouldBeginQuery: true)
Lastly, I create a function that uses logic to verify is the event has been purchased, and if it has been purchased, do something specific. I then call this function in the viewDidLoad() , viewWillAppear(), and viewWillDisappear().
func purchasedStatusVerification() {
db.collection("student_users/\(user?.uid)/events_bought").whereField("event_name", isEqualTo: self.selectedEventName!).getDocuments { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
if querySnapshot.isEmpty {
self.checkIfUserMadePurchase(shouldBeginQuery: false)
} else {
self.checkIfUserMadePurchase(shouldBeginQuery: true)
}
}
}
}
With all this in place, my app runs how i want to, I can successfully purchase an event and it won't show up in another users account. There are a few bugs like when a new event is created, the wrong and the right buttons are all displayed, but the wrong buttons go away after logging in and out. Also, the isHidden() method moves pretty slow, when i load the vc and the event has a status of purchased, the purchaseTicketButton is there for a split second, then disappears, which is quite annoying. All in all, I figured it out, and will try to improve it near production time.
In your document path "student_users/\(self.user?.uid)/events_bought/\(idOfEventBought)" you use self.user?.
? will produce
student_users/Optional(uid)/events_bought string,
but not
student_users/uid/events_bought string.
Use self.user! or if let user = self.user {

CKContainer.discoverAllIdentities always fails

The CKContainer.discoverAllIdentities request always fails in my CloudKit app. It has continually failed over the course of several days.
A simplified version of the code that is failing (which results in the same error) is:
private func getContacts(completion: (([CKUserIdentity]?) -> Void)?) {
container.status(forApplicationPermission: .userDiscoverability) { [weak self] status, error in
if let error = error {
print(error)
}
switch status {
case .granted:
self?.discover(completion: completion)
default:
print("status not granted")
}
}
}
private func discover(completion: (([CKUserIdentity]?) -> Void)?) {
let op = CKDiscoverAllUserIdentitiesOperation()
op.qualityOfService = .userInitiated
op.discoverAllUserIdentitiesCompletionBlock = { error in
if let error = error {
print(error)
}
}
op.userIdentityDiscoveredBlock = { identity in
print(identity)
}
op.start()
}
It results in an error being passed to the op.discoverAllUserIdentitiesCompletionBlock. The description of the error in the log is:
<CKError 0x1c4a51a60: "Server Rejected Request" (15/2000); server message = "Internal server error"; uuid = F67453B9-712D-4E5E-9335-929123E3C978; container ID = "iCloud.com.huntermaximillionmonk.topdraw">
Previously, this operation would work, but only for certain iCloud users. Now it's not for both of my test users.
Problem:
This was a problem in iOS 11.0
Based on my testing:
This works ok in Xcode 9.2 / iOS 11.2.1 on the device (not simulator)
After resetting the simulator works for the first time, doesn't work subsequently, however on the device it works repeatedly.
Code:
let queue = OperationQueue()
func requestPermissions(for permissions: CKApplicationPermissions,
completionHandler: #escaping (CKApplicationPermissionStatus, Error?) -> ()) {
CKContainer.default().requestApplicationPermission(permissions) { status, error in
if let error = error {
print("Error for requesting \(permissions) - \(error)")
}
let statusMessage : String
switch status {
case .granted:
statusMessage = "Granted"
case .denied:
statusMessage = "Denied"
case .couldNotComplete:
statusMessage = "Could not complete"
case .initialState:
statusMessage = "Initial state"
}
print("Permission - \(statusMessage)")
completionHandler(status, error)
}
}
private func discoverAllUsers() {
let operation = CKDiscoverAllUserIdentitiesOperation()
operation.userIdentityDiscoveredBlock = { userIdentity in
print("userIdentity = \(userIdentity)")
}
operation.discoverAllUserIdentitiesCompletionBlock = { error in
if let error = error {
print("Discover all users Error: \(error) ")
}
else {
print("Discover all users completed successfully")
}
}
queue.addOperation(operation)
}
Edit:
Apple fixed this issue day after this answer was posted, coincidence?! I don't think so :)
This is not actually the answer to the question, but a fix that helped me to cross over this error. It will require you to change your app UI interaction and add ContactsUI framework to your project, moreover your user will be responsible for selecting a contact with iCloud related email.
Good news is that the method discoverUserIdentity is still works. So, you can use it to get CKUserIdentity from manually selected contact.
func addContact(_ contact:CNContact) {
var lookUpEmails = [CKUserIdentityLookupInfo]()
for email in contact.emailAddresses {
lookUpEmails.append(CKUserIdentityLookupInfo(emailAddress: (email.value as String)))
}
let checkUserOperation = CKDiscoverUserIdentitiesOperation()
checkUserOperation.userIdentityLookupInfos = lookUpEmails
checkUserOperation.userIdentityDiscoveredBlock = { [unowned self] (identity, info) -> Void in
if identity.hasiCloudAccount {
if let recordID = identity.userRecordID {
//do something with discovered user
}
checkUserOperation.cancel()
}
}
checkUserOperation.queuePriority = Operation.QueuePriority.high
CKContainer.default().add(checkUserOperation)
}
It might sound useless, but in my case, it helped me to solve the Server Rejected Request" (15/2000) error, to fix one of the features of my app and continue to use the other feature related code with less efforts than I thought.
I hope someone will find this helpful.
Just another data point on this that might help with the overall picture. I was still seeing this error on 11.2.5 when I used my own iCloud AppleID (with hundreds of contacts) while running a Test App that called discoverAllIdentitiesWithCompletionHandler. I'd get the dreaded
CKError 0x1c0051730: "Server Rejected Request" (15/2000); server message = "Internal server error".
When I switched to run the exact same code on my daughters iOS11.2.5 device (with just a handful of contacts) the code worked fine.
Leads me to believe there is some rate limiting going on when there are a lot of contacts with iOS11.
(P.S. No errors at all running on iOS10)