Checking for the presence of `Xcode/DerivedData` in the receipt path was
failing for TestFlight builds or if the .app bundle generated by Xcode
was moved from the folder.
Additionally, checking for `MASReceipt/receipt` seems less robust than
simply using compiler directives.
Instead, for macOS apps, we check the environment embedded in the receipt and alternatively check the signature of the app bundle.
---------
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
This was wrong in #3108. `ProcessInfo+Extensions` was added to the
`ReceiptParser` target, but not when building through SPM. This fixes
that.
We didn't catch this with the `spm-receipt-parser` CI job because that
does a release build.
This adds tests and a fix for a scenario discovered by @aboedo and
@mcastany.
### Timeline:
- Make a purchase of product_1
- Close app
- Renewals happen in the background
- Subscription expires
- Open app, renewals are in the queue but not processed yet
- Start a purchase of `product_1` (`SKPaymentQueue.add`)
- StoreKit queue shows renewal from step 3 as finished, it's expired
- SDK sees finished transaction in the queue for `product_1`
- SDK sees completion block for `product_1`, considers it a successful
`.purchase`
- SDK posts receipt, `initiation_source: .purchase` 💥
- Backend replies with 422 because StoreKit hasn't processed the
transaction from 6
### Fix:
The fix looks at the date when the completion block was added (the time
`.purchase()` is called).
If the transaction was created _before_ this, we know it didn't come
from the actual purchase, and therefore it should be identified as
`.queue`.
A bunch of tests were failing on Xcode 15 due to [a change in
Swift](https://github.com/apple/swift/issues/66466), because we relying
on being able to verify that the encoded `Data` saved to caches is
consistent.
Starting on Swift 5.9 `JSONEncoder` has optimizations that mean we can't
rely on it.
To fix this, this PR does 2 things:
- Changed `JSONEncoder.prettyPrinted` to have `.sortedKeys`
- Changed test to verify the decoded response `struct`, not the raw
encoded `Data`
Note that I didn't make `JSONEncoder.default` use `.sortedKeys` because
of the potential performance impact. `Encodable.prettyPrintedData` is
used in tests like `ReceiptFetcherTests` (which this PR fixes) and for
debug purposes, but `Encodable.encodedJSON` is used everywhere.
This prevents users from keeping an outdated active entitlement forever.
One could start a free trial then block all further requests from the
backend in order to keep that active.
When this check fails, a new warning log is generated:
> WARN: ⚠️ Entitlement is no longer active (expired 2023-02-12 18:15:11
+0000) and it's outside grace period window (last updated 2023-02-12
18:13:31 +0000)
- Fixes [TRIAGE-221] for `SK1`.
- Filed `FB11889732` for `SK2`.
### Changes:
- Added 2 `IntroEligibilityCalculatorTests` unit tests to cover this
scenario
- Added 1 `IntroEligibilityCalculatorTests` unit tests to cover the
equivalent of the `testIneligibleForIntroAfterPurchaseExpires`
integration test
- Added 2 `StoreKitIntegrationTests` to cover this scenario (one of them
disabled in SK2)
- Updated `IntroEligibilityCalculator` logic to fix the new tests.
- Refactored `IntroEligibilityCalculatorTests` to avoid duplication and
allow adding new cases more easily.
- Added a few more tests to ensure other cases are correct.
[TRIAGE-221]:
https://revenuecats.atlassian.net/browse/TRIAGE-221?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
Fixes [CSDK-622].
### Changes:
- Extracted `FileReader` to be able to use it in `ReceiptParser`
- Extracted `MockBundle` to be able to use it in `ReceiptParser`
- Added new `public` `ReceiptParser.fetchAndParseLocalReceipt()`, **only
to `ReceiptParser`** (this is not available through `RevenueCat`).
- Added tests for the new API
- Added new API to `ReceiptParserAPITester`
Initially I thought about moving `ReceiptFetcher` to the
`Receipt|Parser` target. Unfortunately that would require moving a whole
bunch of other dependencies (`OperationDispatcher`, etc, etc).
We have no easy way to create a third "Core" framework in which to place
these common dependencies, so that wouldn't be trivial.
This solution adds a much simpler version of `ReceiptFetcher` that
simply reads from `Bundle.appStoreReceiptURL`. As it's documented there,
under normal circumstances `SKReceiptRefreshRequest` is not required, so
this implementation should work for 99% of use cases, and so this way we
avoid increasing the.codebase complexity unnecessarily.
[CSDK-622]:
https://revenuecats.atlassian.net/browse/CSDK-622?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
The most obvious way to create `PurchasesReceiptParser` is using
`.init`.
In order to expose that we'd need to `override` `init` from an
`extension` because each (RC / `ReceiptParser`) `target` needs a
different `LoggerType`. However, `NSObject.init` can't be overridden
from an extension.
That's why the API exposes `default` instead. This will make the usage
clearer:

The name `InAppPurchase` is too general. This type in particular is
specific to the content of `AppleReceipt`, so nesting it makes that
clear.
Follow up to #1933.
This will help us debug flaky tests.
Example:
> INFO: 😻💰 Purchased product -
'com.revenuecat.monthly_4.99.1_week_intro'
DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from
Apple.
DEBUG: ℹ️ Loaded receipt from url
file:///Users/nachosoto/Library/Developer/CoreSimulator/Devices/A3576DC2-355E-45BA-B32C-D2C0A3811BB4/data/Containers/Data/Application/EDF3B7C8-A4BD-4629-813E-0934528AF02E/StoreKit/receipt
INFO: ℹ️ Receipt parsed successfully
INFO: ℹ️ Receipt content:
["inAppPurchases": [], "creationDate": 2022-09-21 21:00:07 +0000,
"opaqueValue": 8 bytes, "applicationVersion": "1",
"originalApplicationVersion": "<unknown>", "bundleId":
"com.revenuecat.StoreKitTestApp", "expirationDate": 4001-01-01 00:00:00
+0000, "sha1Hash": 20 bytes]
/Users/nachosoto/dev/purchases-ios/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift:64:
error: -[BackendIntegrationTests.StoreKit2IntegrationTests
testCanPurchaseProduct] : failed - Expected Entitlement. Got: [:]
expected to have Dictionary<String, EntitlementInfo> with count 1, got 0
Actual Value: [:]
I've also added the receipt as a test attachment so we can download it
from `CircleCI`:

* made a few attributes that aren't present in SKTest receipts optional, added test with IAP
* added more tests for every field in the receipt
* fixed a few places where `asDict` wasn't correctly considering nullability in the IAP receipt fields
* fixed length calculation for indefinite-length containers
* temporarily removed the early return from empty receipts
* fixed length calculation when parsing receipts with indefinite length
* fix linting
* added basic ugly test
* fixed local receipt parsing for sktest, broke regular receipt parsing temporarily
* updated length calculation so that it works for both storekit test and regular receipts
* updated test to allow for missing OriginalAppVersion in receipt
* fixed linting in imports
* fixed checks for non-storekitTest receipts
* added a longer comment to explain the length == 0 case
* cleaned up the still-ugly tests for the receipt parser