Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed purchasing with PromotionalOffers using StoreKit 2 #2020

Merged
merged 2 commits into from
Dec 9, 2022

Conversation

NachoSoto
Copy link
Contributor

@NachoSoto NachoSoto commented Nov 2, 2022

Fixes SDKONCALL-160, SDKONCALL-182, #2114, and RevenueCat/react-native-purchases#455.
Depends on #2118 and https://github.com/RevenueCat/khepri/pull/4843 / https://github.com/RevenueCat/khepri/pull/4852.

Changes:

[Purchases] - ERROR: 😿‼️ The information associated with this PromotionalOffer is not valid.
See https://rev.cat/ios-subscription-offers for more info. The signature generated by RevenueCat could not be decoded: signature 914

@NachoSoto NachoSoto added the pr:fix A bug fix label Nov 2, 2022
@NachoSoto NachoSoto requested a review from a team November 2, 2022 18:54
@@ -122,11 +122,20 @@ extension PromotionalOffer.SignedData {

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
var sk2PurchaseOption: Product.PurchaseOption {
let signature: Data

if let decoded = Data(base64Encoded: self.signature) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was hoping this would fix it, but the test is still failing :/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm out of ideas, I'll probably have to file a Radar and pray? 🙏🏻

Copy link
Contributor Author

Choose a reason for hiding this comment

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

⚠️

Looks like one of our customers tried to do this without RC, using StoreKit directly, and he gets the same result, so that confirms the fact that this is an Apple bug.
He's opened a TSI with Apple, so hopefully they can address this soon.

In the mean time, I'll leave this PR open. I believe the fix here will still be required (doing the base64 decode). But once Apple fixes it on their end we can use this PR to confirm, as well as the now enabled test.

Choose a reason for hiding this comment

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

Do you have the ticket # with Apple?

Choose a reason for hiding this comment

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

Not sure about the original, but we reported it today as well under: FB11764024.

if let decoded = Data(base64Encoded: self.signature) {
signature = decoded
} else {
// TODO: log warning
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, definitely worth logging something loud here.
Stuff logged from here would also show up in the diagnostics API call, right? since we get offerings and parse products?

Copy link
Contributor Author

@NachoSoto NachoSoto Nov 3, 2022

Choose a reason for hiding this comment

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

Yeah, definitely worth logging something loud here.

👍🏻

Stuff logged from here would also show up in the diagnostics API call, right? since we get offerings and parse products?

This property is only used when purchasing though.

NachoSoto added a commit that referenced this pull request Nov 4, 2022
…ons`

This changes the default back to `StoreKit 1`. We decided to do this for the following reasons:
- Purchasing with `PromotionalOffer`s does not work with StoreKit 2 due to an Apple bug (see #2020 (comment))
- `checkTrialOrIntroDiscountEligibility` is significantly slower with StoreKit 2 (#1893). We're adding optimizations to help with that (#2007), but the underlying logic will still be slow.
- A rare race-condition where `StoreKit 2` does not have transactions after a purchase ([TRIAGE-82]). We have some workarounds (#1945), but it's still being investigated.

_ Note: This effectively reverts 0ee540a. That commit made it easier to only change the default in one place which is why this PR is basically just one line._
NachoSoto added a commit that referenced this pull request Nov 4, 2022
…ons` (#2022)

This changes the default back to `StoreKit 1`. We decided to do this for
the following reasons:
- Purchasing with `PromotionalOffer`s does not work with StoreKit 2 due
to an Apple bug (see
#2020 (comment))
- `checkTrialOrIntroDiscountEligibility` is significantly slower with
`StoreKit 2` (#1893). We're adding optimizations to help with that
(#2007), but the underlying logic will still be slow.
- A rare race-condition where `StoreKit 2` does not have transactions
after a purchase ([TRIAGE-82]). We have some workarounds (#1945), but
it's still being investigated.

_Note: This effectively reverts 0ee540a. That commit made it easier to
only change the default in one place which is why this PR is basically
just one line._

[TRIAGE-82]:
https://revenuecats.atlassian.net/browse/TRIAGE-82?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
@NachoSoto
Copy link
Contributor Author

I just realized this cursed PR is # 2020 🙈

@aboedo
Copy link
Member

aboedo commented Nov 9, 2022

I just realized this cursed PR is # 2020 🙈

#destiny

@mamouneyya
Copy link

Update: We noticed today that this started to work actually. Also, I can confirm that the decoding logic in this PR is important. When not decoding the signature, I was getting the invalidOfferSignature error code every time.

Something I'm a bit confused about is the fact that this is now working even when using a simulator and a StoreKit testing store file. I thought that in that scenario, Xcode won't make any remote request, so I cannot understand how this was fixed from the App Store server side. Glad it is though.

@NachoSoto
Copy link
Contributor Author

Interesting, our integration test is still failing. I'll dig more next week though.

@NachoSoto
Copy link
Contributor Author

Update: We noticed today that this started to work actually. Also, I can confirm that the decoding logic in this PR is important. When not decoding the signature, I was getting the invalidOfferSignature error code every time.

Weird, I can't get this to work even with this fix. Are you doing something else differently? Is it running on iOS 16.1.2 only or something?

@NachoSoto NachoSoto requested a review from a team December 5, 2022 18:43
@NachoSoto NachoSoto changed the title StoreKit2: fix purchasing with PromotionalOffers StoreKit 2: fix purchasing with PromotionalOffers Dec 5, 2022
@mamouneyya
Copy link

mamouneyya commented Dec 5, 2022

Weird, I can't get this to work even with this fix. Are you doing something else differently? Is it running on iOS 16.1.2 only or something?

My personal device is still at 15.7. I could get it working on it, plus when using Xcode 14.1 with a simulator and a StoreKit test store file. Another colleague confirmed he could get it working on 15.7 as well.

Not sure this is relevant to you, but I noticed that when using the sample server app Apple attached here to generate the signature, you shouldn't omit the applicationUsername param but instead you should pass an empty string when you don't want to include a appAccountToken(_:) in the options. Otherwise, the token will be included in the signature as a string value of "undefined".

@mamouneyya
Copy link

The test cases we validated included when you're an active member or a former member. Meaning, purchasing a subscription with a promo offer when you have an active subscription already, and purchasing a subscription with a promo offer after cancelling a previous subscription.

@NachoSoto
Copy link
Contributor Author

Thanks, I just ran it on iOS 15.x and it's still failing. I verified in our backend that the username is never empty.

@NachoSoto
Copy link
Contributor Author

I love how the docs aren't even updated for SK2?

@mamouneyya
Copy link

mamouneyya commented Dec 5, 2022

Thanks, I just ran it on iOS 15.x and it's still failing. I verified in our backend that the username is never empty.

By never empty, do you mean that you're actually using a value when generating the signature? If that's the case, then you'll have to pass an .appAccountToken(_:) purchase option with the exact same value.

@NachoSoto
Copy link
Contributor Author

By never empty, do you mean that you're actually using a value when generating the signature? If that's the case, then you'll have to pass an .appAccountToken(_:) purchase option with the exact same value.

Whoa we're definitely not doing that.

This is still not working:

options.insert(signedData.sk2PurchaseOption)
options.insert(.appAccountToken(UUID(uuidString: self.appUserID)!))

But I feel like we're on the right track. I missed this:

If you're providing an appAccountToken in the purchase options, you must include that value when you generate the signature. Use lowercase for the UUID string representations of the app account token and the nonce in the siganture.

@NachoSoto
Copy link
Contributor Author

(Testing now calling lowercased())

@NachoSoto
Copy link
Contributor Author

applicationUsername or appAccountToken
An optional string value that you define; may be an empty string. If your app uses applicationUsername, provide applicationUsername. If your app uses appAccountToken, provide appAccountToken. Use lowercase for the string representation of appAccountToken.

Our backend isn't lowercasing the user ID for SK2, so that's likely the issue.

@NachoSoto
Copy link
Contributor Author

CONFIRMED!
Using a lowercase user ID (and passing the missing .appAccountToken) makes the test pass.

Thanks a lot for that pointer @mamouneyya! We'll get this fixed.

@mamouneyya
Copy link

CONFIRMED! Using a lowercase user ID (and passing the missing .appAccountToken) makes the test pass.

Thanks a lot for that pointer @mamouneyya! We'll get this fixed.

Wohoo! Glad you got it working! 🎉

NachoSoto added a commit that referenced this pull request Dec 5, 2022
@NachoSoto NachoSoto marked this pull request as ready for review December 5, 2022 23:49
@NachoSoto NachoSoto removed the blocked label Dec 5, 2022
@NachoSoto NachoSoto changed the title StoreKit 2: fix purchasing with PromotionalOffers Fixed purchasing with PromotionalOffers using StoreKit 2 Dec 5, 2022
if let decoded = Data(base64Encoded: self.signature) {
signature = decoded
} else {
throw Error.failedToDecodeSignature(self.signature)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I improved error handling here (the last TODO) and added a few tests.

@@ -376,8 +376,6 @@ class StoreKit1IntegrationTests: BaseBackendIntegrationTests {
@available(iOS 15.2, tvOS 15.2, macOS 12.1, watchOS 8.3, *)
func testPurchaseWithPromotionalOffer() async throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()
try XCTSkipIf(Self.storeKit2Setting == .enabledForCompatibleDevices,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🎉

@aboedo
Copy link
Member

aboedo commented Dec 7, 2022

thinking more about this:
if, for affected versions, the backend sent the signature as utf 8 instead of base64 encoded, wouldn't that fix them?

@NachoSoto
Copy link
Contributor Author

thinking more about this:
if, for affected versions, the backend sent the signature as utf 8 instead of base64 encoded, wouldn't that fix them?

Here's an example base64 encoded signature:

MEQCIEQlmZRNfYzKBSE8QnhLTIHZZZWCFgZpRqRxHss65KoFAiAJgJKjdrWdkLUOCCjuEx2RmFS7daRzSVZRVZ8RyMyUXg

And decoded:

0D� D%M}�!<BxKLe��iFq�:�� v��(��TusIVQU�̔^

I suppose if the backend sent it without encoding, and we create the Data with the existing implementation (Data(self.utf8)) it might work.
We should test this locally. Who can help with that?

NachoSoto added a commit that referenced this pull request Dec 9, 2022
@NachoSoto NachoSoto requested a review from aboedo December 9, 2022 18:08
NachoSoto added a commit that referenced this pull request Dec 9, 2022
…d` (#2118)

This will be used for https://github.com/RevenueCat/khepri/pull/4843, to
fix [SDKONCALL-160] and #2020.

`X-StoreKit2-Setting` only told us if the apps had that setting enabled.
However, when running on an older device, `SK2` would still be disabled.

[SDKONCALL-160]:
https://revenuecats.atlassian.net/browse/SDKONCALL-160?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
Comment on lines +482 to +483
// This should be base64
signature: "signature \(Int.random(in: 0..<1000))",
Copy link
Member

Choose a reason for hiding this comment

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

why not base64 the result of this so it looks realistic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is

testPurchaseSK2PackageWithInvalidPromotionalOfferSignatureThrowsError

So I'm testing that if it's not base64 (so it's just a random string), it produces the correct error instead of crashing or something.

@NachoSoto
Copy link
Contributor Author

Merging 🎉

@NachoSoto NachoSoto merged commit 8e32400 into main Dec 9, 2022
@NachoSoto NachoSoto deleted the sk2-promo-purchases branch December 9, 2022 23:04
@KrisJulioRSVP
Copy link

KrisJulioRSVP commented Mar 20, 2023

This solves my problem, Im just passing empty string in applicationUsername and it works. Thank you guys! Apple documentation is pain in the a$$

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

StoreKit 2 - Promotional Offers - .invalidOfferSignature error. Can't purchase any offer.
5 participants