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

iOS - "Missing additionalData.appStore.discount when ordering a discount offer" #1600

Open
Ralle opened this issue Sep 20, 2024 · 5 comments

Comments

@Ralle
Copy link

Ralle commented Sep 20, 2024

Observed behavior

From July 14 to Aug 14 we had zero iOS sign ups. I checked my log and everyone had been getting this error:

Missing additionalData.appStore.discount when ordering a discount offer

It was not related to a new version of our app.

It seemed like the order of the offers was wrong. It wasn't getting the default offer. I fixed it this way:
Old:

const offer = await product?.getOffer();

New:

const offer = await product?.getOffer(
  product.platform == CdvPurchase.Platform.APPLE_APPSTORE ? "$" : undefined
)

But people still get this error once in a while, only on iOS, confirmed to be the new version of the app with my fix applied.

Any suggestions?

@j3k0
Copy link
Owner

j3k0 commented Sep 21, 2024

getOffer() just returns the first offer in the list of offers (which on iOS is generally the only one). Google Play allows multiple offers for the same product. On iOS there will be a single offer. A discount offer is handled a bit differently, it requires a server that generates signed token that gives access to discount offers.

I'm not really sure what you're trying to achieve, can you share your full source code (related to in-app purchases) and log outputs of a purchase session with verbosity set to debug in the plugin.

@Ralle
Copy link
Author

Ralle commented Sep 22, 2024

I am not doing anything particularly exotic. In fact this code worked fine for more than a year but out of the blue it started returning a different offer.

// Setup (This is React)

useEffect(() => {
  if (!isNative()) {
    return;
  }
  (async () => {
    try {
      CdvPurchase.store.verbosity = CdvPurchase.LogLevel.DEBUG;

      IAP_PRODUCT_IDS.forEach((productId) => {
        const p: CdvPurchase.IRegisterProduct = {
          id: productId,
          platform: CdvPurchase.Platform.APPLE_APPSTORE,
          type: CdvPurchase.ProductType.PAID_SUBSCRIPTION,
        };
        CdvPurchase.store.register(p);
        const p2: CdvPurchase.IRegisterProduct = {
          id: productId,
          platform: CdvPurchase.Platform.GOOGLE_PLAY,
          type: CdvPurchase.ProductType.PAID_SUBSCRIPTION,
        };
        CdvPurchase.store.register(p2);
      });

      CdvPurchase.store.error((err) => {
        console.log("Store error", err);
        errorSender.current.sendError({
          name: "CdvPurchase Error",
          message: err.message,
          stack: "",
          time: new Date(),
          context: {
            code: err.code,
            isError: err.isError,
          },
        });
      });

      CdvPurchase.store.validator = `${Env.ApiPrefix}/validate-iap-receipt`;
      CdvPurchase.store.verbosity = CdvPurchase.LogLevel.DEBUG;

      await CdvPurchase.store.initialize([
        CdvPurchase.Platform.APPLE_APPSTORE,
        CdvPurchase.Platform.GOOGLE_PLAY,
      ]);

      // Load products
      const newProducts = new Map(
        CdvPurchase.store.products.map((p) => [p.id, p])
      );
      setProducts(newProducts);
      // End load products

      // Very important to reload currencies/etc when store is initialized
      setLanguageByCode(localStorage.language ?? getBrowserLanguage(), true);

      CdvPurchase.store.when().approved(async (transaction) => {
        setOrderProcessing(true);
        console.log(
          `GlobalContextProvider - Transaction approved - transactionId: ${transaction.transactionId}`,
          transaction
        );
        transaction.verify();
      });

      CdvPurchase.store.when().unverified(async () => {
        setOrderProcessing(false);
      });
      CdvPurchase.store.when().verified(async (verifiedReceipt) => {
        setOrderProcessing(false);
        const receipt = verifiedReceipt.sourceReceipt;
        // A lot of code to verify receipt, etc
      });
    } catch (e) {
      if (e instanceof Error) {
        errorSender.current.sendError({
          name: e.name,
          message: e.message,
          stack: e.stack,
          time: new Date(),
          context: {},
        });
      }
    }
  })();
}, []);

// When they click sign up

const product = CdvPurchase.store.get(getPlanAlias(plan));
const offer = await product?.getOffer(
  product.platform == CdvPurchase.Platform.APPLE_APPSTORE ? "$" : undefined
); // DEFAULT_OFFER_ID
if (!offer) {
  global.trackError({
    name: "Native store error",
    message: "Cannot find products or offers",
    stack: "",
    context: {
      product,
      products: CdvPurchase.store.products,
    },
    time: new Date(),
  });

  setError(global.translate("cannot_start_purchase"));
  setProcessing(false);
  return;
}
console.log("offer.order()");
const purchaseResult = await offer.order();
console.log("offer.order() after");
if (purchaseResult?.isError) {
  if (purchaseResult.message === "USER_CANCELED") {
    setError(global.translate("purchase_aborted"));
  } else {
    setError(purchaseResult.message);
  }
  setProcessing(false);
} else {
  setError("");
  setProcessing(false);
}
};

It happened again Sep 21:
billede

@j3k0
Copy link
Owner

j3k0 commented Sep 23, 2024

First thing I noticed: you shouldn't initialize before setting up the events handlers (store.when goes before store.initialize) -- I wonder how that works because, IIRC, the "initialize" promise should only resolve when pending transactions in the queue have been processed (finished, verified or unverified). At the very least, you might be missing some events.

This is the code in the plugin that emits that particular error:

const discountId = offer.id !== DEFAULT_OFFER_ID ? offer.id : undefined;
const discount = additionalData?.appStore?.discount;
if (discountId && !discount) {
    return callResolve(appStoreError(ErrorCode.MISSING_OFFER_PARAMS, 'Missing additionalData.appStore.discount when ordering a discount offer', offer.productId));
}

With const DEFAULT_OFFER_ID = '$'.

(Side note, you can access that constant as CdvPurchase.AppleAppStore.DEFAULT_OFFER_ID)

So somehow offerId passed is not "$"... Could you add more logs? (Log the full "product" object) and the "offer" object being ordered?

@Ralle
Copy link
Author

Ralle commented Sep 24, 2024

I will move initialize to the end and log the whole purchase attempt on the next build. We plan to release it today, so I might be a few days until I have some logs for you.

Thank you for responding.

@zookz
Copy link

zookz commented Nov 15, 2024

I made this nasty workaround, but it was working for me.

const offer = product.getOffer(alias) || product.getOffer('$') // iOS fallback to the default offer
if (!offer) {
  console.error('offer not found', product, alias)
  return
}
CdvPurchase.store.order(offer) // call the store plugin

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

No branches or pull requests

3 participants