cordova-plugin-purchase icon indicating copy to clipboard operation
cordova-plugin-purchase copied to clipboard

[IOS] InAppPurchase2 restore expired purchase

Open TanyaDolgopolova opened this issue 4 years ago • 4 comments
trafficstars

System info

macOS Big Sur 11.2.3
Cordova 10.0.0
Plugin versions:
    "cc.fovea.cordova.purchase": "^10.1.1",
    "@ionic-native/in-app-purchase-2": "^5.28.0",

Implementation code

export class SelectPackageModalComponent {
  private transactionType: CreditType =
    Capacitor.getPlatform() === 'ios' ? CreditType.IOS : CreditType.ANDROID;
  private destroy: Subject<boolean> = new Subject<boolean>();

  @Input() orderId: number;
  packages: IPackage[] = [];
  dir: string = 'ltr';

  constructor(
    private alert: AlertController,
    private modalCtrl: ModalController,
    private packageService: PackageService,
    private paymentService: PaymentService,
    private platform: Platform,
    private inAppPurchase: InAppPurchase2,
    public translate: TranslateService
  ) {}

  ngOnInit() {
    this.packageService
      .getAll()
      .pipe(takeUntil(this.destroy))
      .subscribe((packages: IPackage[]) => {
        this.packages = packages;
        this.platform.ready().then(() => {
          // Only for debugging!
          this.inAppPurchase.verbosity = this.inAppPurchase.DEBUG;
          this.registerProducts();

          for (const packageData of this.packages) {
            const packageId =
              this.transactionType === CreditType.IOS
                ? packageData.uniqueIosId
                : packageData.uniqueAndroidId;

            this.inAppPurchase
              .when(packageId)
              .cancelled(this.onProductCancelled);

            this.inAppPurchase.when(packageId).error(this.onProductError);
            this.inAppPurchase.when(packageId).updated(this.onProductUpdated);
            this.inAppPurchase.when(packageId).approved(this.onProductApproved);
            this.inAppPurchase.when(packageId).verified((p) => {
              console.log('finish');
              p.finish();
            });
          }

          this.inAppPurchase.refresh();
        });
      });
  }

  ionViewWillLeave(): void {
    this.inAppPurchase.off(this.onProductCancelled);
    this.inAppPurchase.off(this.onProductError);
    this.inAppPurchase.off(this.onProductUpdated);
    this.inAppPurchase.off(this.onProductApproved);
    this.destroy.next();
    this.destroy.complete();
  }

  async goBack(data: null | IPackage): Promise<void> {
    await this.modalCtrl.dismiss(data);
  }

  async select(selected: IPackage): Promise<any> {
    // Once the transaction is approved, the product still isn’t owned:
    // the store needs confirmation that the purchase was delivered
    // before closing the transaction.
    const packageId =
      this.transactionType === CreditType.IOS
        ? selected.uniqueIosId
        : selected.uniqueAndroidId;

    const order = this.inAppPurchase.order(packageId);
    order.error((error) => this.onProductError(error));
  }

  // Cancelled when the purchase is cancelled, it's time to show an error message.
  private onProductCancelled: IAPQueryCallback = async (product: IAPProduct) => {
    console.error(`${product.id} was cancelled`);
  };

  private onProductApproved: IAPQueryCallback = async (product: IAPProduct) => {
    this.paymentService
      .createPayment(product.transaction.appStoreReceipt, this.orderId)
      .pipe(takeUntil(this.destroy))
      .subscribe(
        async () => {
          const selected = this.packages.find(
            (packageData) =>
              packageData.uniqueIosId === product.id ||
              packageData.uniqueAndroidId === product.id
          );
          product.verify();
          await this.modalCtrl.dismiss(selected);
        },
        async (error) => {
          console.error(error);
        }
      );
  };

  // Error when an error occurs during purchase workflow
  private onProductError: IAPQueryCallback = async (product: IAPProduct) => {
    console.error(`${product.id} was cancelled`);
  };

  // Updated it's a push based event,
  // when the product informations are updated (price, label, … )
  private onProductUpdated: IAPQueryCallback = (product: IAPProduct) => {
    this.refreshProduct(product);
  };

  private registerProducts() {
    this.packages.forEach((packageData) => {
      let packageType = this.inAppPurchase.CONSUMABLE;
      switch (packageData.type) {
        case PackageTypeEnum.Consumable: {
          packageType = this.inAppPurchase.CONSUMABLE;
          break;
        }
        case PackageTypeEnum.NonConsumable: {
          packageType = this.inAppPurchase.NON_CONSUMABLE;
          break;
        }
        case PackageTypeEnum.AutoRenewable: {
          packageType = this.inAppPurchase.PAID_SUBSCRIPTION;
          break;
        }
        case PackageTypeEnum.NonRenewing: {
          packageType = this.inAppPurchase.PAID_SUBSCRIPTION;
          break;
        }
      }

      this.inAppPurchase.register({
        id:
          this.transactionType === CreditType.IOS
            ? packageData.uniqueIosId
            : packageData.uniqueAndroidId,
        type: packageType,
      });
    });
  }

  private async refreshProduct(product: IAPProduct) {
    // if user already have monthly subscription
    if (product.owned) {
      const selected = this.packages.find(
        (packageData) =>
          packageData.uniqueIosId === product.id ||
          packageData.uniqueAndroidId === product.id
      );
      const alert = await this.alert.create({
        message: 'You already have subscription',
        buttons: [this.translate.instant('OK')],
      });
      await alert.present();
      await this.goBack(selected);
    }
  }
}

Expected behaviour

User select package with monthly subscription. When subscription is expired user can refresh this subscription. I have auto-renewable subscription in IOS and I expected that we can refresh payment and and continue user subscription: image

Observed behaviour

When the user select monthly subscription and have it expired: approved event will trigger with expired token after trigger this.inAppPurchase.refresh(). And this event is triggered with all transactions that was before, not only last one. I cannot figure out how to prevent triggering this event several times. On the backend I validate token from the transaction and get that payment is expired. I throw error that payment cannot be validated. After validation I think is the best place to refresh the subscription, but I cannot find how to do that.

Steps to reproduce

  1. Buy monthly subscription.
  2. Wait until this subscription is expired.
  3. Try to validate this subscription on the backend
  4. Refresh user subscription if it expired. - how to do that?

TanyaDolgopolova avatar Apr 02 '21 12:04 TanyaDolgopolova

Same problem, any solutions?

Yggdrasilqh avatar Nov 10 '21 08:11 Yggdrasilqh

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Apr 18 '22 18:04 stale[bot]

Same problem, any solutions?

aktivdigital-frontend avatar Dec 12 '22 16:12 aktivdigital-frontend

Same problem, in android it works fine, but in ios subscriptions don't work, at least in sandbox environment.

bdesiderio avatar Jan 24 '23 15:01 bdesiderio