Skip to content

PurchasesOrchestrator: simplified StoreKit2TransactionListenerDelegate transaction handling#2536

Closed
NachoSoto wants to merge 1 commit into
mainfrom
purchases-orchestrator-simplify-handle-purchase
Closed

PurchasesOrchestrator: simplified StoreKit2TransactionListenerDelegate transaction handling#2536
NachoSoto wants to merge 1 commit into
mainfrom
purchases-orchestrator-simplify-handle-purchase

Conversation

@NachoSoto

Copy link
Copy Markdown
Contributor

While working on #2533 I noticed that the implementation for StoreKit2TransactionListenerDelegate was unnecessarily complex:

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
extension PurchasesOrchestrator: StoreKit2TransactionListenerDelegate {
    func storeKit2TransactionListener(
        _ listener: StoreKit2TransactionListener,
        updatedTransaction transaction: StoreTransactionType
    ) async throws {
        let isRestore = self.systemInfo.observerMode

        _ = try await self.syncPurchases(receiptRefreshPolicy: .always,
                                         isRestore: isRestore,
                                         initiationSource: .queue)

        await Async.call { completion in
            self.finishTransactionIfNeeded(transaction) { @MainActor in
                completion(())
            }
        }
    }
}

Instead of only posting the one transaction, we were calling syncPurchases, which is inconsistent with how we handle SK1 transactions.
The new logic in #2533 will also make use of this single-transaction handling code, so it makes sense to unify this as well.

Note that since #1704, this method is only invoked for transactions detected outside of a manual purchase flow.

This only required an overload to specify the InitiationSource, which uncovered one incorrect isRestore parameter in one of our tests.
I verified that this is expected based on other tests, and the implementation of initiationSource(for productIdentifier:restored:), as well as checking with @antoniobg.

…gate` transaction handling

While working on #2533 I noticed that the implementation for `StoreKit2TransactionListenerDelegate` was unnecessarily complex:
```swift
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
extension PurchasesOrchestrator: StoreKit2TransactionListenerDelegate {
    func storeKit2TransactionListener(
        _ listener: StoreKit2TransactionListener,
        updatedTransaction transaction: StoreTransactionType
    ) async throws {
        let isRestore = self.systemInfo.observerMode

        _ = try await self.syncPurchases(receiptRefreshPolicy: .always,
                                         isRestore: isRestore,
                                         initiationSource: .queue)

        await Async.call { completion in
            self.finishTransactionIfNeeded(transaction) { @mainactor in
                completion(())
            }
        }
    }
}
```

Instead of only posting the one transaction, we were calling `syncPurchases`, which is inconsistent with how we handle SK1 transactions.
The new logic in #2533 will also make use of this single-transaction handling code, so it makes sense to unify this as well.

This only required an overload to specify the `InitiationSource`, which uncovered one incorrect `isRestore` parameter in one of our tests.
I verified that this is expected based on other tests, and the implementation of `initiationSource(for productIdentifier:restored:)`, as well as checking with @antoniobg.
@NachoSoto NachoSoto requested a review from a team May 24, 2023 16:27
Comment on lines -861 to -871
let isRestore = self.systemInfo.observerMode

_ = try await self.syncPurchases(receiptRefreshPolicy: .always,
isRestore: isRestore,
initiationSource: .queue)

await Async.call { completion in
self.finishTransactionIfNeeded(transaction) { @MainActor in
completion(())
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Goodbye manual code here.

expect(transaction.finishInvoked) == false
expect(self.backend.invokedPostReceiptData) == true
expect(self.backend.invokedPostReceiptDataParameters?.isRestore) == true
expect(self.backend.invokedPostReceiptDataParameters?.isRestore) == false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explained this in the PR description, this was wrong.

@NachoSoto

Copy link
Copy Markdown
Contributor Author

This breaks testResubscribeAfterExpiration because PurchasesOrchestrator gets notified from the queue right before the second purchase :/
I'll have to look into it, so putting this on hold for now.

@NachoSoto NachoSoto added the HOLD label May 24, 2023
@NachoSoto NachoSoto marked this pull request as draft May 24, 2023 18:49
@NachoSoto NachoSoto changed the base branch from main to transaction-fetcher-2 May 24, 2023 20:05
@NachoSoto NachoSoto changed the base branch from transaction-fetcher-2 to main May 24, 2023 20:05
@NachoSoto

Copy link
Copy Markdown
Contributor Author

This will probably benefit from #2540.

@NachoSoto

Copy link
Copy Markdown
Contributor Author

Done in #2540 instead.

@NachoSoto NachoSoto closed this May 25, 2023
@NachoSoto NachoSoto deleted the purchases-orchestrator-simplify-handle-purchase branch May 25, 2023 01:11
NachoSoto added a commit that referenced this pull request May 25, 2023
This extracts the logic from `PurchasedProductsFetcher` and adds tests.
It will be used in #2536.

Also exposed the duplicated `underlyingTransaction` implementations
NachoSoto added a commit that referenced this pull request May 25, 2023
This extracts the logic from `PurchasedProductsFetcher` and adds tests.
It will be used in #2536.

Also exposed and combined the duplicated `underlyingTransaction`
implementations.
NachoSoto added a commit that referenced this pull request May 26, 2023
Necessary refactor for #2533. This allows `CustomerInfoManager` to have
an instance of `TransactionPosterType` instead of the whole
`PurchasesOrchestrator`.

Essentially this becomes the hierarchy now:

![IMG_B8C56C11B7E5-1](https://github.com/RevenueCat/purchases-ios/assets/685609/36f5c30b-fc82-4590-b574-ce6d95eef13f)

### Changes:
- Created stateless `TransactionPoster`
- Extracted `handlePurchasedTransaction` into `TransactionPoster`
- The new implementation there is _mostly_ the same, with the one
difference that it's stateless. Instead of holding
`purchaseCompleteCallbacksByProductID`, that's done by
`PurchasesOrchestrator`. The methods there all take a callback, so
they're easier to use and don't require keeping a callback around.
- Created `PurchaseSource` to abstract the combo of `isRestore` and
`InitiationSource`. This is still messy using the deprecated
`allowSharingAppStoreAccount`, but at least that's abstracted out and
the new `TransactionPoster` won't deal with that deprecated property.
- I didn't mock `TransactionPoster`, which means that all existing tests
that check `PurchasesOrchestrator` continue working the same way. This
was on purpose to keep this refactor as simple as possible.
- This supersedes #2536: the change made there was now possible here
without any integration test failures, thanks to the fact that we can
process transactions independently without messing with the
`purchaseCompleteCallbacksByProductID` state now.

### Future improvements
This is just a start, there's a lot more than can be done to simplify
the `PurchasesOrchestrator` monster. The main thing would be to remove
all the custom code for `restorePurchases`, which is largely the same as
what's in `TransactionPoster` now.

### Notes:
Just posting this here for posteriority. I had to trace the call
hierarchy for both SK1 and SK2. This "block" is what's now in
`TransactionPoster`:

![IMG_C60274393F7F-1](https://github.com/RevenueCat/purchases-ios/assets/685609/5a7377f0-8301-474f-a774-a90b6e232bc3)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant