Fix logic for apple subscriptions

This commit is contained in:
Phillip Thelen
2022-11-03 17:48:36 +01:00
committed by Phillip Thelen
parent 9b791b4ba0
commit 967717a010
3 changed files with 85 additions and 47 deletions

View File

@@ -229,14 +229,17 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: sku,
transactionId: token,
}, {
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: 'wrongsku',
transactionId: token,
}, {
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: sku,
transactionId: token,
}]);
@@ -251,21 +254,12 @@ describe('Apple Payments', () => {
if (payments.createSubscription.restore) payments.createSubscription.restore();
});
it('should throw an error if sku is empty', async () => {
await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if receipt is invalid', async () => {
iap.isValidated.restore();
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(false);
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -297,13 +291,14 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: new Date(),
productId: option.sku,
transactionId: token,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks[option.subKey];
await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
@@ -336,14 +331,14 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: newOption.sku,
transactionId: `${token}new`,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks[newOption.subKey];
await applePayments.subscribe(newOption.sku,
user,
await applePayments.subscribe(user,
receipt,
headers,
nextPaymentProcessing);
@@ -381,14 +376,14 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: newOption.sku,
transactionId: `${token}new`,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks[newOption.subKey];
await applePayments.subscribe(newOption.sku,
user,
await applePayments.subscribe(user,
receipt,
headers,
nextPaymentProcessing);
@@ -415,6 +410,44 @@ describe('Apple Payments', () => {
}
});
it('uses the most recent subscription data', async () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 4 }).toDate(),
purchaseDate: moment.utc().subtract({ day: 5 }).toDate(),
productId: 'com.habitrpg.ios.habitica.subscription.3month',
transactionId: token + 'oldest',
originalTransactionId: token + 'evenOlder',
}, {
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: moment.utc().subtract({ day: 1 }).toDate(),
productId: 'com.habitrpg.ios.habitica.subscription.12month',
transactionId: token + 'newest',
originalTransactionId: token + 'newest',
}, {
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().subtract({ day: 2 }).toDate(),
productId: 'com.habitrpg.ios.habitica.subscription.6month',
transactionId: token,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks['basic_12mo'];
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token + 'newest',
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
});
})
describe('does not apply multiple times', async () => {
it('errors when a user is using the same subscription', async () => {
payments.createSubscription.restore();
@@ -422,14 +455,15 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: token,
originalTransactionId: token,
}]);
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -445,14 +479,15 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: `${token}renew`,
originalTransactionId: token,
}]);
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -468,16 +503,17 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: token,
originalTransactionId: token,
}]);
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
const secondUser = new User();
await expect(applePayments.subscribe(
sku, secondUser, receipt, headers, nextPaymentProcessing,
secondUser, receipt, headers, nextPaymentProcessing,
))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,

View File

@@ -144,7 +144,7 @@ api.iapSubscriptioniOS = {
if (!req.body.sku) throw new BadRequest(res.t('missingSubscriptionCode'));
if (!req.body.receipt) throw new BadRequest(res.t('missingReceipt'));
await applePayments.subscribe(req.body.sku, res.locals.user, req.body.receipt, req.headers);
await applePayments.subscribe(res.locals.user, req.body.receipt, req.headers);
res.respond(200);
},

View File

@@ -74,8 +74,33 @@ api.verifyPurchase = async function verifyPurchase (options) {
return appleRes;
};
api.subscribe = async function subscribe (sku, user, receipt, headers, nextPaymentProcessing) {
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
api.subscribe = async function subscribe (user, receipt, headers, nextPaymentProcessing) {
await iap.setup();
const appleRes = await iap.validate(iap.APPLE, receipt);
const isValidated = iap.isValidated(appleRes);
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
const purchaseDataList = iap.getPurchaseData(appleRes);
if (purchaseDataList.length === 0) {
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
}
let originalTransactionId;
let newTransactionId;
let newestDate;
let sku;
for (const purchaseData of purchaseDataList) {
const datePurchased = new Date(Number(purchaseData.purchaseDate));
const dateTerminated = new Date(Number(purchaseData.expirationDate));
if ((!newestDate || datePurchased > newestDate) && dateTerminated > new Date()) {
originalTransactionId = purchaseData.originalTransactionId;
newTransactionId = purchaseData.transactionId;
newestDate = datePurchased
sku = purchaseData.productId
}
}
let subCode;
switch (sku) { // eslint-disable-line default-case
@@ -93,29 +118,6 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme
break;
}
const sub = subCode ? shared.content.subscriptionBlocks[subCode] : false;
if (!sub) throw new NotAuthorized(this.constants.RESPONSE_INVALID_ITEM);
await iap.setup();
const appleRes = await iap.validate(iap.APPLE, receipt);
const isValidated = iap.isValidated(appleRes);
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
const purchaseDataList = iap.getPurchaseData(appleRes);
if (purchaseDataList.length === 0) {
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
}
let originalTransactionId;
let newTransactionId;
for (const purchaseData of purchaseDataList) {
const dateTerminated = new Date(Number(purchaseData.expirationDate));
if (purchaseData.productId === sku && dateTerminated > new Date()) {
originalTransactionId = purchaseData.originalTransactionId;
newTransactionId = purchaseData.transactionId;
break;
}
}
if (originalTransactionId) {
let existingSub;