React-native-iap: finishTransactionIOS / finishTransaction does nothing for iOS

Created on 19 Feb 2020  ·  34Comments  ·  Source: dooboolab/react-native-iap

Version of react-native-iap

4.4.1

Version of react-native

0.60.4

Platforms you faced the error (IOS or Android or both?)

iOS

Expected behavior

Upon calling finishTransactionIOS(purchaseId) or finishTransaction(purchase) for a subscription, the transaction should be finished and not be emitted to the app again on next launch. If there was a problem with this, I would expect something to be returned either to the error listener or in a promise from one of the finishTransaction functions, nothing happens.

Android seems to work fine.

Actual behavior

The transaction is emitted on every single launch unless clearTransactionIOS is called, which seems to have other side effects.

Tested environment (Emulator? Real Device?)

Real Device

Steps to reproduce the behavior

  • Create a subscription product.
  • Buy the subscription
  • Simulate backend validation
  • Call finishTransactionIOS or finishTransaction on that subscription. Has no effect.

I've tried this piles of different ways, and absolutely nothing seems to work, no matter what I do, other than clear the transactions, the purchase is again emitted to the app every single launch. Any ideas what is going on or what I could possibly be doing or understanding wrong?

ℹ needs more info 📱 iOS 🙏 help wanted 🚶🏻 stale

Most helpful comment

@hyochan any update on this issue? We have been been stuck for quite some time and unable to publish our app.

All 34 comments

I have the same issue.

Can you share the ackResult?

          try {
            const ackResult = await finishTransaction(purchase);
            console.log('ackResult', ackResult);
          } catch (ackErr) {
            console.warn('ackErr', ackErr);
          }

@hyochan
In my case:
ackResult undefined

@hyochan
In my case:
ackResult undefined

Same here

Also in my case, the returned value is undefined (real device with a sandbox user).

It looks related to #366. Also for ios finishTransaction won't return anything.
Can you all focus on #366 and come back for the update?

It looks related to #366. Also for ios finishTransaction won't return anything.
Can you all focus on #366 and come back for the update?

Just to clarify - since #366 is about testing are you suggesting that this should work fine in production without the need for clearTransactionIOS?

@hyochan When I'm testing debug configuration with sandbox account basically I'm getting notification from Server to Server Notifications(Apple) about a purchase. The problem is that after some time my client listener gets another notification about the purchase but with different transactionId even when purchasing action happened only one time so it looks like problem with properly finish the previous transaction. On Android, everything works fine and I think that is not related to #366 because I don't have any problem to test purchasing process with a sandbox account.

```tsx
useEffect(() => {
purchaseUpdateSubscription.current = purchaseUpdatedListener(
async (purchase: InAppPurchase | SubscriptionPurchase) => {
const receipt = purchase.transactionReceipt;
if (receipt) {
try {
await myBackendHandler({
user,
purchase,
});
const result = await RNIap.finishTransaction(purchase, false);
console.log('result', result);
} catch (e) {
// TODO
}
}
}
);
return () => {
if (purchaseUpdateSubscription.current) {
purchaseUpdateSubscription.current.remove();
purchaseUpdateSubscription.current = null;
}
};
}, []);
````

Hey team, thanks for all the hard work on this library, it really makes our lives easier. I'm commenting because this issue hasn't been resolved and I don't see how old transactions not clearing have anything to do with #366 .

My usecase is

  1. User 1 makes purchase and premium benefit applied to their account.
  2. User 1 signs out
  3. User 2 signs in

Expected result:

  • Transactions from User 1 are not emitted via the purchase listener

Actual result

  • Transactions from User 1 are NOT cleared and are emitted again. Even though a new user has signed in leading to bad data.

This is just an example of why it is important for this issue to be prioritised. Thanks for reading this long comment...

same here

exactly the same problem as above, looks like stack isnt cleared at all, even after disconnecting sandbox/itunes account, reboot device, reinstall app.. :/

Could you verify that the finishTransaction is really doing nothing in RNIapIos.m file?
I've just checked it out and it correctly finishes its transaction.

Please check clearTransaction or finishTransactionWithIdentifier method and try to put any logs inside. Try to find below codes.

RCT_EXPORT_METHOD(clearTransaction) {
    NSArray *pendingTrans = [[SKPaymentQueue defaultQueue] transactions];
    NSLog(@"\n\n\n  ***  clear remaining Transactions. Call this before make a new transaction   \n\n.");
    for (int k = 0; k < pendingTrans.count; k++) {
        [[SKPaymentQueue defaultQueue] finishTransaction:pendingTrans[k]];
    }
-(void)finishTransactionWithIdentifier:(NSString *)transactionIdentifier {
    SKPaymentQueue *queue = [SKPaymentQueue defaultQueue];
    for(SKPaymentTransaction *transaction in queue.transactions) {
        if([transaction.transactionIdentifier isEqualToString:transactionIdentifier]) {
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
        }
    }
}

I can't move this issue forward because I am not getting any idea what's happening. I hope somebody can debug things in their environment and share 🙏

any update about this issue ?

up, I am experiencing this problem too.

same here finishTransactionIOS/finishTransaction this is not working in IOS

I seem to experience the same thing.
The purchase of IAP completes successfully, but the final call to finishTransaction returns undefined.

// Finish transaction
const ackResult = await finishTransaction(purchase);
console.log("Ack result: ", ackResult); // Ack result: undefined

_Real device "iPhone 6s" using App Store Sandbox environment._

What is the expected result from finishTransaction? Is undefined a good result or should I expect something else?

I am also experiencing this issue on iOS, where neither finishTransactionIOS nor finishTransaction is actually removing those transactions, which is preventing me from releasing app. Any update?

@hyochan Though I definitely am not fluent in Objective-C, I added some logs in finishTransaction and finishTransactionWithIdentifier and it seems they are running correctly (id is being passed and matched). The only thing I can think of is that[[SKPaymentQueue defaultQueue] finishTransaction:transaction]; isn't actually connecting to StoreKit and finishing the transaction? I don't really know where to go from here but hopefully this is helpful as you debug

Are you also handling subscription status notifications from the App Store? In my case, I had this problem until I made sure my server handled all notification events properly and responded with a 200.

@espenjanson Interesting. No, I wasn't. But I actually just switched to expo's InAppPurchases module yesterday and it's all working great now.

@lachlanglen Nevermind. It was just a single strike of luck (?) or something else. Built another version of the app and the problem is there again. Thinking of doing the same but a little worried that the expo repo is not very well maintained.

I was having a look into it and it's correct that it returns undefined since the finishTransactions call is not meant to return anything in obj-c.
I've done some tests and in my case, it seems to finish them correctly(simple test, pull pendingTransactions, if there's nothing, it's working good)

@espenjanson I don't know what makes you think that Expo packages are not well-maintained. I think Expo released their iap module pretty recently. If i knew expo had iap module at the time I was implementing iap, i would've picked expo without thinking twice.

Is there some way to solve this?
I have tested this on TestFlight even with new sandbox user and it still keeps happening.

My purchaseUpdateListener keeps receiving "old" transactions.
I am calling finishTransaction(purchase,false) as I am using autorenewable subscriptions.
The result of the call is undefined.

Can anybody offer a workaround or explanation why this is happening?

@zatloeri That is apparently a desired behavior. It took me a while to realize that. App Store will be putting new receipt for renewal in StoreKit which triggers the observer. You should process that receipt and finish the transaction.

https://developer.apple.com/videos/play/wwdc2018/705/
https://developer.apple.com/videos/play/wwdc2020/10671

(watch in Safari for HD quality)

@ziyoshams Thanks for the quick reply and for the resources which I will look through in a bit.

But first I just want to point out one thing cause I don't think I made it apperent.
What you are describing seems logical to me and I expected this to happen, but what puzzles me is this:

Product A has expired as verified by receipt validation.
I receive one old (from previous day) transaction for product A when I start or foreground the app.
Then the next time I foreground the app again I recieve another one old (maybe a little newer then the previous, not sure) transaction for product A.
On another foreground possibly another and so on.

I would expect to get all of them on one app start if the subscription is already after its expiration.
Is this also expected behavior, or is there something fishy happening?

@zatloeri Every transaction that is not finished will appear in that queue, even if it is expired. For monthly subscriptions you should get that receipt every 5 mins or so. Like if you purchase something and come back to the app the next day, you will get 5-6 receipts at the same time. I highly recommend watching those WWDC videos. They are so helpful.

I am having same issue on iOS, transaction is not getting finished so, Queue is not getting empty and receipt is reappearing every time whenever app launches. i have verified receipt at server side, then finished transaction. Please have a look at my code below

i have mentioned few snippets of my code below...

```
import RNIap, {
InAppPurchase,
Product,
PurchaseError,
Subscription,
SubscriptionPurchase,
finishTransaction,
finishTransactionIOS,
purchaseErrorListener,
purchaseUpdatedListener,
clearTransactionIOS,
} from 'react-native-iap';

async componentDidMount() {
const result = await RNIap.initConnection();
const itemSubs = Platform.select({
android: [
get(this.props, 'subscriptionStore.subscription.android_product_id', null)
],
ios: [
get(this.props, 'subscriptionStore.subscription.ios_product_id', null)
]
});

const subscriptions = await RNIap.getSubscriptions(itemSubs);
purchaseUpdateSubscription = purchaseUpdatedListener(
async (purchase) => {
const receipt = purchase.transactionReceipt;
let subscription = {
subscription_id: get(this.props, 'subscriptionStore.subscription.id', null)
};
if (Platform.OS === 'ios') {
subscription = {
...subscription,
order_id: get(purchase, 'transactionId', null),
purchase_token: get(purchase, 'originalTransactionIdentifierIOS', null),
receipt: get(purchase, 'transactionReceipt', null), // (Store it in TEXT)
os: 'IOS'
}
} else if (Platform.OS === 'android') {
const data = JSON.parse(receipt);
subscription = {
...subscription,
order_id: get(data, 'orderId', null),
purchase_token: get(data, 'purchaseToken', null),
os: 'ANDROID'
}
}
try {
finishTransaction(purchase, false); //this is here for temp, it will be placed on success later on.
this.props.notificationStore.showToast('Subscribed successfully. Please wait...', 'success', 5000);
const resp = await this.props.subscriptionStore.onPurchase(subscription); //verifying transactions at server end.
if (resp.success) { //success response
this.props.userStore.setUser({ is_premium: 1 });
this.props.navigation.replace('FixFooter');
}
} catch (error) {
console.log("onPurchase API ERROR: ", error);
}
}
)

purchaseErrorSubscription = purchaseErrorListener(
async (error) => {
console.log('purchase error: ', error);
}
)
}

/* onPurchase Button */
async onPurchase() {
try {
const request = await RNIap.requestSubscription(Platform.OS === 'android' ?
get(this.props, 'subscriptionStore.subscription.android_product_id', null) :
get(this.props, 'subscriptionStore.subscription.ios_product_id', null))
} catch (error) {
console.log('error: ', error)
}
}

@sufyan297 can you please share a snippet from your server-side code? I'm also trying to set up a receipt verification thingy but Apple's document is extremely confusing

@ziyoshams Are you getting null also using expo iap module finishTransaction method?

@bcbcbcbcbcl I am not using the expo module.

+1 to me it returns undefined

@hyochan any update on this issue? We have been been stuck for quite some time and unable to publish our app.

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as "For Discussion" or "Good first issue" and I will leave it open. Thank you for your contributions.

Was this page helpful?
0 / 5 - 0 ratings