'How Firebase Cloudfunction check IAP subscribe changes outside from App?

I do the tutorial from CodeLab his project with all steps are here Github. Codelab helped me lot, Thanks! I do the steps and deleted all IAP products from tutorial and added only subscribable products. i have two purchase product "Normal" and "Ultimate" in the same Family in Appstoreconnect. Its working well, but I found a problem:

Situation A:

When the user subscribed one from them its working all fine, but when the user want to subscribe the other like from "Normal" to "Ultimate" or "Ultimate" to "Normal" in his Validate activ time, then Firebase Cloud don't update his Purchase (Produkt ID and Order ID Is still the old ID's). When Firebase don't update, then get the user not his upgrade to other subscribe instantly. He get there upgrade to the other purchase after a year.

Situation B:

The same Problem, but Outside from the App. User subscribed one product, then he go outside from app in his Appstore settings and change his subscribe product. Firebase get a info from Apple, but Firebase Cloud don't update the subscription information from User. can u or have u solve this problem?

my changes from Codelab ->

Constant.dart

const cloudRegion = 'europe-west1';

const subscriptionList = ["kunde_1_fahrzeug", "kunde_3_fahrzeug"];

//storeKeySubscription
const subscription_kunde_1_fahrzeug = 'kunde_1_fahrzeug';
const subscription_kunde_3_fahrzeug = 'kunde_3_fahrzeug';

IAPRepo.dart


  void updatePurchases() {
 // omitted

      // hasActiveSubscription = purchases.any((element) => element.productId == subscription_kunde_1_fahrzeug && element.status != Status.expired);
      //hasActiveSubscription = purchases.any((element) => element.productId == subscriptionList && element.status != Status.expired);
      hasActiveSubscription = purchases.any((element) => subscriptionList.any((x) => x == element.productId)  && element.status != Status.expired);
      for(PastPurchase x  in purchases){
        print("Gelb hasActiveSubscription IAP-REPO : ${x.productId} - ${x.status}");
      };

      hasUpgrade = purchases.any(
            (element) => subscriptionList.any((x) => x == element.productId),
      );
/*
      hasUpgrade = purchases.any(
            (element) => element.productId == storeKeyUpgrade,
      );
*/
      notifyListeners();

 // omitted
}

XXX_Purchase


  void purchasesUpdate() {
 // omitted

    if (products.isNotEmpty) {
      // subscriptions = products .where((element) => element.productDetails.id == subscription_kunde_1_fahrzeug) .toList();

      subscriptions = products
          .where((element) => subscriptionList.any((x) => x == element.productDetails.id))
          .toList();

      upgrades = products
          .where((element) => subscriptionList.any((x) => x == element.productDetails.id))
          .toList();

    }

 // omitted
}

  Future<void> loadPurchases() async {
 // omitted

    const ids = <String>{
      subscription_kunde_1_fahrzeug,
      subscription_kunde_3_fahrzeug,
      //storeKeyUpgrade,
    };

 // omitted
}

  Future<void> buy(PurchasableProduct product) async {
 // omitted

    // case storeKeyConsumable:
    // await iapConnection.buyConsumable(purchaseParam: purchaseParam);
    //  break;
      case subscription_kunde_1_fahrzeug:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
        break;
      case subscription_kunde_3_fahrzeug:
      //case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
        break;
 // omitted
}

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
 // omitted

      if (validPurchase) {
        // Apply changes locally
        switch (purchaseDetails.productID) {
          case subscription_kunde_1_fahrzeug:
            print("Orange:  ID Produkt:  ${purchaseDetails.productID},  ${purchaseDetails.transactionDate},  ${purchaseDetails.verificationData},  ${purchaseDetails.status},  ${purchaseDetails.purchaseID},  ${purchaseDetails.pendingCompletePurchase}, switch (purchaseDetails.productID)  case: subscription_kunde_1_fahrzeug");
            counter.applyPaidMultiplier_kunde_1_fahrzeug();
            break;
          case subscription_kunde_3_fahrzeug:
            print("Orange: ID Produkt: ${purchaseDetails.productID},  ${purchaseDetails.transactionDate},  ${purchaseDetails.verificationData},  ${purchaseDetails.status},  ${purchaseDetails.purchaseID},  ${purchaseDetails.pendingCompletePurchase},  switch (purchaseDetails.productID)  case: subscription_kunde_3_fahrzeug");
            counter.applyPaidMultiplier_kunde_3_fahrzeug();
            break;
        //   case storeKeyConsumable:
        //    counter.addBoughtDashes(2000);
        //     break;
        /* case storeKeyUpgrade:
            _beautifiedDashUpgrade = true;
            break;

          */
 // omitted
}

PastPurchase


@immutable
class PastPurchase {
 // omitted

  String get title {
    switch (productId) {
      case subscription_kunde_1_fahrzeug:
        return 'Subscription';
      case subscription_kunde_3_fahrzeug:
        return 'Subscription';
      default:
        return productId;
    }
  }
 // omitted
}

Firebase Backend

export interface ProductData {
  productId: string;
  type: "SUBSCRIPTION" | "NON_SUBSCRIPTION";
}
export const productDataMap: { [productId: string]: ProductData } = {
  "kunde_1_fahrzeug": {
    productId: "kunde_1_fahrzeug",
    type: "SUBSCRIPTION",
  },
  "kunde_3_fahrzeug": {
    productId: "kunde_3_fahrzeug",
    type: "SUBSCRIPTION",
  },
};



Solution 1:[1]

The problem is the codelab is unrealistically simplistic related to subscriptions, and relies on a node.js package that also doesn't handle in-family subscription changes. Subscription changes with apple don't provide the new product_id, they provide the new subscription id as auto_renew_product_id, and the product_id stays the same from the original transaction. Turn verbose: true to see it when running your function.

So, to fix, you'd need a third function for in-app subscription changes, which you can't validate and return properly from apple-receipt-verify because that package doesn't provide the auto_renew_product_id that you're switching to. So you'll need a new way to validate the receipt.

For changes outside the app, you'll need to fix the handleServerEvent, because that doesn't work to change subscriptions from outside app.

I suggest RevenueCat. The cost is minimal, and you'll have a company with a vested interest in updating the API's.

Your revenue foundation needs to be firm, and while the Google Play Store performance of the package and server-side code was spotless for me...their interaction with Apple is barely functional for the simplest of scenarios.

Edit: A couple points to clarify, expound on for future readers.

  1. There is a typo as of May 2022 in the code lab. You need to use Type 1 Notifications from Apple.
  2. Apple handles in-group subscription changes itself. It will unsubscribe and subscribe within a group. You also need to rank your in-group subscriptions with apple. Android just changes right away.
  3. Apple handles down-grade, cross-grade and upgrade differently. All downgrades must finish their term before changing. This prevents apple from having to refund. So when you downgrade, the term must finish, then apple will change the sub and fire the server event.
  4. The code lab logic has Android creating/canceling subs and leaving a sub history trail of purchases documents in firestore...this is because every sub change is a cancel and create. Apple however changes the subs internally, and then fires to the server telling you what changed. So the code lab does not have a doc trail detailing any subscription history..each sub is one doc always...just changes product id and status.

In short: doing the codelab, and getting to a functional level for your app will definitely give you a good understanding of how each store handles subs...and how differently they handle them. As it relates to the in_app_purchase plug-in, server side validation and performance...while elegantly done, it was the bare minimum guidance given by the codelab and package.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1