mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17: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 { model as User } from '../../../../../website/server/models/user';
|
||||||
import common from '../../../../../website/common';
|
import common from '../../../../../website/common';
|
||||||
import * as gems from '../../../../../website/server/libs/payments/gems';
|
import * as gems from '../../../../../website/server/libs/payments/gems';
|
||||||
|
import { after } from 'lodash';
|
||||||
|
|
||||||
const { i18n } = common;
|
const { i18n } = common;
|
||||||
|
|
||||||
describe('Apple Payments', () => {
|
describe.only('Apple Payments', () => {
|
||||||
const subKey = 'basic_3mo';
|
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', () => {
|
describe('verifyPurchase', () => {
|
||||||
let sku; let user; let token; let receipt; let
|
let sku; let user; let token; let receipt; let
|
||||||
headers;
|
headers;
|
||||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
|
|
||||||
iapGetPurchaseDataStub; let validateGiftMessageStub;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
token = 'testToken';
|
token = 'testToken';
|
||||||
@@ -25,13 +48,9 @@ describe('Apple Payments', () => {
|
|||||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||||
headers = {};
|
headers = {};
|
||||||
|
|
||||||
iapSetupStub = sinon.stub(iap, 'setup')
|
|
||||||
.resolves();
|
|
||||||
iapValidateStub = sinon.stub(iap, 'validate')
|
|
||||||
.resolves({});
|
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||||
sinon.stub(iap, 'isExpired').returns(false);
|
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||||
sinon.stub(iap, 'isCanceled').returns(false);
|
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||||
@@ -42,12 +61,6 @@ describe('Apple Payments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
iap.setup.restore();
|
|
||||||
iap.validate.restore();
|
|
||||||
iap.isValidated.restore();
|
|
||||||
iap.isExpired.restore();
|
|
||||||
iap.isCanceled.restore();
|
|
||||||
iap.getPurchaseData.restore();
|
|
||||||
payments.buySkuItem.restore();
|
payments.buySkuItem.restore();
|
||||||
gems.validateGiftMessage.restore();
|
gems.validateGiftMessage.restore();
|
||||||
});
|
});
|
||||||
@@ -209,9 +222,6 @@ describe('Apple Payments', () => {
|
|||||||
describe('subscribe', () => {
|
describe('subscribe', () => {
|
||||||
let sub; let sku; let user; let token; let receipt; let headers; let
|
let sub; let sku; let user; let token; let receipt; let headers; let
|
||||||
nextPaymentProcessing;
|
nextPaymentProcessing;
|
||||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub;
|
|
||||||
let paymentsCreateSubscritionStub; let
|
|
||||||
iapGetPurchaseDataStub;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sub = common.content.subscriptionBlocks[subKey];
|
sub = common.content.subscriptionBlocks[subKey];
|
||||||
@@ -223,12 +233,10 @@ describe('Apple Payments', () => {
|
|||||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||||
user = new User();
|
user = new User();
|
||||||
|
|
||||||
iapSetupStub = sinon.stub(iap, 'setup')
|
|
||||||
.resolves();
|
|
||||||
iapValidateStub = sinon.stub(iap, 'validate')
|
|
||||||
.resolves({});
|
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||||
.returns(true);
|
.returns(true);
|
||||||
|
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||||
|
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||||
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(),
|
||||||
@@ -250,10 +258,6 @@ describe('Apple Payments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
iap.setup.restore();
|
|
||||||
iap.validate.restore();
|
|
||||||
iap.isValidated.restore();
|
|
||||||
iap.getPurchaseData.restore();
|
|
||||||
if (payments.createSubscription.restore) payments.createSubscription.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 = [
|
const subOptions = [
|
||||||
{
|
{
|
||||||
sku: 'subscription1month',
|
sku: 'subscription1month',
|
||||||
@@ -574,8 +601,7 @@ describe('Apple Payments', () => {
|
|||||||
describe('cancelSubscribe ', () => {
|
describe('cancelSubscribe ', () => {
|
||||||
let user; let token; let receipt; let headers; let customerId; let
|
let user; let token; let receipt; let headers; let customerId; let
|
||||||
expirationDate;
|
expirationDate;
|
||||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
let paymentCancelSubscriptionSpy;
|
||||||
paymentCancelSubscriptionSpy;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
token = 'test-token';
|
token = 'test-token';
|
||||||
@@ -584,8 +610,7 @@ describe('Apple Payments', () => {
|
|||||||
customerId = 'test-customerId';
|
customerId = 'test-customerId';
|
||||||
expirationDate = moment.utc();
|
expirationDate = moment.utc();
|
||||||
|
|
||||||
iapSetupStub = sinon.stub(iap, 'setup')
|
iapValidateStub.restore();
|
||||||
.resolves();
|
|
||||||
iapValidateStub = sinon.stub(iap, 'validate')
|
iapValidateStub = sinon.stub(iap, 'validate')
|
||||||
.resolves({
|
.resolves({
|
||||||
expirationDate,
|
expirationDate,
|
||||||
@@ -593,8 +618,8 @@ describe('Apple Payments', () => {
|
|||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||||
sinon.stub(iap, 'isCanceled').returns(false);
|
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||||
sinon.stub(iap, 'isExpired').returns(true);
|
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
|
||||||
user = new User();
|
user = new User();
|
||||||
user.profile.name = 'sender';
|
user.profile.name = 'sender';
|
||||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||||
@@ -606,13 +631,7 @@ describe('Apple Payments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
iap.setup.restore();
|
paymentCancelSubscriptionSpy.restore();
|
||||||
iap.validate.restore();
|
|
||||||
iap.isValidated.restore();
|
|
||||||
iap.isExpired.restore();
|
|
||||||
iap.isCanceled.restore();
|
|
||||||
iap.getPurchaseData.restore();
|
|
||||||
payments.cancelSubscription.restore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if we are missing a subscription', async () => {
|
it('should throw an error if we are missing a subscription', async () => {
|
||||||
@@ -695,6 +714,8 @@ describe('Apple Payments', () => {
|
|||||||
expect(iapIsValidatedStub).to.be.calledWith({
|
expect(iapIsValidatedStub).to.be.calledWith({
|
||||||
expirationDate,
|
expirationDate,
|
||||||
});
|
});
|
||||||
|
expect(iapIsCanceledStub).to.be.calledOnce;
|
||||||
|
expect(iapIsExpiredStub).to.be.calledOnce;
|
||||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||||
|
|
||||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||||
|
|||||||
@@ -98,7 +98,8 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="btn btn-danger mt-3"
|
class="btn btn-danger mt-3"
|
||||||
@click="confirmDeleteHero">
|
@click="confirmDeleteHero"
|
||||||
|
>
|
||||||
Begin Member deletion
|
Begin Member deletion
|
||||||
</button>
|
</button>
|
||||||
<b-modal
|
<b-modal
|
||||||
@@ -107,7 +108,8 @@
|
|||||||
ok-title="Delete"
|
ok-title="Delete"
|
||||||
ok-variant="danger"
|
ok-variant="danger"
|
||||||
cancel-title="Cancel"
|
cancel-title="Cancel"
|
||||||
@ok="deleteHero">
|
@ok="deleteHero"
|
||||||
|
>
|
||||||
<b-modal-body>
|
<b-modal-body>
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to delete this member?
|
Are you sure you want to delete this member?
|
||||||
@@ -116,29 +118,37 @@
|
|||||||
Please note: This action cannot be undone!
|
Please note: This action cannot be undone!
|
||||||
</p>
|
</p>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
id="deleteAccountCheck"
|
||||||
type="checkbox"
|
v-model="deleteHabiticaAccount"
|
||||||
v-model="deleteHabiticaAccount"
|
class="form-check-input"
|
||||||
id="deleteAccountCheck">
|
type="checkbox"
|
||||||
<label class="form-check-label" for="deleteAccountCheck">
|
>
|
||||||
Delete Habitica account
|
<label
|
||||||
</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"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
for="deleteAmplitudeCheck"
|
||||||
|
>
|
||||||
|
Delete Amplitude data
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
v-model="deleteAmplitudeData"
|
|
||||||
id="deleteAmplitudeCheck">
|
|
||||||
<label class="form-check-label" for="deleteAmplitudeCheck">
|
|
||||||
Delete Amplitude data
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</b-modal-body>
|
</b-modal-body>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -419,6 +419,57 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div
|
<div
|
||||||
v-if="expand"
|
v-if="expand"
|
||||||
@@ -474,17 +525,36 @@
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.input-group-append {
|
.form-group {
|
||||||
width: auto;
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
.input-group-text {
|
|
||||||
border-bottom-right-radius: 2px;
|
.input-group-append {
|
||||||
border-top-right-radius: 2px;
|
width: auto;
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.8rem;
|
.input-group-text {
|
||||||
color: $gray-200;
|
border-bottom-right-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
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>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -494,6 +564,48 @@ import { getPlanContext } from '@/../../common/script/cron';
|
|||||||
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
|
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
|
||||||
import saveHero from '../mixins/saveHero';
|
import saveHero from '../mixins/saveHero';
|
||||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
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 {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -520,6 +632,7 @@ export default {
|
|||||||
isConvertingToGroupPlan: false,
|
isConvertingToGroupPlan: false,
|
||||||
groupPlanID: '',
|
groupPlanID: '',
|
||||||
subscriptionBlocks,
|
subscriptionBlocks,
|
||||||
|
paymentDetails: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -553,6 +666,9 @@ export default {
|
|||||||
}
|
}
|
||||||
return terminationDate;
|
return terminationDate;
|
||||||
},
|
},
|
||||||
|
playOrdersUrl () {
|
||||||
|
return `${PLAY_CONSOLE_ORDERS_BASE_URL}${this.paymentDetails?.transactionId || ''}`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
dateFormat (date) {
|
dateFormat (date) {
|
||||||
@@ -583,6 +699,20 @@ export default {
|
|||||||
this.isConvertingToGroupPlan = true;
|
this.isConvertingToGroupPlan = true;
|
||||||
this.hero.purchased.plan.owner = '';
|
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) {
|
saveClicked (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.isConvertingToGroupPlan) {
|
if (this.isConvertingToGroupPlan) {
|
||||||
@@ -601,6 +731,15 @@ export default {
|
|||||||
this.$emit('changeUserIdentifier', id);
|
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>
|
</script>
|
||||||
|
|||||||
@@ -11,3 +11,9 @@ export async function getUserHistory (store, payload) {
|
|||||||
const response = await axios.get(url);
|
const response = await axios.get(url);
|
||||||
return response.data.data;
|
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',
|
'TIME_TRAVEL_ENABLED',
|
||||||
'DEBUG_ENABLED',
|
'DEBUG_ENABLED',
|
||||||
'CONTENT_SWITCHOVER_TIME_OFFSET',
|
'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 = {};
|
const envObject = {};
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { model as Blocker } from '../../models/blocker';
|
|||||||
import {
|
import {
|
||||||
NotFound,
|
NotFound,
|
||||||
} from '../../libs/errors';
|
} from '../../libs/errors';
|
||||||
|
import apple from '../../libs/payments/apple';
|
||||||
|
import google from '../../libs/payments/google';
|
||||||
|
|
||||||
const api = {};
|
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;
|
export default api;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ api.verifyPurchase = async function verifyPurchase (options) {
|
|||||||
return appleRes;
|
return appleRes;
|
||||||
};
|
};
|
||||||
|
|
||||||
api.subscribe = async function subscribe (user, receipt, headers, nextPaymentProcessing) {
|
async function findSubscriptionPurchase (receipt, onlyActive = true) {
|
||||||
await iap.setup();
|
await iap.setup();
|
||||||
|
|
||||||
const appleRes = await iap.validate(iap.APPLE, receipt);
|
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) {
|
if (purchaseDataList.length === 0) {
|
||||||
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||||
}
|
}
|
||||||
|
|
||||||
let purchase;
|
let purchase;
|
||||||
let newestDate;
|
let newestDate;
|
||||||
|
|
||||||
for (const purchaseData of purchaseDataList) {
|
for (const purchaseData of purchaseDataList) {
|
||||||
const datePurchased = new Date(Number(purchaseData.purchaseDate));
|
const datePurchased = new Date(Number(purchaseData.purchaseDateMs));
|
||||||
const dateTerminated = new Date(Number(purchaseData.expirationDate));
|
const dateTerminated = new Date(Number(purchaseData.expirationDate || 0));
|
||||||
if ((!newestDate || datePurchased > newestDate) && dateTerminated > new Date()) {
|
if ((!newestDate || datePurchased > newestDate)) {
|
||||||
purchase = purchaseData;
|
if (!onlyActive || dateTerminated > new Date()) {
|
||||||
newestDate = datePurchased;
|
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;
|
let subCode;
|
||||||
switch (purchase.productId) { // eslint-disable-line default-case
|
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) {
|
api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||||
const { plan } = user.purchased;
|
const { plan } = user.purchased;
|
||||||
|
|
||||||
if (plan.paymentMethod !== api.constants.PAYMENT_METHOD_APPLE) throw new NotAuthorized(shared.i18n.t('missingSubscription'));
|
if (plan.paymentMethod !== api.constants.PAYMENT_METHOD_APPLE) throw new NotAuthorized(shared.i18n.t('missingSubscription'));
|
||||||
|
|
||||||
await iap.setup();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const appleRes = await iap.validate(iap.APPLE, plan.additionalData);
|
const details = await findSubscriptionPurchase(plan.additionalData, false);
|
||||||
|
if (!details.isCanceled && !details.isExpired) {
|
||||||
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)) {
|
|
||||||
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
await payments.cancelSubscription({
|
await payments.cancelSubscription({
|
||||||
user,
|
user,
|
||||||
nextBill: new Date(Number(newestPurchase.expirationDate)),
|
nextBill: new Date(Number(details.expirationDate)),
|
||||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,6 +72,53 @@ api.verifyPurchase = async function verifyPurchase (options) {
|
|||||||
return googleRes;
|
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 (
|
api.subscribe = async function subscribe (
|
||||||
sku,
|
sku,
|
||||||
user,
|
user,
|
||||||
@@ -213,22 +260,11 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
|||||||
let dateTerminated;
|
let dateTerminated;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const googleRes = await iap.validate(iap.GOOGLE, plan.additionalData);
|
const details = await findSubscriptionPurchase(plan.additionalData);
|
||||||
|
if (!details.isCanceled && !details.isExpired) {
|
||||||
const isValidated = iap.isValidated(googleRes);
|
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
dateTerminated = details.expirationDate;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Status:410 means that the subsctiption isn't active anymore and we can safely delete it
|
// Status:410 means that the subsctiption isn't active anymore and we can safely delete it
|
||||||
if (err && err.message === 'Status:410') {
|
if (err && err.message === 'Status:410') {
|
||||||
|
|||||||
Reference in New Issue
Block a user