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 ->
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';
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
}
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
}
@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
}
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",
},
};
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.
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.