purchases-capacitor icon indicating copy to clipboard operation
purchases-capacitor copied to clipboard

iOS: Purchases.purchasePackage({ aPackage: packageToPurchase }) hangs.

Open snovak opened this issue 8 months ago • 9 comments

I'd first like to mention that this is working on Android. Only fails on iOS.

Describe the bug Environment package.json...

  "dependencies": {
    "@capacitor/android": "^7.1.0",
    "@capacitor/cli": "^7.1.0",
    "@capacitor/core": "^7.1.0",
    "@capacitor/filesystem": "^7.0.0",
    "@capacitor/ios": "^7.1.0",
    "@capacitor/keyboard": "^7.0.0",
    "@capacitor/local-notifications": "^7.0.0",
    "@capacitor/preferences": "^7.0.0",
    "@capacitor/share": "^7.0.0",
    "@headlessui/vue": "^1.7.23",
    "@heroicons/vue": "^2.2.0",
    "@revenuecat/purchases-capacitor": "^10.2.2",
    "axios": "^1.7.7",
    "marked": "^15.0.2",
    "pinia": "^2.2.6",
    "status-bar-height": "file:status-bar-height",
    "vue": "^3.5.12",
    "vue-matomo": "^4.2.0",
    "vue-router": "^4.4.5"
  },

StoreKit version: 2 Device and/or simulator: - [x] Device - [x] Simulator Environment: - [x] Sandbox - [x] TestFlight - [x] Production How widespread is the issue. Percentage of devices affected. I would suspect it happens on all devices? Or maybe I'm the only one? Same result on all that I've tested.

Debug logs that reproduce the issue. Complete logs with Purchases.logLevel = .verbose will help us debug this issue. I've replaced anything that looks like a key with xxxxxxxxxxxxx

VERBOSE: Creating intermediate key with expiration '2025-06-11 00:00:00 +0000': xxxxxxxxxxxxxx
VERBOSE: Signature passed verification
VERBOSE: Storing etag '4d88e28e0bb7da8a' for request to 'https://api.revenuecat.com/v1/subscribers/$RCAnonymousID%xxxxxxxxxxxxxxxx/offerings' (success)
DEBUG: ℹ️ API request completed: GET '/v1/subscribers/$RCAnonymousID%xxxxxxxxxxxxxxxxx/offerings' (304)
Request-ID: 'xxxxxxxxxx'; Amzn-Trace-ID: 'Root=xxxxxxxxxxxxx'
DEBUG: ℹ️ No existing products cached, starting store products request for: ["full_monthly", "full_yearly"]
DEBUG: ℹ️ GetOfferingsOperation: Finished
DEBUG: ℹ️ Serial request done: GET /v1/subscribers/$RCAnonymousID%xxxxxxxxxxxxxx/offerings, 0 requests left in the queue
DEBUG: ℹ️ GetCustomerInfoOperation: Started
DEBUG: ℹ️ There are no requests currently running, starting request GET /v1/subscribers/$RCAnonymousID%xxxxxxxxxxxxx
VERBOSE: Using etag 'd3c1e29e6a8cdf11' for request to 'https://api.revenuecat.com/v1/subscribers/$RCAnonymousID%xxxxxxxxxxxxxx'. Validation time: 2025-03-31 14:33:21 +0000
DEBUG: ℹ️ API request started: GET '/v1/subscribers/$RCAnonymousID%xxxxxxxxxxxxx'
VERBOSE: Creating intermediate key with expiration '2025-06-11 00:00:00 +0000': xxxxxxxxxxxxxx
VERBOSE: Signature passed verification
VERBOSE: Storing etag 'd3c1e29e6a8cdf11' for request to 'https://api.revenuecat.com/v1/subscribers/$RCAnonymousID%xxxxxxxxxxxxx' (createdSuccess)
DEBUG: ℹ️ API request completed: GET '/v1/subscribers/$RCAnonymousID%xxxxxxxxxxxxx' (304)
Request-ID: 'efa8beb8-0153-4ac6-8625-1c9c38c352d5'; Amzn-Trace-ID: 'Root=1-67eaa824-7bbd95120e61544c12b105fe'
VERBOSE: Updating CustomerInfo '$RCAnonymousID:xxxxxxxxxxxxx' request date: 2025-03-31 14:35:16 +0000
DEBUG: 😻 CustomerInfo updated from network.
DEBUG: ℹ️ GetCustomerInfoOperation: Finished
DEBUG: ℹ️ Serial request done: GET /v1/subscribers/$RCAnonymousID%xxxxxxxxxxxxx, 0 requests left in the queue
DEBUG: 😻 Store products request received response
DEBUG: ℹ️ Store products request finished
DEBUG: 😻 Offerings updated from network.
DEBUG: ℹ️ Warming up intro eligibility cache for 2 products
DEBUG: ℹ️ No existing products cached, starting store products request for: ["full_monthly", "full_yearly"]
VERBOSE: Warming up paywall images cache: [https://assets.pawwalls.com/1151531_1737931765.heic]
DEBUG: 😻 Store products request received response
DEBUG: ℹ️ Store products request finished
DEBUG: ℹ️ Caching trial or intro eligibility for products: ["full_yearly", "full_monthly"]
⚡️  [error] - There was an error setting cookie `_pk_ref.6.1fff`. Please check domain and path.
⚡️  [error] - There was an error setting cookie `_pk_id.6.1fff`. Please check domain and path.
⚡️  [error] - There was an error setting cookie `_pk_ses.6.1fff`. Please check domain and path.
⚡️  To Native ->  Purchases getOfferings 84254070
DEBUG: ℹ️ Vending Offerings from memory cache
⚡️  TO JS {"all":{"Standard Offering.":{"monthly":{"identifier":"$rc_monthly","product":{"pricePerMonth":12.99,"title":"Monthly Subscription","currencyCode":"USD","introPrice":null,"pricePerYearString":"$155.88","productCategory":"SUBSCRIPTION","description":"","pri
⚡️  [log] - Offerings:  {"all":{"Standard Offering.":{"monthly":{"identifier":"$rc_monthly","product":{"pricePerMonth":12.99,"title":"Monthly Subscription","currencyCode":"USD","introPrice":null,"pricePerYearString":"$155.88","productCategory":"SUBSCRIPTION","description":"","pricePerYear":155.88,"discounts":[],"pricePerMonthString":"$12.99","subscriptionPeriod":"P1M","identifier":"full_monthly","pricePerWeekString":"$2.99","price":12.99,"productType":"AUTO_RENEWABLE_SUBSCRIPTION","priceString":"$12.99","pricePerWeek":2.99},"packageType":"MONTHLY","offeringIdentifier":"Standard Offering.","presentedOfferingContext":{"targetingContext":null,"offeringIdentifier":"Standard Offering.","placementIdentifier":null}},"annual":{"product":{"description":"","pricePerYearString":"$129.99","productType":"AUTO_RENEWABLE_SUBSCRIPTION","title":"Yearly Subscription","currencyCode":"USD","pricePerYear":129.99,"subscriptionPeriod":"P1Y","identifier":"full_yearly","pricePerMonth":10.83,"productCategory":"SUBSCRIPTION","discounts":[],"price":129.99,"pricePerWeek":2.49,"priceString":"$129.99","pricePerWeekString":"$2.49","introPrice":null,"pricePerMonthString":"$10.83"},"offeringIdentifier":"Standard Offering.","identifier":"$rc_annual","packageType":"ANNUAL","presentedOfferingContext":{"offeringIdentifier":"Standard Offering.","placementIdentifier":null,"targetingContext":null}},"availablePackages":[{"offeringIdentifier":"Standard Offering.","presentedOfferingContext":{"placementIdentifier":null,"targetingContext":null,"offeringIdentifier":"Standard Offering."},"identifier":"$rc_annual","packageType":"ANNUAL","product":{"discounts":[],"description":"","identifier":"full_yearly","pricePerYear":129.99,"productType":"AUTO_RENEWABLE_SUBSCRIPTION","currencyCode":"USD","subscriptionPeriod":"P1Y","pricePerMonth":10.83,"productCategory":"SUBSCRIPTION","priceString":"$129.99","introPrice":null,"price":129.99,"pricePerWeek":2.49,"pricePerWeekString":"$2.49","pricePerYearString":"$129.99","pricePerMonthString":"$10.83","title":"Yearly Subscription"}},{"offeringIdentifier":"Standard Offering.","presentedOfferingContext":{"targetingContext":null,"placementIdentifier":null,"offeringIdentifier":"Standard Offering."},"packageType":"MONTHLY","identifier":"$rc_monthly","product":{"productCategory":"SUBSCRIPTION","pricePerYear":155.88,"discounts":[],"productType":"AUTO_RENEWABLE_SUBSCRIPTION","title":"Monthly Subscription","currencyCode":"USD","subscriptionPeriod":"P1M","identifier":"full_monthly","pricePerMonthString":"$12.99","pricePerYearString":"$155.88","pricePerWeek":2.99,"pricePerWeekString":"$2.99","pricePerMonth":12.99,"description":"","introPrice":null,"priceString":"$12.99","price":12.99}}],"metadata":{},"identifier":"Standard Offering.","serverDescription":"The standard set of packages"}},"current":{"availablePackages":[{"identifier":"$rc_annual","packageType":"ANNUAL","offeringIdentifier":"Standard Offering.","presentedOfferingContext":{"placementIdentifier":null,"offeringIdentifier":"Standard Offering.","targetingContext":null},"product":{"identifier":"full_yearly","pricePerWeek":2.49,"pricePerYear":129.99,"discounts":[],"priceString":"$129.99","currencyCode":"USD","productCategory":"SUBSCRIPTION","title":"Yearly Subscription","subscriptionPeriod":"P1Y","productType":"AUTO_RENEWABLE_SUBSCRIPTION","price":129.99,"pricePerWeekString":"$2.49","pricePerMonth":10.83,"pricePerMonthString":"$10.83","description":"","pricePerYearString":"$129.99","introPrice":null}},{"offeringIdentifier":"Standard Offering.","product":{"pricePerYearString":"$155.88","price":12.99,"identifier":"full_monthly","description":"","pricePerWeek":2.99,"pricePerWeekString":"$2.99","productCategory":"SUBSCRIPTION","productType":"AUTO_RENEWABLE_SUBSCRIPTION","title":"Monthly Subscription","pricePerMonthString":"$12.99","priceString":"$12.99","subscriptionPeriod":"P1M","currencyCode":"USD","discounts":[],"pricePerMonth":12.99,"introPrice":null,"pricePerYear":155.88},"identifier":"$rc_monthly","presentedOfferingContext":{"placementIdentifier":null,"targetin
⚡️  [log] - purchasePackage {"offeringIdentifier":"Standard Offering.","product":{"pricePerYearString":"$155.88","price":12.99,"identifier":"full_monthly","description":"","pricePerWeek":2.99,"pricePerWeekString":"$2.99","productCategory":"SUBSCRIPTION","productType":"AUTO_RENEWABLE_SUBSCRIPTION","title":"Monthly Subscription","pricePerMonthString":"$12.99","priceString":"$12.99","subscriptionPeriod":"P1M","currencyCode":"USD","discounts":[],"pricePerMonth":12.99,"introPrice":null,"pricePerYear":155.88},"identifier":"$rc_monthly","presentedOfferingContext":{"placementIdentifier":null,"targetingContext":null,"offeringIdentifier":"Standard Offering."},"packageType":"MONTHLY"}

Steps to reproduce, with a description of expected vs. actual behavior ✅ Initialization works.
✅ getOfferings works.
❌ A product is selected and passed to purchasePackage. I would expect RevCat to pop a payment completion view, like it does on the Android side. Instead, it hangs indefinitely. For example:

          await RevenueCatService.purchasePackage ({
              "presentedOfferingContext": {
                  "offeringIdentifier": "Standard Offering.",
                  "placementIdentifier": null,
                  "targetingContext": null
              },
              "offeringIdentifier": "Standard Offering.",
              "product": {
                  "priceString": "$12.99",
                  "pricePerMonthString": "$12.99",
                  "pricePerWeek": 2.99,
                  "pricePerYear": 155.88,
                  "pricePerYearString": "$155.88",
                  "pricePerMonth": 12.99,
                  "title": "Monthly Subscription",
                  "price": 12.99,
                  "productType": "AUTO_RENEWABLE_SUBSCRIPTION",
                  "subscriptionPeriod": "P1M",
                  "currencyCode": "USD",
                  "discounts": [],
                  "pricePerWeekString": "$2.99",
                  "productCategory": "SUBSCRIPTION",
                  "introPrice": null,
                  "description": "",
                  "identifier": "full_monthly"
              },
              "identifier": "$rc_monthly",
              "packageType": "MONTHLY"
          })

This is the RevenueCatService.purchasePackage function:

  static async purchasePackage(packageToPurchase) {
    try{
      const result = await Purchases.purchasePackage({ aPackage: packageToPurchase })
      console.log("Purchase Result:", result)  //never gets here.
      return result
    }catch (e){
      console.log("revenuecat.purchasePackage Failed: ", e)
    }
  }

Uploaded some images here: https://community.revenuecat.com/general-questions-7/ios-capacitor-plugin-purchases-purchasepackage-apackage-packagetopurchase-hangs-6161

snovak avatar Mar 31 '25 14:03 snovak

👀 We've just linked this issue to our internal tracker and notified the team. Thank you for reporting, we're checking this out!

RCGitBot avatar Mar 31 '25 14:03 RCGitBot

Image To help illustrate, on android the payment view appears. I would expect the equivalent action in iOS, but it hangs silently. No errors.

snovak avatar Apr 01 '25 15:04 snovak

Update on this. Instead of waiting for this to get fixed, I decided to roll my own Capacitor plugin for PlayStore and StoreKit. I got it working for Android first, then I got stuck on iOS in the exact same spot the RevenueCat plugin was failing. I added a ton of logs, trying to figure out what was keeping getOffers from returning a result. So, I started over again, with https://github.com/jordancalhoun/StoreKit2 as a working pay flow example. Its working now!

Still not 100% sure what the deal was, but I suspect it has something to do with the way getOfferings returns it's results. Perhaps a change in StoreKit2? If this is not a widespread issue, it could be due to using a VueJS frontend.

Anyways, do you want me to close this?

snovak avatar Apr 06 '25 16:04 snovak

Hi @snovak, Thank you for sharing it! Do you know with which iOS version where you testing it with?

nyeu avatar Apr 08 '25 07:04 nyeu

This is still an issue - the call never returns. I am on the latest iOS version - 18.3.2

hahagu avatar Apr 08 '25 08:04 hahagu

I have resolved this by using toRaw() on the function call.

hahagu avatar Apr 08 '25 17:04 hahagu

Oh! Thanks for looking into this @hahagu ! I see my error. I was unintentionally passing in the selectedProduct.value (a VueJS reactive proxy), and I need toRaw() or something like {...selectedProduct.value} for it to work.

If I were to request anything here it might be better logging, letting me know my input wasn't matching expectations.

Thanks again!

snovak avatar Apr 08 '25 19:04 snovak

Yes, I do agree a better logging would've helped me out a ton @snovak . Also weird that it works on the Android side but not on iOS.

hahagu avatar Apr 08 '25 20:04 hahagu

Very weird @hahagu . That's probably what threw me. I was more focused on StoreKit being the culprit since android was working.

snovak avatar Apr 08 '25 20:04 snovak

Summarizing the issue here:

This stems from passing a Vue Proxy-wrapped object (from Vue’s reactivity system) into a Capacitor-native bridge. These native bridges can’t serialize Proxy objects, so the call silently fails — it never resolves or rejects.

To confirm this is the case, you can check whether your product object is reactive like this:

import { isProxy } from 'vue';

console.log('Is proxy?', isProxy(this.products[productId]));

If it logs true, that means you’re passing a Proxy and need to unwrap it before passing it into purchasePackage.

To resolve the issue, you could do:

import { toRaw } from 'vue';

// …

let rawPackage = toRaw(this.products[productId]);
let result = await Purchases.purchasePackage({ aPackage: rawPackage });

This will strip away the Proxy wrapper and pass the raw object into the RevenueCat SDK.

cperriam-rc avatar May 30 '25 22:05 cperriam-rc