mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
Fix logic for apple subscriptions
This commit is contained in:
committed by
Phillip Thelen
parent
9b791b4ba0
commit
967717a010
@@ -229,14 +229,17 @@ describe('Apple Payments', () => {
|
|||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().valueOf(),
|
||||||
productId: sku,
|
productId: sku,
|
||||||
transactionId: token,
|
transactionId: token,
|
||||||
}, {
|
}, {
|
||||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().valueOf(),
|
||||||
productId: 'wrongsku',
|
productId: 'wrongsku',
|
||||||
transactionId: token,
|
transactionId: token,
|
||||||
}, {
|
}, {
|
||||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().valueOf(),
|
||||||
productId: sku,
|
productId: sku,
|
||||||
transactionId: token,
|
transactionId: token,
|
||||||
}]);
|
}]);
|
||||||
@@ -251,21 +254,12 @@ describe('Apple Payments', () => {
|
|||||||
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
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 () => {
|
it('should throw an error if receipt is invalid', async () => {
|
||||||
iap.isValidated.restore();
|
iap.isValidated.restore();
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||||
.returns(false);
|
.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({
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
httpCode: 401,
|
httpCode: 401,
|
||||||
name: 'NotAuthorized',
|
name: 'NotAuthorized',
|
||||||
@@ -297,13 +291,14 @@ describe('Apple Payments', () => {
|
|||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||||
|
purchaseDate: new Date(),
|
||||||
productId: option.sku,
|
productId: option.sku,
|
||||||
transactionId: token,
|
transactionId: token,
|
||||||
originalTransactionId: token,
|
originalTransactionId: token,
|
||||||
}]);
|
}]);
|
||||||
sub = common.content.subscriptionBlocks[option.subKey];
|
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(iapSetupStub).to.be.calledOnce;
|
||||||
expect(iapValidateStub).to.be.calledOnce;
|
expect(iapValidateStub).to.be.calledOnce;
|
||||||
@@ -336,14 +331,14 @@ describe('Apple Payments', () => {
|
|||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
expirationDate: moment.utc().add({ day: 2 }).toDate(),
|
expirationDate: moment.utc().add({ day: 2 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().valueOf(),
|
||||||
productId: newOption.sku,
|
productId: newOption.sku,
|
||||||
transactionId: `${token}new`,
|
transactionId: `${token}new`,
|
||||||
originalTransactionId: token,
|
originalTransactionId: token,
|
||||||
}]);
|
}]);
|
||||||
sub = common.content.subscriptionBlocks[newOption.subKey];
|
sub = common.content.subscriptionBlocks[newOption.subKey];
|
||||||
|
|
||||||
await applePayments.subscribe(newOption.sku,
|
await applePayments.subscribe(user,
|
||||||
user,
|
|
||||||
receipt,
|
receipt,
|
||||||
headers,
|
headers,
|
||||||
nextPaymentProcessing);
|
nextPaymentProcessing);
|
||||||
@@ -381,14 +376,14 @@ describe('Apple Payments', () => {
|
|||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
expirationDate: moment.utc().add({ day: 2 }).toDate(),
|
expirationDate: moment.utc().add({ day: 2 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().valueOf(),
|
||||||
productId: newOption.sku,
|
productId: newOption.sku,
|
||||||
transactionId: `${token}new`,
|
transactionId: `${token}new`,
|
||||||
originalTransactionId: token,
|
originalTransactionId: token,
|
||||||
}]);
|
}]);
|
||||||
sub = common.content.subscriptionBlocks[newOption.subKey];
|
sub = common.content.subscriptionBlocks[newOption.subKey];
|
||||||
|
|
||||||
await applePayments.subscribe(newOption.sku,
|
await applePayments.subscribe(user,
|
||||||
user,
|
|
||||||
receipt,
|
receipt,
|
||||||
headers,
|
headers,
|
||||||
nextPaymentProcessing);
|
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 () => {
|
describe('does not apply multiple times', async () => {
|
||||||
it('errors when a user is using the same subscription', async () => {
|
it('errors when a user is using the same subscription', async () => {
|
||||||
payments.createSubscription.restore();
|
payments.createSubscription.restore();
|
||||||
@@ -422,14 +455,15 @@ describe('Apple Payments', () => {
|
|||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().toDate(),
|
||||||
productId: sku,
|
productId: sku,
|
||||||
transactionId: token,
|
transactionId: token,
|
||||||
originalTransactionId: 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({
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
httpCode: 401,
|
httpCode: 401,
|
||||||
name: 'NotAuthorized',
|
name: 'NotAuthorized',
|
||||||
@@ -445,14 +479,15 @@ describe('Apple Payments', () => {
|
|||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().toDate(),
|
||||||
productId: sku,
|
productId: sku,
|
||||||
transactionId: `${token}renew`,
|
transactionId: `${token}renew`,
|
||||||
originalTransactionId: 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({
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
httpCode: 401,
|
httpCode: 401,
|
||||||
name: 'NotAuthorized',
|
name: 'NotAuthorized',
|
||||||
@@ -468,16 +503,17 @@ describe('Apple Payments', () => {
|
|||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().toDate(),
|
||||||
productId: sku,
|
productId: sku,
|
||||||
transactionId: token,
|
transactionId: token,
|
||||||
originalTransactionId: token,
|
originalTransactionId: token,
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
|
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
|
||||||
|
|
||||||
const secondUser = new User();
|
const secondUser = new User();
|
||||||
await expect(applePayments.subscribe(
|
await expect(applePayments.subscribe(
|
||||||
sku, secondUser, receipt, headers, nextPaymentProcessing,
|
secondUser, receipt, headers, nextPaymentProcessing,
|
||||||
))
|
))
|
||||||
.to.eventually.be.rejected.and.to.eql({
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
httpCode: 401,
|
httpCode: 401,
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ api.iapSubscriptioniOS = {
|
|||||||
if (!req.body.sku) throw new BadRequest(res.t('missingSubscriptionCode'));
|
if (!req.body.sku) throw new BadRequest(res.t('missingSubscriptionCode'));
|
||||||
if (!req.body.receipt) throw new BadRequest(res.t('missingReceipt'));
|
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);
|
res.respond(200);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,8 +74,33 @@ api.verifyPurchase = async function verifyPurchase (options) {
|
|||||||
return appleRes;
|
return appleRes;
|
||||||
};
|
};
|
||||||
|
|
||||||
api.subscribe = async function subscribe (sku, user, receipt, headers, nextPaymentProcessing) {
|
api.subscribe = async function subscribe (user, receipt, headers, nextPaymentProcessing) {
|
||||||
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
|
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;
|
let subCode;
|
||||||
switch (sku) { // eslint-disable-line default-case
|
switch (sku) { // eslint-disable-line default-case
|
||||||
@@ -93,29 +118,6 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const sub = subCode ? shared.content.subscriptionBlocks[subCode] : false;
|
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) {
|
if (originalTransactionId) {
|
||||||
let existingSub;
|
let existingSub;
|
||||||
|
|||||||
Reference in New Issue
Block a user