Not sure if this helps, but these users are international users.
«Are you certain these users are logged in using a real production environment»
Yes, they are real end users, not testers.
«Can you purchase an IAP in the production environment?»
Yes, we had a production user help us test this out by purchasing one of our IAPs. We checked, and his credit card was in fact charged.
«If not, is your IAP approved and marked ‘cleared for sale'»
Yes, all of my IAPs are both «approved» and marked «cleared for sale.»
«Are you certain these users are really trying to make a purchase?»
I can’t be so sure, but from my remote logs, I do know that they are:
1.) Initiating a purchase in my UI which triggers addPayment on the defaultQueue SKPaymentQueue as such:
- (void)InitiatePurchase:(NSString*)product_name
RLOGI(@"IAP: OBJC initiatePurchase: %@", product_name);
if (!products_initialized) {
RLOGE(@"IAP: Could not initiatePurchase, products not initialized!");
return;
}
if (!product_name) {
RLOGE(@"IAP: Could not initiatePurchase, nil product_name!");
return;
}
if (!user_email) {
RLOGE(@"IAP: Could not initiatePurchase, user not logged in!");
return;
}
if (![[products_dict allKeys] containsObject:product_name]) {
RLOGE(@"IAP: Could not find productToPurchase in dict!");
return;
}
SKProduct *productToPurchase = products_dict[product_name];
if (!productToPurchase) {
RLOGE(@"IAP: Invalid productToPurchase in dict!");
return;
}
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:productToPurchase];
payment.quantity = 1;
payment.applicationUsername = [self hashedValueForAccountName:user_email];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
^ note that no error conditions were logged.
2.) The purchase shows up in updatedTransactions as an SKPaymentTransaction with transactionState SKPaymentTransactionStatePurchasing:
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing: {
RLOGW(@"IAP: Purchase purchasing");
break;
}
case SKPaymentTransactionStateDeferred: {
RLOGW(@"IAP: Purchase deferred");
break;
}
case SKPaymentTransactionStateFailed: {
RLOGE(@"IAP: Purchase failed: error=%@", transaction.error);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Purchase failed"
message:@"You were not charged."
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
if (self.gameUIRef != nil) { [self.gameUIRef OnPurchaseFailedCallback]; }
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
}
case SKPaymentTransactionStatePurchased: {
RLOGI(@"IAP: Purchase purchased");
[self validatePurchase:transaction];
break;
}
case SKPaymentTransactionStateRestored: {
RLOGW(@"IAP: Purchase restored");
[self validatePurchase:transaction];
break;
}
default:
RLOGE(@"IAP: Unexpected transaction state %@", @(transaction.transactionState));
}
}
}
3.) The purchase shows up in updatedTransactions as an SKPaymentTransaction with transactionState SKPaymentTransactionStateFailed and the error field is as shown:
- Jul 10 11:49:53 melodyyin18@gmail.comLingolandNative: WARNING OBJC sendAnalyticsEvent buy button pressed
- Jul 10 11:49:53 melodyyin18@gmail.comLingolandNative: INFO IAP: OBJC initiatePurchase: 1hourenglishenglish
- Jul 10 11:49:53 melodyyin18@gmail.comLingolandNative: WARNING IAP: Purchase purchasing
- Jul 10 11:49:56 melodyyin18@gmail.comLingolandNative: ERR IAP: Purchase failed: error=Error Domain=SKErrorDomain Code=2 «Cannot connect to iTunes Store» UserInfo={NSLocalizedDescription=Cannot connect to iTunes Store}
At any rate, none of these suggestions would seem like resolutions that would come to mind as possible resolutions to a reasonable person upon seeing the error message: «Cannot connect to iTunes Store.» As feedback, for example, one of the suggestions was that my product may not have been «approved»; if the IAP product is not approved, perhaps a more descriptive error message such as «IAP product is not approved» instead of the ambiguously vague (if not misleading) error message: «Cannot connect to iTunes Store,» which suggests there either that there is an Internet connectivity problem or that the iTunes Store is down.
I have two auto-renewing subscriptions, monthly and yearly (iOS). I can make a purchase when I use a new fresh sandbox user. Although I have to enter my password three times. Here’s the flow:
- Tap on a subscription
- Enter Password
- Promted to enter password again
- get «cannot connect to iTunes Store» error
- try again and enter password
- purchase succeeds.
Moving on, so once it succeeds, I’m now subscribed and my UI is updated via a listener in appDelegate which posts a notification that I subscribe to. But when I try to switch subscriptions, from monthly to yearly, or vice versa, it always gives me the «Cannot connect to iTunes Store» error. No UI updates. Here’s the flow:
- Tap on the other subscription
- Prompted to enter iTunes password
- Receive the «confirm purchase» dialog which states I’m modifying my subscription
- Tap continue
- Receive the «You’re all set» success alert.
- Dismiss alert
- Receive «Cannot connect to iTunes Store» error
- My listener wasn’t called, UI wasn’t updated, etc.
But then if I dismiss the error and tap the subscription again, I get an alert that says I’m already subscribed to that plan, even though the error was thrown and my listener didn’t pick up the change.
I’m using firebase. I followed the quickstart and the Firebase specific instructions in the RevenueCat docs. All of my debug logs seem to be fine, no non-200 statuses, no invalid product Ids. Here are some code snippets:
AppDelegate:
Purchases.debugLogsEnabled = true
Purchases.configure(withAPIKey: Constants.revenueCatKey)
Purchases.shared.delegate = self
FirebaseApp.configure()
authHandle = Auth.auth().addStateDidChangeListener() { (auth, user) in
if let uid = user?.uid {
Purchases.shared.identify(uid, { (info, error) in
if let e = error {
print("sign in error: (e.localizedDescription)")
} else {
print("User (uid) signed in")
}
})
}
...
}
}
extension AppDelegate: PurchasesDelegate {
func purchases(_ purchases: Purchases, didReceiveUpdated purchaserInfo: PurchaserInfo) {
if self.currentUser != nil {
if purchaserInfo.activeSubscriptions.contains(Constants.audiomeshSubscriptions.monthly) {
guard let myRef = DataService.instance.REF_PRIVATE else { return }
myRef.updateData(["plan" : "paidMonthly"]) { err in
if let err = err {
print("error updating user-private with new subscription: (err)")
} else {
NotificationCenter.default.post(name: Notification.Name(rawValue: "purchaserInfoUpdated"), object: nil)
}
}
}
else if purchaserInfo.activeSubscriptions.contains(Constants.audiomeshSubscriptions.yearly) {
//do the same for yearly subscription
}
else {
//handle non-paying users here
}
}
}
}
UpgradeController (the purchasing UI):
@objc func purchaseButtonSelected(sender: AudiomeshButton) {
let buttonTag = sender.tag
guard let option = options?[buttonTag] else { return }
let product:SKProduct = option.product
Purchases.shared.makePurchase(product, { (transaction, purchaserInfo, error) in
if let error = error {
print("error making purchase: (error)")
} else {
print("Purchase Successful")
}
})
}
I have two auto-renewing subscriptions, monthly and yearly (iOS). I can make a purchase when I use a new fresh sandbox user. Although I have to enter my password three times. Here’s the flow:
- Tap on a subscription
- Enter Password
- Promted to enter password again
- get «cannot connect to iTunes Store» error
- try again and enter password
- purchase succeeds.
Moving on, so once it succeeds, I’m now subscribed and my UI is updated via a listener in appDelegate which posts a notification that I subscribe to. But when I try to switch subscriptions, from monthly to yearly, or vice versa, it always gives me the «Cannot connect to iTunes Store» error. No UI updates. Here’s the flow:
- Tap on the other subscription
- Prompted to enter iTunes password
- Receive the «confirm purchase» dialog which states I’m modifying my subscription
- Tap continue
- Receive the «You’re all set» success alert.
- Dismiss alert
- Receive «Cannot connect to iTunes Store» error
- My listener wasn’t called, UI wasn’t updated, etc.
But then if I dismiss the error and tap the subscription again, I get an alert that says I’m already subscribed to that plan, even though the error was thrown and my listener didn’t pick up the change.
I’m using firebase. I followed the quickstart and the Firebase specific instructions in the RevenueCat docs. All of my debug logs seem to be fine, no non-200 statuses, no invalid product Ids. Here are some code snippets:
AppDelegate:
Purchases.debugLogsEnabled = true
Purchases.configure(withAPIKey: Constants.revenueCatKey)
Purchases.shared.delegate = self
FirebaseApp.configure()
authHandle = Auth.auth().addStateDidChangeListener() { (auth, user) in
if let uid = user?.uid {
Purchases.shared.identify(uid, { (info, error) in
if let e = error {
print("sign in error: (e.localizedDescription)")
} else {
print("User (uid) signed in")
}
})
}
...
}
}
extension AppDelegate: PurchasesDelegate {
func purchases(_ purchases: Purchases, didReceiveUpdated purchaserInfo: PurchaserInfo) {
if self.currentUser != nil {
if purchaserInfo.activeSubscriptions.contains(Constants.audiomeshSubscriptions.monthly) {
guard let myRef = DataService.instance.REF_PRIVATE else { return }
myRef.updateData(["plan" : "paidMonthly"]) { err in
if let err = err {
print("error updating user-private with new subscription: (err)")
} else {
NotificationCenter.default.post(name: Notification.Name(rawValue: "purchaserInfoUpdated"), object: nil)
}
}
}
else if purchaserInfo.activeSubscriptions.contains(Constants.audiomeshSubscriptions.yearly) {
//do the same for yearly subscription
}
else {
//handle non-paying users here
}
}
}
}
UpgradeController (the purchasing UI):
@objc func purchaseButtonSelected(sender: AudiomeshButton) {
let buttonTag = sender.tag
guard let option = options?[buttonTag] else { return }
let product:SKProduct = option.product
Purchases.shared.makePurchase(product, { (transaction, purchaserInfo, error) in
if let error = error {
print("error making purchase: (error)")
} else {
print("Purchase Successful")
}
})
}
У меня есть две подписки с автоматическим продлением, ежемесячная и ежегодная (iOS). Я могу совершить покупку, если воспользуюсь новым пользователем песочницы. Хотя я должен вводить свой пароль трижды. Вот поток:
- Нажмите на подписку
- Введите пароль
- Предлагается ввести пароль еще раз
- получить ошибку «не удается подключиться к iTunes Store»
- попробуйте еще раз и введите пароль
- покупка удалась.
Двигаемся дальше, так что, как только все получится, я подписан, и мой пользовательский интерфейс обновляется через слушателя в appDelegate, который отправляет уведомление, на которое я подписываюсь. Но когда я пытаюсь переключить подписку с ежемесячной на годовую или наоборот, всегда выдает ошибку «Не удается подключиться к iTunes Store». Нет обновлений пользовательского интерфейса. Вот поток:
- Нажмите на другую подписку
- Предлагается ввести пароль iTunes
- Получите диалоговое окно «Подтвердить покупку», в котором говорится, что я изменяю свою подписку.
- Нажмите «Продолжить»
- Получите уведомление об успешном завершении «Все готово».
- Закрыть оповещение
- Получение ошибки «Не удается подключиться к iTunes Store»
- Мой слушатель не был вызван, пользовательский интерфейс не обновлен и т. Д.
Но затем, если я отклоняю ошибку и снова нажимаю на подписку, я получаю предупреждение, в котором говорится, что я уже подписан на этот план, даже если ошибка была выдана и мой слушатель не уловил изменение.
Я использую firebase. Я выполнил краткое руководство и конкретные инструкции Firebase в документации RevenueCat. Кажется, все мои журналы отладки в порядке, нет статусов, отличных от 200, и нет недопустимых идентификаторов продуктов. Вот несколько фрагментов кода:
AppDelegate:
Purchases.debugLogsEnabled = true
Purchases.configure(withAPIKey: Constants.revenueCatKey)
Purchases.shared.delegate = self
FirebaseApp.configure()
authHandle = Auth.auth().addStateDidChangeListener() { (auth, user) in
if let uid = user?.uid {
Purchases.shared.identify(uid, { (info, error) in
if let e = error {
print("sign in error: (e.localizedDescription)")
} else {
print("User (uid) signed in")
}
})
}
...
}
}
extension AppDelegate: PurchasesDelegate {
func purchases(_ purchases: Purchases, didReceiveUpdated purchaserInfo: PurchaserInfo) {
if self.currentUser != nil {
if purchaserInfo.activeSubscriptions.contains(Constants.audiomeshSubscriptions.monthly) {
guard let myRef = DataService.instance.REF_PRIVATE else { return }
myRef.updateData(["plan" : "paidMonthly"]) { err in
if let err = err {
print("error updating user-private with new subscription: (err)")
} else {
NotificationCenter.default.post(name: Notification.Name(rawValue: "purchaserInfoUpdated"), object: nil)
}
}
}
else if purchaserInfo.activeSubscriptions.contains(Constants.audiomeshSubscriptions.yearly) {
//do the same for yearly subscription
}
else {
//handle non-paying users here
}
}
}
}
UpgradeController (пользовательский интерфейс покупки):
@objc func purchaseButtonSelected(sender: AudiomeshButton) {
let buttonTag = sender.tag
guard let option = options?[buttonTag] else { return }
let product:SKProduct = option.product
Purchases.shared.makePurchase(product, { (transaction, purchaserInfo, error) in
if let error = error {
print("error making purchase: (error)")
} else {
print("Purchase Successful")
}
})
}
2 ответа
Так что на этот вопрос на самом деле относительно легко ответить, но ответ довольно неудовлетворительный.
В тестовой среде не работают обновления и кросс-версии.
В этом случае эта ошибка почти всегда возвращается. Хорошей новостью является то, что он работает в производственной среде и RevenueCat правильно отслеживает все случаи.
9
Jacob Eiting
3 Мар 2019 в 00:57
Итак, это общее сообщение NSError, которое выдается для всех SKError. Код ошибки 2 — «платеж отменен». Однако это также ошибка, которая возникает, когда вы уже подписаны на элемент.
Вы уверены, что истекает срок годовой подписки, прежде чем вы попытаетесь ее повторно подписать? При годовой подписке они будут обновляться каждый час, 6 раз, прежде чем истечет срок их действия.
Чтобы увидеть конкретную ошибку SKError, сделайте что-то вроде:
if let error = error as? SKError {
print("SKError - ")
switch error.code { // https://developer.apple.com/reference/storekit/skerror.code
case .unknown:
print("unknown error")
case .paymentCancelled:
print("cancelled error")
case .clientInvalid:
print("client invalid")
case .paymentInvalid:
print("payment invalid")
case .paymentNotAllowed:
print("payment not allowed")
case .cloudServiceNetworkConnectionFailed:
print("cloud service network connection failed")
case .cloudServicePermissionDenied:
print("cloud service permission denied")
case .storeProductNotAvailable:
print("store product not available")
case .cloudServiceRevoked:
print("cloud service revoked")
}
}
Как только вы узнаете, что возвращается SKError, я могу при необходимости обновить свой ответ, добавив дополнительную информацию о том, что может происходить.
4
enc_life
2 Мар 2019 в 21:05
Manufacture: Apple
Model: iPhone11,6
Platform: iOS
OS version: 14.0.1
Plugin version: 10.1.1
A user had purchased a subscription should be able to use it and see that he purchased
The app doesn’t get to owned state and calling order returns an error.
Hard to tell, this doesn’t reproduce to most users.
Here are the logs from the app:
2020-10-28 09:04:04 | DEBUG | [Store] Approved, verifing: il.org.osm.israelhiking
2020-10-28 09:04:04 | DEBUG | [Store] scheduleValidation()
2020-10-28 09:04:05 | DEBUG | [Store] scheduleValidation()
2020-10-28 09:04:07 | DEBUG | [Store] runValidation()
2020-10-28 09:04:07 | DEBUG | [Store] ajax -> send request to https://validator.fovea.cc/v1/validate?appName=il.org.osm.israelhiking&apiKey=...
2020-10-28 09:04:09 | DEBUG | [Store] validator success, response: ...{"ok":true,"data":{"id":"il.org.osm.israelhiking",...
2020-10-28 09:04:09 | DEBUG | [Store] verify -> {"success":true,"data":{"id":"il.org.osm.israelhiking"...
2020-10-28 09:04:09 | DEBUG | [Store] transaction fields for il.org.osm.israelhiking
2020-10-28 09:04:09 | DEBUG | [Store] verify -> success: {"id":"il.org.osm.israelhiking",...
2020-10-28 09:04:09 | DEBUG | [Store] syncWithAppStoreReceipt
2020-10-28 09:04:09 | DEBUG | [Store] {"type":"ios-appstore",...
2020-10-28 09:04:09 | DEBUG | [Store] transaction fields for offline_map
2020-10-28 09:04:09 | DEBUG | [Store] expiryDate: 2021-03-22T19:06:37.000Z
2020-10-28 09:04:09 | DEBUG | [Store] Verified, Finishing: il.org.osm.israelhiking
2020-10-28 09:04:09 | DEBUG | [Store] product -> defer finishing il.org.osm.israelhiking
2020-10-28 09:04:09 | DEBUG | [Store] transaction fields for il.org.osm.israelhiking
2020-10-28 09:04:09 | DEBUG | [Store] verify -> success: {"id":"il.org.osm.israelhiking"...
2020-10-28 09:04:09 | DEBUG | [Store] transaction fields for il.org.osm.israelhiking
2020-10-28 09:04:09 | DEBUG | [Store] verify -> success: {"id":"il.org.osm.israelhiking"...
2020-10-28 09:04:09 | DEBUG | [Store] ios -> finishing il.org.osm.israelhiking (a application)
2020-10-28 09:04:09 | DEBUG | [Store] product -> finishing il.org.osm.israelhiking
2020-10-28 09:04:09 | DEBUG | [Store] state: il.org.osm.israelhiking -> owned
offline_map
never gets to owned state although it was purchased.
When clicking order we get the following error:
2020-10-28 09:04:14 | DEBUG | [Store] Ordering product
2020-10-28 09:04:14 | DEBUG | [Store] state: offline_map -> requested
2020-10-28 09:04:14 | DEBUG | [Store] ios -> is purchasing offline_map
2020-10-28 09:04:14 | DEBUG | [Store] state: offline_map -> initiated
2020-10-28 09:04:18 | ERROR | [Store] ios -> ERROR 6777006: The operation couldn’t be completed. (SKErrorDomain error 2.) - {"productId":"offline_map"}
2020-10-28 09:04:18 | DEBUG | [Store] state: offline_map -> valid
2020-10-28 09:04:18 | ERROR | [Store] error handler: The operation couldn’t be completed. (SKErrorDomain error 2.) (6777006)