cordova-plugin-purchase
cordova-plugin-purchase copied to clipboard
[IOS] InAppPurchase2 restore expired purchase
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:

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
- Buy monthly subscription.
- Wait until this subscription is expired.
- Try to validate this subscription on the backend
- Refresh user subscription if it expired. - how to do that?
Same problem, any solutions?
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.
Same problem, any solutions?
Same problem, in android it works fine, but in ios subscriptions don't work, at least in sandbox environment.