mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 07:37:25 +01:00
add detailed information about sub payment for google and apple
This commit is contained in:
@@ -6,17 +6,40 @@ import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import * as gems from '../../../../../website/server/libs/payments/gems';
|
||||
import { after } from 'lodash';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('Apple Payments', () => {
|
||||
describe.only('Apple Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
let iapSetupStub;
|
||||
let iapValidateStub;
|
||||
let iapIsValidatedStub;
|
||||
let iapIsCanceledStub;
|
||||
let iapIsExpiredStub;
|
||||
let paymentBuySkuStub;
|
||||
let iapGetPurchaseDataStub;
|
||||
let validateGiftMessageStub;
|
||||
let paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
iapSetupStub = sinon.stub(iap, 'setup').resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
});
|
||||
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
|
||||
iapGetPurchaseDataStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
token = 'testToken';
|
||||
@@ -25,13 +48,9 @@ describe('Apple Payments', () => {
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
@@ -42,12 +61,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
@@ -209,9 +222,6 @@ describe('Apple Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub;
|
||||
let paymentsCreateSubscritionStub; let
|
||||
iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -223,12 +233,10 @@ describe('Apple Payments', () => {
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
user = new User();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||
@@ -250,10 +258,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
@@ -270,6 +274,29 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if no active subscription is found', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled')
|
||||
.returns(true);
|
||||
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: -2 }).toDate(),
|
||||
purchaseDate: new Date(),
|
||||
productId: 'subscription1month',
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
|
||||
});
|
||||
});
|
||||
|
||||
const subOptions = [
|
||||
{
|
||||
sku: 'subscription1month',
|
||||
@@ -574,8 +601,7 @@ describe('Apple Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
@@ -584,8 +610,7 @@ describe('Apple Payments', () => {
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub.restore();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
@@ -593,8 +618,8 @@ describe('Apple Payments', () => {
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
@@ -606,13 +631,7 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
paymentCancelSubscriptionSpy.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
@@ -695,6 +714,8 @@ describe('Apple Payments', () => {
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapIsCanceledStub).to.be.calledOnce;
|
||||
expect(iapIsExpiredStub).to.be.calledOnce;
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
|
||||
@@ -98,7 +98,8 @@
|
||||
/>
|
||||
<button
|
||||
class="btn btn-danger mt-3"
|
||||
@click="confirmDeleteHero">
|
||||
@click="confirmDeleteHero"
|
||||
>
|
||||
Begin Member deletion
|
||||
</button>
|
||||
<b-modal
|
||||
@@ -107,7 +108,8 @@
|
||||
ok-title="Delete"
|
||||
ok-variant="danger"
|
||||
cancel-title="Cancel"
|
||||
@ok="deleteHero">
|
||||
@ok="deleteHero"
|
||||
>
|
||||
<b-modal-body>
|
||||
<p>
|
||||
Are you sure you want to delete this member?
|
||||
@@ -118,21 +120,29 @@
|
||||
<div class="ml-4">
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="deleteAccountCheck"
|
||||
v-model="deleteHabiticaAccount"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model="deleteHabiticaAccount"
|
||||
id="deleteAccountCheck">
|
||||
<label class="form-check-label" for="deleteAccountCheck">
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="deleteAccountCheck"
|
||||
>
|
||||
Delete Habitica account
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="deleteAmplitudeCheck"
|
||||
v-model="deleteAmplitudeData"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model="deleteAmplitudeData"
|
||||
id="deleteAmplitudeCheck">
|
||||
<label class="form-check-label" for="deleteAmplitudeCheck">
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="deleteAmplitudeCheck"
|
||||
>
|
||||
Delete Amplitude data
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -419,6 +419,57 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<h2>Payment Details</h2>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="getSubscriptionPaymentDetails"
|
||||
>
|
||||
Get Subscription Payment Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="paymentDetails">
|
||||
<div
|
||||
v-for="(value, key) in paymentDetails"
|
||||
class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
{{ getHumandReadablePaymentDetails(key).label }}:
|
||||
<span
|
||||
:id="`${key}_tooltip`"
|
||||
class="info-icon"
|
||||
v-b-tooltip.hover.right="getHumandReadablePaymentDetails(key).help"
|
||||
>?</span>
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
<span v-if="value === true">Yes</span>
|
||||
<span v-else-if="value === false">No</span>
|
||||
<span v-else-if="isDate(value)"
|
||||
v-b-tooltip.hover="value">
|
||||
{{ formatDate(value) }}
|
||||
</span>
|
||||
<span v-else-if="value === null">---</span>
|
||||
<span v-else>{{ value }}</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<a
|
||||
v-if="hero.purchased.plan.paymentMethod === 'Google'"
|
||||
class="btn btn-primary btn-sm"
|
||||
target="_blank"
|
||||
:href="playOrdersUrl">
|
||||
Play Console
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
@@ -474,7 +525,11 @@
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.input-group-append {
|
||||
.form-group {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.input-group-append {
|
||||
width: auto;
|
||||
|
||||
.input-group-text {
|
||||
@@ -484,7 +539,22 @@
|
||||
font-size: 0.8rem;
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 0.8rem;
|
||||
color: $purple-400;
|
||||
cursor: pointer;
|
||||
margin-left: 0.2rem;
|
||||
background-color: $gray-500;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.info-icon:hover {
|
||||
background-color: $purple-400;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -494,6 +564,48 @@ import { getPlanContext } from '@/../../common/script/cron';
|
||||
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
|
||||
|
||||
const humandReadablePaymentDetails = {
|
||||
customerId: {
|
||||
label: 'Customer ID',
|
||||
help: 'The unique identifier for the customer in the payment system.',
|
||||
},
|
||||
purchaseDate: {
|
||||
label: 'Purchase Date',
|
||||
help: 'The date when the subscription was purchased or renewed.',
|
||||
},
|
||||
originalPurchaseDate: {
|
||||
label: 'Original Purchase Date',
|
||||
help: 'The date when the subscription was first purchased.',
|
||||
},
|
||||
productId: {
|
||||
label: 'Product ID',
|
||||
help: 'The identifier for the product associated with the subscription.',
|
||||
},
|
||||
transactionId: {
|
||||
label: 'Transaction ID',
|
||||
help: 'The unique identifier for the last transaction in the payment system.',
|
||||
},
|
||||
isCanceled: {
|
||||
label: 'Is Canceled',
|
||||
help: 'Indicates whether the subscription has been canceled by the user or the system.',
|
||||
},
|
||||
isExpired: {
|
||||
label: 'Is Expired',
|
||||
help: 'Indicates whether the subscription has expired. A cancelled subscription may still be active until the end of the billing cycle.',
|
||||
},
|
||||
expirationDate: {
|
||||
label: 'Termination Date',
|
||||
help: 'The date when the subscription will expire or has expired.',
|
||||
},
|
||||
nextPaymentDate: {
|
||||
label: 'Next Payment Date',
|
||||
help: 'The date when the next payment is due. If the subscription is canceled or expired, this may be null.',
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -520,6 +632,7 @@ export default {
|
||||
isConvertingToGroupPlan: false,
|
||||
groupPlanID: '',
|
||||
subscriptionBlocks,
|
||||
paymentDetails: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -553,6 +666,9 @@ export default {
|
||||
}
|
||||
return terminationDate;
|
||||
},
|
||||
playOrdersUrl () {
|
||||
return `${PLAY_CONSOLE_ORDERS_BASE_URL}${this.paymentDetails?.transactionId || ''}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dateFormat (date) {
|
||||
@@ -583,6 +699,20 @@ export default {
|
||||
this.isConvertingToGroupPlan = true;
|
||||
this.hero.purchased.plan.owner = '';
|
||||
},
|
||||
getSubscriptionPaymentDetails () {
|
||||
this.$store.dispatch('adminPanel:getSubscriptionPaymentDetails', { userIdentifier: this.hero._id })
|
||||
.then((details) => {
|
||||
if (details) {
|
||||
this.paymentDetails = details
|
||||
} else {
|
||||
alert('No payment details found.');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching subscription payment details:', error);
|
||||
alert(`Failed to fetch payment details: ${error.message || 'Unknown error'}`);
|
||||
});
|
||||
},
|
||||
saveClicked (e) {
|
||||
e.preventDefault();
|
||||
if (this.isConvertingToGroupPlan) {
|
||||
@@ -601,6 +731,15 @@ export default {
|
||||
this.$emit('changeUserIdentifier', id);
|
||||
}
|
||||
},
|
||||
getHumandReadablePaymentDetails (key) {
|
||||
return humandReadablePaymentDetails[key] || { label: key, help: '' };
|
||||
},
|
||||
isDate (date) {
|
||||
return moment(date).isValid();
|
||||
},
|
||||
formatDate (date) {
|
||||
return date ? moment(date).format('MM/DD/YYYY') : '---';
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -11,3 +11,9 @@ export async function getUserHistory (store, payload) {
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getSubscriptionPaymentDetails (store, payload) {
|
||||
const url = `/api/v4/admin/user/${payload.userIdentifier}/subscription-payment-details`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
@@ -36,7 +36,7 @@ const envVars = [
|
||||
'TIME_TRAVEL_ENABLED',
|
||||
'DEBUG_ENABLED',
|
||||
'CONTENT_SWITCHOVER_TIME_OFFSET',
|
||||
// TODO necessary? if yes how not to mess up with vue cli? 'NODE_ENV'
|
||||
'PLAY_CONSOLE_ORDERS_BASE_URL',
|
||||
];
|
||||
|
||||
const envObject = {};
|
||||
|
||||
@@ -9,6 +9,8 @@ import { model as Blocker } from '../../models/blocker';
|
||||
import {
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
import apple from '../../libs/payments/apple';
|
||||
import google from '../../libs/payments/google';
|
||||
|
||||
const api = {};
|
||||
|
||||
@@ -188,4 +190,46 @@ api.deleteBlocker = {
|
||||
},
|
||||
};
|
||||
|
||||
api.validateSubscriptionPaymentDetails = {
|
||||
method: 'GET',
|
||||
url: '/admin/user/:userId/subscription-payment-details',
|
||||
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('userId', res.t('heroIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const { userId } = req.params;
|
||||
|
||||
const user = await User.findById(userId)
|
||||
.select('purchased')
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (!user) throw new NotFound(res.t('userWithIDNotFound', { userId }));
|
||||
if (!user.purchased || !user.purchased.plan || !user.purchased.plan.paymentMethod || !user.purchased.plan.paymentMethod === '') {
|
||||
throw new NotFound(res.t('subscriptionNotFoundForUser', { userId }));
|
||||
}
|
||||
|
||||
let paymentDetails;
|
||||
if (user.purchased.plan.paymentMethod === 'Apple') {
|
||||
paymentDetails = await apple.getSubscriptionPaymentDetails(userId, user.purchased.plan);
|
||||
} else if (user.purchased.plan.paymentMethod === 'Google') {
|
||||
paymentDetails = await google.getSubscriptionPaymentDetails(userId, user.purchased.plan);
|
||||
} else if (user.purchased.plan.paymentMethod === 'Paypal') {
|
||||
throw new NotFound(res.t('paypalSubscriptionNotValidated'));
|
||||
} else if (user.purchased.plan.paymentMethod === 'Stripe') {
|
||||
throw new NotFound(res.t('stripeSubscriptionNotValidated'));
|
||||
} else if (user.purchased.plan.paymentMethod === 'Amazon Payments') {
|
||||
throw new NotFound(res.t('amazonSubscriptionNotValidated'));
|
||||
} else if (user.purchased.plan.paymentMethod === 'Gift') {
|
||||
throw new NotFound(res.t('giftSubscriptionNotValidated'));
|
||||
} else {
|
||||
throw new NotFound(res.t('unknownSubscriptionPaymentMethod', { method: user.purchased.paymentMethod }));
|
||||
}
|
||||
res.respond(200, paymentDetails);
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -74,7 +74,7 @@ api.verifyPurchase = async function verifyPurchase (options) {
|
||||
return appleRes;
|
||||
};
|
||||
|
||||
api.subscribe = async function subscribe (user, receipt, headers, nextPaymentProcessing) {
|
||||
async function findSubscriptionPurchase (receipt, onlyActive = true) {
|
||||
await iap.setup();
|
||||
|
||||
const appleRes = await iap.validate(iap.APPLE, receipt);
|
||||
@@ -85,18 +85,51 @@ api.subscribe = async function subscribe (user, receipt, headers, nextPaymentPro
|
||||
if (purchaseDataList.length === 0) {
|
||||
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||
}
|
||||
|
||||
let purchase;
|
||||
let newestDate;
|
||||
|
||||
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()) {
|
||||
const datePurchased = new Date(Number(purchaseData.purchaseDateMs));
|
||||
const dateTerminated = new Date(Number(purchaseData.expirationDate || 0));
|
||||
if ((!newestDate || datePurchased > newestDate)) {
|
||||
if (!onlyActive || dateTerminated > new Date()) {
|
||||
purchase = purchaseData;
|
||||
newestDate = datePurchased;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!purchase) {
|
||||
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||
}
|
||||
return {
|
||||
purchase,
|
||||
isCanceled: iap.isCanceled(purchase),
|
||||
isExpired: iap.isExpired(purchase),
|
||||
expirationDate: new Date(Number(purchase.expirationDate)),
|
||||
};
|
||||
}
|
||||
|
||||
api.getSubscriptionPaymentDetails = async function getDetails (userId, subscriptionPlan) {
|
||||
if (!subscriptionPlan || !subscriptionPlan.additionalData) {
|
||||
throw new NotAuthorized(shared.i18n.t('missingSubscription'));
|
||||
}
|
||||
const details = await findSubscriptionPurchase(subscriptionPlan.additionalData);
|
||||
return {
|
||||
customerId: details.purchase.originalTransactionId || details.purchase.transactionId,
|
||||
purchaseDate: new Date(Number(details.purchase.purchaseDateMs)),
|
||||
originalPurchaseDate: new Date(Number(details.purchase.originalPurchaseDateMs)),
|
||||
expirationDate: details.isCanceled || details.isExpired ? details.expirationDate : null,
|
||||
nextPaymentDate: details.isCanceled || details.isExpired ? null : details.expirationDate,
|
||||
productId: details.purchase.productId,
|
||||
transactionId: details.purchase.transactionId,
|
||||
isCanceled: details.isCanceled,
|
||||
isExpired: details.isExpired,
|
||||
};
|
||||
};
|
||||
|
||||
api.subscribe = async function subscribe (user, receipt, headers, nextPaymentProcessing) {
|
||||
const details = await findSubscriptionPurchase(receipt);
|
||||
const { purchase } = details;
|
||||
|
||||
let subCode;
|
||||
switch (purchase.productId) { // eslint-disable-line default-case
|
||||
@@ -250,37 +283,17 @@ api.noRenewSubscribe = async function noRenewSubscribe (options) {
|
||||
|
||||
api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||
const { plan } = user.purchased;
|
||||
|
||||
if (plan.paymentMethod !== api.constants.PAYMENT_METHOD_APPLE) throw new NotAuthorized(shared.i18n.t('missingSubscription'));
|
||||
|
||||
await iap.setup();
|
||||
|
||||
try {
|
||||
const appleRes = await iap.validate(iap.APPLE, plan.additionalData);
|
||||
|
||||
const isValidated = iap.isValidated(appleRes);
|
||||
if (!isValidated) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
||||
|
||||
const purchases = iap.getPurchaseData(appleRes);
|
||||
if (purchases.length === 0) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
||||
let newestDate;
|
||||
let newestPurchase;
|
||||
|
||||
for (const purchaseData of purchases) {
|
||||
const datePurchased = new Date(Number(purchaseData.purchaseDate));
|
||||
if (!newestDate || datePurchased > newestDate) {
|
||||
newestDate = datePurchased;
|
||||
newestPurchase = purchaseData;
|
||||
}
|
||||
}
|
||||
|
||||
if (!iap.isCanceled(newestPurchase) && !iap.isExpired(newestPurchase)) {
|
||||
const details = await findSubscriptionPurchase(plan.additionalData, false);
|
||||
if (!details.isCanceled && !details.isExpired) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||
}
|
||||
|
||||
await payments.cancelSubscription({
|
||||
user,
|
||||
nextBill: new Date(Number(newestPurchase.expirationDate)),
|
||||
nextBill: new Date(Number(details.expirationDate)),
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||
headers,
|
||||
});
|
||||
|
||||
@@ -72,6 +72,53 @@ api.verifyPurchase = async function verifyPurchase (options) {
|
||||
return googleRes;
|
||||
};
|
||||
|
||||
async function findSubscriptionPurchase (additionalData) {
|
||||
const googleRes = await iap.validate(iap.GOOGLE, additionalData);
|
||||
|
||||
const isValidated = iap.isValidated(googleRes);
|
||||
if (!isValidated) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
||||
|
||||
const purchases = iap.getPurchaseData(googleRes);
|
||||
if (purchases.length === 0) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
||||
|
||||
let purchase;
|
||||
let newestDate;
|
||||
|
||||
for (const i in purchases) {
|
||||
if (Object.prototype.hasOwnProperty.call(purchases, i)) {
|
||||
const thisPurchase = purchases[i];
|
||||
const purchaseDate = new Date(Number(thisPurchase.startTimeMillis));
|
||||
if (!newestDate || purchaseDate > newestDate) {
|
||||
newestDate = purchaseDate;
|
||||
purchase = purchases[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
purchase,
|
||||
isCanceled: iap.isCanceled(purchase),
|
||||
isExpired: iap.isExpired(purchase),
|
||||
expirationDate: new Date(Number(purchase.expirationDate)),
|
||||
};
|
||||
}
|
||||
|
||||
api.getSubscriptionPaymentDetails = async function getDetails (userId, subscriptionPlan) {
|
||||
if (!subscriptionPlan || !subscriptionPlan.additionalData) {
|
||||
throw new NotAuthorized(shared.i18n.t('missingSubscription'));
|
||||
}
|
||||
const details = await findSubscriptionPurchase(subscriptionPlan.additionalData);
|
||||
return {
|
||||
customerId: details.purchase.purchaseToken,
|
||||
originalPurchaseDate: new Date(Number(details.purchase.startTimeMillis)),
|
||||
expirationDate: details.isCanceled || details.isExpired ? details.expirationDate : null,
|
||||
nextPaymentDate: details.isCanceled || details.isExpired ? null : details.expirationDate,
|
||||
productId: details.purchase.productId,
|
||||
transactionId: details.purchase.orderId,
|
||||
isCanceled: details.isCanceled,
|
||||
isExpired: details.isExpired,
|
||||
};
|
||||
};
|
||||
|
||||
api.subscribe = async function subscribe (
|
||||
sku,
|
||||
user,
|
||||
@@ -213,22 +260,11 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||
let dateTerminated;
|
||||
|
||||
try {
|
||||
const googleRes = await iap.validate(iap.GOOGLE, plan.additionalData);
|
||||
|
||||
const isValidated = iap.isValidated(googleRes);
|
||||
if (!isValidated) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
||||
|
||||
const purchases = iap.getPurchaseData(googleRes);
|
||||
if (purchases.length === 0) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
||||
for (const i in purchases) {
|
||||
if (Object.prototype.hasOwnProperty.call(purchases, i)) {
|
||||
const purchase = purchases[i];
|
||||
if (purchase.autoRenewing !== false) return;
|
||||
if (!dateTerminated || Number(purchase.expirationDate) > Number(dateTerminated)) {
|
||||
dateTerminated = new Date(Number(purchase.expirationDate));
|
||||
}
|
||||
}
|
||||
const details = await findSubscriptionPurchase(plan.additionalData);
|
||||
if (!details.isCanceled && !details.isExpired) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||
}
|
||||
dateTerminated = details.expirationDate;
|
||||
} catch (err) {
|
||||
// Status:410 means that the subsctiption isn't active anymore and we can safely delete it
|
||||
if (err && err.message === 'Status:410') {
|
||||
|
||||
Reference in New Issue
Block a user